/* * Copyright (C) 2014 Michell Bak * * 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 com.miz.loader; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.SharedPreferences; import android.database.Cursor; import android.preference.PreferenceManager; import android.text.TextUtils; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Lists; import com.miz.db.DbAdapterMovieMappings; import com.miz.db.DbAdapterMovies; import com.miz.functions.ColumnIndexCache; import com.miz.functions.FileSource; import com.miz.functions.Filepath; import com.miz.functions.LibrarySectionAsyncTask; import com.miz.functions.MediumMovie; import com.miz.functions.MizLib; import com.miz.functions.PreferenceKeys; import com.miz.mizuu.MizuuApplication; import com.miz.mizuu.R; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Locale; import java.util.TreeMap; import java.util.regex.Pattern; import jcifs.smb.SmbFile; public class MovieLoader { // For MovieLibraryType public static final int ALL_MOVIES = 1, FAVORITES = 2, NEW_RELEASES = 3, WATCHLIST = 4, WATCHED = 5, UNWATCHED = 6, COLLECTIONS = 7; // For MovieSortType public static final int TITLE = 1, RELEASE = 2, DURATION = 3, RATING = 4, WEIGHTED_RATING = 5, DATE_ADDED = 6, COLLECTION_TITLE = 7; // For saving the sorting type preference public static final String SORT_TITLE = "sortTitle", SORT_RELEASE = "sortRelease", SORT_RATING = "sortRating", SORT_WEIGHTED_RATING = "sortWeightedRating", SORT_DATE_ADDED = "sortAdded", SORT_DURATION = "sortDuration"; private final Context mContext; private final MovieLibraryType mLibraryType; private final OnLoadCompletedCallback mCallback; private final DbAdapterMovies mDatabase; private MovieSortType mSortType; private ArrayList<MediumMovie> mResults = new ArrayList<>(); private HashSet<MovieFilter> mFilters = new HashSet<>(); private MovieLoaderAsyncTask mAsyncTask; private boolean mShowingSearchResults = false; public MovieLoader(Context context, MovieLibraryType libraryType, OnLoadCompletedCallback callback) { mContext = context; mLibraryType = libraryType; mCallback = callback; mDatabase = MizuuApplication.getMovieAdapter(); setupSortType(); } /** * Get movie library type. Can be either <code>ALL_MOVIES</code>, * <code>FAVORITES</code>, <code>NEW_RELEASES</code>, <code>WATCHLIST</code>, * <code>WATCHED</code>, <code>UNWATCHED</code> or <code>COLLECTIONS</code>. * @return Movie library type */ public MovieLibraryType getType() { return mLibraryType; } /** * Set the movie sort type. * @param type */ public void setSortType(MovieSortType type) { if (getSortType() == type) { getSortType().toggleSortOrder(); } else { mSortType = type; // If we're setting a sort type for the "All movies" // section, we want to save the sort type as the // default way of sorting that section. if (getType() == MovieLibraryType.ALL_MOVIES) { SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(mContext).edit(); switch (getSortType()) { case TITLE: editor.putString(PreferenceKeys.SORTING_MOVIES, SORT_TITLE); break; case RELEASE: editor.putString(PreferenceKeys.SORTING_MOVIES, SORT_RELEASE); break; case RATING: editor.putString(PreferenceKeys.SORTING_MOVIES, SORT_RATING); break; case WEIGHTED_RATING: editor.putString(PreferenceKeys.SORTING_MOVIES, SORT_WEIGHTED_RATING); break; case DATE_ADDED: editor.putString(PreferenceKeys.SORTING_MOVIES, SORT_DATE_ADDED); break; case DURATION: editor.putString(PreferenceKeys.SORTING_MOVIES, SORT_DURATION); break; } editor.apply(); } } } /** * Get the movie sort type. Can be either <code>TITLE</code>, * <code>RELEASE</code>, <code>DURATION</code>, <code>RATING</code>, * <code>WEIGHTED_RATING</code>, <code>DATE_ADDED</code> or <code>COLLECTION_TITLE</code>. * @return Movie sort type */ public MovieSortType getSortType() { return mSortType; } /** * Used to know if the MovieLoader is currently * showing search results. * @return True if showing search results, false otherwise. */ public boolean isShowingSearchResults() { return mShowingSearchResults; } /** * Add a movie filter. Filters are unique and only one * can be present at a time. It is, however, possible to * have multiple filters for different filter types, i.e. * two filters for genres. * @param filter */ public void addFilter(MovieFilter filter) { mFilters.remove(filter); mFilters.add(filter); } /** * Clears all filters. */ public void clearFilters() { mFilters.clear(); } /** * Get all movie filters. * @return Set of all currently added movie filters. */ public HashSet<MovieFilter> getFilters() { return mFilters; } /** * Starts loading movies using any active filters, * sorting types and settings, i.e. prefix ignoring. */ public void load() { load(""); } /** * Similar to <code>load()</code>, but filters the results * based on the search query. * @param query */ public void search(String query) { load(query); } /** * Starts loading movies. * @param query */ private void load(String query) { if (mAsyncTask != null) { mAsyncTask.cancel(true); } mShowingSearchResults = !TextUtils.isEmpty(query); mAsyncTask = new MovieLoaderAsyncTask(query); mAsyncTask.execute(); } /** * Creates movie objects from a Cursor and adds them to a list. * @param cursor * @return List of movie objects from the supplied Cursor. */ private ArrayList<MediumMovie> listFromCursor(Cursor cursor) { // Normally we'd have to go through each movie and add filepaths mapped to that movie // one by one. This is a hacky approach that gets all filepaths at once and creates a // map of them. That way it's easy to get filepaths for a specific movie - and it's // 2-3x faster with ~750 movies. ArrayListMultimap<String, String> filepaths = ArrayListMultimap.create(); Cursor paths = MizuuApplication.getMovieMappingAdapter().getAllFilepaths(false); if (paths != null) { try { while (paths.moveToNext()) { filepaths.put(paths.getString(paths.getColumnIndex(DbAdapterMovieMappings.KEY_TMDB_ID)), paths.getString(paths.getColumnIndex(DbAdapterMovieMappings.KEY_FILEPATH))); } } catch (Exception e) {} finally { paths.close(); MizuuApplication.setMovieFilepaths(filepaths); } } HashMap<String, String> collectionsMap = MizuuApplication.getCollectionsAdapter().getCollectionsMap(); ArrayList<MediumMovie> list = new ArrayList<MediumMovie>(); if (cursor != null) { ColumnIndexCache cache = new ColumnIndexCache(); try { while (cursor.moveToNext()) { list.add(new MediumMovie(mContext, cursor.getString(cache.getColumnIndex(cursor, DbAdapterMovies.KEY_TITLE)), cursor.getString(cache.getColumnIndex(cursor, DbAdapterMovies.KEY_TMDB_ID)), cursor.getString(cache.getColumnIndex(cursor, DbAdapterMovies.KEY_RATING)), cursor.getString(cache.getColumnIndex(cursor, DbAdapterMovies.KEY_RELEASEDATE)), cursor.getString(cache.getColumnIndex(cursor, DbAdapterMovies.KEY_GENRES)), cursor.getString(cache.getColumnIndex(cursor, DbAdapterMovies.KEY_FAVOURITE)), cursor.getString(cache.getColumnIndex(cursor, DbAdapterMovies.KEY_ACTORS)), collectionsMap.get(cursor.getString(cache.getColumnIndex(cursor, DbAdapterMovies.KEY_COLLECTION_ID))), cursor.getString(cache.getColumnIndex(cursor, DbAdapterMovies.KEY_COLLECTION_ID)), cursor.getString(cache.getColumnIndex(cursor, DbAdapterMovies.KEY_TO_WATCH)), cursor.getString(cache.getColumnIndex(cursor, DbAdapterMovies.KEY_HAS_WATCHED)), cursor.getString(cache.getColumnIndex(cursor, DbAdapterMovies.KEY_DATE_ADDED)), cursor.getString(cache.getColumnIndex(cursor, DbAdapterMovies.KEY_CERTIFICATION)), cursor.getString(cache.getColumnIndex(cursor, DbAdapterMovies.KEY_RUNTIME)) )); } } catch (Exception e) {} finally { cursor.close(); cache.clear(); } } mResults = list; return list; } /** * Get the results of the most recently loaded movies. * @return List of movie objects. */ public ArrayList<MediumMovie> getResults() { return mResults; } /** * Handles everything related to loading, filtering, sorting * and delivering the callback when everything is finished. */ private class MovieLoaderAsyncTask extends LibrarySectionAsyncTask<Void, Void, Void> { private final ArrayList<MediumMovie> mMovieList; private final String mSearchQuery; public MovieLoaderAsyncTask(String searchQuery) { // Lowercase in order to search more efficiently mSearchQuery = searchQuery.toLowerCase(Locale.getDefault()); mMovieList = new ArrayList<MediumMovie>(); } @Override protected Void doInBackground(Void... params) { switch (mLibraryType) { case ALL_MOVIES: mMovieList.addAll(listFromCursor(mDatabase.getAllMovies())); break; case COLLECTIONS: mMovieList.addAll(listFromCursor(mDatabase.getCollections())); break; case FAVORITES: mMovieList.addAll(listFromCursor(mDatabase.getFavorites())); break; case NEW_RELEASES: mMovieList.addAll(listFromCursor(mDatabase.getNewReleases())); break; case UNWATCHED: mMovieList.addAll(listFromCursor(mDatabase.getUnwatched())); break; case WATCHED: mMovieList.addAll(listFromCursor(mDatabase.getWatched())); break; case WATCHLIST: mMovieList.addAll(listFromCursor(mDatabase.getWatchlist())); break; default: break; } int totalSize = mMovieList.size(); for (MovieFilter filter : getFilters()) { for (int i = 0; i < totalSize; i++) { if (isCancelled()) return null; boolean condition = false; switch (filter.getType()) { case MovieFilter.GENRE: if (mMovieList.get(i).getGenres().contains(filter.getFilter())) { String[] genres = mMovieList.get(i).getGenres().split(","); for (String genre : genres) { if (genre.trim().equals(filter.getFilter())) { condition = true; break; } } } break; case MovieFilter.CERTIFICATION: condition = mMovieList.get(i).getCertification().trim().equals(filter.getFilter()); break; case MovieFilter.FILE_SOURCE: for (Filepath path : mMovieList.get(i).getFilepaths()) { condition = path.getTypeAsString(mContext).equals(filter.getFilter()); if (condition) break; } break; case MovieFilter.RELEASE_YEAR: condition = mMovieList.get(i).getReleaseYear().trim().contains(filter.getFilter()); break; case MovieFilter.FOLDER: for (Filepath path : mMovieList.get(i).getFilepaths()) { condition = path.getFilepath().trim().startsWith(filter.getFilter()); if (condition) break; } break; case MovieFilter.OFFLINE_FILES: for (Filepath path : mMovieList.get(i).getFilepaths()) { condition = mMovieList.get(i).hasOfflineCopy(path); if (condition) break; } break; case MovieFilter.AVAILABLE_FILES: ArrayList<FileSource> filesources = MizLib.getFileSources(MizLib.TYPE_MOVIE, true); if (isCancelled()) return null; for (Filepath path : mMovieList.get(i).getFilepaths()) { if (path.isNetworkFile()) if (mMovieList.get(i).hasOfflineCopy(path)) { condition = true; break; // break inner loop to continue to the next movie } else { if (path.getType() == FileSource.SMB) { if (MizLib.isWifiConnected(mContext)) { FileSource source = null; for (int j = 0; j < filesources.size(); j++) if (path.getFilepath().contains(filesources.get(j).getFilepath())) { source = filesources.get(j); break; } if (source == null) continue; try { final SmbFile file = new SmbFile( MizLib.createSmbLoginString( source.getDomain(), source.getUser(), source.getPassword(), path.getFilepath(), false )); if (file.exists()) { condition = true; break; // break inner loop to continue to the next movie } } catch (Exception e) {} // Do nothing - the file isn't available (either MalformedURLException or SmbException) } } else if (path.getType() == FileSource.UPNP) { if (MizLib.exists(path.getFilepath())) { condition = true; break; // break inner loop to continue to the next movie } } } else { if (new File(path.getFilepath()).exists()) { condition = true; break; // break inner loop to continue to the next movie } } } break; } if (!condition && mMovieList.size() > i) { mMovieList.remove(i); i--; totalSize--; } } } // If we've got a search query, we should search based on it if (!TextUtils.isEmpty(mSearchQuery)) { ArrayList<MediumMovie> tempCollection = Lists.newArrayList(); if (mSearchQuery.startsWith("actor:")) { for (int i = 0; i < mMovieList.size(); i++) { if (isCancelled()) return null; if (mMovieList.get(i).getCast().toLowerCase(Locale.ENGLISH).contains(mSearchQuery.replace("actor:", "").trim())) tempCollection.add(mMovieList.get(i)); } } else if (mSearchQuery.equalsIgnoreCase("missing_genres")) { for (int i = 0; i < mMovieList.size(); i++) { if (isCancelled()) return null; if (TextUtils.isEmpty(mMovieList.get(i).getGenres())) tempCollection.add(mMovieList.get(i)); } } else if (mSearchQuery.equalsIgnoreCase("multiple_versions")) { DbAdapterMovieMappings db = MizuuApplication.getMovieMappingAdapter(); for (int i = 0; i < mMovieList.size(); i++) { if (isCancelled()) return null; if (db.hasMultipleFilepaths(mMovieList.get(i).getTmdbId())) tempCollection.add(mMovieList.get(i)); } } else { Pattern p = Pattern.compile(MizLib.CHARACTER_REGEX); // Use a pre-compiled pattern as it's a lot faster (approx. 3x for ~700 movies) for (int i = 0; i < mMovieList.size(); i++) { if (isCancelled()) return null; String lowerCaseTitle = (getType() == MovieLibraryType.COLLECTIONS) ? mMovieList.get(i).getCollection().toLowerCase(Locale.ENGLISH) : mMovieList.get(i).getTitle().toLowerCase(Locale.ENGLISH); boolean foundInTitle = false; if (lowerCaseTitle.contains(mSearchQuery) || p.matcher(lowerCaseTitle).replaceAll("").indexOf(mSearchQuery) != -1) { tempCollection.add(mMovieList.get(i)); foundInTitle = true; } if (!foundInTitle) { for (Filepath path : mMovieList.get(i).getFilepaths()) { String filepath = path.getFilepath().toLowerCase(Locale.ENGLISH); if (filepath.indexOf(mSearchQuery) != -1) { tempCollection.add(mMovieList.get(i)); break; // Break the loop } } } } } // Clear the movie list mMovieList.clear(); // Add all the temporary ones after search completed mMovieList.addAll(tempCollection); // Clear the temporary list tempCollection.clear(); } // Sort Collections.sort(mMovieList, getSortType().getComparator()); return null; } @Override protected void onPostExecute(Void result) { if (!isCancelled()) { mResults = new ArrayList<>(mMovieList); mCallback.onLoadCompleted(); } else mMovieList.clear(); } } /** * Show genres filter dialog. * @param activity */ public void showGenresFilterDialog(Activity activity) { final TreeMap<String, Integer> map = new TreeMap<String, Integer>(); String[] splitGenres; for (int i = 0; i < mResults.size(); i++) { if (!mResults.get(i).getGenres().isEmpty()) { splitGenres = mResults.get(i).getGenres().split(","); for (int j = 0; j < splitGenres.length; j++) { if (map.containsKey(splitGenres[j].trim())) { map.put(splitGenres[j].trim(), map.get(splitGenres[j].trim()) + 1); } else { map.put(splitGenres[j].trim(), 1); } } } } createAndShowAlertDialog(activity, setupItemArray(map), R.string.selectGenre, MovieFilter.GENRE); } /** * Show certifications filter dialog. * @param activity */ public void showCertificationsFilterDialog(Activity activity) { final TreeMap<String, Integer> map = new TreeMap<String, Integer>(); for (int i = 0; i < mResults.size(); i++) { String certification = mResults.get(i).getCertification(); if (!TextUtils.isEmpty(certification)) { if (map.containsKey(certification.trim())) { map.put(certification.trim(), map.get(certification.trim()) + 1); } else { map.put(certification.trim(), 1); } } } createAndShowAlertDialog(activity, setupItemArray(map), R.string.selectCertification, MovieFilter.CERTIFICATION); } /** * Show release year filter dialog. * @param activity */ public void showReleaseYearFilterDialog(Activity activity) { final TreeMap<String, Integer> map = new TreeMap<String, Integer>(); for (int i = 0; i < mResults.size(); i++) { String year = mResults.get(i).getReleaseYear().trim(); if (!TextUtils.isEmpty(year)) { if (map.containsKey(year)) { map.put(year, map.get(year) + 1); } else { map.put(year, 1); } } } createAndShowAlertDialog(activity, setupItemArray(map), R.string.selectReleaseYear, MovieFilter.RELEASE_YEAR); } /** * Show file sources filter dialog. * @param activity */ public void showFileSourcesFilterDialog(Activity activity) { final TreeMap<String, Integer> map = new TreeMap<String, Integer>(); for (int i = 0; i < mResults.size(); i++) { for (Filepath path : mResults.get(i).getFilepaths()) { String type = path.getTypeAsString(activity); if (map.containsKey(type)) { map.put(type, map.get(type) + 1); } else { map.put(type, 1); } } } createAndShowAlertDialog(activity, setupItemArray(map), R.string.selectFileSource, MovieFilter.FILE_SOURCE); } /** * Show folders filter dialog. * @param activity */ public void showFoldersFilterDialog(Activity activity) { final TreeMap<String, Integer> map = new TreeMap<String, Integer>(); for (int i = 0; i < mResults.size(); i++) { for (Filepath path : mResults.get(i).getFilepaths()) { String folder = path.getFolder(); if (!TextUtils.isEmpty(folder)) { if (map.containsKey(folder)) { map.put(folder, map.get(folder) + 1); } else { map.put(folder, 1); } } } } createAndShowAlertDialog(activity, setupItemArray(map), R.string.selectFolder, MovieFilter.FOLDER); } /** * Used to set up an array of items for the alert dialog. * @param map * @return List of dialog options. */ private CharSequence[] setupItemArray(TreeMap<String, Integer> map) { final CharSequence[] tempArray = map.keySet().toArray(new CharSequence[map.keySet().size()]); for (int i = 0; i < tempArray.length; i++) tempArray[i] = tempArray[i] + " (" + map.get(tempArray[i]) + ")"; return tempArray; } /** * Shows an alert dialog and handles the user selection. * @param activity * @param temp * @param title * @param type */ private void createAndShowAlertDialog(Activity activity, final CharSequence[] temp, int title, final int type) { AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setTitle(title) .setItems(temp, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { // Let's get what the user selected and remove the parenthesis at the end String selected = temp[which].toString(); selected = selected.substring(0, selected.lastIndexOf("(")).trim(); // Add filter MovieFilter filter = new MovieFilter(type); filter.setFilter(selected); addFilter(filter); // Re-load the library with the new filter load(); // Dismiss the dialog dialog.dismiss(); } }); builder.show(); } /** * Sets the sort type depending on the movie * library type. The collections library will * always be sorted by collection title, the * "New releases" library will be sorted by * release date and the "All movies" library * will be sorted by the user's preference, if * such exists. If not, it'll sort by movie title * like the other library types do by default. */ private void setupSortType() { if (getType() == MovieLibraryType.ALL_MOVIES) { // Load the saved sort type and set it String savedSortType = PreferenceManager.getDefaultSharedPreferences(mContext).getString(PreferenceKeys.SORTING_MOVIES, SORT_TITLE); switch (savedSortType) { case SORT_TITLE: setSortType(MovieSortType.TITLE); break; case SORT_RELEASE: setSortType(MovieSortType.RELEASE); break; case SORT_RATING: setSortType(MovieSortType.RATING); break; case SORT_WEIGHTED_RATING: setSortType(MovieSortType.WEIGHTED_RATING); break; case SORT_DATE_ADDED: setSortType(MovieSortType.DATE_ADDED); break; case SORT_DURATION: setSortType(MovieSortType.DURATION); break; } } else if (getType() == MovieLibraryType.COLLECTIONS) { setSortType(MovieSortType.COLLECTION_TITLE); } else if (getType() == MovieLibraryType.NEW_RELEASES) { setSortType(MovieSortType.RELEASE); } else { setSortType(MovieSortType.TITLE); } } }