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());