diff --git a/app/src/main/java/org/xbmc/kore/ui/AbstractSearchableFragment.java b/app/src/main/java/org/xbmc/kore/ui/AbstractSearchableFragment.java new file mode 100644 index 0000000..730eb11 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/ui/AbstractSearchableFragment.java @@ -0,0 +1,212 @@ +package org.xbmc.kore.ui; + +import android.app.Activity; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.widget.SearchView; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; + +import org.xbmc.kore.R; +import org.xbmc.kore.jsonrpc.event.MediaSyncEvent; + +public abstract class AbstractSearchableFragment extends Fragment implements SearchView.OnQueryTextListener { + + private String searchFilter = null; + private String savedSearchFilter; + private boolean supportsSearch; + private boolean isPaused; + + private SearchView searchView; + + private final String BUNDLE_KEY_SEARCH_QUERY = "search_query"; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View root = super.onCreateView(inflater, container, savedInstanceState); + + if (savedInstanceState != null) { + savedSearchFilter = savedInstanceState.getString(BUNDLE_KEY_SEARCH_QUERY); + } + searchFilter = savedSearchFilter; + + return root; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.abstractcursorlistfragment, menu); + + if (supportsSearch) { + setupSearchMenuItem(menu, inflater); + } + + super.onCreateOptionsMenu(menu, inflater); + } + + /** + * Use this to indicate your fragment supports search queries. + * Get the entered search query using {@link #getSearchFilter()} + *
+ * Note: make sure this is set before {@link #onCreateOptionsMenu(Menu, MenuInflater)} is called. + * For instance in {@link #onAttach(Activity)} + * + * @param supportsSearch true if you support search queries, false otherwise + */ + public void setSupportsSearch(boolean supportsSearch) { + this.supportsSearch = supportsSearch; + } + + /** + * Save the search state of the list fragment + */ + public void saveSearchState() { + savedSearchFilter = searchFilter; + } + + /** + * @return text entered in searchview + */ + public String getSearchFilter() { + return searchFilter; + } + + private void setupSearchMenuItem(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.media_search, menu); + MenuItem searchMenuItem = menu.findItem(R.id.action_search); + if (searchMenuItem != null) { + searchView = (SearchView) MenuItemCompat.getActionView(searchMenuItem); + searchView.setOnQueryTextListener(this); + searchView.setQueryHint(getString(R.string.action_search)); + if (!TextUtils.isEmpty(savedSearchFilter)) { + searchMenuItem.expandActionView(); + searchView.setQuery(savedSearchFilter, false); + //noinspection RestrictedApi + searchView.clearFocus(); + } + + MenuItemCompat.setOnActionExpandListener(searchMenuItem, new MenuItemCompat.OnActionExpandListener() { + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + return true; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + searchFilter = savedSearchFilter = null; + refreshList(); + return true; + } + }); + } + + //Handle clearing search query using the close button (X button). + View view = searchView.findViewById(R.id.search_close_btn); + if (view != null) { + view.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + EditText editText = (EditText) searchView.findViewById(R.id.search_src_text); + editText.setText(""); + searchView.setQuery("", false); + searchFilter = savedSearchFilter = ""; + refreshList(); + } + }); + } + } + + + /** + * Search view callbacks + */ + /** {@inheritDoc} */ + @Override + public boolean onQueryTextChange(String newText) { + if ((!searchView.hasFocus()) && TextUtils.isEmpty(newText)) { + //onQueryTextChange called as a result of manually expanding the searchView in setupSearchMenuItem(...) + return true; + } + + /** + * When this fragment is paused, onQueryTextChange is called with an empty string. + * This causes problems restoring the list fragment when returning. + */ + if (isPaused) + return true; + + searchFilter = newText; + + refreshList(); + + return true; + } + + /** {@inheritDoc} */ + @Override + public boolean onQueryTextSubmit(String newText) { + // All is handled in onQueryTextChange + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch(item.getItemId()) { + case R.id.action_refresh: + refreshList(); + break; + } + return super.onOptionsItemSelected(item); + } + + + + @Override + public void onResume() { + super.onResume(); + isPaused = false; + } + + @Override + public void onPause() { + super.onPause(); + isPaused = true; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + if (!TextUtils.isEmpty(searchFilter)) { + savedSearchFilter = searchFilter; + } + outState.putString(BUNDLE_KEY_SEARCH_QUERY, savedSearchFilter); + super.onSaveInstanceState(outState); + } + + /** + * Event bus post. Called when the syncing process ended + * + * @param event Refreshes data + */ + public void onEventMainThread(MediaSyncEvent event) { + onSyncProcessEnded(event); + } + + protected void onSyncProcessEnded(MediaSyncEvent event) { + + } + + /** + * Use this to reload the items in the list + */ + protected abstract void refreshList(); +} \ No newline at end of file diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/video/PVRChannelEPGListFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/video/PVRChannelEPGListFragment.java index 6d8e138..2f807c0 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/video/PVRChannelEPGListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/video/PVRChannelEPGListFragment.java @@ -20,6 +20,7 @@ import android.os.Bundle; import android.os.Handler; import android.support.v4.app.Fragment; import android.support.v4.widget.SwipeRefreshLayout; +import android.text.TextUtils; import android.text.format.DateUtils; import android.view.LayoutInflater; import android.view.View; @@ -34,6 +35,7 @@ import org.xbmc.kore.host.HostManager; import org.xbmc.kore.jsonrpc.ApiCallback; import org.xbmc.kore.jsonrpc.method.PVR; import org.xbmc.kore.jsonrpc.type.PVRType; +import org.xbmc.kore.ui.AbstractSearchableFragment; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.UIUtils; @@ -49,7 +51,7 @@ import butterknife.Unbinder; /** * Fragment that presents the Guide for a channel */ -public class PVRChannelEPGListFragment extends Fragment +public class PVRChannelEPGListFragment extends AbstractSearchableFragment implements SwipeRefreshLayout.OnRefreshListener { private static final String TAG = LogUtils.makeLogTag(PVRChannelEPGListFragment.class); @@ -113,13 +115,16 @@ public class PVRChannelEPGListFragment extends Fragment }); listView.setEmptyView(emptyView); + super.onCreateView(inflater, container, savedInstanceState); + return root; } @Override public void onActivityCreated (Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - setHasOptionsMenu(false); + setHasOptionsMenu(true); + setSupportsSearch(true); browseEPG(); } @@ -144,6 +149,11 @@ public class PVRChannelEPGListFragment extends Fragment } } + @Override + protected void refreshList() { + onRefresh(); + } + /** * Get the EPF for the channel and setup the listview */ @@ -155,7 +165,10 @@ public class PVRChannelEPGListFragment extends Fragment if (!isAdded()) return; // To prevent the empty text from appearing on the first load, set it now emptyView.setText(getString(R.string.no_broadcasts_found_refresh)); - setupEPGListview(result); + + List finalResult = filter(result); + + setupEPGListview(finalResult); swipeRefreshLayout.setRefreshing(false); } @@ -173,6 +186,47 @@ public class PVRChannelEPGListFragment extends Fragment }, callbackHandler); } + + private List filter(List itemList) { + String searchFilter = getSearchFilter(); + + if (TextUtils.isEmpty(searchFilter)) { + return itemList; + } + + // Split searchFilter to multiple lowercase words + String[] lcWords = searchFilter.toLowerCase().split(" ");; + + List result = new ArrayList<>(itemList.size()); + for (PVRType.DetailsBroadcast item:itemList) { + // Require all words to match the item: + boolean allWordsMatch = true; + for (String lcWord:lcWords) { + if (!searchFilterWordMatches(lcWord, item)) { + allWordsMatch = false; + break; + } + } + if (!allWordsMatch) { + continue; // skip this item + } + + result.add(item); + } + + return result; + } + + public boolean searchFilterWordMatches(String lcWord, PVRType.DetailsBroadcast item) { + if (item.title.toLowerCase().contains(lcWord)) { + return true; + } + if (item.plot.toLowerCase().contains(lcWord)){ + return true; + } + return false; + } + /** * Called when we get the Guide * diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/video/PVRChannelsListFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/video/PVRChannelsListFragment.java index dcb5804..e2cf71e 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/video/PVRChannelsListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/video/PVRChannelsListFragment.java @@ -20,8 +20,8 @@ import android.content.Context; import android.content.res.Resources; import android.os.Bundle; import android.os.Handler; -import android.support.v4.app.Fragment; import android.support.v4.widget.SwipeRefreshLayout; +import android.text.TextUtils; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; @@ -41,10 +41,12 @@ import org.xbmc.kore.jsonrpc.ApiException; import org.xbmc.kore.jsonrpc.method.PVR; import org.xbmc.kore.jsonrpc.method.Player; import org.xbmc.kore.jsonrpc.type.PVRType; +import org.xbmc.kore.ui.AbstractSearchableFragment; import org.xbmc.kore.ui.OnBackPressedListener; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.UIUtils; +import java.util.ArrayList; import java.util.List; import butterknife.BindView; @@ -54,7 +56,7 @@ import butterknife.Unbinder; /** * Fragment that presents the movie list */ -public class PVRChannelsListFragment extends Fragment +public class PVRChannelsListFragment extends AbstractSearchableFragment implements SwipeRefreshLayout.OnRefreshListener, OnBackPressedListener { private static final String TAG = LogUtils.makeLogTag(PVRChannelsListFragment.class); @@ -118,13 +120,16 @@ public class PVRChannelsListFragment extends Fragment }); gridView.setEmptyView(emptyView); + super.onCreateView(inflater, container, savedInstanceState); + return root; } @Override public void onActivityCreated (Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - setHasOptionsMenu(false); + setHasOptionsMenu(true); + setSupportsSearch(true); if (selectedChannelGroupId == -1) { if ((channelGroupAdapter == null) || @@ -174,6 +179,11 @@ public class PVRChannelsListFragment extends Fragment unbinder.unbind(); } + @Override + protected void refreshList() { + onRefresh(); + } + /** * Swipe refresh layout callback */ @@ -252,6 +262,46 @@ public class PVRChannelsListFragment extends Fragment }, callbackHandler); } + private List filter(List itemList) { + String searchFilter = getSearchFilter(); + + if (TextUtils.isEmpty(searchFilter)) { + return itemList; + } + + // Split searchFilter to multiple lowercase words + String[] lcWords = searchFilter.toLowerCase().split(" ");; + + List result = new ArrayList<>(itemList.size()); + for (PVRType.DetailsChannel item:itemList) { + // Require all words to match the item: + boolean allWordsMatch = true; + for (String lcWord:lcWords) { + if (!searchFilterWordMatches(lcWord, item)) { + allWordsMatch = false; + break; + } + } + if (!allWordsMatch) { + continue; // skip this item + } + + result.add(item); + } + + return result; + } + + public boolean searchFilterWordMatches(String lcWord, PVRType.DetailsChannel item) { + if (item.label.toLowerCase().contains(lcWord)) { + return true; + } + if (item.broadcastnow != null && item.broadcastnow.title.toLowerCase().contains(lcWord)){ + return true; + } + return false; + } + /** * Called when we get the channel groups * @@ -296,7 +346,10 @@ public class PVRChannelsListFragment extends Fragment // To prevent the empty text from appearing on the first load, set it now emptyView.setText(getString(R.string.no_channels_found_refresh)); - setupChannelsGridview(result); + + List finalResult = filter(result); + + setupChannelsGridview(finalResult); swipeRefreshLayout.setRefreshing(false); } diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/video/PVRRecordingsListFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/video/PVRRecordingsListFragment.java index c203077..35b5a2b 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/video/PVRRecordingsListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/video/PVRRecordingsListFragment.java @@ -21,8 +21,8 @@ import android.content.res.Resources; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; -import android.support.v4.app.Fragment; import android.support.v4.widget.SwipeRefreshLayout; +import android.text.TextUtils; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -43,6 +43,7 @@ import org.xbmc.kore.jsonrpc.ApiCallback; import org.xbmc.kore.jsonrpc.method.PVR; import org.xbmc.kore.jsonrpc.method.Player; import org.xbmc.kore.jsonrpc.type.PVRType; +import org.xbmc.kore.ui.AbstractSearchableFragment; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.UIUtils; @@ -58,7 +59,7 @@ import butterknife.Unbinder; /** * Fragment that presents the PVR recordings list */ -public class PVRRecordingsListFragment extends Fragment +public class PVRRecordingsListFragment extends AbstractSearchableFragment implements SwipeRefreshLayout.OnRefreshListener { private static final String TAG = LogUtils.makeLogTag(PVRRecordingsListFragment.class); @@ -85,6 +86,7 @@ public class PVRRecordingsListFragment extends Fragment @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_pvr_list, container, false); + unbinder = ButterKnife.bind(this, root); hostManager = HostManager.getInstance(getActivity()); @@ -99,6 +101,8 @@ public class PVRRecordingsListFragment extends Fragment }); gridView.setEmptyView(emptyView); + super.onCreateView(inflater, container, savedInstanceState); + return root; } @@ -106,6 +110,7 @@ public class PVRRecordingsListFragment extends Fragment public void onActivityCreated (Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); setHasOptionsMenu(true); + setSupportsSearch(true); browseRecordings(); } @@ -153,9 +158,7 @@ public class PVRRecordingsListFragment extends Fragment } - /** - * Use this to reload the items in the list - */ + @Override public void refreshList() { onRefresh(); } @@ -199,7 +202,6 @@ public class PVRRecordingsListFragment extends Fragment return super.onOptionsItemSelected(item); } - /** * Swipe refresh layout callback */ @@ -243,7 +245,12 @@ public class PVRRecordingsListFragment extends Fragment SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); boolean hideWatched = preferences.getBoolean(Settings.KEY_PREF_PVR_RECORDINGS_FILTER_HIDE_WATCHED, Settings.DEFAULT_PREF_PVR_RECORDINGS_FILTER_HIDE_WATCHED); - if (!hideWatched) { + String searchFilter = getSearchFilter(); + boolean hasSearchFilter = !TextUtils.isEmpty(searchFilter); + // Split searchFilter to multiple lowercase words + String[] lcWords = hasSearchFilter ? searchFilter.toLowerCase().split(" ") : null; + + if (!(hideWatched || hasSearchFilter)) { return itemList; } @@ -268,7 +275,19 @@ public class PVRRecordingsListFragment extends Fragment } } - // more conditions may be added here + if (hasSearchFilter) { + // Require all lowercase words to match the item: + boolean allWordsMatch = true; + for (String lcWord:lcWords) { + if (!searchFilterWordMatches(lcWord, item)) { + allWordsMatch = false; + break; + } + } + if (!allWordsMatch) { + continue; // skip this item + } + } result.add(item); } @@ -276,6 +295,14 @@ public class PVRRecordingsListFragment extends Fragment return result; } + private boolean searchFilterWordMatches(String lcWord, PVRType.DetailsRecording item) { + if (item.title.toLowerCase().contains(lcWord) + || item.channel.toLowerCase().contains(lcWord)) { + return true; + } + return false; + } + private void sort(List itemList) { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());