package com.airlocksoftware.hackernews.activity; import java.util.Arrays; import org.apache.commons.lang3.StringUtils; import android.annotation.SuppressLint; import android.content.Context; import android.os.Bundle; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.ListView; import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; import com.airlocksoftware.hackernews.R; import com.airlocksoftware.hackernews.adapter.SearchAdapter; import com.airlocksoftware.hackernews.loader.SearchLoader; import com.airlocksoftware.hackernews.loader.SearchLoader.SearchResult; import com.airlocksoftware.hackernews.model.Request; import com.airlocksoftware.hackernews.model.Result; import com.airlocksoftware.holo.actionbar.ActionBarButton; import com.airlocksoftware.holo.actionbar.ActionBarButton.Priority; import com.airlocksoftware.holo.adapters.SpinnerArrayAdapter; import com.airlocksoftware.holo.type.FontEdit; import com.airlocksoftware.holo.type.FontSpinner; import com.airlocksoftware.holo.utils.ViewUtils; /** * Perform searches using the www.hnsearch.com API. EditText goes in the ActionBar, and the SortType and SearchType as * set via Spinners in the header of the ListView. The search is performed in the background by SearchLoader. **/ public class SearchActivity extends BackActivity implements LoaderManager.LoaderCallbacks<SearchResult> { // State private Request mRequest = Request.NEW; private SortType mSort = SortType.RELEVANCE; private SearchType mSearchType = SearchType.ALL; private String mQuery; private int mPage = 0; private Result mLastResult; private boolean mIsLoading; // Views & Adapters private SearchAdapter mAdapter; private ListView mListView; private View mSearchHeader; private View mMoreFooter; private View mError, mLoading, mNoResults; private ActionBarButton mSearchButton; private FontSpinner mSortSpinner, mSearchTypeSpinner; private FontEdit mSearchBox; // Listeners private OnClickListener mMoreListener = new OnClickListener() { @Override public void onClick(View v) { mRequest = Request.MORE; mPage += 1; getSupportLoaderManager().restartLoader(0, null, SearchActivity.this); } }; private OnClickListener mSearchBtnListener = new OnClickListener() { @Override public void onClick(View v) { performSearch(); } }; private OnEditorActionListener mSearchKeyboardListener = new OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if (actionId == EditorInfo.IME_ACTION_SEARCH) { performSearch(); return true; } else return false; } }; private SpinnerArrayAdapter<SortType> mSortAdapter = new SpinnerArrayAdapter<SortType>(this) { @Override public String getItemText(int position) { return getItem(position).getDisplayName(); } @Override public void onItemClick(int position) { mSort = getItem(position); mSortSpinner.setText(mSort.getDisplayName()); mSortSpinner.getDialog() .dismiss(); } }; private SpinnerArrayAdapter<SearchType> mSearchTypeAdapter = new SpinnerArrayAdapter<SearchType>(this) { @Override public String getItemText(int position) { return getItem(position).getDisplayName(); } @Override public void onItemClick(int position) { SearchType current = getItem(position); mSearchType = current; mSearchTypeSpinner.setText(current.getDisplayName()); mSearchTypeSpinner.getDialog() .dismiss(); } }; // Constants public static final String QUERY = SearchActivity.class.getSimpleName() + ".query"; public static final String SORT = SearchActivity.class.getSimpleName() + ".sort"; public static final String SEARCH_TYPE = SearchActivity.class.getSimpleName() + ".searchType"; public static final int NUM_ITEMS_PER_PAGE = 10; @Override public void onCreate(Bundle savedState) { super.onCreate(savedState); retrieveBundles(savedState, getIntent().getExtras()); if (StringUtils.isBlank(mQuery)) mRequest = Request.EMPTY; LayoutInflater inflater = getLayoutInflater(); setContentView(R.layout.act_search); findViews(); addHeader(inflater); setupFooter(inflater); setupSpinners(); setupListAdapter(); setupActionBar(inflater); getSupportLoaderManager().initLoader(0, null, this); refreshContentVisibility(); } private void setupListAdapter() { mAdapter = new SearchAdapter(this); mListView.setAdapter(mAdapter); } private void setupActionBar(LayoutInflater inflater) { mSearchBox = (FontEdit) inflater.inflate(R.layout.vw_actionbar_search, getActionBarView().getController() .getTitleGroup(), false); mSearchBox.setOnEditorActionListener(mSearchKeyboardListener); if (mQuery != null) mSearchBox.setText(mQuery); getActionBarView().getController() .getTitleGroup() .addView(mSearchBox); mSearchButton = new ActionBarButton(this).priority(Priority.HIGH); mSearchButton.setOnClickListener(mSearchBtnListener); getActionBarView().getController() .addButton(mSearchButton.text("Search") .icon(R.drawable.ic_action_search)); } /** Create and add "more" footer **/ private void setupFooter(LayoutInflater inflater) { mMoreFooter = inflater.inflate(R.layout.vw_more_link, mListView, false); mMoreFooter.setOnClickListener(mMoreListener); mMoreFooter.setVisibility(View.GONE); mListView.addFooterView(mMoreFooter, null, false); } private void setupSpinners() { // setup sort & search type spinners mSortAdapter.addAll(Arrays.asList(SortType.values())); mSortAdapter.setSelectedPosition(mSort.ordinal()); mSearchTypeAdapter.addAll(Arrays.asList(SearchType.values())); mSearchTypeAdapter.setSelectedPosition(mSearchType.ordinal()); mSortSpinner = (FontSpinner) mSearchHeader.findViewById(R.id.spin_sort); mSortSpinner.setAdapter(mSortAdapter); mSortSpinner.setText(mSortAdapter.getItemText(mSort.ordinal())); mSearchTypeSpinner = (FontSpinner) mSearchHeader.findViewById(R.id.spin_type); mSearchTypeSpinner.setAdapter(mSearchTypeAdapter); mSearchTypeSpinner.setText(mSearchTypeAdapter.getItemText(mSearchType.ordinal())); } /** setup header & adapters -- TODO put header in ActionBar if there's room (tablet layout) **/ private void addHeader(LayoutInflater inflater) { mSearchHeader = inflater.inflate(R.layout.vw_search_header, mListView, false); ViewUtils.fixBackgroundRepeat(mSearchHeader); mListView.addHeaderView(mSearchHeader); } private void findViews() { mListView = (ListView) findViewById(android.R.id.list); mError = findViewById(R.id.cnt_error); mLoading = findViewById(R.id.cnt_loading); mNoResults = findViewById(R.id.cnt_noresults); // fix background repeat issues ViewUtils.fixBackgroundRepeat(mError); ViewUtils.fixBackgroundRepeat(mLoading); ViewUtils.fixBackgroundRepeat(mNoResults); mListView.setItemsCanFocus(true); } private void retrieveBundles(Bundle savedState, Bundle extras) { if (savedState != null) { mQuery = savedState.getString(QUERY); mSort = (SortType) savedState.getSerializable(SORT); mSearchType = (SearchType) savedState.getSerializable(SEARCH_TYPE); } if (extras != null) { mQuery = extras.getString(QUERY); } } @Override public void onSaveInstanceState(Bundle outState) { outState.putString(QUERY, mQuery); outState.putSerializable(SORT, mSort); outState.putSerializable(SEARCH_TYPE, mSearchType); super.onSaveInstanceState(outState); } // LOADER INTERFACE @Override public Loader<SearchResult> onCreateLoader(int id, Bundle args) { mIsLoading = true; refreshContentVisibility(); return new SearchLoader(this, mRequest, mQuery, mSort, mSearchType, mPage); } @Override public void onLoadFinished(Loader<SearchResult> loader, SearchResult data) { mIsLoading = false; mLastResult = data.result; if (data.result != Result.MORE) { // clear adapter unless this is a "more" result mAdapter.clear(); } // hide more footer mMoreFooter.setVisibility(View.GONE); if ((data.result == Result.SUCCESS || data.result == Result.MORE) && data.items.size() > 0) { // successful run with new query or "more" mAdapter.addAll(data.items); // if there are more results for this query, show more button. Otherwise leave it hidden. if (data.hits - (data.page * NUM_ITEMS_PER_PAGE) > 0) mMoreFooter.setVisibility(View.VISIBLE); } refreshContentVisibility(); } @Override public void onLoaderReset(Loader<SearchResult> loader) { // do nothing } // PRIVATE METHODS private void performSearch() { // hide keyboard InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); inputManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); // get search term, reset start position, if (mSearchBox.getText() != null) { mQuery = mSearchBox.getText().toString(); } mPage = 0; mRequest = Request.NEW; getSupportLoaderManager().restartLoader(0, null, SearchActivity.this); } /** Determines which view (Error, NoResults, Loading) show be visible. **/ private void refreshContentVisibility() { // can't refresh visibility until / unless all views are created if (mAdapter == null || mNoResults == null || mError == null || mLoading == null) return; boolean adapterEmpty = mAdapter.getCount() < 1; boolean loadingVis = mIsLoading && adapterEmpty; boolean errorVis = mLastResult == Result.FAILURE && !mIsLoading; // Result.EMPTY refers to initial run of loader, not "no results" TODO should probably make this more logical boolean noResultsVis = !errorVis && !mIsLoading && adapterEmpty && mLastResult != Result.EMPTY; mSearchButton.showProgress(mIsLoading); mNoResults.setVisibility(ViewUtils.boolToVis(noResultsVis)); mError.setVisibility(ViewUtils.boolToVis(errorVis)); mLoading.setVisibility(ViewUtils.boolToVis(loadingVis)); } // Enums @SuppressLint("DefaultLocale") public enum SortType { RELEVANCE, DATE; // RELEVANCE, DATE, POINTS; public String getDisplayName() { return StringUtils.capitalize(name().toLowerCase()); } } @SuppressLint("DefaultLocale") public enum SearchType { ALL, STORIES, COMMENTS, USERS; public String getDisplayName() { return StringUtils.capitalize(name().toLowerCase()); } } }