package de.geeksfactory.opacclient.frontend; import android.app.Activity; import android.content.DialogInterface; import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.app.CustomListFragment; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.animation.AnimationUtils; import android.widget.AdapterView; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import org.json.JSONException; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; import de.geeksfactory.opacclient.OpacClient; import de.geeksfactory.opacclient.R; import de.geeksfactory.opacclient.apis.OpacApi; import de.geeksfactory.opacclient.apis.OpacApi.OpacErrorException; import de.geeksfactory.opacclient.frontend.ResultsAdapterEndless.OnLoadMoreListener; import de.geeksfactory.opacclient.networking.NotReachableException; import de.geeksfactory.opacclient.networking.SSLSecurityException; import de.geeksfactory.opacclient.objects.Account; import de.geeksfactory.opacclient.objects.SearchRequestResult; import de.geeksfactory.opacclient.objects.SearchResult; import de.geeksfactory.opacclient.searchfields.SearchField; import de.geeksfactory.opacclient.searchfields.SearchField.Meaning; import de.geeksfactory.opacclient.searchfields.SearchQuery; import de.geeksfactory.opacclient.storage.AccountDataSource; import de.geeksfactory.opacclient.storage.JsonSearchFieldDataSource; import de.geeksfactory.opacclient.storage.SearchFieldDataSource; import de.geeksfactory.opacclient.utils.ErrorReporter; /** * A list fragment representing a list of SearchResults. This fragment also supports tablet devices * by allowing list items to be given an 'activated' state upon selection. This helps indicate which * item is currently being viewed in a {@link SearchResultDetailFragment}. * <p/> * Activities containing this fragment MUST implement the {@link Callbacks} interface. */ public class SearchResultListFragment extends CustomListFragment { /** * The serialization (saved instance state) Bundle key representing the activated item position. * Only used on tablets. */ protected static final String STATE_ACTIVATED_POSITION = "activated_position"; protected static final String ARG_QUERY = "query"; protected static final String ARG_VOLUME_QUERY = "volumeQuery"; protected static final String ARG_GOOGLE_QUERY = "googleQuery"; /** * A dummy implementation of the {@link Callbacks} interface that does nothing. Used only when * this fragment is not attached to an activity. */ private static Callbacks dummyCallbacks = new Callbacks() { @Override public void onItemSelected(SearchResult result, View coverView, int touchX, int touchY) { } public boolean isTwoPane() { return false; } }; /** * The fragment's current callback object, which is notified of list item clicks. */ protected Callbacks callbacks = dummyCallbacks; public ResultsAdapterEndless adapter; /** * The current activated item position. Only used on tablets. */ protected int activatedPosition = ListView.INVALID_POSITION; protected SearchRequestResult searchresult; protected OpacClient app; protected int lastLoadedPage; protected SearchStartTask st; protected LinearLayout progressContainer; protected FrameLayout errorView; private int touchPositionX = 0; private int touchPositionY = 0; /** * Mandatory empty constructor for the fragment manager to instantiate the fragment (e.g. upon * screen orientation changes). */ public SearchResultListFragment() { } public static SearchResultListFragment getInstance(Bundle query) { SearchResultListFragment frag = new SearchResultListFragment(); Bundle args = new Bundle(); args.putBundle(ARG_QUERY, query); frag.setArguments(args); return frag; } public static SearchResultListFragment getVolumeSearchInstance(Bundle query) { SearchResultListFragment frag = new SearchResultListFragment(); Bundle args = new Bundle(); args.putBundle(ARG_VOLUME_QUERY, query); frag.setArguments(args); return frag; } public static SearchResultListFragment getGoogleSearchInstance(String query) { SearchResultListFragment frag = new SearchResultListFragment(); Bundle args = new Bundle(); args.putString(ARG_GOOGLE_QUERY, query); frag.setArguments(args); return frag; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceSate) { setRetainInstance(true); setHasOptionsMenu(true); View view = inflater.inflate(R.layout.fragment_searchresult_list, container, false); progressContainer = (LinearLayout) view .findViewById(R.id.progressContainer); errorView = (FrameLayout) view.findViewById( R.id.error_view); setupIds(view); return view; } public void performsearch() { if (getArguments().containsKey(ARG_GOOGLE_QUERY)) { String query = getArguments().getString(ARG_GOOGLE_QUERY); performGoogleSearch(query); } else { if (getArguments().containsKey(ARG_VOLUME_QUERY)) { st = new SearchStartTask(OpacClient.bundleToMap(getArguments().getBundle( ARG_VOLUME_QUERY))); } else { st = new SearchStartTask(OpacClient.bundleToQuery(getArguments().getBundle( ARG_QUERY))); } st.execute(); } } private void performGoogleSearch(final String query) { AccountDataSource data = new AccountDataSource(getActivity()); final List<Account> accounts = data.getAllAccounts(); if (accounts.size() == 0) { Toast.makeText(getActivity(), R.string.welcome_select, Toast.LENGTH_LONG).show(); } else if (accounts.size() == 1) { startGoogleSearch(accounts.get(0), query); } else { new AlertDialog.Builder(getActivity()) .setTitle(R.string.account_select) .setAdapter( new AccountListAdapter(getActivity(), accounts) .setHighlightActiveAccount(false), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { startGoogleSearch(accounts.get(which), query); } }).create().show(); } } private void startGoogleSearch(Account account, String query) { app.setAccount(account.getId()); new GoogleSearchTask().execute(query); } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); setActivateOnItemClick(callbacks.isTwoPane()); // Restore the previously serialized activated item position. if (savedInstanceState != null && savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) { setActivatedPosition(savedInstanceState .getInt(STATE_ACTIVATED_POSITION)); } if (savedInstanceState == null && searchresult == null) { performsearch(); } else if (searchresult != null) { if (searchresult.getTotal_result_count() >= 0) { ((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle( getResources().getQuantityString(R.plurals.result_number, searchresult.getTotal_result_count(), searchresult.getTotal_result_count())); } } getListView().setOnTouchListener(new View.OnTouchListener() { public boolean onTouch(View view, MotionEvent event) { touchPositionX = (int) event.getX(); touchPositionY = (int) event.getY(); return false; } }); } @Override public void onAttach(Activity activity) { super.onAttach(activity); // Activities containing this fragment must implement its callbacks. if (!(activity instanceof Callbacks)) { throw new IllegalStateException( "Activity must implement fragment's callbacks."); } callbacks = (Callbacks) activity; app = (OpacClient) activity.getApplication(); } @Override public void onDetach() { super.onDetach(); // Reset the active callbacks interface to the dummy implementation. callbacks = dummyCallbacks; } @Override public void onListItemClick(ListView listView, View view, int position, long id) { super.onListItemClick(listView, view, position, id); setActivatedPosition(position); // Notify the active callbacks interface (the activity, if the // fragment is attached to one) that an item has been selected. callbacks.onItemSelected(searchresult.getResults().get(position), view.findViewById(R.id.ivType), touchPositionX, touchPositionY); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (activatedPosition != AdapterView.INVALID_POSITION) { // Serialize and persist the activated item position. outState.putInt(STATE_ACTIVATED_POSITION, activatedPosition); } } /** * Turns on activate-on-click mode. When this mode is on, list items will be given the * 'activated' state when touched. */ public void setActivateOnItemClick(boolean activateOnItemClick) { // When setting CHOICE_MODE_SINGLE, ListView will automatically // give items the 'activated' state when touched. getListView().setChoiceMode( activateOnItemClick ? ListView.CHOICE_MODE_SINGLE : ListView.CHOICE_MODE_NONE); } private void setActivatedPosition(int position) { if (position == AdapterView.INVALID_POSITION) { getListView().setItemChecked(activatedPosition, false); } else { getListView().setItemChecked(position, true); } activatedPosition = position; } public void setSearchResult(SearchRequestResult searchresult) { for (SearchResult result : searchresult.getResults()) { result.setPage(searchresult.getPage_index()); } if (searchresult.getTotal_result_count() >= 0) { ((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle( getResources().getQuantityString(R.plurals.result_number, searchresult.getTotal_result_count(), searchresult.getTotal_result_count())); } if (searchresult.getResults().size() == 0 && searchresult.getTotal_result_count() == 0) { setEmptyText(getString(R.string.no_results)); } this.searchresult = searchresult; OpacApi api = null; try { api = app.getApi(); } catch (OpacClient.LibraryRemovedException ignored) { } adapter = new ResultsAdapterEndless(getActivity(), searchresult, new OnLoadMoreListener() { @Override public SearchRequestResult onLoadMore(int page) throws Exception { SearchRequestResult res = app.getApi().searchGetPage( page); setLastLoadedPage(page); return res; } @Override public void onError(Exception e) { if (getActivity() != null) { if (e instanceof OpacErrorException) { showConnectivityError(e.getMessage()); } else if (e instanceof SSLSecurityException) { showConnectivityError(getResources().getString( R.string.connection_error_detail_security)); } else if (e instanceof NotReachableException) { showConnectivityError(getResources().getString( R.string.connection_error_detail_nre)); } else { e.printStackTrace(); showConnectivityError(); } } } @Override public void updateResultCount(int resultCount) { /* * When IOpac finds more than 200 results, the real * result count is not known until the second page is * loaded */ if (resultCount >= 0 && getActivity() != null) { ((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle( getResources().getQuantityString(R.plurals.result_number, resultCount, resultCount)); } } }, api); setListAdapter(adapter); getListView().setTextFilterEnabled(true); setListShown(true); } public void showConnectivityError() { showConnectivityError(null); } public void showConnectivityError(String description) { if (getView() == null || getActivity() == null) { return; } errorView.removeAllViews(); View connError = getActivity().getLayoutInflater().inflate( R.layout.error_connectivity, errorView); connError.findViewById(R.id.btRetry) .setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { errorView.removeAllViews(); setListShown(false); progressContainer.setVisibility(View.VISIBLE); performsearch(); } }); if (description != null) { ((TextView) connError.findViewById(R.id.tvErrBody)) .setText(description); } setListShown(false); progressContainer.startAnimation(AnimationUtils.loadAnimation( getActivity(), android.R.anim.fade_out)); connError.startAnimation(AnimationUtils.loadAnimation(getActivity(), android.R.anim.fade_in)); progressContainer.setVisibility(View.GONE); connError.setVisibility(View.VISIBLE); } public int getLastLoadedPage() { return lastLoadedPage; } public void setLastLoadedPage(int lastLoadedPage) { this.lastLoadedPage = lastLoadedPage; } public void loaded(SearchRequestResult searchresult) { try { if (searchresult.getPage_index() == 0 && searchresult.getTotal_result_count() > 0 && searchresult.getResults().size() == 0) { showConnectivityError(getResources().getString(R.string.connection_error_detail)); } setListShown(true); setSearchResult(searchresult); } catch (IllegalStateException e) { e.printStackTrace(); } } /** * A callback interface that all activities containing this fragment must implement. This * mechanism allows activities to be notified of item selections. */ public interface Callbacks { /** * Callback for when an item has been selected. */ public void onItemSelected(SearchResult result, View coverView, int touchX, int touchY); public boolean isTwoPane(); } public class SearchStartTask extends AsyncTask<Void, Void, SearchRequestResult> { protected Exception exception; protected Map<String, String> volumeQuery = null; protected List<SearchQuery> query = null; public SearchStartTask(Map<String, String> volumeQuery) { this.volumeQuery = volumeQuery; } public SearchStartTask(List<SearchQuery> query) { this.query = query; } @Override protected SearchRequestResult doInBackground(Void... voids) { OpacApi api; try { api = app.getApi(); } catch (OpacClient.LibraryRemovedException e) { exception = e; return null; } if (volumeQuery != null) { try { return api.volumeSearch(volumeQuery); } catch (IOException | OpacErrorException e) { exception = e; e.printStackTrace(); } catch (Exception e) { exception = e; ErrorReporter.handleException(e); } } else if (query != null) { try { // Load cover images, if search worked and covers available return api.search(query); } catch (IOException | OpacErrorException e) { exception = e; e.printStackTrace(); } catch (Exception e) { exception = e; ErrorReporter.handleException(e); } } return null; } @Override protected void onPostExecute(SearchRequestResult result) { if (result == null) { if (exception instanceof OpacErrorException) { showConnectivityError(exception.getMessage()); } else if (exception instanceof OpacClient.LibraryRemovedException) { if (getActivity() != null) { showConnectivityError(getResources().getString( R.string.library_removed_error)); } } else if (exception instanceof SSLSecurityException) { if (getActivity() != null) { showConnectivityError(getResources().getString( R.string.connection_error_detail_security)); } } else if (exception instanceof NotReachableException) { if (getActivity() != null) { showConnectivityError(getResources().getString( R.string.connection_error_detail_nre)); } } else { showConnectivityError(); } } else { loaded(result); } } } public class GoogleSearchTask extends AsyncTask<String, Void, List<SearchField>> { protected Exception exception; protected String queryString; @Override protected List<SearchField> doInBackground(String... arg0) { queryString = arg0[0]; SearchFieldDataSource dataSource = new JsonSearchFieldDataSource( app); if (dataSource.hasSearchFields(app.getLibrary().getIdent())) { return dataSource.getSearchFields(app.getLibrary().getIdent()); } else { try { List<SearchField> fields = app.getApi().getSearchFields(); if (getActivity() == null) { return null; } if (fields.size() == 0) { throw new OpacErrorException( getString(R.string.no_fields_found)); } return fields; } catch (JSONException | IOException | OpacErrorException | OpacClient .LibraryRemovedException e) { exception = e; e.printStackTrace(); } return null; } } protected void onPostExecute(List<SearchField> result) { if (getActivity() == null) { return; } if (exception != null) { if (exception instanceof OpacErrorException) { showConnectivityError(exception.getMessage()); } else { showConnectivityError(); } return; } SearchField fieldToUse = findSearchFieldByMeaning(result, Meaning.FREE); if (fieldToUse == null) { fieldToUse = findSearchFieldByMeaning(result, Meaning.TITLE); } if (fieldToUse == null) { showConnectivityError(getString(R.string.no_fields_found)); return; } List<SearchQuery> query = new ArrayList<>(); query.add(new SearchQuery(fieldToUse, queryString)); st = new SearchStartTask(query); st.execute(); } private SearchField findSearchFieldByMeaning(List<SearchField> fields, Meaning meaning) { for (SearchField field : fields) { if (field.getMeaning() == meaning) { return field; } } return null; } } }