/* * Copyright 2015. Appsi Mobile * * 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 com.appsimobile.appsii.module.search; import android.content.ComponentName; import android.content.Context; import android.content.CursorLoader; import android.content.Intent; import android.content.Loader; import android.content.SharedPreferences; import android.content.res.Resources; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.provider.ContactsContract; import android.speech.RecognitionListener; import android.speech.SpeechRecognizer; import android.support.v4.view.ViewCompat; import android.support.v7.widget.DefaultItemAnimator; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import com.appsimobile.appsii.AnalyticsManager; import com.appsimobile.appsii.BuildConfig; import com.appsimobile.appsii.LoaderManager; import com.appsimobile.appsii.PageController; import com.appsimobile.appsii.R; import com.appsimobile.appsii.compat.LauncherAppsCompat; import com.appsimobile.appsii.compat.UserHandleCompat; import com.appsimobile.appsii.dagger.AppsiInjector; import com.appsimobile.appsii.module.BaseContactInfo; import com.appsimobile.appsii.module.PeopleQuery; import com.appsimobile.appsii.module.apps.AppEntry; import com.appsimobile.appsii.module.apps.AppHistoryUtils; import com.appsimobile.appsii.module.people.ContactView; import com.appsimobile.appsii.preference.PreferenceHelper; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; /** * Created by nick on 25/05/14. */ public class SearchController extends PageController implements AdapterView.OnItemClickListener, OnAppClickedListener, OnPersonClickedListener, ContactView.PeopleActionListener { static final int APPS_SEARCH_LOADER = 334001; static final int PEOPLE_SEARCH_LOADER = 334002; static final int RECENT_SEARCHES_LOADER = 334003; /** * The recycler-view containing the agenda-items */ RecyclerView mRecyclerView; /** * The view where the user can enter the search query */ EditText mSearchView; /** * The speech recognized used for speech search */ SpeechRecognizer mSpeechRecognizer; /** * The query the user entered */ String mQuery; /** * The search adapter. This shows the search results */ SearchAdapter mSearchAdapter; /** * Loader callbacks for the people loader */ PeopleCallbacks mPeopleCallbacks; /** * Loader callbacks for the apps loader */ AppsCallbacks mAppsCallbacks; /** * Loader callbacks for the recent searches loader */ RecentSearchedCallbacks mRecentSearchedCallbacks; /** * The shared-preferences we can get the configuration from */ SharedPreferences mPreferences; RecyclerView mSearchRecyclerView; View mSearchDivider; GridLayoutManager mGridLayoutManager; SearchSuggestionsAdapter mSearchSuggestionsAdapter; View mSuggestionsAnchor; @Inject PreferenceHelper mPreferenceHelper; @Inject InputMethodManager mInputMethodManager; @Inject LauncherAppsCompat mLauncherAppsCompat; public SearchController(Context context, String title) { super(context, title); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.page_search, container, false); } @Override public void onDestroy() { super.onDestroy(); mSpeechRecognizer.destroy(); } @Override protected void onViewDestroyed(View view) { } // @Override // public int getFlags() { // return AbstractSidebarPagerAdapter.FLAG_NO_DECORATIONS; // } @Override protected void onViewCreated(View view, Bundle savedInstanceState) { mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler); mSearchView = (EditText) view.findViewById(R.id.search_view); mSuggestionsAnchor = view.findViewById(R.id.search_wrapper); mSearchRecyclerView = (RecyclerView) view.findViewById(R.id.search_recycler); mSearchDivider = view.findViewById(R.id.search_divider); mSearchRecyclerView.setLayoutManager(new WrapContentLinearLayoutManager(getContext())); mSearchRecyclerView.setAdapter(mSearchSuggestionsAdapter); mRecyclerView.setLayoutManager(new WrapContentLinearLayoutManager(getContext())); mRecyclerView.setAdapter(mSearchAdapter); mRecyclerView.setItemAnimator(new DefaultItemAnimator()); // mRecyclerView.setOnScrollListener(new ToolbarScrollListener(this, mToolbar)); Resources res = getResources(); float density = res.getDisplayMetrics().density; // calculate the width of the sidebar int contentWidth = getContentWidth(); if (contentWidth == 0) { float pct = mPreferenceHelper.getSidebarWidth() / 100f; contentWidth = (int) (res.getConfiguration().smallestScreenWidthDp * density * pct); } int dp104 = (int) (density * 104); int cellCount = contentWidth / dp104; if (cellCount < 3) cellCount = 3; if (cellCount > 3) cellCount--; mGridLayoutManager = new GridLayoutManager(getContext(), cellCount); mSearchAdapter.setSpanCount(cellCount); mRecyclerView.setLayoutManager(mGridLayoutManager); mGridLayoutManager.setSpanSizeLookup(mSearchAdapter.mSpanSizeLookup); mSearchView.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { onSearchTextChanged(s); } }); mSearchView.setOnFocusChangeListener(new View.OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { onSearchFocusChanged(hasFocus); } }); mSearchSuggestionsAdapter.setOnItemClickListener(this); Button speechButton; /* speechButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM); intent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true); // intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS,5); mSpeechRecognizer.startListening(intent); Log.i("111111", "11111111"); } }); */ } void onSearchTextChanged(CharSequence charSequence) { mQuery = String.valueOf(charSequence); if (mQuery.length() > 0) { getLoaderManager().restartLoader(PEOPLE_SEARCH_LOADER, null, mPeopleCallbacks); getLoaderManager().restartLoader(APPS_SEARCH_LOADER, null, mAppsCallbacks); hideSearchSuggestions(); } else { mQuery = null; mSearchAdapter.clear(); showSearchSuggestions(); getLoaderManager().destroyLoader(PEOPLE_SEARCH_LOADER); getLoaderManager().destroyLoader(APPS_SEARCH_LOADER); } } void onSearchFocusChanged(boolean hasFocus) { if (hasFocus) { showSearchSuggestions(); } } private void hideSearchSuggestions() { mSearchRecyclerView.setVisibility(View.GONE); mSearchDivider.setVisibility(View.GONE); } private void showSearchSuggestions() { mSearchRecyclerView.setVisibility(View.VISIBLE); mSearchDivider.setVisibility(View.VISIBLE); } @Override protected void onUserVisible() { super.onUserVisible(); trackPageView(AnalyticsManager.CATEGORY_SEARCH); // focusSearchView(); } @Override protected void onUserInvisible() { super.onUserInvisible(); InputMethodManager imm = mInputMethodManager; imm.hideSoftInputFromWindow(mSearchView.getWindowToken(), 0); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); AppsiInjector.inject(this); mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(getContext()); mSpeechRecognizer.setRecognitionListener(new RecognitionListenerImpl()); mSearchAdapter = new SearchAdapter(this, this, this); mPeopleCallbacks = new PeopleCallbacks(); mAppsCallbacks = new AppsCallbacks(); mRecentSearchedCallbacks = new RecentSearchedCallbacks(); mSearchSuggestionsAdapter = new SearchSuggestionsAdapter(); getLoaderManager().initLoader(RECENT_SEARCHES_LOADER, null, mRecentSearchedCallbacks); } @Override protected void applyToolbarColor(int color) { } public void setDeferLoads(boolean deferLoads) { super.setDeferLoads(deferLoads); if (!deferLoads) { // focusSearchView(); } } void focusSearchView() { if (mSearchView != null && isUserVisible()) { mSearchView.requestFocus(); InputMethodManager imm = mInputMethodManager; imm.showSoftInput(mSearchView, InputMethodManager.SHOW_IMPLICIT); } } @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { hideSearchSuggestions(); SearchSuggestion searchSuggestion = mSearchSuggestionsAdapter.getItem(position); SearchSuggestionUtils.getInstance(getContext()).saveQuery(searchSuggestion.query); mSearchView.setText(searchSuggestion.query); } @Override public void onAppClicked(AppEntry app) { LauncherAppsCompat launcherApps = mLauncherAppsCompat; launcherApps.startActivityForProfile(app.getComponentName(), UserHandleCompat.myUserHandle(), null, null); // track a launch of the app in the app history ComponentName componentName = app.getComponentName(); AppHistoryUtils.trackAppLaunch(getContext(), componentName); // track in google analytics track(AnalyticsManager.ACTION_OPEN_ITEM, AnalyticsManager.CATEGORY_APPS, app.getComponentName().flattenToShortString()); SearchSuggestionUtils.getInstance(getContext()).saveQuery(mQuery); } @Override public void onPersonClicked(BaseContactInfo entry) { Uri uri = entry.mContactLookupUri; Intent intent = new Intent(Intent.ACTION_VIEW, uri); track(AnalyticsManager.ACTION_OPEN_ITEM, AnalyticsManager.CATEGORY_SEARCH); getContext().startActivity(intent); SearchSuggestionUtils.getInstance(getContext()).saveQuery(mQuery); } @Override public void onEditSelected(BaseContactInfo entry) { Uri uri = entry.mContactLookupUri; Intent intent = new Intent(Intent.ACTION_EDIT, uri); track(AnalyticsManager.ACTION_EDIT_ITEM, AnalyticsManager.CATEGORY_SEARCH); getContext().startActivity(intent); SearchSuggestionUtils.getInstance(getContext()).saveQuery(mQuery); } private static class RecognitionListenerImpl implements RecognitionListener { RecognitionListenerImpl() { } @Override public void onReadyForSpeech(Bundle params) { } @Override public void onBeginningOfSpeech() { } @Override public void onRmsChanged(float rmsdB) { } @Override public void onBufferReceived(byte[] buffer) { } @Override public void onEndOfSpeech() { } @Override public void onError(int error) { } @Override public void onResults(Bundle results) { // implement result here List<String> voice = results.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION); } @Override public void onPartialResults(Bundle partialResults) { // implement partial results here } @Override public void onEvent(int eventType, Bundle params) { } } static class SearchSuggestionViewHolder extends RecyclerView.ViewHolder { final TextView mTitleView; final AdapterView.OnItemClickListener mOnItemClickListener; public SearchSuggestionViewHolder(View itemView, final AdapterView.OnItemClickListener l) { super(itemView); mOnItemClickListener = l; mTitleView = (TextView) itemView.findViewById(R.id.recent_item_title); itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mOnItemClickListener.onItemClick(null, null, getPosition(), getItemId()); } }); } public void bind(SearchSuggestion item) { mTitleView.setText(item.query); } } static class SearchSuggestionsAdapter extends RecyclerView.Adapter<SearchSuggestionViewHolder> { final List<SearchSuggestion> mSearchSuggestions = new ArrayList<>(); AdapterView.OnItemClickListener mOnItemClickListener; SearchSuggestionsAdapter() { setHasStableIds(true); } @Override public SearchSuggestionViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { LayoutInflater inflater = LayoutInflater.from(parent.getContext()); View view = inflater.inflate(R.layout.list_item_recent_search, parent, false); return new SearchSuggestionViewHolder(view, mOnItemClickListener); } @Override public void onBindViewHolder(SearchSuggestionViewHolder holder, int position) { SearchSuggestion item = getItem(position); holder.bind(item); } public SearchSuggestion getItem(int position) { return mSearchSuggestions.get(position); } @Override public long getItemId(int position) { return mSearchSuggestions.get(position).id; } @Override public int getItemCount() { return mSearchSuggestions.size(); } public void setItems(List<SearchSuggestion> suggestions) { mSearchSuggestions.clear(); mSearchSuggestions.addAll(suggestions); notifyDataSetChanged(); } public void setOnItemClickListener(AdapterView.OnItemClickListener listener) { mOnItemClickListener = listener; } } /** * {@link android.support.v7.widget.LinearLayoutManager} which wraps its content. Note that * this class will always * wrap the content regardless of {@link android.support.v7.widget.RecyclerView} layout * parameters. * <p/> * Now it's impossible to run add/remove animations with child views which have arbitrary * dimensions (height for * VERTICAL orientation and width for HORIZONTAL). However if child views have fixed dimensions * {@link #setChildSize(int)} method might be used to let the layout manager know how big * they are going to be. * If animations are not used at all then a normal measuring procedure will run and child * views will be measured during * the measure pass. */ public static class WrapContentLinearLayoutManager extends android.support.v7.widget.LinearLayoutManager { private static final int CHILD_WIDTH = 0; private static final int CHILD_HEIGHT = 1; private static final int DEFAULT_CHILD_SIZE = 100; private final int[] childDimensions = new int[2]; private final RecyclerView view; private int childSize = DEFAULT_CHILD_SIZE; private boolean hasChildSize; private int overScrollMode = ViewCompat.OVER_SCROLL_ALWAYS; @SuppressWarnings("UnusedDeclaration") public WrapContentLinearLayoutManager(Context context) { super(context); this.view = null; } @SuppressWarnings("UnusedDeclaration") public WrapContentLinearLayoutManager(Context context, int orientation, boolean reverseLayout) { super(context, orientation, reverseLayout); this.view = null; } @SuppressWarnings("UnusedDeclaration") public WrapContentLinearLayoutManager(RecyclerView view) { super(view.getContext()); this.view = view; this.overScrollMode = ViewCompat.getOverScrollMode(view); } @SuppressWarnings("UnusedDeclaration") public WrapContentLinearLayoutManager(RecyclerView view, int orientation, boolean reverseLayout) { super(view.getContext(), orientation, reverseLayout); this.view = view; this.overScrollMode = ViewCompat.getOverScrollMode(view); } public static int makeUnspecifiedSpec() { return View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); } public void setOverScrollMode(int overScrollMode) { if (overScrollMode < ViewCompat.OVER_SCROLL_ALWAYS || overScrollMode > ViewCompat.OVER_SCROLL_NEVER) { throw new IllegalArgumentException("Unknown overscroll mode: " + overScrollMode); } if (this.view == null) throw new IllegalStateException("view == null"); this.overScrollMode = overScrollMode; ViewCompat.setOverScrollMode(view, overScrollMode); } @Override public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) { final int widthMode = View.MeasureSpec.getMode(widthSpec); final int heightMode = View.MeasureSpec.getMode(heightSpec); final int widthSize = View.MeasureSpec.getSize(widthSpec); final int heightSize = View.MeasureSpec.getSize(heightSpec); final boolean exactWidth = widthMode == View.MeasureSpec.EXACTLY; final boolean exactHeight = heightMode == View.MeasureSpec.EXACTLY; final int unspecified = makeUnspecifiedSpec(); if (exactWidth && exactHeight) { // in case of exact calculations for both dimensions let's use default // "onMeasure" implementation super.onMeasure(recycler, state, widthSpec, heightSpec); return; } final boolean vertical = getOrientation() == VERTICAL; initChildDimensions(widthSize, heightSize, vertical); int width = 0; int height = 0; // it's possible to get scrap views in recycler which are bound to old (invalid) // adapter entities. This // happens because their invalidation happens after "onMeasure" method. As a // workaround let's clear the // recycler now (it should not cause any performance issues while scrolling as // "onMeasure" is never // called whiles scrolling) recycler.clear(); final int stateItemCount = state.getItemCount(); final int adapterItemCount = getItemCount(); // adapter always contains actual data while state might contain old data (f.e. data // before the animation is // done). As we want to measure the view with actual data we must use data from the // adapter and not from the // state for (int i = 0; i < adapterItemCount; i++) { if (vertical) { if (!hasChildSize) { if (i < stateItemCount) { // we should not exceed state count, otherwise we'll get // IndexOutOfBoundsException. For such items // we will use previously calculated dimensions measureChild(recycler, i, widthSpec, unspecified, childDimensions); } else { logMeasureWarning(i); } } height += childDimensions[CHILD_HEIGHT]; if (i == 0) { width = childDimensions[CHILD_WIDTH]; } if (height >= heightSize) { break; } } else { if (!hasChildSize) { if (i < stateItemCount) { // we should not exceed state count, otherwise we'll get // IndexOutOfBoundsException. For such items // we will use previously calculated dimensions measureChild(recycler, i, unspecified, heightSpec, childDimensions); } else { logMeasureWarning(i); } } width += childDimensions[CHILD_WIDTH]; if (i == 0) { height = childDimensions[CHILD_HEIGHT]; } if (width >= widthSize) { break; } } } final boolean fit = (vertical && height < heightSize) || (!vertical && width < widthSize); if (fit) { // we really should wrap the contents of the view, let's do it if (exactWidth) { width = widthSize; } else { width += getPaddingLeft() + getPaddingRight(); } if (exactHeight) { height = heightSize; } else { height += getPaddingTop() + getPaddingBottom(); } setMeasuredDimension(width, height); } else { // if calculated height/width exceeds requested height/width let's use default // "onMeasure" implementation super.onMeasure(recycler, state, widthSpec, heightSpec); } if (view != null && overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS) { ViewCompat.setOverScrollMode(view, fit ? ViewCompat.OVER_SCROLL_NEVER : ViewCompat.OVER_SCROLL_ALWAYS); } } private void initChildDimensions(int width, int height, boolean vertical) { if (childDimensions[CHILD_WIDTH] != 0 || childDimensions[CHILD_HEIGHT] != 0) { // already initialized, skipping return; } if (vertical) { childDimensions[CHILD_WIDTH] = width; childDimensions[CHILD_HEIGHT] = childSize; } else { childDimensions[CHILD_WIDTH] = childSize; childDimensions[CHILD_HEIGHT] = height; } } private void measureChild(RecyclerView.Recycler recycler, int position, int widthSpec, int heightSpec, int[] dimensions) { final View child = recycler.getViewForPosition(position); final RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) child.getLayoutParams(); final int hPadding = getPaddingLeft() + getPaddingRight(); final int vPadding = getPaddingTop() + getPaddingBottom(); final int hMargin = p.leftMargin + p.rightMargin; final int vMargin = p.topMargin + p.bottomMargin; final int hDecoration = getRightDecorationWidth(child) + getLeftDecorationWidth(child); final int vDecoration = getTopDecorationHeight(child) + getBottomDecorationHeight(child); final int childWidthSpec = getChildMeasureSpec(widthSpec, hPadding + hMargin + hDecoration, p.width, canScrollHorizontally()); final int childHeightSpec = getChildMeasureSpec(heightSpec, vPadding + vMargin + vDecoration, p.height, canScrollVertically()); child.measure(childWidthSpec, childHeightSpec); dimensions[CHILD_WIDTH] = getDecoratedMeasuredWidth(child) + p.leftMargin + p.rightMargin; dimensions[CHILD_HEIGHT] = getDecoratedMeasuredHeight(child) + p.bottomMargin + p.topMargin; recycler.recycleView(child); } private void logMeasureWarning(int child) { if (BuildConfig.DEBUG) { Log.w("LinearLayoutManager", "Can't measure child #" + child + ", previously used dimensions will be reused." + "To remove this message either use #setChildSize() method or don't run " + "RecyclerView animations"); } } @Override public void setOrientation(int orientation) { // might be called before the constructor of this class is called //noinspection ConstantConditions if (childDimensions != null) { if (getOrientation() != orientation) { childDimensions[CHILD_WIDTH] = 0; childDimensions[CHILD_HEIGHT] = 0; } } super.setOrientation(orientation); } public void clearChildSize() { hasChildSize = false; setChildSize(DEFAULT_CHILD_SIZE); } public void setChildSize(int childSize) { hasChildSize = true; if (this.childSize != childSize) { this.childSize = childSize; requestLayout(); } } } class PeopleCallbacks implements LoaderManager.LoaderCallbacks<Cursor> { @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { Uri baseUri; if (mQuery != null) { baseUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_URI, Uri.encode(mQuery)); } else { baseUri = ContactsContract.Contacts.CONTENT_URI.buildUpon() .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(ContactsContract.Directory.DEFAULT)).build(); } // Now create and return a CursorLoader that will take care of // creating a Cursor for the data being displayed. String select = "((" + ContactsContract.Contacts.DISPLAY_NAME + " NOTNULL) AND (" + ContactsContract.Contacts.DISPLAY_NAME + " != '' ))"; return new CursorLoader(getContext(), baseUri, PeopleQuery.CONTACTS_SUMMARY_PROJECTION, select, null, ContactsContract.Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"); } public void onLoadFinished(Loader<Cursor> loader, Cursor data) { List<? extends BaseContactInfo> contactInfos = PeopleQuery.cursorToContactInfos(data); mSearchAdapter.setPeopleInfos(contactInfos); } @Override public void onLoaderReset(Loader<Cursor> loader) { } } class AppsCallbacks implements LoaderManager.LoaderCallbacks<List<AppEntry>> { @Override public Loader<List<AppEntry>> onCreateLoader(int id, Bundle args) { return new AppSearchLoader(getContext(), mLauncherAppsCompat, mQuery); } public void onLoadFinished(Loader<List<AppEntry>> loader, List<AppEntry> apps) { mSearchAdapter.setApps(apps); } @Override public void onLoaderReset(Loader<List<AppEntry>> loader) { } } class RecentSearchedCallbacks implements LoaderManager.LoaderCallbacks<List<SearchSuggestion>> { @Override public Loader<List<SearchSuggestion>> onCreateLoader(int id, Bundle args) { return new RecentSearchLoader(getContext()); } public void onLoadFinished(Loader<List<SearchSuggestion>> loader, List<SearchSuggestion> suggestions) { mSearchSuggestionsAdapter.setItems(suggestions); } @Override public void onLoaderReset(Loader<List<SearchSuggestion>> loader) { } } }