diff --git a/mobile/android/base/AwesomeBar.java b/mobile/android/base/AwesomeBar.java index 9c4a24b51fc..7f2dc2f8464 100644 --- a/mobile/android/base/AwesomeBar.java +++ b/mobile/android/base/AwesomeBar.java @@ -11,6 +11,7 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.ContentResolver; import android.content.Context; +import android.content.SharedPreferences; import android.content.res.Configuration; import android.database.Cursor; import android.graphics.Bitmap; @@ -42,6 +43,7 @@ import android.widget.TabWidget; import android.widget.Toast; import java.net.URLEncoder; +import java.util.ArrayList; import java.util.Map; import org.mozilla.gecko.db.BrowserContract.Bookmarks; @@ -54,6 +56,9 @@ import org.json.JSONObject; public class AwesomeBar extends GeckoActivity implements GeckoEventListener { private static final String LOGTAG = "GeckoAwesomeBar"; + private static final int SUGGESTION_TIMEOUT = 2000; + private static final int SUGGESTION_MAX = 3; + static final String URL_KEY = "url"; static final String CURRENT_URL_KEY = "currenturl"; static final String TYPE_KEY = "type"; @@ -67,6 +72,11 @@ public class AwesomeBar extends GeckoActivity implements GeckoEventListener { private ImageButton mGoButton; private ContentResolver mResolver; private ContextMenuSubject mContextMenuSubject; + private SuggestClient mSuggestClient; + private AsyncTask> mSuggestTask; + + private static String sSuggestEngine; + private static String sSuggestTemplate; @Override public void onCreate(Bundle savedInstanceState) { @@ -91,8 +101,20 @@ public class AwesomeBar extends GeckoActivity implements GeckoEventListener { openUrlAndFinish(url); } - public void onSearch(String engine) { - openSearchAndFinish(mText.getText().toString(), engine); + public void onSearch(String engine, String text) { + openSearchAndFinish(text, engine); + } + + public void onEditSuggestion(final String text) { + GeckoApp.mAppContext.mMainHandler.post(new Runnable() { + public void run() { + mText.setText(text); + mText.setSelection(mText.getText().length()); + mText.requestFocus(); + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(mText, InputMethodManager.SHOW_IMPLICIT); + } + }); } }); @@ -159,6 +181,24 @@ public class AwesomeBar extends GeckoActivity implements GeckoEventListener { // no composition string. It is safe to update IME flags. updateGoButton(text); + + // cancel previous query + if (mSuggestTask != null) { + mSuggestTask.cancel(true); + } + + if (mSuggestClient != null) { + mSuggestTask = new AsyncTask>() { + protected ArrayList doInBackground(String... query) { + return mSuggestClient.query(query[0]); + } + + protected void onPostExecute(ArrayList suggestions) { + mAwesomeTabs.setSuggestions(suggestions); + } + }; + mSuggestTask.execute(text); + } } public void beforeTextChanged(CharSequence s, int start, int count, @@ -190,14 +230,46 @@ public class AwesomeBar extends GeckoActivity implements GeckoEventListener { registerForContextMenu(mAwesomeTabs.findViewById(R.id.bookmarks_list)); registerForContextMenu(mAwesomeTabs.findViewById(R.id.history_list)); + if (sSuggestTemplate == null) { + loadSuggestClientFromPrefs(); + } else { + loadSuggestClient(); + } + GeckoAppShell.registerGeckoEventListener("SearchEngines:Data", this); GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:Get", null)); } + private void loadSuggestClientFromPrefs() { + GeckoAppShell.getHandler().post(new Runnable() { + public void run() { + SharedPreferences prefs = getSearchPreferences(); + sSuggestEngine = prefs.getString("suggestEngine", null); + sSuggestTemplate = prefs.getString("suggestTemplate", null); + if (sSuggestTemplate != null) { + loadSuggestClient(); + mAwesomeTabs.setSuggestEngine(sSuggestEngine, null); + } + } + }); + } + + private void loadSuggestClient() { + mSuggestClient = new SuggestClient(GeckoApp.mAppContext, sSuggestTemplate, SUGGESTION_TIMEOUT, SUGGESTION_MAX); + } + public void handleMessage(String event, JSONObject message) { try { if (event.equals("SearchEngines:Data")) { - mAwesomeTabs.setSearchEngines(message.getJSONArray("searchEngines")); + final String suggestEngine = message.optString("suggestEngine"); + final String suggestTemplate = message.optString("suggestTemplate"); + if (!TextUtils.equals(suggestTemplate, sSuggestTemplate)) { + saveSuggestEngineData(suggestEngine, suggestTemplate); + sSuggestEngine = suggestEngine; + sSuggestTemplate = suggestTemplate; + loadSuggestClient(); + } + mAwesomeTabs.setSearchEngines(suggestEngine, message.getJSONArray("searchEngines")); } } catch (Exception e) { // do nothing @@ -205,6 +277,18 @@ public class AwesomeBar extends GeckoActivity implements GeckoEventListener { } } + private void saveSuggestEngineData(final String suggestEngine, final String suggestTemplate) { + GeckoAppShell.getHandler().post(new Runnable() { + public void run() { + SharedPreferences prefs = getSearchPreferences(); + SharedPreferences.Editor editor = prefs.edit(); + editor.putString("suggestEngine", suggestEngine); + editor.putString("suggestTemplate", suggestTemplate); + editor.commit(); + } + }); + } + @Override public void onConfigurationChanged(Configuration newConfiguration) { super.onConfigurationChanged(newConfiguration); @@ -678,4 +762,8 @@ public class AwesomeBar extends GeckoActivity implements GeckoEventListener { mOnKeyPreImeListener = listener; } } + + private SharedPreferences getSearchPreferences() { + return getSharedPreferences("search.prefs", MODE_PRIVATE); + } } diff --git a/mobile/android/base/AwesomeBarTabs.java b/mobile/android/base/AwesomeBarTabs.java index 1730860828e..f94e98e2714 100644 --- a/mobile/android/base/AwesomeBarTabs.java +++ b/mobile/android/base/AwesomeBarTabs.java @@ -39,6 +39,7 @@ import android.widget.TextView; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.LinkedList; @@ -67,9 +68,10 @@ public class AwesomeBarTabs extends TabHost { private boolean mInflated; private LayoutInflater mInflater; private OnUrlOpenListener mUrlOpenListener; - private JSONArray mSearchEngines; private ContentResolver mContentResolver; private ContentObserver mContentObserver; + private SearchEngine mSuggestEngine; + private ArrayList mSearchEngines; private BookmarksQueryTask mBookmarksQueryTask; private HistoryQueryTask mHistoryQueryTask; @@ -86,16 +88,24 @@ public class AwesomeBarTabs extends TabHost { public interface OnUrlOpenListener { public void onUrlOpen(String url); - public void onSearch(String engine); + public void onSearch(String engine, String text); + public void onEditSuggestion(String suggestion); } - private class ViewHolder { + private class AwesomeEntryViewHolder { public TextView titleView; public TextView urlView; public ImageView faviconView; public ImageView starView; } + private class SearchEntryViewHolder { + public FlowLayout suggestionView; + public ImageView iconView; + public LinearLayout userEnteredView; + public TextView userEnteredTextView; + } + private class HistoryListAdapter extends SimpleExpandableListAdapter { public HistoryListAdapter(Context context, List> groupData, int groupLayout, String[] groupFrom, int[] groupTo, @@ -108,12 +118,12 @@ public class AwesomeBarTabs extends TabHost { @Override public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) { - ViewHolder viewHolder = null; + AwesomeEntryViewHolder viewHolder = null; if (convertView == null) { convertView = mInflater.inflate(R.layout.awesomebar_row, null); - viewHolder = new ViewHolder(); + viewHolder = new AwesomeEntryViewHolder(); viewHolder.titleView = (TextView) convertView.findViewById(R.id.title); viewHolder.urlView = (TextView) convertView.findViewById(R.id.url); viewHolder.faviconView = (ImageView) convertView.findViewById(R.id.favicon); @@ -121,7 +131,7 @@ public class AwesomeBarTabs extends TabHost { convertView.setTag(viewHolder); } else { - viewHolder = (ViewHolder) convertView.getTag(); + viewHolder = (AwesomeEntryViewHolder) convertView.getTag(); } @SuppressWarnings("unchecked") @@ -256,7 +266,7 @@ public class AwesomeBarTabs extends TabHost { @Override public View getView(int position, View convertView, ViewGroup parent) { int viewType = getItemViewType(position); - ViewHolder viewHolder = null; + AwesomeEntryViewHolder viewHolder = null; if (convertView == null) { if (viewType == VIEW_TYPE_ITEM) @@ -264,7 +274,7 @@ public class AwesomeBarTabs extends TabHost { else convertView = mInflater.inflate(R.layout.awesomebar_folder_row, null); - viewHolder = new ViewHolder(); + viewHolder = new AwesomeEntryViewHolder(); viewHolder.titleView = (TextView) convertView.findViewById(R.id.title); viewHolder.faviconView = (ImageView) convertView.findViewById(R.id.favicon); @@ -273,7 +283,7 @@ public class AwesomeBarTabs extends TabHost { convertView.setTag(viewHolder); } else { - viewHolder = (ViewHolder) convertView.getTag(); + viewHolder = (AwesomeEntryViewHolder) convertView.getTag(); } Cursor cursor = getCursor(); @@ -612,42 +622,45 @@ public class AwesomeBarTabs extends TabHost { public void onClick(); } - private class AwesomeBarCursorItem implements AwesomeBarItem { - private Cursor mCursor; - - public AwesomeBarCursorItem(Cursor cursor) { - mCursor = cursor; - } - - public void onClick() { - String url = mCursor.getString(mCursor.getColumnIndexOrThrow(URLColumns.URL)); - if (mUrlOpenListener != null) { - int display = mCursor.getInt(cursor.getColumnIndexOrThrow(Combined.DISPLAY)); - if (display == Combined.DISPLAY_READER) { - url = getReaderForUrl(url); - } - - mUrlOpenListener.onUrlOpen(url); - } - } - } - - private class AwesomeBarSearchEngineItem implements AwesomeBarItem { - private String mSearchEngine; - - public AwesomeBarSearchEngineItem(String searchEngine) { - mSearchEngine = searchEngine; - } - - public void onClick() { - if (mUrlOpenListener != null) - mUrlOpenListener.onSearch(mSearchEngine); - } - } - private class AwesomeBarCursorAdapter extends SimpleCursorAdapter { private String mSearchTerm; + private static final int ROW_SEARCH = 0; + private static final int ROW_STANDARD = 1; + + private class AwesomeBarCursorItem implements AwesomeBarItem { + private Cursor mCursor; + + public AwesomeBarCursorItem(Cursor cursor) { + mCursor = cursor; + } + + public void onClick() { + String url = mCursor.getString(mCursor.getColumnIndexOrThrow(URLColumns.URL)); + if (mUrlOpenListener != null) { + int display = mCursor.getInt(mCursor.getColumnIndexOrThrow(Combined.DISPLAY)); + if (display == Combined.DISPLAY_READER) { + url = getReaderForUrl(url); + } + + mUrlOpenListener.onUrlOpen(url); + } + } + } + + private class AwesomeBarSearchEngineItem implements AwesomeBarItem { + private String mSearchEngine; + + public AwesomeBarSearchEngineItem(String searchEngine) { + mSearchEngine = searchEngine; + } + + public void onClick() { + if (mUrlOpenListener != null) + mUrlOpenListener.onSearch(mSearchEngine, mSearchTerm); + } + } + public AwesomeBarCursorAdapter(Context context) { super(context, -1, null, new String[] {}, new int[] {}); mSearchTerm = ""; @@ -658,58 +671,123 @@ public class AwesomeBarTabs extends TabHost { getFilter().filter(searchTerm); } + private int getSuggestEngineCount() { + return (mSearchTerm.length() == 0 || mSuggestEngine == null) ? 0 : 1; + } + // Add the search engines to the number of reported results. @Override public int getCount() { final int resultCount = super.getCount(); - // don't show additional search engines if search field is empty + // don't show search engines or suggestions if search field is empty if (mSearchTerm.length() == 0) return resultCount; - return resultCount + mSearchEngines.length(); + return resultCount + mSearchEngines.size() + getSuggestEngineCount(); } // If an item is part of the cursor result set, return that entry. // Otherwise, return the search engine data. @Override public Object getItem(int position) { - final int resultCount = super.getCount(); - if (position < resultCount) - return new AwesomeBarCursorItem((Cursor) super.getItem(position)); + int engineIndex = getEngineIndex(position); - JSONObject engine; - String engineName = null; - try { - engine = mSearchEngines.getJSONObject(position - resultCount); - engineName = engine.getString("name"); - } catch (JSONException e) { - Log.e(LOGTAG, "Error getting search engine JSON", e); + if (engineIndex == -1) { + // return awesomebar result + position -= getSuggestEngineCount(); + return new AwesomeBarCursorItem((Cursor) super.getItem(position)); } - return new AwesomeBarSearchEngineItem(engineName); + // return search engine + return new AwesomeBarSearchEngineItem(getEngine(engineIndex).name); + } + + private SearchEngine getEngine(int index) { + final int suggestEngineCount = getSuggestEngineCount(); + if (index < suggestEngineCount) + return mSuggestEngine; + return mSearchEngines.get(index - suggestEngineCount); + } + + private int getEngineIndex(int position) { + final int resultCount = super.getCount(); + final int suggestEngineCount = getSuggestEngineCount(); + + // return suggest engine index + if (position < suggestEngineCount) + return 0; + + // not an engine + if (position - suggestEngineCount < resultCount) + return -1; + + // return search engine index + return position - resultCount; + } + + @Override + public int getItemViewType(int position) { + return getEngineIndex(position) == -1 ? ROW_STANDARD : ROW_SEARCH; + } + + @Override + public int getViewTypeCount() { + // view can be either a standard awesomebar row or a search engine row + return 2; + } + + @Override + public boolean isEnabled(int position) { + // If the suggestion row only contains one item (the user-entered + // query), allow the entire row to be clickable; clicking the row + // has the same effect as clicking the single suggestion. If the + // row contains multiple items, clicking the row will do nothing. + int index = getEngineIndex(position); + if (index != -1) { + return getEngine(index).suggestions.isEmpty(); + } + return true; } @Override public View getView(int position, View convertView, ViewGroup parent) { - ViewHolder viewHolder = null; + if (getItemViewType(position) == ROW_SEARCH) { + SearchEntryViewHolder viewHolder = null; - if (convertView == null) { - convertView = mInflater.inflate(R.layout.awesomebar_row, null); + if (convertView == null) { + convertView = mInflater.inflate(R.layout.awesomebar_suggestion_row, null); - viewHolder = new ViewHolder(); - viewHolder.titleView = (TextView) convertView.findViewById(R.id.title); - viewHolder.urlView = (TextView) convertView.findViewById(R.id.url); - viewHolder.faviconView = (ImageView) convertView.findViewById(R.id.favicon); - viewHolder.starView = (ImageView) convertView.findViewById(R.id.bookmark_star); + viewHolder = new SearchEntryViewHolder(); + viewHolder.suggestionView = (FlowLayout) convertView.findViewById(R.id.suggestion_layout); + viewHolder.iconView = (ImageView) convertView.findViewById(R.id.suggestion_icon); + viewHolder.userEnteredView = (LinearLayout) convertView.findViewById(R.id.suggestion_user_entered); + viewHolder.userEnteredTextView = (TextView) convertView.findViewById(R.id.suggestion_text); - convertView.setTag(viewHolder); + convertView.setTag(viewHolder); + } else { + viewHolder = (SearchEntryViewHolder) convertView.getTag(); + } + + bindSearchEngineView(getEngine(getEngineIndex(position)), viewHolder); } else { - viewHolder = (ViewHolder) convertView.getTag(); - } + AwesomeEntryViewHolder viewHolder = null; - final int resultCount = super.getCount(); - if (position < resultCount) { + if (convertView == null) { + convertView = mInflater.inflate(R.layout.awesomebar_row, null); + + viewHolder = new AwesomeEntryViewHolder(); + viewHolder.titleView = (TextView) convertView.findViewById(R.id.title); + viewHolder.urlView = (TextView) convertView.findViewById(R.id.url); + viewHolder.faviconView = (ImageView) convertView.findViewById(R.id.favicon); + viewHolder.starView = (ImageView) convertView.findViewById(R.id.bookmark_star); + + convertView.setTag(viewHolder); + } else { + viewHolder = (AwesomeEntryViewHolder) convertView.getTag(); + } + + position -= getSuggestEngineCount(); Cursor cursor = getCursor(); if (!cursor.moveToPosition(position)) throw new IllegalStateException("Couldn't move cursor to position " + position); @@ -718,45 +796,69 @@ public class AwesomeBarTabs extends TabHost { updateUrl(viewHolder.urlView, cursor); updateFavicon(viewHolder.faviconView, cursor); updateBookmarkStar(viewHolder.starView, cursor); - } else { - bindSearchEngineView(position - resultCount, viewHolder); } return convertView; } - private Drawable getDrawableFromDataURI(String dataURI) { - String base64 = dataURI.substring(dataURI.indexOf(',') + 1); - Drawable drawable = null; - try { - byte[] bytes = GeckoAppShell.decodeBase64(base64, GeckoAppShell.BASE64_DEFAULT); - ByteArrayInputStream stream = new ByteArrayInputStream(bytes); - drawable = Drawable.createFromStream(stream, "src"); - stream.close(); - } catch (IllegalArgumentException e) { - Log.i(LOGTAG, "exception while decoding drawable: " + base64, e); - } catch (IOException e) { } - return drawable; - } + private void bindSearchEngineView(final SearchEngine engine, SearchEntryViewHolder viewHolder) { + // when a suggestion is clicked, do a search + OnClickListener clickListener = new OnClickListener() { + public void onClick(View v) { + if (mUrlOpenListener != null) { + String suggestion = ((TextView) v.findViewById(R.id.suggestion_text)).getText().toString(); + mUrlOpenListener.onSearch(engine.name, suggestion); + } + } + }; - private void bindSearchEngineView(int position, ViewHolder viewHolder) { - String name; - String iconURI; - String searchText = getResources().getString(R.string.awesomebar_search_engine, mSearchTerm); - try { - JSONObject searchEngine = mSearchEngines.getJSONObject(position); - name = searchEngine.getString("name"); - iconURI = searchEngine.getString("iconURI"); - } catch (JSONException e) { - Log.e(LOGTAG, "Error getting search engine JSON", e); - return; + // when a suggestion is long-clicked, copy the suggestion into the URL EditText + OnLongClickListener longClickListener = new OnLongClickListener() { + public boolean onLongClick(View v) { + if (mUrlOpenListener != null) { + String suggestion = ((TextView) v.findViewById(R.id.suggestion_text)).getText().toString(); + mUrlOpenListener.onEditSuggestion(suggestion); + return true; + } + return false; + } + }; + + // set the search engine icon (e.g., Google) for the row + FlowLayout suggestionView = viewHolder.suggestionView; + viewHolder.iconView.setImageDrawable(engine.icon); + + // user-entered search term is first suggestion + viewHolder.userEnteredTextView.setText(mSearchTerm); + viewHolder.userEnteredView.setOnClickListener(clickListener); + + // add additional suggestions given by this engine + int recycledSuggestionCount = suggestionView.getChildCount(); + int suggestionCount = engine.suggestions.size(); + int i = 0; + for (i = 0; i < suggestionCount; i++) { + String suggestion = engine.suggestions.get(i); + View suggestionItem = null; + + // reuse suggestion views from recycled view, if possible + if (i+1 < recycledSuggestionCount) { + suggestionItem = suggestionView.getChildAt(i+1); + suggestionItem.setVisibility(View.VISIBLE); + } else { + suggestionItem = mInflater.inflate(R.layout.awesomebar_suggestion_item, null); + ((ImageView) suggestionItem.findViewById(R.id.suggestion_magnifier)).setVisibility(View.GONE); + suggestionView.addView(suggestionItem); + } + ((TextView) suggestionItem.findViewById(R.id.suggestion_text)).setText(suggestion); + + suggestionItem.setOnClickListener(clickListener); + suggestionItem.setOnLongClickListener(longClickListener); + } + + // hide extra suggestions that have been recycled + for (++i; i < recycledSuggestionCount; i++) { + suggestionView.getChildAt(i).setVisibility(View.GONE); } - - viewHolder.titleView.setText(name); - viewHolder.urlView.setText(searchText); - Drawable drawable = getDrawableFromDataURI(iconURI); - viewHolder.faviconView.setImageDrawable(drawable); - viewHolder.starView.setVisibility(View.GONE); } }; @@ -767,7 +869,7 @@ public class AwesomeBarTabs extends TabHost { mContext = context; mInflated = false; - mSearchEngines = new JSONArray(); + mSearchEngines = new ArrayList(); mContentResolver = context.getContentResolver(); mContentObserver = null; mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); @@ -1043,10 +1145,98 @@ public class AwesomeBarTabs extends TabHost { mAllPagesCursorAdapter.filter(searchTerm); } - public void setSearchEngines(final JSONArray engines) { + private Drawable getDrawableFromDataURI(String dataURI) { + String base64 = dataURI.substring(dataURI.indexOf(',') + 1); + Drawable drawable = null; + try { + byte[] bytes = GeckoAppShell.decodeBase64(base64, GeckoAppShell.BASE64_DEFAULT); + ByteArrayInputStream stream = new ByteArrayInputStream(bytes); + drawable = Drawable.createFromStream(stream, "src"); + stream.close(); + } catch (IllegalArgumentException e) { + Log.i(LOGTAG, "exception while decoding drawable: " + base64, e); + } catch (IOException e) { } + return drawable; + } + + private class SearchEngine { + public String name; + public Drawable icon; + public ArrayList suggestions; + + public SearchEngine(String name) { + this(name, null); + } + + public SearchEngine(String name, Drawable icon) { + this.name = name; + this.icon = icon; + this.suggestions = new ArrayList(); + } + }; + + /** + * Sets the suggest engine, which will show suggestions for user-entered queries. + * If the suggest engine has already been set, it will be replaced, and its + * suggestions will be copied to the new suggest engine. + */ + public void setSuggestEngine(String name, Drawable icon) { + // We currently save the suggest engine in shared preferences, so this + // method is called immediately when the AwesomeBar is created. It's + // called again in setSuggestions(), when the list of search engines is + // received from Gecko (in case the suggestion engine has changed). + final SearchEngine suggestEngine = new SearchEngine(name, icon); + if (mSuggestEngine != null) + suggestEngine.suggestions = mSuggestEngine.suggestions; + GeckoAppShell.getMainHandler().post(new Runnable() { public void run() { - mSearchEngines = engines; + mSuggestEngine = suggestEngine; + mAllPagesCursorAdapter.notifyDataSetChanged(); + } + }); + } + + /** + * Sets suggestions associated with the current suggest engine. + * If there is no suggest engine, this does nothing. + */ + public void setSuggestions(final ArrayList suggestions) { + GeckoAppShell.getMainHandler().post(new Runnable() { + public void run() { + if (mSuggestEngine != null) { + mSuggestEngine.suggestions = suggestions; + mAllPagesCursorAdapter.notifyDataSetChanged(); + } + } + }); + } + + /** + * Sets search engines to be shown for user-entered queries. + */ + public void setSearchEngines(String suggestEngine, JSONArray engines) { + final ArrayList searchEngines = new ArrayList(); + for (int i = 0; i < engines.length(); i++) { + try { + JSONObject engineJSON = engines.getJSONObject(i); + String name = engineJSON.getString("name"); + String iconURI = engineJSON.getString("iconURI"); + Drawable icon = getDrawableFromDataURI(iconURI); + if (name.equals(suggestEngine)) { + setSuggestEngine(name, icon); + } else { + searchEngines.add(new SearchEngine(name, icon)); + } + } catch (JSONException e) { + Log.e(LOGTAG, "Error getting search engine JSON", e); + return; + } + } + + GeckoAppShell.getMainHandler().post(new Runnable() { + public void run() { + mSearchEngines = searchEngines; mAllPagesCursorAdapter.notifyDataSetChanged(); } }); @@ -1058,7 +1248,10 @@ public class AwesomeBarTabs extends TabHost { @Override public boolean onInterceptTouchEvent(MotionEvent ev) { - hideSoftInput(this); + // we should only have to hide the soft keyboard once - when the user + // initially touches the screen + if (ev.getAction() == MotionEvent.ACTION_DOWN) + hideSoftInput(this); // the android docs make no sense, but returning false will cause this and other // motion events to be sent to the view the user tapped on diff --git a/mobile/android/base/Makefile.in b/mobile/android/base/Makefile.in index cc541469709..a200360a905 100644 --- a/mobile/android/base/Makefile.in +++ b/mobile/android/base/Makefile.in @@ -98,6 +98,7 @@ FENNEC_JAVA_FILES = \ RemoteTabs.java \ SetupScreen.java \ SiteIdentityPopup.java \ + SuggestClient.java \ SurfaceBits.java \ Tab.java \ Tabs.java \ @@ -254,6 +255,8 @@ RES_LAYOUT = \ res/layout/awesomebar_folder_row.xml \ res/layout/awesomebar_header_row.xml \ res/layout/awesomebar_row.xml \ + res/layout/awesomebar_suggestion_item.xml \ + res/layout/awesomebar_suggestion_row.xml \ res/layout/awesomebar_search.xml \ res/layout/awesomebar_tab_indicator.xml \ res/layout/awesomebar_tabs.xml \ @@ -815,6 +818,7 @@ MOZ_ANDROID_DRAWABLES += \ mobile/android/base/resources/drawable/remote_tabs_group_bg_repeat.xml \ mobile/android/base/resources/drawable/start.png \ mobile/android/base/resources/drawable/site_security_level.xml \ + mobile/android/base/resources/drawable/suggestion_selector.xml \ mobile/android/base/resources/drawable/tabs_button.xml \ mobile/android/base/resources/drawable/tabs_level.xml \ mobile/android/base/resources/drawable/tabs_tray_bg_repeat.xml \ diff --git a/mobile/android/base/SuggestClient.java b/mobile/android/base/SuggestClient.java new file mode 100644 index 00000000000..7c462532cd8 --- /dev/null +++ b/mobile/android/base/SuggestClient.java @@ -0,0 +1,127 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + +import org.json.JSONArray; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.text.TextUtils; +import android.util.Log; + +import java.io.BufferedInputStream; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.util.ArrayList; + +/** + * Use network-based search suggestions. + */ +public class SuggestClient { + private static final String LOGTAG = "GeckoSuggestClient"; + private static final String USER_AGENT = GeckoApp.mAppContext.getDefaultUAString(); + + private final Context mContext; + private final int mTimeout; + + // should contain the string "__searchTerms__", which is replaced with the query + private final String mSuggestTemplate; + + // the maximum number of suggestions to return + private final int mMaxResults; + + public SuggestClient(Context context, String suggestTemplate, int timeout, int maxResults) { + mContext = context; + mMaxResults = maxResults; + mSuggestTemplate = suggestTemplate; + mTimeout = timeout; + } + + public SuggestClient(Context context, String suggestTemplate, int timeout) { + this(context, suggestTemplate, timeout, Integer.MAX_VALUE); + } + + /** + * Queries for a given search term and returns an ArrayList of suggestions. + */ + public ArrayList query(String query) { + ArrayList suggestions = new ArrayList(); + if (TextUtils.isEmpty(mSuggestTemplate) || TextUtils.isEmpty(query)) { + return suggestions; + } + + if (!isNetworkConnected()) { + Log.i(LOGTAG, "Not connected to network"); + return suggestions; + } + + try { + String encoded = URLEncoder.encode(query, "UTF-8"); + String suggestUri = mSuggestTemplate.replace("__searchTerms__", encoded); + + URL url = new URL(suggestUri); + String json = null; + HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); + try { + urlConnection.setConnectTimeout(mTimeout); + urlConnection.setRequestProperty("User-Agent", USER_AGENT); + InputStream in = new BufferedInputStream(urlConnection.getInputStream()); + json = convertStreamToString(in); + } finally { + urlConnection.disconnect(); + } + + if (json != null) { + /* + * Sample result: + * ["foo",["food network","foothill college","foot locker",...]] + */ + JSONArray results = new JSONArray(json); + JSONArray jsonSuggestions = results.getJSONArray(1); + + int added = 0; + for (int i = 0; (i < jsonSuggestions.length()) && (added < mMaxResults); i++) { + String suggestion = jsonSuggestions.getString(i); + if (!suggestion.equalsIgnoreCase(query)) { + suggestions.add(suggestion); + added++; + } + } + } else { + Log.d(LOGTAG, "Suggestion query failed"); + } + } catch (InterruptedIOException e) { + Log.d(LOGTAG, "Suggestion query interrupted"); + } catch (Exception e) { + Log.w(LOGTAG, "Error", e); + } + return suggestions; + } + + private boolean isNetworkConnected() { + NetworkInfo networkInfo = getActiveNetworkInfo(); + return networkInfo != null && networkInfo.isConnected(); + } + + private NetworkInfo getActiveNetworkInfo() { + ConnectivityManager connectivity = (ConnectivityManager) mContext + .getSystemService(Context.CONNECTIVITY_SERVICE); + if (connectivity == null) + return null; + return connectivity.getActiveNetworkInfo(); + } + + private String convertStreamToString(java.io.InputStream is) { + try { + return new java.util.Scanner(is).useDelimiter("\\A").next(); + } catch (java.util.NoSuchElementException e) { + return ""; + } + } +} diff --git a/mobile/android/base/resources/drawable/suggestion_selector.xml b/mobile/android/base/resources/drawable/suggestion_selector.xml new file mode 100644 index 00000000000..99a0034c3f3 --- /dev/null +++ b/mobile/android/base/resources/drawable/suggestion_selector.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/base/resources/layout/awesomebar_suggestion_item.xml b/mobile/android/base/resources/layout/awesomebar_suggestion_item.xml new file mode 100644 index 00000000000..7bc91803692 --- /dev/null +++ b/mobile/android/base/resources/layout/awesomebar_suggestion_item.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/mobile/android/base/resources/layout/awesomebar_suggestion_row.xml b/mobile/android/base/resources/layout/awesomebar_suggestion_row.xml new file mode 100644 index 00000000000..8dcab9ab864 --- /dev/null +++ b/mobile/android/base/resources/layout/awesomebar_suggestion_row.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + diff --git a/mobile/android/base/resources/values/colors.xml b/mobile/android/base/resources/values/colors.xml index 478718b3e99..8fa047ccfd2 100644 --- a/mobile/android/base/resources/values/colors.xml +++ b/mobile/android/base/resources/values/colors.xml @@ -14,5 +14,7 @@ #B7D46A #C7D1DB #FF9500 + #dddddd + #bbbbbb diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index 0d61a3aa1c1..6bd7fface6e 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -4999,15 +4999,41 @@ var SearchEngines = { }; }); + let suggestTemplate = null; + let suggestEngine = null; + if (Services.prefs.getBoolPref("browser.search.suggest.enabled")) { + let engine = this.getSuggestionEngine(); + if (engine != null) { + suggestEngine = engine.name; + suggestTemplate = engine.getSubmission("__searchTerms__", "application/x-suggestions+json").uri.spec; + } + } + sendMessageToJava({ gecko: { type: "SearchEngines:Data", - searchEngines: searchEngines + searchEngines: searchEngines, + suggestEngine: suggestEngine, + suggestTemplate: suggestTemplate } }); } }, + getSuggestionEngine: function () { + let engines = [ Services.search.currentEngine, + Services.search.defaultEngine, + Services.search.originalDefaultEngine ]; + + for (let i = 0; i < engines.length; i++) { + let engine = engines[i]; + if (engine && engine.supportsResponseType("application/x-suggestions+json")) + return engine; + } + + return null; + }, + addEngine: function addEngine(aElement) { let form = aElement.form; let charset = aElement.ownerDocument.characterSet;