Kore/app/src/main/java/org/xbmc/kore/ui/AbstractCursorListFragment....

416 lines
12 KiB
Java

/*
* Copyright 2015 Martijn Brekhof. All rights reserved.
*
* 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 org.xbmc.kore.ui;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Intent;
import android.content.ServiceConnection;
import android.database.Cursor;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SearchView;
import androidx.core.view.MenuItemCompat;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.CursorLoader;
import androidx.loader.content.Loader;
import androidx.recyclerview.widget.RecyclerView;
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 android.widget.Toast;
import org.xbmc.kore.R;
import org.xbmc.kore.host.HostInfo;
import org.xbmc.kore.host.HostManager;
import org.xbmc.kore.jsonrpc.ApiException;
import org.xbmc.kore.jsonrpc.event.MediaSyncEvent;
import org.xbmc.kore.service.library.LibrarySyncService;
import org.xbmc.kore.service.library.SyncItem;
import org.xbmc.kore.service.library.SyncUtils;
import org.xbmc.kore.ui.viewgroups.RecyclerViewEmptyViewSupport;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.UIUtils;
import de.greenrobot.event.EventBus;
public abstract class AbstractCursorListFragment extends AbstractListFragment
implements LoaderManager.LoaderCallbacks<Cursor>,
SyncUtils.OnServiceListener,
SearchView.OnQueryTextListener {
private static final String TAG = LogUtils.makeLogTag(AbstractCursorListFragment.class);
private final String BUNDLE_KEY_SEARCH_QUERY = "search_query";
private ServiceConnection serviceConnection;
private EventBus bus;
// Loader IDs
private static final int LOADER = 0;
// The search filter to use in the loader
private String searchFilter = null;
private boolean loaderLoading;
private String savedSearchFilter;
private boolean supportsSearch;
private SearchView searchView;
private boolean isPaused;
abstract protected void onListItemClicked(View view);
abstract protected CursorLoader createCursorLoader();
abstract protected RecyclerViewCursorAdapter createCursorAdapter();
@TargetApi(16)
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View root = super.onCreateView(inflater, container, savedInstanceState);
bus = EventBus.getDefault();
if (savedInstanceState != null) {
savedSearchFilter = savedInstanceState.getString(BUNDLE_KEY_SEARCH_QUERY);
}
searchFilter = savedSearchFilter;
return root;
}
@Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getLoaderManager().initLoader(LOADER, null, this);
}
@Override
public void onStart() {
super.onStart();
serviceConnection = SyncUtils.connectToLibrarySyncService(getActivity(), this);
}
@Override
public void onResume() {
bus.register(this);
super.onResume();
isPaused = false;
}
@Override
public void onPause() {
bus.unregister(this);
super.onPause();
isPaused = true;
}
@Override
public void onStop() {
super.onStop();
SyncUtils.disconnectFromLibrarySyncService(getActivity(), serviceConnection);
}
@Override
public void onSaveInstanceState(Bundle outState) {
if (!TextUtils.isEmpty(searchFilter)) {
savedSearchFilter = searchFilter;
}
outState.putString(BUNDLE_KEY_SEARCH_QUERY, savedSearchFilter);
super.onSaveInstanceState(outState);
}
@Override
protected RecyclerViewEmptyViewSupport.OnItemClickListener createOnItemClickListener() {
return new RecyclerViewEmptyViewSupport.OnItemClickListener() {
@Override
public void onItemClick(View view, int position) {
saveSearchState();
onListItemClicked(view);
}
};
}
@Override
final protected RecyclerView.Adapter createAdapter() {
return createCursorAdapter();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.abstractcursorlistfragment, menu);
if (supportsSearch) {
setupSearchMenuItem(menu, inflater);
}
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch(item.getItemId()) {
case R.id.action_refresh:
onRefresh();
break;
}
return super.onOptionsItemSelected(item);
}
/**
* Should return the {@link LibrarySyncService} SyncType that
* this list initiates
* @return {@link LibrarySyncService} SyncType
*/
abstract protected String getListSyncType();
/**
* Should return the {@link LibrarySyncService} syncID if this fragment
* synchronizes a single item. The itemId that should be synced must returned by {@link #getSyncItemID()}
* @return {@link LibrarySyncService} SyncID if syncing a single item. Null if not aplicable
*/
protected String getSyncID() {
return null;
}
/**
* Should return the item ID for SyncID returned by {@link #getSyncID()}
* @return -1 if not used.
*/
protected int getSyncItemID() {
return -1;
}
/**
* Event bus post. Called when the syncing process ended
*
* @param event Refreshes data
*/
public void onEventMainThread(MediaSyncEvent event) {
onSyncProcessEnded(event);
}
/**
* Called each time a MediaSyncEvent is received.
* @param event
*/
protected void onSyncProcessEnded(MediaSyncEvent event) {
boolean silentSync = false;
if (event.syncExtras != null) {
silentSync = event.syncExtras.getBoolean(LibrarySyncService.SILENT_SYNC, false);
}
if (event.syncType.equals(getListSyncType())) {
hideRefreshAnimation();
if (event.status == MediaSyncEvent.STATUS_SUCCESS) {
refreshList();
if (!silentSync) {
Toast.makeText(getActivity(), R.string.sync_successful, Toast.LENGTH_SHORT)
.show();
}
} else if (!silentSync) {
String msg = (event.errorCode == ApiException.API_ERROR) ?
String.format(getString(R.string.error_while_syncing), event.errorMessage) :
getString(R.string.unable_to_connect_to_xbmc);
Toast.makeText(getActivity(), msg, Toast.LENGTH_SHORT).show();
}
}
}
@Override
public void onServiceConnected(LibrarySyncService librarySyncService) {
HostInfo hostInfo = HostManager.getInstance(getActivity()).getHostInfo();
SyncItem syncItem = SyncUtils.getCurrentSyncItem(librarySyncService, hostInfo, getListSyncType());
if (syncItem != null) {
boolean silentRefresh = (syncItem.getSyncExtras() != null) &&
syncItem.getSyncExtras().getBoolean(LibrarySyncService.SILENT_SYNC, false);
if (!silentRefresh)
UIUtils.showRefreshAnimation(swipeRefreshLayout);
}
}
/**
* Use this to indicate your fragment supports search queries.
* Get the entered search query using {@link #getSearchFilter()}
* <br/>
* 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;
}
@Override
public void onRefresh() {
UIUtils.showRefreshAnimation(swipeRefreshLayout);
Intent syncIntent = new Intent(this.getActivity(), LibrarySyncService.class);
syncIntent.putExtra(getListSyncType(), true);
String syncID = getSyncID();
int itemId = getSyncItemID();
if ((syncID != null) && (itemId != -1)) {
syncIntent.putExtra(syncID, itemId);
}
getActivity().startService(syncIntent);
}
/**
* 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. On return the fragment
* is recreated, which will cause the cursor adapter to be recreated. Although
* the search view will have the query restored to its saved state, the CursorAdapter
* will use the empty search filter. This is due to the fact that we don't restart the
* loader when it is still loading after its been created.
*/
if (isPaused)
return true;
searchFilter = newText;
restartLoader();
return true;
}
/** {@inheritDoc} */
@Override
public boolean onQueryTextSubmit(String newText) {
// All is handled in onQueryTextChange
return true;
}
/**
* Loader callbacks
*/
/** {@inheritDoc} */
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
loaderLoading = true;
return createCursorLoader();
}
/** {@inheritDoc} */
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
((RecyclerViewCursorAdapter) getAdapter()).swapCursor(cursor);
if (TextUtils.isEmpty(searchFilter)) {
// To prevent the empty text from appearing on the first load, set it now
emptyView.setText(getString(R.string.swipe_down_to_refresh));
}
loaderLoading = false;
}
/** {@inheritDoc} */
@Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
((RecyclerViewCursorAdapter) getAdapter()).swapCursor(null);
}
/**
* Save the search state of the list fragment
*/
public void saveSearchState() {
savedSearchFilter = searchFilter;
}
/**
* @return text entered in searchview
*/
public String getSearchFilter() {
return searchFilter;
}
/**
* Use this to reload the items in the list
*/
public void refreshList() {
restartLoader();
}
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;
restartLoader();
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 = "";
restartLoader();
}
});
}
}
private void restartLoader() {
//When loader is restarted while current loader hasn't finished yet,
//it may result in none of the loaders finishing.
if(!loaderLoading) {
getLoaderManager().restartLoader(LOADER, null, this);
}
}
}