package net.osmand.plus.activities.search; import android.annotation.SuppressLint; import android.app.Activity; import android.app.Dialog; import android.content.Intent; import android.content.pm.ActivityInfo; import android.graphics.Typeface; import android.os.AsyncTask; import android.os.AsyncTask.Status; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.text.Editable; import android.text.Spannable; import android.text.TextWatcher; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.view.inputmethod.EditorInfo; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.Filter; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.TextView.BufferType; import android.widget.TextView.OnEditorActionListener; import net.osmand.Collator; import net.osmand.CollatorStringMatcher; import net.osmand.CollatorStringMatcher.StringMatcherMode; import net.osmand.OsmAndCollator; import net.osmand.PlatformUtil; import net.osmand.data.LatLon; import net.osmand.data.MapObject; import net.osmand.data.PointDescription; import net.osmand.plus.OsmAndConstants; import net.osmand.plus.OsmandApplication; import net.osmand.plus.OsmandSettings; import net.osmand.plus.R; import net.osmand.plus.activities.MapActivity; import net.osmand.plus.activities.OsmandListActivity; import net.osmand.plus.activities.search.SearchAddressFragment.AddressInformation; import net.osmand.plus.dialogs.DirectionsDialogs; import net.osmand.plus.dialogs.FavoriteDialogs; import org.apache.commons.logging.Log; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @SuppressLint("NewApi") public abstract class SearchByNameAbstractActivity<T> extends OsmandListActivity { private static final String ENDING_TEXT = "ending_text"; private EditText searchText; private AsyncTask<Object, ?, ?> initializeTask; protected static final int MESSAGE_CLEAR_LIST = OsmAndConstants.UI_HANDLER_SEARCH + 2; protected static final int MESSAGE_ADD_ENTITY = OsmAndConstants.UI_HANDLER_SEARCH + 3; protected static final int MESSAGE_ADD_ENTITIES = OsmAndConstants.UI_HANDLER_SEARCH + 4; protected static final String SELECT_ADDRESS = "SEQUENTIAL_SEARCH"; protected ProgressBar progress; protected LatLon locationToSearch; protected OsmandSettings settings; protected List<T> initialListToFilter = new ArrayList<>(); protected Handler uiHandler; protected Collator collator; protected NamesFilter namesFilter; private String currentFilter = ""; private boolean initFilter = false; private String endingText = ""; private T endingObject; private StyleSpan previousSpan = new StyleSpan(Typeface.BOLD_ITALIC); private static final Log log = PlatformUtil.getLog(SearchByNameAbstractActivity.class); private static final int NAVIGATE_TO = 3; private static final int ADD_WAYPOINT = 4; private static final int SHOW_ON_MAP = 5; private static final int ADD_TO_FAVORITE = 6; private void separateMethod() { getWindow().setUiOptions(ActivityInfo.UIOPTION_SPLIT_ACTION_BAR_WHEN_NARROW); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); settings = ((OsmandApplication) getApplication()).getSettings(); setContentView(R.layout.search_by_name); initializeTask = getInitializeTask(); uiHandler = new UIUpdateHandler(); namesFilter = new NamesFilter(); addFooterViews(); final NamesAdapter namesAdapter = new NamesAdapter(new ArrayList<T>(), createComparator()); //$NON-NLS-1$ setListAdapter(namesAdapter); collator = OsmAndCollator.primaryCollator(); progress = (ProgressBar) findViewById(R.id.ProgressBar); searchText = (EditText) findViewById(R.id.SearchText); // ppenguin 2016-03-07: try to avoid full screen input in landscape mode (when softKB too large) searchText.setImeOptions(searchText.getImeOptions() | EditorInfo.IME_FLAG_NO_EXTRACT_UI | EditorInfo.IME_FLAG_NO_FULLSCREEN); searchText.addTextChangedListener(new TextWatcher(){ @Override public void afterTextChanged(Editable s) { String newFilter = s.toString(); String newEndingText = endingText; if (newEndingText.length() > 0) { while(!newFilter.endsWith(newEndingText) && newEndingText.length() > 0) { newEndingText = newEndingText.substring(1); } newFilter = newFilter.substring(0, newFilter.length() - newEndingText.length()); } updateTextBox(newFilter, newEndingText, endingObject, false); querySearch(newFilter); } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } }); // Not perfect // filter.setOnClickListener(new OnClickListener() { // } // }); // ppenguin 2016-03-07: try to avoid full screen input in landscape mode (when softKB too large) => IME-flags necessary here too! searchText.setImeOptions(EditorInfo.IME_ACTION_DONE | EditorInfo.IME_FLAG_NO_EXTRACT_UI | EditorInfo.IME_FLAG_NO_FULLSCREEN); searchText.requestFocus(); searchText.setOnEditorActionListener(new OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) { if(endingObject != null) { itemSelectedBase(endingObject, v); } return true; } return false; } }); progress.setVisibility(View.INVISIBLE); findViewById(R.id.ResetButton).setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View v) { reset(); } }); selectAddress = getIntent() != null && getIntent().hasExtra(SELECT_ADDRESS); getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); if (initializeTask != null){ initializeTask.execute(); } } protected void reset() { searchText.setText(""); } public String getLangPreferredName(MapObject mo) { return mo.getName(settings.MAP_PREFERRED_LOCALE.get(), settings.MAP_TRANSLITERATE_NAMES.get()); } protected void addFooterViews() { } public void setLabelText(int res) { getSupportActionBar().setSubtitle(getString(res)); } protected int getZoomToDisplay(T item){ return 15; } protected LatLon getLocation(T item) { if (item instanceof MapObject) { return ((MapObject) item).getLocation(); } return null; } public AsyncTask<Object, ?, ?> getInitializeTask(){ return null; } public Editable getFilter(){ return searchText.getText(); } public boolean initializeTaskIsFinished(){ return initializeTask == null || initializeTask.getStatus() == Status.FINISHED; } private int MAX_VISIBLE_NAME = 18; private boolean selectAddress; public String getCurrentFilter() { return currentFilter; } public void research() { initFilter = false; querySearch(currentFilter); } protected View getFooterView() { return null; } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString(ENDING_TEXT, endingText); } @Override protected void onRestoreInstanceState(Bundle prevState) { endingText = prevState.getString(ENDING_TEXT); if(endingText == null) { endingText = ""; } super.onRestoreInstanceState(prevState); } protected boolean isSelectAddres() { return selectAddress; } private void querySearch(final String filter) { if (!currentFilter.equals(filter) || !initFilter) { currentFilter = filter; initFilter = true; progress.setVisibility(View.VISIBLE); namesFilter.cancelPreviousFilter(filter); namesFilter.filter(filter); } } private void updateTextBox(String currentFilter, String locEndingText, T obj, boolean updateText) { String prevEndtext = endingText; endingText = locEndingText; endingObject = obj; if(updateText) { searchText.getText().replace(currentFilter.length(), currentFilter.length() + prevEndtext.length(), locEndingText); } searchText.getText().removeSpan(previousSpan); if (locEndingText.length() > 0) { searchText.getText().setSpan(previousSpan, currentFilter.length(), currentFilter.length() + locEndingText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); if (searchText.getSelectionEnd() > currentFilter.length()) { searchText.setSelection(currentFilter.length()); } } } protected void addObjectToInitialList(T initial){ initialListToFilter.add(initial); if (!namesFilter.active) { if (filterObject(initial, currentFilter)) { Message msg = uiHandler.obtainMessage(MESSAGE_ADD_ENTITY, initial); msg.sendToTarget(); } } } protected void finishInitializing(List<T> list){ Comparator<? super T> cmp = createComparator(); getListAdapter().sort(cmp); if (list != null) { Collections.sort(list,cmp); initialListToFilter = list; } research(); } protected abstract Comparator<? super T> createComparator(); public String getDistanceText(T obj) { return null; } public abstract String getText(T obj); public String getAdditionalFilterText(T obj) { return null; } public String getShortText(T obj) { return getText(obj); } public void itemSelectedBase(final T obj, View v) { itemSelected(obj); } public abstract void itemSelected(T obj); public boolean filterObject(T obj, String filter){ if(filter == null || filter.length() == 0){ return true; } boolean matches = CollatorStringMatcher.cmatches(collator, getText(obj), filter, StringMatcherMode.CHECK_STARTS_FROM_SPACE); if(!matches && getAdditionalFilterText(obj) != null) { matches = CollatorStringMatcher.cmatches(collator, getAdditionalFilterText(obj), filter, StringMatcherMode.CHECK_STARTS_FROM_SPACE); } return matches; } @Override protected void onResume() { super.onResume(); selectAddress = getIntent() != null && getIntent().getBooleanExtra(SELECT_ADDRESS, false); setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { T repo = getListAdapter().getItem(position); itemSelectedBase(repo, view); } }); Intent intent = getIntent(); if(intent != null){ if(intent.hasExtra(SearchActivity.SEARCH_LAT) && intent.hasExtra(SearchActivity.SEARCH_LON)){ double lat = intent.getDoubleExtra(SearchActivity.SEARCH_LAT, 0); double lon = intent.getDoubleExtra(SearchActivity.SEARCH_LON, 0); locationToSearch = new LatLon(lat, lon); } } if(locationToSearch == null){ locationToSearch = settings.getLastKnownMapLocation(); } } @Override public NamesAdapter getListAdapter() { return (NamesAdapter) super.getListAdapter(); } @Override protected void onPause() { super.onPause(); namesFilter.cancelPreviousFilter(currentFilter); } protected boolean filterLoop(String query, Collection<T> list) { boolean result = false; for (T obj : list) { if (namesFilter.isCancelled){ break; } if (filterObject(obj, query)){ result = true; Message msg = uiHandler.obtainMessage(MESSAGE_ADD_ENTITY, obj); msg.sendToTarget(); } } return result; } class UIUpdateHandler extends Handler { private Map<String, Integer> endingMap = new HashMap<>(); private int minimalIndex = Integer.MAX_VALUE; private String minimalText = null; @SuppressWarnings("unchecked") @Override public void handleMessage(Message msg) { String currentFilter = SearchByNameAbstractActivity.this.currentFilter; if (msg.what == MESSAGE_CLEAR_LIST) { minimalIndex = Integer.MAX_VALUE; minimalText = null; getListAdapter().clear(); if (currentFilter.length() == 0) { endingMap.clear(); } updateTextBox(currentFilter, "", null, true); } else if(msg.what == MESSAGE_ADD_ENTITY){ final Object obj = msg.obj; addObjectToAdapter(currentFilter, (T) obj); } else if (msg.what == MESSAGE_ADD_ENTITIES) { final List<T> objects = (List<T>) msg.obj; for (T object : objects) { addObjectToAdapter(currentFilter, object); } } } private void addObjectToAdapter(String currentFilter, T obj) { getListAdapter().add(obj); if (currentFilter.length() > 0) { String shortText = getShortText(obj); int entries = !endingMap.containsKey(shortText) ? 0 : endingMap.get(shortText); if (entries < minimalIndex) { if(minimalText != null) { endingMap.put(minimalText, endingMap.get(minimalText) - 1); } minimalIndex = entries; minimalText = shortText; endingMap.put(shortText, entries + 1); String locEndingText; if (shortText.toLowerCase().startsWith(currentFilter.toLowerCase())) { locEndingText = shortText.substring(currentFilter.length()); } else { locEndingText = " - " + shortText; } if (locEndingText.length() > MAX_VISIBLE_NAME) { locEndingText = locEndingText.substring(0, MAX_VISIBLE_NAME) + ".."; } updateTextBox(currentFilter, locEndingText, obj, true); } } } } class NamesFilter extends Filter { protected boolean isCancelled = false; private String newFilter; private boolean active = false; private long startTime; protected void cancelPreviousFilter(String newFilter){ this.newFilter = newFilter; isCancelled = true; } @Override protected FilterResults performFiltering(CharSequence constraint) { isCancelled = false; String query = constraint.toString(); if (query.equals(newFilter)) { active = true; startTime = System.currentTimeMillis(); uiHandler.sendEmptyMessage(MESSAGE_CLEAR_LIST); // make link copy Collection<T> list = initialListToFilter; filterLoop(query, list); active = false; } if (!isCancelled) { return new FilterResults(); } return null; } @Override protected void publishResults(CharSequence constraint, FilterResults results) { if(results != null && initializeTaskIsFinished()){ log.debug("Search " + constraint + " finished in " + (System.currentTimeMillis() - startTime)); progress.setVisibility(View.INVISIBLE); } } } protected class NamesAdapter extends ArrayAdapter<T> { NamesAdapter(List<T> list, Comparator<? super T> cmp) { super(SearchByNameAbstractActivity.this, R.layout.searchbyname_list, list); Collections.sort(list, cmp); } @Override public View getView(int position, View convertView, ViewGroup parent) { View row; if (convertView != null) { row = convertView; } else { LayoutInflater inflater = getLayoutInflater(); row = inflater.inflate(R.layout.searchbyname_list, parent, false); } TextView label = (TextView) row.findViewById(R.id.NameLabel); String distanceText = getDistanceText(getItem(position)); String text = getText(getItem(position)); if(distanceText == null) { label.setText(text); } else { label.setText(distanceText + " " + text, BufferType.SPANNABLE); ((Spannable) label.getText()).setSpan(new ForegroundColorSpan(getResources().getColor(R.color.color_distance)), 0, distanceText.length(), 0); } return row; } } protected void quitActivity(Class<? extends Activity> next) { finish(); if(next != null) { Intent intent = new Intent(this, next); if(getIntent() != null){ Intent cintent = getIntent(); if(cintent.hasExtra(SearchActivity.SEARCH_LAT) && cintent.hasExtra(SearchActivity.SEARCH_LON)){ intent.putExtra(SearchActivity.SEARCH_LAT, cintent.getDoubleExtra(SearchActivity.SEARCH_LAT, 0)); intent.putExtra(SearchActivity.SEARCH_LON, cintent.getDoubleExtra(SearchActivity.SEARCH_LON, 0)); } } intent.putExtra(SELECT_ADDRESS, selectAddress); startActivity(intent); } } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == 1) { finish(); return true; } else { select(item.getItemId()); } return super.onOptionsItemSelected(item); } @Override public boolean onCreateOptionsMenu(Menu menu) { if (!selectAddress && getAddressInformation() != null) { createMenuItem(menu, SHOW_ON_MAP, R.string.shared_string_show_on_map, R.drawable.ic_action_marker_dark, MenuItem.SHOW_AS_ACTION_ALWAYS); } else { createMenuItem(menu, 1, R.string.shared_string_ok, R.drawable.ic_action_done, MenuItem.SHOW_AS_ACTION_ALWAYS); } return super.onCreateOptionsMenu(menu); } protected AddressInformation getAddressInformation() { return null; } protected void select(int mode) { LatLon searchPoint = settings.getLastSearchedPoint(); AddressInformation ai = getAddressInformation(); if (ai != null && searchPoint != null) { if (mode == ADD_TO_FAVORITE) { Bundle b = new Bundle(); Dialog dlg = FavoriteDialogs.createAddFavouriteDialog(getActivity(), b); dlg.show(); FavoriteDialogs.prepareAddFavouriteDialog(getActivity(), dlg, b, searchPoint.getLatitude(), searchPoint.getLongitude(), new PointDescription(PointDescription.POINT_TYPE_ADDRESS, ai.objectName)); } else if (mode == NAVIGATE_TO) { DirectionsDialogs.directionsToDialogAndLaunchMap(getActivity(), searchPoint.getLatitude(), searchPoint.getLongitude(), ai.getHistoryName()); } else if (mode == ADD_WAYPOINT) { DirectionsDialogs.addWaypointDialogAndLaunchMap(getActivity(), searchPoint.getLatitude(), searchPoint.getLongitude(), ai.getHistoryName()); } else if (mode == SHOW_ON_MAP) { showOnMap(searchPoint, ai); } } } public void showOnMap(LatLon searchPoint, AddressInformation ai) { settings.setMapLocationToShow(searchPoint.getLatitude(), searchPoint.getLongitude(), ai.zoom, ai.getHistoryName()); MapActivity.launchMapActivityMoveToTop(getActivity()); } private Activity getActivity() { return this; } }