Java tutorial
/* * Copyright (C) 2016 Jorge Ruesga * * 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.ruesga.rview; import android.animation.Animator; import android.annotation.TargetApi; import android.content.Intent; import android.content.res.TypedArray; import android.databinding.DataBindingUtil; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.Parcel; import android.support.annotation.DrawableRes; import android.support.annotation.Keep; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.support.v4.graphics.drawable.DrawableCompat; import android.support.v4.view.ViewCompat; import android.support.v7.widget.ListPopupWindow; import android.text.Spannable; import android.text.Spanned; import android.text.TextUtils; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.view.Gravity; import android.view.KeyEvent; import android.view.View; import android.view.ViewAnimationUtils; import android.view.animation.AccelerateInterpolator; import com.arlib.floatingsearchview.FloatingSearchView; import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion; import com.google.gson.annotations.Since; import com.ruesga.rview.adapters.SimpleDropDownAdapter; import com.ruesga.rview.databinding.SearchActivityBinding; import com.ruesga.rview.gerrit.GerritApi; import com.ruesga.rview.gerrit.filter.ChangeQuery; import com.ruesga.rview.gerrit.filter.Option; import com.ruesga.rview.gerrit.filter.antlr.QueryParseException; import com.ruesga.rview.gerrit.model.AccountInfo; import com.ruesga.rview.gerrit.model.DocResult; import com.ruesga.rview.gerrit.model.ProjectInfo; import com.ruesga.rview.gerrit.model.ProjectType; import com.ruesga.rview.gerrit.model.ServerVersion; import com.ruesga.rview.misc.ActivityHelper; import com.ruesga.rview.misc.AndroidHelper; import com.ruesga.rview.misc.ModelHelper; import com.ruesga.rview.misc.StringHelper; import com.ruesga.rview.model.Account; import com.ruesga.rview.preferences.Constants; import com.ruesga.rview.preferences.Preferences; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import me.tatarka.rxloader2.RxLoader1; import me.tatarka.rxloader2.RxLoader2; import me.tatarka.rxloader2.RxLoaderManager; import me.tatarka.rxloader2.RxLoaderManagerCompat; import me.tatarka.rxloader2.RxLoaderObserver; import me.tatarka.rxloader2.safe.SafeObservable; public class SearchActivity extends AppCompatDelegateActivity { private static final int MAX_SUGGESTIONS = 5; private static final int FETCH_SUGGESTIONS_MESSAGE = 1; private static final int SHOW_HISTORY_MESSAGE = 2; private static final String EXTRA_REVEALED = "revealed"; @Keep @SuppressWarnings({ "UnusedParameters", "unused" }) public static class EventHandlers { private SearchActivity mActivity; public EventHandlers(SearchActivity activity) { mActivity = activity; } public void onDismissByOutsideTouch(View v) { mActivity.exitReveal(); } } private static class Suggestion implements SearchSuggestion { private final String mFilter; private final String mPartial; private final String mSuggestionText; private final String mSuggestionData; private final int mSuggestionIcon; private final boolean mHistory; Suggestion(String filter, String partial, String suggestion, String data, @DrawableRes int icon, boolean history) { mFilter = filter; mPartial = partial; mSuggestionText = suggestion; mSuggestionData = data; mSuggestionIcon = icon; mHistory = history; } Suggestion(Parcel in) { mFilter = in.readString(); mPartial = in.readString(); mSuggestionText = in.readString(); if (in.readInt() == 1) { mSuggestionData = in.readString(); } else { mSuggestionData = null; } mSuggestionIcon = in.readInt(); mHistory = in.readInt() == 1; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int flags) { parcel.writeString(mFilter); parcel.writeString(mPartial); parcel.writeString(mSuggestionText); parcel.writeInt(TextUtils.isEmpty(mSuggestionData) ? 0 : 1); if (!TextUtils.isEmpty(mSuggestionData)) { parcel.writeString(mSuggestionData); } parcel.writeInt(mSuggestionIcon); parcel.writeInt(mHistory ? 1 : 0); } @Override public String getBody() { return mPartial + mSuggestionText; } public static final Creator<Suggestion> CREATOR = new Creator<Suggestion>() { @Override public Suggestion createFromParcel(Parcel in) { return new Suggestion(in); } @Override public Suggestion[] newArray(int size) { return new Suggestion[size]; } }; } private static class AccountInfoResult { String mFilter; String mPartial; List<AccountInfo> mAccounts; } private static class ProjectInfoResult { String mFilter; String mPartial; List<String> mProjects; } private static class DocInfoResult { String mFilter; List<DocResult> mDocs; } private final RxLoaderObserver<AccountInfoResult> mAccountSuggestionsObserver = new RxLoaderObserver<AccountInfoResult>() { @Override public void onNext(AccountInfoResult response) { if (mBinding.searchView != null) { List<Suggestion> suggestions = new ArrayList<>(response.mAccounts.size()); for (AccountInfo account : response.mAccounts) { String suggestion = getString(R.string.account_suggest_format, account.name, account.email); suggestions.add(new Suggestion(response.mFilter, response.mPartial, suggestion, null, R.drawable.ic_search, false)); } mBinding.searchView.swapSuggestions(suggestions); } } }; @SuppressWarnings("Convert2streamapi") private final RxLoaderObserver<ProjectInfoResult> mProjectSuggestionsObserver = new RxLoaderObserver<ProjectInfoResult>() { @Override public void onNext(ProjectInfoResult result) { if (mBinding.searchView != null) { List<Suggestion> suggestions = new ArrayList<>(result.mProjects.size()); for (String project : result.mProjects) { try { suggestions.add(new Suggestion(result.mFilter, result.mPartial, URLDecoder.decode(project, "UTF-8"), null, R.drawable.ic_search, false)); } catch (UnsupportedEncodingException ex) { // Ignore } } mBinding.searchView.swapSuggestions(suggestions); } } }; private final RxLoaderObserver<DocInfoResult> mDocSuggestionsObserver = new RxLoaderObserver<DocInfoResult>() { @Override public void onNext(DocInfoResult response) { if (mBinding.searchView != null) { List<Suggestion> suggestions = new ArrayList<>(response.mDocs.size()); for (DocResult doc : response.mDocs) { suggestions.add( new Suggestion(response.mFilter, "", doc.title, doc.url, R.drawable.ic_search, false)); } mBinding.searchView.swapSuggestions(suggestions); } } }; private Handler.Callback mMessenger = message -> { if (message.what == FETCH_SUGGESTIONS_MESSAGE) { performFilter((String) message.obj); } else if (message.what == SHOW_HISTORY_MESSAGE) { performShowHistory(); } return false; }; private Handler mHandler; private Account mAccount; private SearchActivityBinding mBinding; private int mCurrentOption; private int[] mIcons; private RxLoader2<String, String, AccountInfoResult> mAccountSuggestionsLoader; private RxLoader2<String, String, ProjectInfoResult> mProjectSuggestionsLoader; private RxLoader1<String, DocInfoResult> mDocSuggestionsLoader; private List<String> mSuggestions; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mHandler = new Handler(mMessenger); mAccount = Preferences.getAccount(this); mCurrentOption = Preferences.getAccountSearchMode(this, mAccount); fillSuggestions(); mBinding = DataBindingUtil.setContentView(this, R.layout.search_activity); mBinding.setHandlers(new EventHandlers(this)); mIcons = loadSearchIcons(); if (getSupportActionBar() != null) { getSupportActionBar().setTitle(R.string.menu_search); getSupportActionBar().setDisplayHomeAsUpEnabled(false); getSupportActionBar().setDefaultDisplayHomeAsUpEnabled(false); } // Configure the suggestions loaders RxLoaderManager loaderManager = RxLoaderManagerCompat.get(this); mAccountSuggestionsLoader = loaderManager.create("accounts", this::fetchAccountSuggestions, mAccountSuggestionsObserver); mProjectSuggestionsLoader = loaderManager.create("projects", this::fetchProjectSuggestions, mProjectSuggestionsObserver); mDocSuggestionsLoader = loaderManager.create("docs", this::fetchDocSuggestions, mDocSuggestionsObserver); // Configure the search view mBinding.searchView.setOnSearchListener(new FloatingSearchView.OnSearchListener() { @Override public boolean onSuggestionClicked(SearchSuggestion suggestion) { final Suggestion s = (Suggestion) suggestion; // Let type more if (!s.mHistory && mCurrentOption == Constants.SEARCH_MODE_CUSTOM) { return true; } // Directly complete the search performSearch(s.mSuggestionText, s.mSuggestionData); return false; } @Override public void onSearchAction(String currentQuery) { performSearch(currentQuery, null); } }); mBinding.searchView.setOnQueryChangeListener((oldFilter, newFilter) -> { mHandler.removeMessages(SHOW_HISTORY_MESSAGE); mHandler.removeMessages(FETCH_SUGGESTIONS_MESSAGE); final Message msg; if (TextUtils.isEmpty(newFilter)) { clearSuggestions(); msg = Message.obtain(mHandler, SHOW_HISTORY_MESSAGE); } else { msg = Message.obtain(mHandler, FETCH_SUGGESTIONS_MESSAGE, newFilter); msg.arg1 = mCurrentOption; } mHandler.sendMessageDelayed(msg, 500L); }); mBinding.searchView.setOnBindSuggestionCallback((v, imageView, textView, suggestion, position) -> { final Suggestion s = (Suggestion) suggestion; textView.setText(performFilterHighlight(s)); if (s.mSuggestionIcon != 0) { Drawable dw = ContextCompat.getDrawable(this, s.mSuggestionIcon); DrawableCompat.setTint(dw, ContextCompat.getColor(this, R.color.gray_active_icon)); imageView.setImageDrawable(dw); } else { imageView.setImageDrawable(null); } }); mBinding.searchView.setOnMenuItemClickListener(item -> performShowOptions()); mBinding.searchView.setOnFocusChangeListener(new FloatingSearchView.OnFocusChangeListener() { @Override public void onFocus() { mHandler.removeMessages(FETCH_SUGGESTIONS_MESSAGE); mHandler.removeMessages(SHOW_HISTORY_MESSAGE); final Message msg = Message.obtain(mHandler, SHOW_HISTORY_MESSAGE); mHandler.sendMessageDelayed(msg, 500L); } @Override public void onFocusCleared() { // Ignore } }); mBinding.searchView.setOnClearSearchActionListener(this::performShowHistory); clearSuggestions(); mBinding.searchView.setCustomIcon(ContextCompat.getDrawable(this, mIcons[mCurrentOption])); configureSearchHint(); boolean revealed = false; if (savedInstanceState != null) { revealed = savedInstanceState.getBoolean(EXTRA_REVEALED, false); } if (!revealed) { enterReveal(); } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(EXTRA_REVEALED, true); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == ActivityHelper.LIST_RESULT_CODE && resultCode == RESULT_OK) { // Directly finish this activity. The search data was used finish(); } } @Override public void onBackPressed() { exitReveal(); } @Override public void onPause() { super.onPause(); if (AndroidHelper.isLollipopOrGreater()) { overridePendingTransition(0, 0); } } @Override public boolean dispatchKeyEvent(KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_DOWN) { switch (event.getKeyCode()) { case KeyEvent.KEYCODE_MENU: mBinding.searchView.openMenu(true); return true; } } return super.dispatchKeyEvent(event); } @Override public boolean onKeyDown(int keycode, KeyEvent e) { switch (keycode) { case KeyEvent.KEYCODE_MENU: mBinding.searchView.openMenu(true); return true; } return super.onKeyDown(keycode, e); } private void configureSearchHint() { switch (mCurrentOption) { case Constants.SEARCH_MODE_CHANGE: mBinding.searchView.setSearchHint(getString(R.string.search_by_change_hint)); break; case Constants.SEARCH_MODE_COMMIT: mBinding.searchView.setSearchHint(getString(R.string.search_by_commit_hint)); break; case Constants.SEARCH_MODE_PROJECT: mBinding.searchView.setSearchHint(getString(R.string.search_by_project_hint)); break; case Constants.SEARCH_MODE_USER: mBinding.searchView.setSearchHint(getString(R.string.search_by_user_hint)); break; case Constants.SEARCH_MODE_COMMIT_MESSAGE: mBinding.searchView.setSearchHint(getString(R.string.search_by_commit_message_hint)); break; case Constants.SEARCH_MODE_CUSTOM: mBinding.searchView.setSearchHint(getString(R.string.search_by_custom_hint)); break; case Constants.SEARCH_MODE_DOCS: mBinding.searchView.setSearchHint(getString(R.string.search_by_docs_hint)); break; } mBinding.searchView.setSearchText(null); } private void performShowOptions() { final ListPopupWindow popupWindow = new ListPopupWindow(this); ArrayList<String> values = new ArrayList<>( Arrays.asList(getResources().getStringArray(R.array.search_options_labels))); String value = values.get(mCurrentOption); SimpleDropDownAdapter adapter = new SimpleDropDownAdapter(this, values, mIcons, value); popupWindow.setAnchorView(mBinding.anchor); popupWindow.setDropDownGravity(Gravity.END); popupWindow.setAdapter(adapter); popupWindow.setContentWidth(adapter.measureContentWidth()); popupWindow.setOnItemClickListener((parent, view, position, id) -> { popupWindow.dismiss(); mCurrentOption = position; Preferences.setAccountSearchMode(this, mAccount, mCurrentOption); configureSearchHint(); mBinding.searchView.setCustomIcon(ContextCompat.getDrawable(this, mIcons[position])); clearSuggestions(); mHandler.removeMessages(FETCH_SUGGESTIONS_MESSAGE); mHandler.removeMessages(SHOW_HISTORY_MESSAGE); final Message msg = Message.obtain(mHandler, SHOW_HISTORY_MESSAGE); mHandler.sendMessageDelayed(msg, 500L); }); popupWindow.setModal(true); popupWindow.show(); } private void performShowHistory() { if (!TextUtils.isEmpty(mBinding.searchView.getText())) { return; } ArrayList<Suggestion> suggestions = new ArrayList<>(); if (mCurrentOption != Constants.SEARCH_MODE_DOCS) { String[] history = Preferences.getAccountSearchHistory(this, mAccount, mCurrentOption); if (history != null) { for (String s : history) { suggestions.add(new Suggestion("", "", s, null, R.drawable.ic_history, true)); } } Collections.reverse(suggestions); } mBinding.searchView.swapSuggestions(suggestions); } private void performFilter(String filter) { if (TextUtils.isEmpty(filter)) { return; } switch (mCurrentOption) { case Constants.SEARCH_MODE_CHANGE: case Constants.SEARCH_MODE_COMMIT: case Constants.SEARCH_MODE_COMMIT_MESSAGE: // We cannot show suggestion on this modes break; case Constants.SEARCH_MODE_PROJECT: requestProjectSuggestions(filter, ""); break; case Constants.SEARCH_MODE_USER: requestAccountSuggestions(filter, ""); break; case Constants.SEARCH_MODE_CUSTOM: requestCustomSuggestions(filter); break; case Constants.SEARCH_MODE_DOCS: requestDocSuggestions(filter); break; } } private void clearSuggestions() { mBinding.searchView.swapSuggestions(new ArrayList<>()); } private void performSearch(String query, String data) { if (TextUtils.isEmpty(query)) { clearSuggestions(); return; } ChangeQuery filter = null; switch (mCurrentOption) { case Constants.SEARCH_MODE_CHANGE: boolean isLegacyChangeNumber; try { int i = Integer.parseInt(query); isLegacyChangeNumber = i > 0; } catch (NumberFormatException ex) { isLegacyChangeNumber = false; } if (isLegacyChangeNumber || StringHelper.GERRIT_CHANGE.matcher(query).matches()) { filter = new ChangeQuery().change(query); } else { // Not a valid filter AndroidHelper.showErrorSnackbar(this, mBinding.getRoot(), R.string.search_not_a_valid_change); return; } break; case Constants.SEARCH_MODE_COMMIT: if (StringHelper.GERRIT_COMMIT.matcher(query).matches()) { filter = new ChangeQuery().commit(query); } else { // Not a valid filter AndroidHelper.showErrorSnackbar(this, mBinding.getRoot(), R.string.search_not_a_valid_commit); return; } break; case Constants.SEARCH_MODE_PROJECT: filter = new ChangeQuery().project(query); break; case Constants.SEARCH_MODE_USER: String cleanedQuery = clearOwnerQuery(query); filter = new ChangeQuery().owner(cleanedQuery); break; case Constants.SEARCH_MODE_COMMIT_MESSAGE: filter = new ChangeQuery().message(query); break; case Constants.SEARCH_MODE_CUSTOM: try { filter = ChangeQuery.parse(query); } catch (QueryParseException ex) { // Not a valid filter AndroidHelper.showErrorSnackbar(this, mBinding.getRoot(), R.string.search_not_a_valid_custom_query); return; } break; case Constants.SEARCH_MODE_DOCS: if (!TextUtils.isEmpty(data)) { final GerritApi api = ModelHelper.getGerritApi(this); //noinspection ConstantConditions ActivityHelper.openUriInCustomTabs(this, api.getDocumentationUri(data)); finish(); } return; } // Open the activity ActivityHelper.openChangeListByFilterActivity(this, null, filter, true, false); // Persist history String history = mCurrentOption != Constants.SEARCH_MODE_CUSTOM ? query : String.valueOf(filter); Preferences.addAccountSearchHistory(this, mAccount, mCurrentOption, history); } private int[] loadSearchIcons() { TypedArray ta = getResources().obtainTypedArray(R.array.search_options_icons); int count = ta.length(); int[] icons = new int[count]; for (int i = 0; i < count; i++) { icons[i] = ta.getResourceId(i, -1); } ta.recycle(); return icons; } @SuppressWarnings("ConstantConditions") private Observable<AccountInfoResult> fetchAccountSuggestions(String filter, String partial) { final GerritApi api = ModelHelper.getGerritApi(this); return SafeObservable.fromNullCallable(() -> { AccountInfoResult result = new AccountInfoResult(); result.mFilter = filter; result.mPartial = partial; result.mAccounts = api.getAccountsSuggestions(filter, MAX_SUGGESTIONS, Option.INSTANCE).blockingFirst(); return result; }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); } private void requestAccountSuggestions(String filter, String partial) { mAccountSuggestionsLoader.clear(); mAccountSuggestionsLoader.restart(filter, partial); } @SuppressWarnings("ConstantConditions") private Observable<ProjectInfoResult> fetchProjectSuggestions(String filter, String partial) { final GerritApi api = ModelHelper.getGerritApi(this); return SafeObservable.fromNullCallable(() -> { ProjectInfoResult result = new ProjectInfoResult(); result.mFilter = filter; result.mPartial = partial; result.mProjects = new ArrayList<>(api .getProjects(MAX_SUGGESTIONS, null, null, null, filter, null, null, null, ProjectType.ALL, null) .blockingFirst().keySet()); return result; }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); } private void requestProjectSuggestions(String filter, String partial) { mProjectSuggestionsLoader.clear(); mProjectSuggestionsLoader.restart(filter, partial); } @SuppressWarnings("ConstantConditions") private Observable<DocInfoResult> fetchDocSuggestions(String filter) { final GerritApi api = ModelHelper.getGerritApi(this); return SafeObservable.fromNullCallable(() -> { DocInfoResult result = new DocInfoResult(); result.mFilter = filter; result.mDocs = api.findDocumentation(filter).blockingFirst(); if (result.mDocs.size() > MAX_SUGGESTIONS) { result.mDocs = result.mDocs.subList(0, MAX_SUGGESTIONS); } return result; }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); } private void requestDocSuggestions(String filter) { mDocSuggestionsLoader.clear(); mDocSuggestionsLoader.restart(filter); } @SuppressWarnings("Convert2streamapi") private void requestCustomSuggestions(String filter) { // Do no perform suggestion when there are selection or cursor is not at the end // of the textview if (mBinding.searchView.getSelectionStart() != mBinding.searchView.getSelectionStart() || mBinding.searchView.getSelectionStart() < mBinding.searchView.getText().length()) { clearSuggestions(); return; } // Extract the current filter int pos = filter.lastIndexOf(" "); pos = pos == -1 ? 0 : ++pos; String currentFilter = filter.substring(pos); String partial = filter.substring(0, pos); // Some sanitize checks if (TextUtils.isEmpty(currentFilter)) { clearSuggestions(); return; } char c = filter.charAt(pos); if (!Character.isLetter(c) && !Character.isDigit(c)) { clearSuggestions(); return; } // Extract the token pos = currentFilter.indexOf(":"); if (pos != -1) { String token = currentFilter.substring(0, pos); currentFilter = currentFilter.substring(pos + 1); partial += token + ":"; if (TextUtils.isEmpty(currentFilter)) { clearSuggestions(); return; } final int index = Arrays.asList(ChangeQuery.FIELDS_NAMES).indexOf(token); if (index != -1) { Double version = ChangeQuery.SUPPORTED_VERSION[index]; ServerVersion serverVersion = mAccount.getServerVersion(); if (version == null || (serverVersion != null && version <= serverVersion.getVersion())) { Class clazz = ChangeQuery.SUGGEST_TYPES[index]; if (clazz != null) { if (clazz.equals(AccountInfo.class)) { requestAccountSuggestions(currentFilter, partial); return; } else if (clazz.equals(ProjectInfo.class)) { requestProjectSuggestions(currentFilter, partial); return; } } } } } final List<Suggestion> suggestions = new ArrayList<>(); String f = partial + currentFilter; pos = f.trim().lastIndexOf(" "); if (pos != -1) { partial = f.substring(0, pos + 1); f = f.substring(pos + 1); } else if (f.contains(":")) { partial = ""; } for (String s : mSuggestions) { if (s.startsWith(f) && !s.trim().equals(f)) { suggestions.add(new Suggestion(f, partial, s, null, R.drawable.ic_search, false)); } } mBinding.searchView.swapSuggestions(suggestions); } private CharSequence performFilterHighlight(Suggestion suggestion) { Spannable span = Spannable.Factory.getInstance().newSpannable(suggestion.mSuggestionText); int pos = 0; final Locale locale = AndroidHelper.getCurrentLocale(getApplicationContext()); final String suggestionNoCase = suggestion.mSuggestionText.toLowerCase(locale); final String filterNoCase = suggestion.mFilter.toLowerCase(locale); while ((pos = suggestionNoCase.indexOf(filterNoCase, pos)) != -1) { final int length = suggestion.mFilter.length(); if (length == 0) { break; } final StyleSpan bold = new StyleSpan(android.graphics.Typeface.BOLD); final ForegroundColorSpan color = new ForegroundColorSpan(ContextCompat.getColor(this, R.color.accent)); span.setSpan(bold, pos, pos + length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); span.setSpan(color, pos, pos + length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); pos += length; if (pos >= suggestionNoCase.length()) { break; } } return span; } private void fillSuggestions() { mSuggestions = new ArrayList<>(); int count = ChangeQuery.FIELDS_NAMES.length; for (int i = 0; i < count; i++) { Double version = ChangeQuery.SUPPORTED_VERSION[i]; ServerVersion serverVersion = mAccount.getServerVersion(); if (version == null || (serverVersion != null && version <= serverVersion.getVersion())) { mSuggestions.add(ChangeQuery.FIELDS_NAMES[i] + ":"); if (ChangeQuery.SUGGEST_TYPES[i] != null && ChangeQuery.SUGGEST_TYPES[i].isEnum()) { for (Object o : ChangeQuery.SUGGEST_TYPES[i].getEnumConstants()) { try { Since since = o.getClass().getDeclaredField(o.toString()).getAnnotation(Since.class); if (since != null && since.value() > serverVersion.getVersion()) { continue; } } catch (NoSuchFieldException ex) { // Ignore } String val = String.valueOf(o).toLowerCase(Locale.US); mSuggestions.add(ChangeQuery.FIELDS_NAMES[i] + ":" + val + " "); } } } } Collections.sort(mSuggestions); } private String clearOwnerQuery(String query) { if (query.startsWith("\"") && query.endsWith("\"") && query.length() >= 3) { return query.substring(1, query.length() - 1); } return query; } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private void enterReveal() { if (AndroidHelper.isLollipopOrGreater()) { ViewCompat.postOnAnimation(mBinding.toolbar, new RevealAnimationRunnable(mBinding.toolbar, mBinding.searchView, true)); } } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private void exitReveal() { if (!AndroidHelper.isLollipopOrGreater()) { finish(); return; } ViewCompat.postOnAnimation(mBinding.toolbar, new RevealAnimationRunnable(mBinding.toolbar, mBinding.searchView, false)); } private class RevealAnimationRunnable implements Runnable { private final View mTarget; private final View mParent; private final boolean mIn; RevealAnimationRunnable(View parent, View target, boolean in) { mTarget = target; mParent = parent; mIn = in; } @Override @TargetApi(Build.VERSION_CODES.LOLLIPOP) public void run() { int cx = mParent.getMeasuredWidth(); int cy = mParent.getMeasuredHeight() / 2; Animator anim = ViewAnimationUtils.createCircularReveal(mTarget, cx, cy, mIn ? 0 : cx, mIn ? cx : 0); anim.setInterpolator(new AccelerateInterpolator()); anim.setDuration(mIn ? 350L : 250L); anim.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { if (!mIn) { mBinding.toolbar.setVisibility(View.GONE); } } @Override public void onAnimationEnd(Animator animation) { mBinding.searchView.setVisibility(mIn ? View.VISIBLE : View.INVISIBLE); if (!mIn) { finish(); } } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); anim.start(); } } }