Bug 586885 - Add search suggestions to AwesomeBar. r=mfinkle,lucasr

This commit is contained in:
Brian Nicholson 2012-06-05 14:07:14 -07:00
parent 469c975cb5
commit 4ab9b4aa7f
9 changed files with 631 additions and 108 deletions

View File

@ -11,6 +11,7 @@ import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Bitmap; import android.graphics.Bitmap;
@ -42,6 +43,7 @@ import android.widget.TabWidget;
import android.widget.Toast; import android.widget.Toast;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Map; import java.util.Map;
import org.mozilla.gecko.db.BrowserContract.Bookmarks; import org.mozilla.gecko.db.BrowserContract.Bookmarks;
@ -54,6 +56,9 @@ import org.json.JSONObject;
public class AwesomeBar extends GeckoActivity implements GeckoEventListener { public class AwesomeBar extends GeckoActivity implements GeckoEventListener {
private static final String LOGTAG = "GeckoAwesomeBar"; 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 URL_KEY = "url";
static final String CURRENT_URL_KEY = "currenturl"; static final String CURRENT_URL_KEY = "currenturl";
static final String TYPE_KEY = "type"; static final String TYPE_KEY = "type";
@ -67,6 +72,11 @@ public class AwesomeBar extends GeckoActivity implements GeckoEventListener {
private ImageButton mGoButton; private ImageButton mGoButton;
private ContentResolver mResolver; private ContentResolver mResolver;
private ContextMenuSubject mContextMenuSubject; private ContextMenuSubject mContextMenuSubject;
private SuggestClient mSuggestClient;
private AsyncTask<String, Void, ArrayList<String>> mSuggestTask;
private static String sSuggestEngine;
private static String sSuggestTemplate;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
@ -91,8 +101,20 @@ public class AwesomeBar extends GeckoActivity implements GeckoEventListener {
openUrlAndFinish(url); openUrlAndFinish(url);
} }
public void onSearch(String engine) { public void onSearch(String engine, String text) {
openSearchAndFinish(mText.getText().toString(), engine); 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. // no composition string. It is safe to update IME flags.
updateGoButton(text); updateGoButton(text);
// cancel previous query
if (mSuggestTask != null) {
mSuggestTask.cancel(true);
}
if (mSuggestClient != null) {
mSuggestTask = new AsyncTask<String, Void, ArrayList<String>>() {
protected ArrayList<String> doInBackground(String... query) {
return mSuggestClient.query(query[0]);
}
protected void onPostExecute(ArrayList<String> suggestions) {
mAwesomeTabs.setSuggestions(suggestions);
}
};
mSuggestTask.execute(text);
}
} }
public void beforeTextChanged(CharSequence s, int start, int count, 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.bookmarks_list));
registerForContextMenu(mAwesomeTabs.findViewById(R.id.history_list)); registerForContextMenu(mAwesomeTabs.findViewById(R.id.history_list));
if (sSuggestTemplate == null) {
loadSuggestClientFromPrefs();
} else {
loadSuggestClient();
}
GeckoAppShell.registerGeckoEventListener("SearchEngines:Data", this); GeckoAppShell.registerGeckoEventListener("SearchEngines:Data", this);
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:Get", null)); 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) { public void handleMessage(String event, JSONObject message) {
try { try {
if (event.equals("SearchEngines:Data")) { 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) { } catch (Exception e) {
// do nothing // 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 @Override
public void onConfigurationChanged(Configuration newConfiguration) { public void onConfigurationChanged(Configuration newConfiguration) {
super.onConfigurationChanged(newConfiguration); super.onConfigurationChanged(newConfiguration);
@ -678,4 +762,8 @@ public class AwesomeBar extends GeckoActivity implements GeckoEventListener {
mOnKeyPreImeListener = listener; mOnKeyPreImeListener = listener;
} }
} }
private SharedPreferences getSearchPreferences() {
return getSharedPreferences("search.prefs", MODE_PRIVATE);
}
} }

View File

@ -39,6 +39,7 @@ import android.widget.TextView;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList; import java.util.LinkedList;
@ -67,9 +68,10 @@ public class AwesomeBarTabs extends TabHost {
private boolean mInflated; private boolean mInflated;
private LayoutInflater mInflater; private LayoutInflater mInflater;
private OnUrlOpenListener mUrlOpenListener; private OnUrlOpenListener mUrlOpenListener;
private JSONArray mSearchEngines;
private ContentResolver mContentResolver; private ContentResolver mContentResolver;
private ContentObserver mContentObserver; private ContentObserver mContentObserver;
private SearchEngine mSuggestEngine;
private ArrayList<SearchEngine> mSearchEngines;
private BookmarksQueryTask mBookmarksQueryTask; private BookmarksQueryTask mBookmarksQueryTask;
private HistoryQueryTask mHistoryQueryTask; private HistoryQueryTask mHistoryQueryTask;
@ -86,16 +88,24 @@ public class AwesomeBarTabs extends TabHost {
public interface OnUrlOpenListener { public interface OnUrlOpenListener {
public void onUrlOpen(String url); 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 titleView;
public TextView urlView; public TextView urlView;
public ImageView faviconView; public ImageView faviconView;
public ImageView starView; public ImageView starView;
} }
private class SearchEntryViewHolder {
public FlowLayout suggestionView;
public ImageView iconView;
public LinearLayout userEnteredView;
public TextView userEnteredTextView;
}
private class HistoryListAdapter extends SimpleExpandableListAdapter { private class HistoryListAdapter extends SimpleExpandableListAdapter {
public HistoryListAdapter(Context context, List<? extends Map<String, ?>> groupData, public HistoryListAdapter(Context context, List<? extends Map<String, ?>> groupData,
int groupLayout, String[] groupFrom, int[] groupTo, int groupLayout, String[] groupFrom, int[] groupTo,
@ -108,12 +118,12 @@ public class AwesomeBarTabs extends TabHost {
@Override @Override
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
View convertView, ViewGroup parent) { View convertView, ViewGroup parent) {
ViewHolder viewHolder = null; AwesomeEntryViewHolder viewHolder = null;
if (convertView == null) { if (convertView == null) {
convertView = mInflater.inflate(R.layout.awesomebar_row, null); convertView = mInflater.inflate(R.layout.awesomebar_row, null);
viewHolder = new ViewHolder(); viewHolder = new AwesomeEntryViewHolder();
viewHolder.titleView = (TextView) convertView.findViewById(R.id.title); viewHolder.titleView = (TextView) convertView.findViewById(R.id.title);
viewHolder.urlView = (TextView) convertView.findViewById(R.id.url); viewHolder.urlView = (TextView) convertView.findViewById(R.id.url);
viewHolder.faviconView = (ImageView) convertView.findViewById(R.id.favicon); viewHolder.faviconView = (ImageView) convertView.findViewById(R.id.favicon);
@ -121,7 +131,7 @@ public class AwesomeBarTabs extends TabHost {
convertView.setTag(viewHolder); convertView.setTag(viewHolder);
} else { } else {
viewHolder = (ViewHolder) convertView.getTag(); viewHolder = (AwesomeEntryViewHolder) convertView.getTag();
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@ -256,7 +266,7 @@ public class AwesomeBarTabs extends TabHost {
@Override @Override
public View getView(int position, View convertView, ViewGroup parent) { public View getView(int position, View convertView, ViewGroup parent) {
int viewType = getItemViewType(position); int viewType = getItemViewType(position);
ViewHolder viewHolder = null; AwesomeEntryViewHolder viewHolder = null;
if (convertView == null) { if (convertView == null) {
if (viewType == VIEW_TYPE_ITEM) if (viewType == VIEW_TYPE_ITEM)
@ -264,7 +274,7 @@ public class AwesomeBarTabs extends TabHost {
else else
convertView = mInflater.inflate(R.layout.awesomebar_folder_row, null); convertView = mInflater.inflate(R.layout.awesomebar_folder_row, null);
viewHolder = new ViewHolder(); viewHolder = new AwesomeEntryViewHolder();
viewHolder.titleView = (TextView) convertView.findViewById(R.id.title); viewHolder.titleView = (TextView) convertView.findViewById(R.id.title);
viewHolder.faviconView = (ImageView) convertView.findViewById(R.id.favicon); viewHolder.faviconView = (ImageView) convertView.findViewById(R.id.favicon);
@ -273,7 +283,7 @@ public class AwesomeBarTabs extends TabHost {
convertView.setTag(viewHolder); convertView.setTag(viewHolder);
} else { } else {
viewHolder = (ViewHolder) convertView.getTag(); viewHolder = (AwesomeEntryViewHolder) convertView.getTag();
} }
Cursor cursor = getCursor(); Cursor cursor = getCursor();
@ -612,42 +622,45 @@ public class AwesomeBarTabs extends TabHost {
public void onClick(); 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 class AwesomeBarCursorAdapter extends SimpleCursorAdapter {
private String mSearchTerm; 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) { public AwesomeBarCursorAdapter(Context context) {
super(context, -1, null, new String[] {}, new int[] {}); super(context, -1, null, new String[] {}, new int[] {});
mSearchTerm = ""; mSearchTerm = "";
@ -658,58 +671,123 @@ public class AwesomeBarTabs extends TabHost {
getFilter().filter(searchTerm); getFilter().filter(searchTerm);
} }
private int getSuggestEngineCount() {
return (mSearchTerm.length() == 0 || mSuggestEngine == null) ? 0 : 1;
}
// Add the search engines to the number of reported results. // Add the search engines to the number of reported results.
@Override @Override
public int getCount() { public int getCount() {
final int resultCount = super.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) if (mSearchTerm.length() == 0)
return resultCount; return resultCount;
return resultCount + mSearchEngines.length(); return resultCount + mSearchEngines.size() + getSuggestEngineCount();
} }
// If an item is part of the cursor result set, return that entry. // If an item is part of the cursor result set, return that entry.
// Otherwise, return the search engine data. // Otherwise, return the search engine data.
@Override @Override
public Object getItem(int position) { public Object getItem(int position) {
final int resultCount = super.getCount(); int engineIndex = getEngineIndex(position);
if (position < resultCount)
return new AwesomeBarCursorItem((Cursor) super.getItem(position));
JSONObject engine; if (engineIndex == -1) {
String engineName = null; // return awesomebar result
try { position -= getSuggestEngineCount();
engine = mSearchEngines.getJSONObject(position - resultCount); return new AwesomeBarCursorItem((Cursor) super.getItem(position));
engineName = engine.getString("name");
} catch (JSONException e) {
Log.e(LOGTAG, "Error getting search engine JSON", e);
} }
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 @Override
public View getView(int position, View convertView, ViewGroup parent) { public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder = null; if (getItemViewType(position) == ROW_SEARCH) {
SearchEntryViewHolder viewHolder = null;
if (convertView == null) { if (convertView == null) {
convertView = mInflater.inflate(R.layout.awesomebar_row, null); convertView = mInflater.inflate(R.layout.awesomebar_suggestion_row, null);
viewHolder = new ViewHolder(); viewHolder = new SearchEntryViewHolder();
viewHolder.titleView = (TextView) convertView.findViewById(R.id.title); viewHolder.suggestionView = (FlowLayout) convertView.findViewById(R.id.suggestion_layout);
viewHolder.urlView = (TextView) convertView.findViewById(R.id.url); viewHolder.iconView = (ImageView) convertView.findViewById(R.id.suggestion_icon);
viewHolder.faviconView = (ImageView) convertView.findViewById(R.id.favicon); viewHolder.userEnteredView = (LinearLayout) convertView.findViewById(R.id.suggestion_user_entered);
viewHolder.starView = (ImageView) convertView.findViewById(R.id.bookmark_star); 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 { } else {
viewHolder = (ViewHolder) convertView.getTag(); AwesomeEntryViewHolder viewHolder = null;
}
final int resultCount = super.getCount(); if (convertView == null) {
if (position < resultCount) { 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(); Cursor cursor = getCursor();
if (!cursor.moveToPosition(position)) if (!cursor.moveToPosition(position))
throw new IllegalStateException("Couldn't move cursor to position " + position); throw new IllegalStateException("Couldn't move cursor to position " + position);
@ -718,45 +796,69 @@ public class AwesomeBarTabs extends TabHost {
updateUrl(viewHolder.urlView, cursor); updateUrl(viewHolder.urlView, cursor);
updateFavicon(viewHolder.faviconView, cursor); updateFavicon(viewHolder.faviconView, cursor);
updateBookmarkStar(viewHolder.starView, cursor); updateBookmarkStar(viewHolder.starView, cursor);
} else {
bindSearchEngineView(position - resultCount, viewHolder);
} }
return convertView; return convertView;
} }
private Drawable getDrawableFromDataURI(String dataURI) { private void bindSearchEngineView(final SearchEngine engine, SearchEntryViewHolder viewHolder) {
String base64 = dataURI.substring(dataURI.indexOf(',') + 1); // when a suggestion is clicked, do a search
Drawable drawable = null; OnClickListener clickListener = new OnClickListener() {
try { public void onClick(View v) {
byte[] bytes = GeckoAppShell.decodeBase64(base64, GeckoAppShell.BASE64_DEFAULT); if (mUrlOpenListener != null) {
ByteArrayInputStream stream = new ByteArrayInputStream(bytes); String suggestion = ((TextView) v.findViewById(R.id.suggestion_text)).getText().toString();
drawable = Drawable.createFromStream(stream, "src"); mUrlOpenListener.onSearch(engine.name, suggestion);
stream.close(); }
} catch (IllegalArgumentException e) { }
Log.i(LOGTAG, "exception while decoding drawable: " + base64, e); };
} catch (IOException e) { }
return drawable;
}
private void bindSearchEngineView(int position, ViewHolder viewHolder) { // when a suggestion is long-clicked, copy the suggestion into the URL EditText
String name; OnLongClickListener longClickListener = new OnLongClickListener() {
String iconURI; public boolean onLongClick(View v) {
String searchText = getResources().getString(R.string.awesomebar_search_engine, mSearchTerm); if (mUrlOpenListener != null) {
try { String suggestion = ((TextView) v.findViewById(R.id.suggestion_text)).getText().toString();
JSONObject searchEngine = mSearchEngines.getJSONObject(position); mUrlOpenListener.onEditSuggestion(suggestion);
name = searchEngine.getString("name"); return true;
iconURI = searchEngine.getString("iconURI"); }
} catch (JSONException e) { return false;
Log.e(LOGTAG, "Error getting search engine JSON", e); }
return; };
// 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; mContext = context;
mInflated = false; mInflated = false;
mSearchEngines = new JSONArray(); mSearchEngines = new ArrayList<SearchEngine>();
mContentResolver = context.getContentResolver(); mContentResolver = context.getContentResolver();
mContentObserver = null; mContentObserver = null;
mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
@ -1043,10 +1145,98 @@ public class AwesomeBarTabs extends TabHost {
mAllPagesCursorAdapter.filter(searchTerm); 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<String> suggestions;
public SearchEngine(String name) {
this(name, null);
}
public SearchEngine(String name, Drawable icon) {
this.name = name;
this.icon = icon;
this.suggestions = new ArrayList<String>();
}
};
/**
* 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() { GeckoAppShell.getMainHandler().post(new Runnable() {
public void run() { 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<String> 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<SearchEngine> searchEngines = new ArrayList<SearchEngine>();
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(); mAllPagesCursorAdapter.notifyDataSetChanged();
} }
}); });
@ -1058,7 +1248,10 @@ public class AwesomeBarTabs extends TabHost {
@Override @Override
public boolean onInterceptTouchEvent(MotionEvent ev) { 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 // 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 // motion events to be sent to the view the user tapped on

View File

@ -98,6 +98,7 @@ FENNEC_JAVA_FILES = \
RemoteTabs.java \ RemoteTabs.java \
SetupScreen.java \ SetupScreen.java \
SiteIdentityPopup.java \ SiteIdentityPopup.java \
SuggestClient.java \
SurfaceBits.java \ SurfaceBits.java \
Tab.java \ Tab.java \
Tabs.java \ Tabs.java \
@ -254,6 +255,8 @@ RES_LAYOUT = \
res/layout/awesomebar_folder_row.xml \ res/layout/awesomebar_folder_row.xml \
res/layout/awesomebar_header_row.xml \ res/layout/awesomebar_header_row.xml \
res/layout/awesomebar_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_search.xml \
res/layout/awesomebar_tab_indicator.xml \ res/layout/awesomebar_tab_indicator.xml \
res/layout/awesomebar_tabs.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/remote_tabs_group_bg_repeat.xml \
mobile/android/base/resources/drawable/start.png \ mobile/android/base/resources/drawable/start.png \
mobile/android/base/resources/drawable/site_security_level.xml \ 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_button.xml \
mobile/android/base/resources/drawable/tabs_level.xml \ mobile/android/base/resources/drawable/tabs_level.xml \
mobile/android/base/resources/drawable/tabs_tray_bg_repeat.xml \ mobile/android/base/resources/drawable/tabs_tray_bg_repeat.xml \

View File

@ -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<String> query(String query) {
ArrayList<String> suggestions = new ArrayList<String>();
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 "";
}
}
}

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape>
<solid android:color="@color/suggestion_pressed" />
<padding android:left="7dp"
android:top="7dp"
android:right="7dp"
android:bottom="7dp" />
<corners android:bottomRightRadius="4dp"
android:bottomLeftRadius="4dp"
android:topLeftRadius="4dp"
android:topRightRadius="4dp"/>
</shape>
</item>
<item android:state_enabled="true">
<shape>
<solid android:color="@color/suggestion_primary" />
<padding android:left="7dp"
android:top="7dp"
android:right="7dp"
android:bottom="7dp" />
<corners android:bottomRightRadius="4dp"
android:bottomLeftRadius="4dp"
android:topLeftRadius="4dp"
android:topRightRadius="4dp"/>
</shape>
</item>
</selector>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/suggestion_selector"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/suggestion_magnifier"
android:src="@drawable/ic_awesomebar_search"
android:layout_marginRight="3dip"
android:layout_width="16dip"
android:layout_height="16dip" />
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/suggestion_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorPrimary"/>
</LinearLayout>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="6dip">
<ImageView android:id="@+id/suggestion_icon"
android:layout_width="32dip"
android:layout_height="32dip"
android:layout_marginRight="10dip"
android:minWidth="32dip"
android:minHeight="32dip"
android:scaleType="fitCenter"/>
<org.mozilla.gecko.FlowLayout android:id="@+id/suggestion_layout"
android:layout_toRightOf="@id/suggestion_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<include layout="@layout/awesomebar_suggestion_item"
android:id="@+id/suggestion_user_entered"/>
</org.mozilla.gecko.FlowLayout>
</RelativeLayout>

View File

@ -14,5 +14,7 @@
<color name="identity_identified">#B7D46A</color> <color name="identity_identified">#B7D46A</color>
<color name="tabs_counter_color">#C7D1DB</color> <color name="tabs_counter_color">#C7D1DB</color>
<color name="url_bar_text_highlight">#FF9500</color> <color name="url_bar_text_highlight">#FF9500</color>
<color name="suggestion_primary">#dddddd</color>
<color name="suggestion_pressed">#bbbbbb</color>
</resources> </resources>

View File

@ -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({ sendMessageToJava({
gecko: { gecko: {
type: "SearchEngines:Data", 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) { addEngine: function addEngine(aElement) {
let form = aElement.form; let form = aElement.form;
let charset = aElement.ownerDocument.characterSet; let charset = aElement.ownerDocument.characterSet;