com.cuddlesoft.nori.SearchActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.cuddlesoft.nori.SearchActivity.java

Source

/*
 * This file is part of nori.
 * Copyright (c) 2014 vomitcuddle <shinku@dollbooru.org>
 * License: ISC
 */

package com.cuddlesoft.nori;

import android.app.SearchManager;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v4.view.MenuItemCompat;
import android.support.v4.widget.CursorAdapter;
import android.support.v7.app.ActionBar;
import android.support.v7.app.ActionBarActivity;
import android.text.TextUtils;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.BaseAdapter;
import android.widget.TextView;
import android.widget.Toast;

import com.cuddlesoft.nori.database.APISettingsDatabase;
import com.cuddlesoft.nori.database.SearchSuggestionDatabase;
import com.cuddlesoft.nori.fragment.SearchResultGridFragment;
import com.cuddlesoft.norilib.Image;
import com.cuddlesoft.norilib.SearchResult;
import com.cuddlesoft.norilib.Tag;
import com.cuddlesoft.norilib.clients.SearchClient;

import java.io.IOException;
import java.util.List;

import io.github.vomitcuddle.SearchViewAllowEmpty.SearchView;

/** Searches for images and displays the results in a scrollable grid of thumbnails. */
public class SearchActivity extends ActionBarActivity
        implements SearchResultGridFragment.OnSearchResultGridFragmentInteractionListener {
    /** Identifier used to send the active {@link com.cuddlesoft.norilib.SearchResult} to {@link com.cuddlesoft.nori.ImageViewerActivity}. */
    public static final String BUNDLE_ID_SEARCH_RESULT = "com.cuddlesoft.nori.SearchResult";
    /** Identifier used to send the position of the selected {@link com.cuddlesoft.norilib.Image} to {@link com.cuddlesoft.nori.ImageViewerActivity}. */
    public static final String BUNDLE_ID_IMAGE_INDEX = "com.cuddlesoft.nori.ImageIndex";
    /** Identifier used to send {@link com.cuddlesoft.norilib.clients.SearchClient} settings to {@link com.cuddlesoft.nori.ImageViewerActivity}. */
    public static final String BUNDLE_ID_SEARCH_CLIENT_SETTINGS = "com.cuddlesoft.nori.SearchClient.Settings";
    /** Identifier used for the query string to search when starting this activity with an {@link android.content.Intent} */
    public static final String INTENT_EXTRA_SEARCH_QUERY = "com.cuddlesoft.nori.SearchQuery";
    /** Identifier used to include {@link SearchClient.Settings} objects in search intents. */
    public static final String INTENT_EXTRA_SEARCH_CLIENT_SETTINGS = "com.cuddlesoft.nori.SearchClient.Settings";
    /** Identifier used to preserve current search query in {@link #onSaveInstanceState(android.os.Bundle)}. */
    private static final String BUNDLE_ID_SEARCH_QUERY = "com.cuddlesoft.nori.SearchQuery";
    /** Identifier used to preserve iconified/expanded state of the SearchView in {@link #onSaveInstanceState(android.os.Bundle)}. */
    private static final String BUNDLE_ID_SEARCH_VIEW_IS_EXPANDED = "com.cuddlesoft.nori.SearchView.isExpanded";
    /** Identifier used to preserve search view focused state. */
    private static final String BUNDLE_ID_SEARCH_VIEW_IS_FOCUSED = "com.cuddlesoft.nori.SearchView.isFocused";
    /** Default {@link android.content.SharedPreferences} object. */
    private SharedPreferences sharedPreferences;
    /* {@link SearchClient.Settings} object selected from the service dropdown menu. */
    private SearchClient.Settings searchClientSettings;
    /** Search API client. */
    private SearchClient searchClient;
    /** Search view menu item. */
    private MenuItem searchMenuItem;
    /** Action bar search view. */
    private SearchView searchView;
    /** Search callback currently awaiting a response from the Search API. */
    private SearchResultCallback searchCallback;
    /** Search result grid fragment shown in this activity. */
    private SearchResultGridFragment searchResultGridFragment;
    /** Bundle used when restoring saved instance state (after screen rotation, app restored from background, etc.) */
    private Bundle savedInstanceState;

    /**
     * Set up the action bar SearchView and its event handlers.
     *
     * @param menu Menu after being inflated in {@link #onCreateOptionsMenu(android.view.Menu)}.
     */
    private void setUpSearchView(Menu menu) {
        // Extract SearchView from the MenuItem object.
        searchMenuItem = menu.findItem(R.id.action_search);
        searchMenuItem.setVisible(searchClientSettings != null);
        searchView = (SearchView) MenuItemCompat.getActionView(searchMenuItem);

        // Set Searchable XML configuration.
        SearchManager searchManager = (SearchManager) getSystemService(SEARCH_SERVICE);
        searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));

        // Set SearchView attributes.
        searchView.setFocusable(false);
        searchView.setQueryRefinementEnabled(true);

        // Set SearchView event listeners.
        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
            @Override
            public boolean onQueryTextSubmit(String query) {
                if (searchClientSettings != null) {
                    // Prepare a intent to send to a new instance of this activity.
                    Intent intent = new Intent(SearchActivity.this, SearchActivity.class);
                    intent.setAction(Intent.ACTION_SEARCH);
                    intent.putExtra(BUNDLE_ID_SEARCH_CLIENT_SETTINGS, searchClientSettings);
                    intent.putExtra(BUNDLE_ID_SEARCH_QUERY, query);

                    // Collapse the ActionView. This makes navigating through previous results using the back key less painful.
                    MenuItemCompat.collapseActionView(searchMenuItem);

                    // Start a new activity with the created Intent.
                    startActivity(intent);
                }

                // Returns true to override the default behaviour and stop another search Intent from being sent.
                return true;
            }

            @Override
            public boolean onQueryTextChange(String newText) {
                // Returns false to perform the default action.
                return false;
            }
        });
        searchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() {
            @Override
            public boolean onSuggestionSelect(int position) {
                return onSuggestionClick(position);
            }

            @Override
            public boolean onSuggestionClick(int position) {
                if (searchClientSettings != null) {
                    // Get the SearchView's suggestion adapter.
                    CursorAdapter adapter = searchView.getSuggestionsAdapter();
                    // Get the suggestion at given position.
                    Cursor c = adapter.getCursor();
                    c.moveToPosition(position);

                    // Create and send a search intent.
                    Intent intent = new Intent(SearchActivity.this, SearchActivity.class);
                    intent.setAction(Intent.ACTION_SEARCH);
                    intent.putExtra(BUNDLE_ID_SEARCH_CLIENT_SETTINGS, searchClientSettings);
                    intent.putExtra(BUNDLE_ID_SEARCH_QUERY,
                            c.getString(c.getColumnIndex(SearchSuggestionDatabase.COLUMN_NAME)));
                    startActivity(intent);

                    // Release native resources.
                    c.close();
                }

                // Return true to override default behaviour and prevent another intent from being sent.
                return true;
            }
        });

        if (savedInstanceState != null) {
            // Restore state from saved instance state bundle (after screen rotation, app restored from background, etc.)
            if (savedInstanceState.containsKey(BUNDLE_ID_SEARCH_QUERY)) {
                // Restore search query from saved instance state.
                searchView.setQuery(savedInstanceState.getCharSequence(BUNDLE_ID_SEARCH_QUERY), false);
            }
            // Restore iconified/expanded search view state from saved instance state.
            if (savedInstanceState.getBoolean(BUNDLE_ID_SEARCH_VIEW_IS_EXPANDED, false)) {
                MenuItemCompat.expandActionView(searchMenuItem);
                // Restore focus state.
                if (!savedInstanceState.getBoolean(BUNDLE_ID_SEARCH_VIEW_IS_FOCUSED, false)) {
                    searchView.clearFocus();
                }
            }
        } else if (getIntent() != null && getIntent().getAction().equals(Intent.ACTION_SEARCH)) {
            // Set SearchView query string to match the one sent in the SearchIntent.
            searchView.setQuery(getIntent().getStringExtra(BUNDLE_ID_SEARCH_QUERY), false);
        }
    }

    /** Set up the {@link android.support.v7.app.ActionBar}, including the API service picker dropdown. */
    private void setUpActionBar() {
        ActionBar actionBar = getSupportActionBar();
        ServiceDropdownAdapter serviceDropdownAdapter = new ServiceDropdownAdapter();
        actionBar.setDisplayShowHomeEnabled(false);
        actionBar.setDisplayShowTitleEnabled(false);
        actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
        actionBar.setListNavigationCallbacks(serviceDropdownAdapter, serviceDropdownAdapter);
    }

    /**
     * Request a {@link SearchResult} object to be fetched from the background.
     *
     * @param query Query string (a space-separated list of tags).
     */
    private void doSearch(String query) {
        // Show progress bar in ActionBar.
        setSupportProgressBarIndeterminateVisibility(true);
        // Request a search result from the API client.
        searchCallback = new SearchResultCallback();
        searchClient.search(query, searchCallback);
    }

    /**
     * Called when a new Search API is selected by the user from the action bar dropdown.
     *
     * @param settings Selected {@link com.cuddlesoft.norilib.clients.SearchClient.Settings} object.
     */
    protected void onSearchAPISelected(SearchClient.Settings settings) {
        if (settings == null) {
            // The SearchClient setting database is empty.
            return;
        }

        // Show search action bar icon (if not already visible).
        if (searchMenuItem != null) {
            searchMenuItem.setVisible(true);
        }
        // Expand the SearchView when an API is selected manually by the user.
        // (and not automatically restored from previous state when the app is first launched)
        if (searchClientSettings != null && searchMenuItem != null) {
            MenuItemCompat.expandActionView(searchMenuItem);
        }

        searchClientSettings = settings;

        // If a SearchClient wasn't included in the Intent that started this activity, create one now and search for the default query.
        // Only do this if NSFW images would not be included in the search result.
        if (searchClient == null && searchResultGridFragment.getSearchResult() == null) {
            searchClient = settings.createSearchClient();
            if (shouldLoadDefaultQuery()) {
                doSearch(searchClient.getDefaultQuery());
            } else if (searchMenuItem != null) {
                MenuItemCompat.expandActionView(searchMenuItem);
            }
        }
    }

    /**
     * Only load the default query on app launch if NSFW images would not be shown.
     *
     * @return True if default query search results should be shown on first launch.
     */
    protected boolean shouldLoadDefaultQuery() {
        String filters = sharedPreferences.getString(getString(R.string.preference_nsfwFilter_key), "");

        return !(filters.contains("questionable") || filters.contains("explicit"));
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Request window manager features.
        supportRequestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);

        // Get shared preferences.
        sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);

        // Inflate views.
        setContentView(R.layout.activity_search);

        // Get search result grid fragment from fragment manager.
        searchResultGridFragment = (SearchResultGridFragment) getSupportFragmentManager()
                .findFragmentById(R.id.fragment_searchResultGrid);

        // If the activity was started from a Search intent, create the SearchClient object and submit search.
        Intent intent = getIntent();
        if (intent != null && intent.getAction().equals(Intent.ACTION_SEARCH)
                && searchResultGridFragment.getSearchResult() == null) {
            SearchClient.Settings searchClientSettings = intent
                    .getParcelableExtra(BUNDLE_ID_SEARCH_CLIENT_SETTINGS);
            searchClient = searchClientSettings.createSearchClient();
            doSearch(intent.getStringExtra(BUNDLE_ID_SEARCH_QUERY));
        }

        // Set up the dropdown API server picker.
        setUpActionBar();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        // Cancel pending API callbacks.
        if (searchCallback != null) {
            searchCallback.cancel();
        }
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        // Make saved instance state available to other methods.
        this.savedInstanceState = savedInstanceState;
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        // Preserve SearchView state.
        if (searchView != null) {
            outState.putCharSequence(BUNDLE_ID_SEARCH_QUERY, searchView.getQuery());
            outState.putBoolean(BUNDLE_ID_SEARCH_VIEW_IS_EXPANDED,
                    MenuItemCompat.isActionViewExpanded(searchMenuItem));
            outState.putBoolean(BUNDLE_ID_SEARCH_VIEW_IS_FOCUSED, searchView.isFocused());
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.search, menu);
        // Set up action bar search view.
        setUpSearchView(menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        switch (item.getItemId()) {
        case R.id.action_settings:
            startActivity(new Intent(SearchActivity.this, SettingsActivity.class));
            return true;
        default:
            return super.onOptionsItemSelected(item);
        }
    }

    @Override
    public void onImageSelected(Image image, int position) {
        // Open ImageViewerActivity.
        final Intent intent = new Intent(SearchActivity.this, ImageViewerActivity.class);
        intent.putExtra(BUNDLE_ID_IMAGE_INDEX, position);
        intent.putExtra(BUNDLE_ID_SEARCH_RESULT, searchResultGridFragment.getSearchResult());
        intent.putExtra(BUNDLE_ID_SEARCH_CLIENT_SETTINGS, searchClient.getSettings());
        startActivity(intent);
    }

    @Override
    public void fetchMoreImages(SearchResult searchResult) {
        // Ignore request if there is another API request pending.
        if (searchCallback != null) {
            return;
        }
        // Show progress bar in ActionBar.
        setSupportProgressBarIndeterminateVisibility(true);
        // Request search result from API client.
        searchCallback = new SearchResultCallback(searchResult);
        searchClient.search(Tag.stringFromArray(searchResult.getQuery()), searchResult.getCurrentOffset() + 1,
                searchCallback);
    }

    /** Callback waiting for a SearchResult received on a background thread from the Search API. */
    private class SearchResultCallback implements SearchClient.SearchCallback {
        /** Search result to extend when fetching more images for endless scrolling. */
        private final SearchResult searchResult;
        /** Callback cancelled and should no longer respond to received SearchResult. */
        private boolean isCancelled = false;

        /** Default constructor. */
        public SearchResultCallback() {
            this.searchResult = null;
        }

        /** Constructor used to add more images to an existing SearchResult to implement endless scrolling. */
        public SearchResultCallback(SearchResult searchResult) {
            this.searchResult = searchResult;
        }

        @Override
        public void onFailure(IOException e) {
            if (!isCancelled) {
                // Show error message to user.
                Toast.makeText(SearchActivity.this,
                        String.format(getString(R.string.toast_networkError), e.getLocalizedMessage()),
                        Toast.LENGTH_LONG).show();
                // Clear callback and hide progress indicator in Action Bar.
                setSupportProgressBarIndeterminateVisibility(false);
                searchCallback = null;
            }
        }

        @Override
        public void onSuccess(SearchResult searchResult) {
            if (!isCancelled) {
                // Clear callback and hide progress indicator in Action Bar.
                setSupportProgressBarIndeterminateVisibility(false);
                searchCallback = null;

                // Filter the received SearchResult.
                final int resultCount = searchResult.getImages().length;
                if (sharedPreferences.contains(getString(R.string.preference_nsfwFilter_key)) && !TextUtils.isEmpty(
                        sharedPreferences.getString(getString(R.string.preference_nsfwFilter_key), "").trim())) {
                    // Get filter from shared preferences.
                    searchResult.filter(Image.ObscenityRating.arrayFromStrings(sharedPreferences
                            .getString(getString(R.string.preference_nsfwFilter_key), "").split(" ")));
                } else {
                    // Get default filter from resources.
                    searchResult.filter(Image.ObscenityRating.arrayFromStrings(
                            getResources().getStringArray(R.array.preference_nsfwFilter_defaultValues)));
                }
                if (sharedPreferences.contains(getString(R.string.preference_tagFilter_key))) {
                    // Get tag filters from shared preferences and filter the result.
                    searchResult.filter(Tag.arrayFromString(
                            sharedPreferences.getString(getString(R.string.preference_tagFilter_key), "")));
                }

                if (this.searchResult != null) {
                    // Set onLastPage if no more images were fetched.
                    if (resultCount == 0) {
                        this.searchResult.onLastPage();
                    } else {
                        // Extend existing search result for endless scrolling.
                        this.searchResult.addImages(searchResult.getImages(), searchResult.getCurrentOffset());
                        searchResultGridFragment.setSearchResult(this.searchResult);
                    }
                } else {
                    // Show search result.
                    if (resultCount == 0) {
                        searchResult.onLastPage();
                    } else {
                        addSearchHistoryEntry(Tag.stringFromArray(searchResult.getQuery()));
                    }
                    searchResultGridFragment.setSearchResult(searchResult);
                }
            }
        }

        /**
         * Adds a new entry to the {@link SearchSuggestionDatabase} on a background thread
         * (to prevent blocking the UI thread with database I/O).
         *
         * @param query Query string searched for by the user.
         */
        private void addSearchHistoryEntry(final String query) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    // Add query string to the database.
                    SearchSuggestionDatabase searchSuggestionDatabase = new SearchSuggestionDatabase(
                            SearchActivity.this);
                    searchSuggestionDatabase.insert(query);
                    searchSuggestionDatabase.close();
                }
            }).run();
        }

        /** Cancels this callback. */
        public void cancel() {
            this.isCancelled = true;
        }
    }

    /** Adapter populating the Search API picker in the ActionBar. */
    private class ServiceDropdownAdapter extends BaseAdapter
            implements LoaderManager.LoaderCallbacks<List<Pair<Integer, SearchClient.Settings>>>,
            ActionBar.OnNavigationListener {
        /** Search client settings loader ID. */
        private static final int LOADER_ID_API_SETTINGS = 0x00;
        /** Shared preference key used to store the last active {@link com.cuddlesoft.norilib.clients.SearchClient}. */
        private static final String SHARED_PREFERENCE_LAST_SELECTED_INDEX = "com.cuddlesoft.nori.SearchActivity.lastSelectedServiceIndex";
        /** List of service settings loaded from {@link com.cuddlesoft.nori.database.APISettingsDatabase}. */
        private List<Pair<Integer, SearchClient.Settings>> settingsList;
        /** ID of the last selected item. */
        private long lastSelectedItem;

        public ServiceDropdownAdapter() {
            // Restore last active item from SharedPreferences.
            lastSelectedItem = sharedPreferences.getLong(SHARED_PREFERENCE_LAST_SELECTED_INDEX, 1L);
            // Initialize the search client settings database loader.
            getSupportLoaderManager().initLoader(LOADER_ID_API_SETTINGS, null, this);
        }

        @Override
        public int getCount() {
            if (settingsList == null) {
                return 0;
            } else {
                return settingsList.size();
            }
        }

        @Override
        public SearchClient.Settings getItem(int position) {
            return settingsList.get(position).second;
        }

        @Override
        public long getItemId(int position) {
            // Return database row ID.
            return settingsList.get(position).first;
        }

        /**
         * Get position of the item with given database row ID.
         *
         * @param id Row ID.
         * @return Position of the item.
         */
        public int getPositionByItemId(long id) {
            for (int i = 0; i < getCount(); i++) {
                if (getItemId(i) == id) {
                    return i;
                }
            }
            return 0;
        }

        @Override
        public View getView(int position, View recycledView, ViewGroup container) {
            // Reuse recycled view, if possible.
            View view = recycledView;
            if (view == null) {
                // View could not be recycled, inflate new view.
                LayoutInflater inflater = LayoutInflater.from(SearchActivity.this);
                view = inflater.inflate(R.layout.simple_dropdown_item, container, false);
            }

            // Populate views with content.
            SearchClient.Settings settings = getItem(position);
            TextView text1 = (TextView) view.findViewById(android.R.id.text1);
            text1.setText(settings.getName());

            return view;
        }

        @Override
        public Loader<List<Pair<Integer, SearchClient.Settings>>> onCreateLoader(int id, Bundle args) {
            if (id == LOADER_ID_API_SETTINGS) {
                return new APISettingsDatabase.Loader(SearchActivity.this);
            }
            return null;
        }

        @Override
        public void onLoadFinished(Loader<List<Pair<Integer, SearchClient.Settings>>> loader,
                List<Pair<Integer, SearchClient.Settings>> data) {
            if (loader.getId() == LOADER_ID_API_SETTINGS) {
                // Update adapter data.
                settingsList = data;
                notifyDataSetChanged();
                // Reselect last active item.
                if (!data.isEmpty()) {
                    getSupportActionBar().setSelectedNavigationItem(getPositionByItemId(lastSelectedItem));
                }
            }
        }

        @Override
        public void onLoaderReset(Loader<List<Pair<Integer, SearchClient.Settings>>> loader) {
            // Invalidate adapter's data.
            settingsList = null;
            notifyDataSetInvalidated();
        }

        @Override
        public boolean onNavigationItemSelected(int position, long id) {
            // Save last active item to SharedPreferences.
            lastSelectedItem = id;
            sharedPreferences.edit().putLong(SHARED_PREFERENCE_LAST_SELECTED_INDEX, id).apply();
            // Notify parent activity.
            onSearchAPISelected(getItem(position));

            return true;
        }
    }
}