package com.gh4a.fragment; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.MatrixCursor; import android.database.MergeCursor; import android.net.Uri; import android.os.Bundle; import android.support.annotation.LayoutRes; import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v4.widget.CursorAdapter; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.SearchView; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.Spinner; import android.widget.SpinnerAdapter; import android.widget.TextView; import com.gh4a.BackgroundTask; import com.gh4a.R; import com.gh4a.activities.FileViewerActivity; import com.gh4a.activities.RepositoryActivity; import com.gh4a.activities.UserActivity; import com.gh4a.adapter.CodeSearchAdapter; import com.gh4a.adapter.RepositoryAdapter; import com.gh4a.adapter.RootAdapter; import com.gh4a.adapter.SearchUserAdapter; import com.gh4a.db.SuggestionsProvider; import com.gh4a.loader.CodeSearchLoader; import com.gh4a.loader.LoaderCallbacks; import com.gh4a.loader.LoaderResult; import com.gh4a.loader.RepositorySearchLoader; import com.gh4a.loader.UserSearchLoader; import com.gh4a.utils.StringUtils; import com.gh4a.utils.UiUtils; import org.eclipse.egit.github.core.CodeSearchResult; import org.eclipse.egit.github.core.Repository; import org.eclipse.egit.github.core.RequestError; import org.eclipse.egit.github.core.SearchUser; import org.eclipse.egit.github.core.client.RequestException; import java.io.IOException; import java.util.List; public class SearchFragment extends LoadingListFragmentBase implements SearchView.OnQueryTextListener, SearchView.OnCloseListener, SearchView.OnSuggestionListener, AdapterView.OnItemSelectedListener, RootAdapter.OnItemClickListener { public static SearchFragment newInstance(int initialType, String initialQuery) { SearchFragment f = new SearchFragment(); Bundle args = new Bundle(); args.putInt("search_type", initialType); args.putString("initial_search", initialQuery); f.setArguments(args); return f; } private static final int SEARCH_TYPE_NONE = -1; public static final int SEARCH_TYPE_REPO = 0; public static final int SEARCH_TYPE_USER = 1; public static final int SEARCH_TYPE_CODE = 2; private static final int[][] HINT_AND_EMPTY_TEXTS = { { R.string.search_hint_repo, R.string.no_search_repos_found }, { R.string.search_hint_user, R.string.no_search_users_found }, { R.string.search_hint_code, R.string.no_search_code_found } }; private static final String[] SUGGESTION_PROJECTION = { SuggestionsProvider.Columns._ID, SuggestionsProvider.Columns.SUGGESTION }; private static final String SUGGESTION_SELECTION = SuggestionsProvider.Columns.TYPE + " = ? AND " + SuggestionsProvider.Columns.SUGGESTION + " LIKE ?"; private static final String SUGGESTION_ORDER = SuggestionsProvider.Columns.DATE + " DESC"; private static final String STATE_KEY_QUERY = "query"; private static final String STATE_KEY_SEARCH_TYPE = "search_type"; private final LoaderCallbacks<List<Repository>> mRepoCallback = new LoaderCallbacks<List<Repository>>(this) { @Override protected Loader<LoaderResult<List<Repository>>> onCreateLoader() { RepositorySearchLoader loader = new RepositorySearchLoader(getActivity(), null); loader.setQuery(mQuery); return loader; } @Override protected void onResultReady(List<Repository> result) { RepositoryAdapter adapter = new RepositoryAdapter(getActivity()); adapter.addAll(result); setAdapter(adapter); } }; private final LoaderCallbacks<List<SearchUser>> mUserCallback = new LoaderCallbacks<List<SearchUser>>(this) { @Override protected Loader<LoaderResult<List<SearchUser>>> onCreateLoader() { return new UserSearchLoader(getActivity(), mQuery); } @Override protected void onResultReady(List<SearchUser> result) { SearchUserAdapter adapter = new SearchUserAdapter(getActivity()); adapter.addAll(result); setAdapter(adapter); } }; private final LoaderCallbacks<List<CodeSearchResult>> mCodeCallback = new LoaderCallbacks<List<CodeSearchResult>>(this) { @Override protected Loader<LoaderResult<List<CodeSearchResult>>> onCreateLoader() { return new CodeSearchLoader(getActivity(), mQuery); } @Override protected void onResultReady(List<CodeSearchResult> result) { CodeSearchAdapter adapter = new CodeSearchAdapter(getActivity()); adapter.addAll(result); setAdapter(adapter); } @Override protected boolean onError(Exception e) { if (e instanceof RequestException) { RequestError error = ((RequestException) e).getError(); if (error != null && error.getErrors() != null && !error.getErrors().isEmpty()) { updateEmptyText(R.string.code_search_too_broad); if (mAdapter != null) { mAdapter.clear(); } updateEmptyState(); setContentShown(true); return true; } } return super.onError(e); } }; private final LoaderManager.LoaderCallbacks<Cursor> mSuggestionCallback = new LoaderManager.LoaderCallbacks<Cursor>() { @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { int type = mSearchType.getSelectedItemPosition(); return new CursorLoader(getActivity(), SuggestionsProvider.Columns.CONTENT_URI, SUGGESTION_PROJECTION, SUGGESTION_SELECTION, new String[] { String.valueOf(type), mQuery + "%" }, SUGGESTION_ORDER); } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { mSearch.getSuggestionsAdapter().changeCursor(data); } @Override public void onLoaderReset(Loader<Cursor> loader) { mSearch.getSuggestionsAdapter().changeCursor(null); } }; private RecyclerView mRecyclerView; private RootAdapter<?, ?> mAdapter; private Spinner mSearchType; private SearchView mSearch; private int mInitialSearchType; private String mQuery; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); if (savedInstanceState != null) { mQuery = savedInstanceState.getString(STATE_KEY_QUERY); mInitialSearchType = savedInstanceState.getInt(STATE_KEY_SEARCH_TYPE, SEARCH_TYPE_NONE); LoaderManager lm = getLoaderManager(); switch (mInitialSearchType) { case SEARCH_TYPE_REPO: lm.initLoader(0, null, mRepoCallback); break; case SEARCH_TYPE_USER: lm.initLoader(0, null, mUserCallback); break; case SEARCH_TYPE_CODE: lm.initLoader(0, null, mCodeCallback); break; } } else { Bundle args = getArguments(); mInitialSearchType = args.getInt("search_type", SEARCH_TYPE_REPO); mQuery = args.getString("initial_search"); } } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.search, menu); mSearchType = (Spinner) menu.findItem(R.id.type).getActionView(); mSearchType.setAdapter(new SearchTypeAdapter(mSearchType.getContext(), getActivity())); mSearchType.setOnItemSelectedListener(this); if (mInitialSearchType != SEARCH_TYPE_NONE) { mSearchType.setSelection(mInitialSearchType); mInitialSearchType = SEARCH_TYPE_NONE; } mSearch = (SearchView) menu.findItem(R.id.search).getActionView(); mSearch.setIconifiedByDefault(true); mSearch.requestFocus(); mSearch.onActionViewExpanded(); mSearch.setIconified(false); mSearch.setOnQueryTextListener(this); mSearch.setOnCloseListener(this); mSearch.setOnSuggestionListener(this); mSearch.setSuggestionsAdapter(new SuggestionAdapter(getActivity())); if (mQuery != null) { mSearch.setQuery(mQuery, false); } getLoaderManager().initLoader(1, null, mSuggestionCallback); updateSelectedSearchType(); super.onCreateOptionsMenu(menu, inflater); } @Override public void onSaveInstanceState(Bundle outState) { if (mAdapter instanceof RepositoryAdapter) { outState.putInt(STATE_KEY_SEARCH_TYPE, SEARCH_TYPE_REPO); } else if (mAdapter instanceof SearchUserAdapter) { outState.putInt(STATE_KEY_SEARCH_TYPE, SEARCH_TYPE_USER); } else if (mAdapter instanceof CodeSearchAdapter) { outState.putInt(STATE_KEY_SEARCH_TYPE, SEARCH_TYPE_CODE); } outState.putString(STATE_KEY_QUERY, mQuery); super.onSaveInstanceState(outState); } @Override public void onRefresh() { if (mAdapter != null) { hideContentAndRestartLoaders(0); } } @Override protected int getEmptyTextResId() { return 0; // will be updated later } @Override protected void onRecyclerViewInflated(RecyclerView view, LayoutInflater inflater) { super.onRecyclerViewInflated(view, inflater); mRecyclerView = view; } @Override public void onItemClick(Object item) { if (item instanceof Repository) { Repository repository = (Repository) item; startActivity(RepositoryActivity.makeIntent(getActivity(), repository)); } else if (item instanceof CodeSearchResult) { CodeSearchResult result = (CodeSearchResult) item; Repository repo = result.getRepository(); Uri uri = Uri.parse(result.getUrl()); String ref = uri.getQueryParameter("ref"); startActivity(FileViewerActivity.makeIntent(getActivity(), repo.getOwner().getLogin(), repo.getName(), ref, result.getPath())); } else { SearchUser user = (SearchUser) item; startActivity(UserActivity.makeIntent(getActivity(), user.getLogin(), user.getName())); } } @Override public boolean onQueryTextSubmit(String query) { LoaderManager lm = getLoaderManager(); int type = mSearchType.getSelectedItemPosition(); switch (type) { case SEARCH_TYPE_USER: lm.restartLoader(0, null, mUserCallback); break; case SEARCH_TYPE_CODE: lm.restartLoader(0, null, mCodeCallback); break; default: lm.restartLoader(0, null, mRepoCallback); break; } mQuery = query; if (!StringUtils.isBlank(query)) { new SaveSearchSuggestionTask(query, type).schedule(); } setContentShown(false); mSearch.clearFocus(); return true; } @Override public boolean onQueryTextChange(String newText) { CursorAdapter adapter = mSearch.getSuggestionsAdapter(); if (adapter != null) { Cursor cursor = adapter.getCursor(); int count = cursor != null ? cursor.getCount() - 1 : -1; //noinspection StatementWithEmptyBody if (mQuery != null && newText.startsWith(mQuery) && count == 0) { // nothing found on previous query } else { mQuery = newText; adapter.changeCursor(null); getLoaderManager().restartLoader(1, null, mSuggestionCallback); } } mQuery = newText; return true; } @Override public boolean onClose() { if (mAdapter != null) { mAdapter.clear(); } mQuery = null; return true; } @Override public boolean onSuggestionSelect(int position) { return false; } @Override public boolean onSuggestionClick(int position) { Cursor cursor = mSearch.getSuggestionsAdapter().getCursor(); if (cursor.moveToPosition(position)) { if (position == cursor.getCount() - 1) { new SuggestionDeletionTask(mSearchType.getSelectedItemPosition()).schedule(); } else { mQuery = cursor.getString(1); mSearch.setQuery(mQuery, false); } } return true; } @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { updateSelectedSearchType(); if (getLoaderManager().getLoader(0) != null) { onQueryTextSubmit(mQuery); } } @Override public void onNothingSelected(AdapterView<?> parent) { updateSelectedSearchType(); } private void updateSelectedSearchType() { int[] hintAndEmptyTextResIds = HINT_AND_EMPTY_TEXTS[mSearchType.getSelectedItemPosition()]; mSearch.setQueryHint(getString(hintAndEmptyTextResIds[0])); updateEmptyText(hintAndEmptyTextResIds[1]); mSearch.getSuggestionsAdapter().changeCursor(null); getLoaderManager().restartLoader(1, null, mSuggestionCallback); } private void setAdapter(RootAdapter<?, ?> adapter) { adapter.setOnItemClickListener(this); mRecyclerView.setAdapter(adapter); mAdapter = adapter; setContentShown(true); updateEmptyState(); } private void updateEmptyText(@StringRes int emptyTextResId) { TextView emptyView = (TextView) getView().findViewById(android.R.id.empty); emptyView.setText(emptyTextResId); } private class SaveSearchSuggestionTask extends BackgroundTask<Void> { private final String mSuggestion; private final int mType; public SaveSearchSuggestionTask(String suggestion, int type) { super(getBaseActivity()); mSuggestion = suggestion; mType = type; } @Override protected Void run() throws IOException { ContentValues cv = new ContentValues(); cv.put(SuggestionsProvider.Columns.TYPE, mType); cv.put(SuggestionsProvider.Columns.SUGGESTION, mSuggestion); cv.put(SuggestionsProvider.Columns.DATE, System.currentTimeMillis()); getContext().getContentResolver().insert(SuggestionsProvider.Columns.CONTENT_URI, cv); return null; } @Override protected void onSuccess(Void result) { } } private class SuggestionDeletionTask extends BackgroundTask<Void> { private final int mType; public SuggestionDeletionTask(int type) { super(getBaseActivity()); mType = type; } @Override protected Void run() throws Exception { getContext().getContentResolver().delete(SuggestionsProvider.Columns.CONTENT_URI, SuggestionsProvider.Columns.TYPE + " = ?", new String[] { String.valueOf(mType) }); return null; } @Override protected void onSuccess(Void result) { mSearch.getSuggestionsAdapter().changeCursor(null); } } private static class SearchTypeAdapter extends BaseAdapter implements SpinnerAdapter { private final Context mContext; private final LayoutInflater mInflater; private final LayoutInflater mPopupInflater; private final int[][] mResources = new int[][] { { R.string.search_type_repo, R.drawable.icon_repositories_dark, R.attr.searchRepoIcon, 0 }, { R.string.search_type_user, R.drawable.search_users_dark, R.attr.searchUserIcon, 0 }, { R.string.search_type_code, R.drawable.search_code_dark, R.attr.searchCodeIcon, 0 } }; private SearchTypeAdapter(Context context, Context popupContext) { mContext = context; mInflater = LayoutInflater.from(context); mPopupInflater = LayoutInflater.from(popupContext); for (int i = 0; i < mResources.length; i++) { mResources[i][3] = UiUtils.resolveDrawable(popupContext, mResources[i][2]); } } @Override public int getCount() { return mResources.length; } @Override public CharSequence getItem(int position) { return mContext.getString(mResources[position][0]); } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = mInflater.inflate(R.layout.search_type_small, null); } ImageView icon = (ImageView) convertView.findViewById(R.id.icon); icon.setImageResource(mResources[position][1]); return convertView; } @Override public View getDropDownView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = mPopupInflater.inflate(R.layout.search_type_popup, null); } ImageView icon = (ImageView) convertView.findViewById(R.id.icon); icon.setImageResource(mResources[position][3]); TextView label = (TextView) convertView.findViewById(R.id.label); label.setText(mResources[position][0]); return convertView; } } private static class SuggestionAdapter extends CursorAdapter { private final LayoutInflater mInflater; public SuggestionAdapter(Context context) { super(context, null, false); mInflater = LayoutInflater.from(context); } @Override public Cursor swapCursor(Cursor newCursor) { if (newCursor != null && newCursor.getCount() > 0) { MatrixCursor clearRowCursor = new MatrixCursor(SUGGESTION_PROJECTION); clearRowCursor.addRow(new Object[] { Long.MAX_VALUE, mContext.getString(R.string.clear_suggestions) }); newCursor = new MergeCursor(new Cursor[] { newCursor, clearRowCursor }); } return super.swapCursor(newCursor); } @Override public int getItemViewType(int position) { return isClearRow(position) ? 1 : 0; } @Override public int getViewTypeCount() { return 2; } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { @LayoutRes int layoutResId = isClearRow(cursor.getPosition()) ? R.layout.row_suggestion_clear : R.layout.row_suggestion; return mInflater.inflate(layoutResId, parent, false); } @Override public void bindView(View view, Context context, Cursor cursor) { TextView textView = (TextView) view; int columnIndex = cursor.getColumnIndexOrThrow(SuggestionsProvider.Columns.SUGGESTION); textView.setText(cursor.getString(columnIndex)); } private boolean isClearRow(int position) { return position == getCount() - 1; } } }