From 04747a3f78b9b6e80a44a93908da026af11662bd Mon Sep 17 00:00:00 2001 From: vng Date: Fri, 15 Jun 2012 23:33:50 -0700 Subject: [PATCH] [android] Initial implementation of search. --- android/AndroidManifest.xml | 6 + android/jni/Android.mk | 1 + android/jni/com/mapswithme/maps/Framework.cpp | 11 + android/jni/com/mapswithme/maps/Framework.hpp | 3 + .../com/mapswithme/maps/SearchActivity.cpp | 225 +++++++++++ android/res/layout/search_item.xml | 55 +++ android/res/layout/search_list_view.xml | 18 + .../src/com/mapswithme/maps/MWMActivity.java | 2 + .../com/mapswithme/maps/SearchActivity.java | 355 ++++++++++++++++++ 9 files changed, 676 insertions(+) create mode 100644 android/jni/com/mapswithme/maps/SearchActivity.cpp create mode 100644 android/res/layout/search_item.xml create mode 100644 android/res/layout/search_list_view.xml create mode 100644 android/src/com/mapswithme/maps/SearchActivity.java diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 86fa379a59..dbd325a4e7 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -50,5 +50,11 @@ android:noHistory="true" android:configChanges="orientation"> + + diff --git a/android/jni/Android.mk b/android/jni/Android.mk index 98dcd0bea7..33ec0e92c0 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -34,6 +34,7 @@ LOCAL_SRC_FILES := \ com/mapswithme/maps/Lifecycle.cpp \ com/mapswithme/maps/MapStorage.cpp \ com/mapswithme/maps/DownloadResourcesActivity.cpp \ + com/mapswithme/maps/SearchActivity.cpp \ com/mapswithme/platform/Platform.cpp \ com/mapswithme/platform/HttpThread.cpp \ com/mapswithme/platform/Language.cpp \ diff --git a/android/jni/com/mapswithme/maps/Framework.cpp b/android/jni/com/mapswithme/maps/Framework.cpp index eac74ecf39..d1f375b96a 100644 --- a/android/jni/com/mapswithme/maps/Framework.cpp +++ b/android/jni/com/mapswithme/maps/Framework.cpp @@ -376,6 +376,17 @@ namespace android m_work.ShowRect(r); } + void Framework::ShowSearchResult(search::Result const & r) + { + m_doLoadState = false; + m_work.ShowSearchResult(r); + } + + void Framework::Search(search::SearchParams const & params) + { + m_work.Search(params); + } + void Framework::LoadState() { if (!m_work.LoadState()) diff --git a/android/jni/com/mapswithme/maps/Framework.hpp b/android/jni/com/mapswithme/maps/Framework.hpp index 02ec226089..d830b7212f 100644 --- a/android/jni/com/mapswithme/maps/Framework.hpp +++ b/android/jni/com/mapswithme/maps/Framework.hpp @@ -83,6 +83,9 @@ namespace android /// Show rect from another activity. Ensure that no LoadState will be called, /// when maim map activity will become active. void ShowCountry(m2::RectD const & r); + void ShowSearchResult(search::Result const & r); + + void Search(search::SearchParams const & params); void LoadState(); void SaveState(); diff --git a/android/jni/com/mapswithme/maps/SearchActivity.cpp b/android/jni/com/mapswithme/maps/SearchActivity.cpp new file mode 100644 index 0000000000..46e8315502 --- /dev/null +++ b/android/jni/com/mapswithme/maps/SearchActivity.cpp @@ -0,0 +1,225 @@ +#include "Framework.hpp" + +#include "../../../../../search/result.hpp" + +#include "../../../../../map/measurement_utils.hpp" + +#include "../../../../../base/thread.hpp" + +#include "../core/jni_helper.hpp" + + +class SearchAdapter +{ + /// @name Results holder. Store last valid results from search threads (m_storeID) + /// and current result to show in GUI (m_ID). + //@{ + search::Results m_storeResults, m_results; + int m_storeID, m_ID; + //@} + + threads::Mutex m_updateMutex; + + /// Last saved activity to run update UI. + jobject m_activity; + + // This function may be called several times for one queryID. + // In that case we should increment m_storeID to distinguish different results. + // Main queryID is incremented by 5-step to leave space for middle queries. + // This constant should be equal with SearchActivity.QUERY_STEP; + static int const QUERY_STEP = 5; + + void OnResults(search::Results const & res, int queryID) + { + if (s_pInstance == 0) + { + // In case when activity is destroyed, but search thread passed any results. + return; + } + + threads::MutexGuard guard(m_updateMutex); + + // store current results + m_storeResults = res; + + if (m_storeID >= queryID && m_storeID < queryID + QUERY_STEP) + { + ++m_storeID; + // not more than QUERY_STEP results per query + ASSERT_LESS ( m_storeID, queryID + QUERY_STEP, () ); + } + else + { + ASSERT_LESS ( m_storeID, queryID, () ); + m_storeID = queryID; + } + + // get new environment pointer here because of different thread + JNIEnv * env = jni::GetEnv(); + + // post message to update ListView in UI thread + jmethodID const id = jni::GetJavaMethodID(env, m_activity, "updateData", "(II)V"); + env->CallVoidMethod(m_activity, id, + static_cast(m_storeResults.GetCount()), + static_cast(m_storeID)); + } + + bool AcquireShowResults(int resultID) + { + if (resultID != m_ID) + { + { + // Grab last results. + threads::MutexGuard guard(m_updateMutex); + m_results.Swap(m_storeResults); + m_ID = m_storeID; + } + + if (resultID != m_ID) + { + // It happens only when better results came faster than GUI. + // It is a rare case, skip this query. + ASSERT_GREATER ( m_ID, resultID, () ); + return false; + } + } + + return true; + } + + bool CheckPosition(int position) const + { + int const count = static_cast(m_results.GetCount()); + + // for safety reasons do actual check always + ASSERT_LESS ( position, count, () ); + return (position < count); + } + + SearchAdapter(jobject activity) : m_ID(0), m_storeID(0), m_activity(activity) + { + } + + static SearchAdapter * s_pInstance; + +public: + /// @name Instance lifetime functions. + /// TODO May be we should increment/deincrement global reference for m_activity + //@{ + static void CreateInstance(jobject activity) + { + ASSERT ( s_pInstance == 0, () ); + s_pInstance = new SearchAdapter(activity); + } + + static void DestroyInstance() + { + ASSERT ( s_pInstance, () ); + delete s_pInstance; + s_pInstance = 0; + } + + static SearchAdapter & Instance() + { + ASSERT ( s_pInstance, () ); + return *s_pInstance; + } + //@} + + void RunSearch(JNIEnv * env, search::SearchParams & params, int queryID) + { + params.m_callback = bind(&SearchAdapter::OnResults, this, _1, queryID); + + g_framework->Search(params); + } + + void ShowItem(int position) + { + if (CheckPosition(position)) + g_framework->ShowSearchResult(m_results.GetResult(position)); + } + + search::Result const * GetResult(int position, int resultID) + { + if (AcquireShowResults(resultID) && CheckPosition(position)) + return &(m_results.GetResult(position)); + return 0; + } +}; + +SearchAdapter * SearchAdapter::s_pInstance = 0; + +extern "C" +{ + +JNIEXPORT void JNICALL +Java_com_mapswithme_maps_SearchActivity_nativeInitSearch(JNIEnv * env, jobject thiz) +{ + SearchAdapter::CreateInstance(thiz); +} + +JNIEXPORT void JNICALL +Java_com_mapswithme_maps_SearchActivity_nativeFinishSearch(JNIEnv * env, jobject thiz) +{ + SearchAdapter::DestroyInstance(); +} + +JNIEXPORT void JNICALL +Java_com_mapswithme_maps_SearchActivity_nativeRunSearch( + JNIEnv * env, jobject thiz, + jstring s, jstring lang, jdouble lat, jdouble lon, jint mode, jint queryID) +{ + search::SearchParams params; + params.m_query = jni::ToNativeString(env, s); + params.SetInputLanguage(jni::ToNativeString(env, lang)); + if (mode != 0) + params.SetPosition(lat, lon); + + SearchAdapter::Instance().RunSearch(env, params, queryID); +} + +JNIEXPORT void JNICALL +Java_com_mapswithme_maps_SearchActivity_nativeShowItem(JNIEnv * env, jobject thiz, jint position) +{ + SearchAdapter::Instance().ShowItem(position); +} + +JNIEXPORT jobject JNICALL +Java_com_mapswithme_maps_SearchActivity_nativeGetResult( + JNIEnv * env, jobject thiz, jint position, jint queryID) +{ + search::Result const * res = SearchAdapter::Instance().GetResult(position, queryID); + if (res == 0) return 0; + + jclass klass = env->FindClass("com/mapswithme/maps/SearchActivity$SearchAdapter$SearchResult"); + ASSERT ( klass, () ); + + if (res->GetResultType() == search::Result::RESULT_FEATURE) + { + jmethodID methodID = env->GetMethodID( + klass, "", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); + ASSERT ( methodID, () ); + + string distance; + double const d = res->GetDistanceFromCenter(); + if (d >= 0.0) + CHECK ( MeasurementUtils::FormatDistance(d, distance), () ); + + return env->NewObject(klass, methodID, + jni::ToJavaString(env, res->GetString()), + jni::ToJavaString(env, res->GetRegionString()), + jni::ToJavaString(env, res->GetFeatureType()), + jni::ToJavaString(env, distance.c_str()), + jni::ToJavaString(env, res->GetRegionFlag())); + } + else + { + jmethodID methodID = env->GetMethodID(klass, "", "(Ljava/lang/String;)V"); + ASSERT ( methodID, () ); + + return env->NewObject(klass, methodID, jni::ToJavaString(env, res->GetSuggestionString())); + } +} + +} diff --git a/android/res/layout/search_item.xml b/android/res/layout/search_item.xml new file mode 100644 index 0000000000..6ef6c407f1 --- /dev/null +++ b/android/res/layout/search_item.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/android/res/layout/search_list_view.xml b/android/res/layout/search_list_view.xml new file mode 100644 index 0000000000..42a3540021 --- /dev/null +++ b/android/res/layout/search_list_view.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/src/com/mapswithme/maps/MWMActivity.java b/android/src/com/mapswithme/maps/MWMActivity.java index 782f9c657e..04443af1a7 100644 --- a/android/src/com/mapswithme/maps/MWMActivity.java +++ b/android/src/com/mapswithme/maps/MWMActivity.java @@ -262,6 +262,8 @@ public class MWMActivity extends NvEventQueueActivity implements LocationService { if (!nativeIsProVersion()) showProVersionBanner(getString(R.string.search_available_in_pro_version)); + else + startActivity(new Intent(this, SearchActivity.class)); } public void onDownloadClicked(View v) diff --git a/android/src/com/mapswithme/maps/SearchActivity.java b/android/src/com/mapswithme/maps/SearchActivity.java new file mode 100644 index 0000000000..ccb008bc69 --- /dev/null +++ b/android/src/com/mapswithme/maps/SearchActivity.java @@ -0,0 +1,355 @@ +package com.mapswithme.maps; + +import java.util.Locale; + +import android.app.Activity; +import android.app.ListActivity; +import android.content.Context; +import android.os.Bundle; +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.widget.BaseAdapter; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.TextView; + +import com.mapswithme.maps.location.LocationService; + + +public class SearchActivity extends ListActivity implements LocationService.Listener +{ + private static String TAG = "SearchActivity"; + + private static class SearchAdapter extends BaseAdapter + { + private Activity m_context; + private LayoutInflater m_inflater; + + int m_count = 0; + int m_resultID = 0; + + public SearchAdapter(Activity context) + { + m_context = context; + m_inflater = (LayoutInflater) m_context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + @Override + public int getItemViewType(int position) + { + return 0; + } + + @Override + public int getViewTypeCount() + { + return 1; + } + + @Override + public int getCount() + { + return m_count; + } + + @Override + public Object getItem(int position) + { + return position; + } + + @Override + public long getItemId(int position) + { + return position; + } + + private static class ViewHolder + { + public TextView m_name = null; + public TextView m_country = null; + public TextView m_distance = null; + public TextView m_amenity = null; + + void initFromView(View v) + { + m_name = (TextView) v.findViewById(R.id.name); + m_country = (TextView) v.findViewById(R.id.country); + m_distance = (TextView) v.findViewById(R.id.distance); + m_amenity = (TextView) v.findViewById(R.id.amenity); + } + } + + /// Created from native code. + public static class SearchResult + { + public String m_name; + public String m_country; + public String m_amenity; + public String m_distance; + public String m_flag; + + /// 0 - suggestion result + /// 1 - feature result + public int m_type; + + public SearchResult(String suggestion) + { + m_name = suggestion; + + m_type = 0; + } + public SearchResult(String name, String country, String amenity, + String distance, String flag) + { + m_name = name; + m_country = country; + m_amenity = amenity; + m_distance = distance; + m_flag = flag; + + m_type = 1; + } + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) + { + ViewHolder holder = null; + + if (convertView == null) + { + holder = new ViewHolder(); + + switch (getItemViewType(position)) + { + case 0: + convertView = m_inflater.inflate(R.layout.search_item, null); + holder.initFromView(convertView); + break; + } + + convertView.setTag(holder); + } + else + { + holder = (ViewHolder) convertView.getTag(); + } + + final SearchResult r = SearchActivity.nativeGetResult(position, m_resultID); + if (r != null) + { + holder.m_name.setText(r.m_name); + holder.m_country.setText(r.m_country); + holder.m_amenity.setText(r.m_amenity); + holder.m_distance.setText(r.m_distance); + } + + return convertView; + } + + /// Update list data. + public void updateData(int count, int resultID) + { + m_count = count; + m_resultID = resultID; + notifyDataSetChanged(); + } + + /// Show tapped country or get suggestion. + public String showCountry(int position) + { + final SearchResult r = SearchActivity.nativeGetResult(position, m_resultID); + if (r != null) + { + if (r.m_type == 1) + { + // show country and close activity + SearchActivity.nativeShowItem(position); + return null; + } + else + { + // advise suggestion + return r.m_name; + } + } + + // return an empty string as a suggestion + return ""; + } + } + + private EditText getSearchBox() + { + return (EditText) findViewById(R.id.search_string); + } + + private String getSearchString() + { + final String s = getSearchBox().getText().toString(); + Log.d(TAG, "Search string = " + s); + return s; + } + + private LocationService m_location; + + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + nativeInitSearch(); + + m_location = ((MWMApplication) getApplication()).getLocationService(); + + setContentView(R.layout.search_list_view); + + EditText v = getSearchBox(); + v.addTextChangedListener(new TextWatcher() + { + @Override + public void afterTextChanged(Editable s) + { + runSearch(); + } + + @Override + public void beforeTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) + { + } + + @Override + public void onTextChanged(CharSequence s, int arg1, int arg2, int arg3) + { + } + }); + + setListAdapter(new SearchAdapter(this)); + } + + @Override + protected void onDestroy() + { + super.onDestroy(); + + nativeFinishSearch(); + } + + @Override + protected void onResume() + { + super.onResume(); + + m_location.startUpdate(this); + + // do the search immediately after resume + runSearch(); + } + + @Override + protected void onPause() + { + super.onPause(); + + m_location.stopUpdate(this); + } + + private SearchAdapter getSA() + { + return (SearchAdapter) getListView().getAdapter(); + } + + @Override + protected void onListItemClick(ListView l, View v, int position, long id) + { + super.onListItemClick(l, v, position, id); + + final String suggestion = getSA().showCountry(position); + if (suggestion == null) + { + // close activity + finish(); + } + else + { + // set suggestion string and run search + getSearchBox().setText(suggestion); + runSearch(); + } + } + + /// Current position. + private double m_lat; + private double m_lon; + + /// It's should be equal to search::SearchParams::ModeT + /// Now it's just a flag to ensure that current position exists (!= 0). + int m_mode = 0; + + @Override + public void onLocationUpdated(long time, double lat, double lon, float accuracy) + { + m_mode = 1; + m_lat = lat; + m_lon = lon; + + runSearch(); + } + + @Override + public void onCompassUpdated(long time, double magneticNorth, double trueNorth, double accuracy) + { + } + + @Override + public void onLocationStatusChanged(int status) + { + } + + private int m_queryID = 0; + + /// Make 5-step increment to leave space for middle queries. + /// This constant should be equal with native SearchAdapter::QUERY_STEP; + private final static int QUERY_STEP = 5; + + public void updateData(final int count, final int resultID) + { + runOnUiThread(new Runnable() + { + @Override + public void run() + { + // emit only last query + if (resultID >= m_queryID && resultID < m_queryID + QUERY_STEP) + { + Log.d(TAG, "Show " + count + " results for id = " + resultID); + getSA().updateData(count, resultID); + } + } + }); + } + + private void runSearch() + { + // TODO Need to get input language + final String lang = Locale.getDefault().getLanguage(); + Log.d(TAG, "Current language = " + lang); + + m_queryID += QUERY_STEP; + nativeRunSearch(getSearchString(), lang, m_lat, m_lon, m_mode, m_queryID); + } + + private native void nativeInitSearch(); + private native void nativeFinishSearch(); + + private static native SearchAdapter.SearchResult nativeGetResult(int position, int queryID); + + private native void nativeRunSearch(String s, String lang, + double lat, double lon, int mode, int queryID); + private static native void nativeShowItem(int position); +}