Commit 58c2aed4 authored by luca020400's avatar luca020400 Committed by Danny Baumann

Jelly: Add suggestions

* Credits to Lightning-Browser for base suggestions model
  -> Adapted to AOSP ecosystem removing okhttp3

Change-Id: I04b6a4a6dbd56d00cc0a39243ca42a1716e3e034
parent 69173dd2
......@@ -28,6 +28,7 @@ import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.net.Uri;
import android.net.http.HttpResponseCache;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
......@@ -58,16 +59,18 @@ import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.webkit.CookieManager;
import android.webkit.URLUtil;
import android.widget.AutoCompleteTextView;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import org.lineageos.jelly.favorite.Favorite;
import org.lineageos.jelly.favorite.FavoriteActivity;
import org.lineageos.jelly.favorite.FavoriteDatabaseHandler;
import org.lineageos.jelly.history.HistoryActivity;
import org.lineageos.jelly.ui.EditTextExt;
import org.lineageos.jelly.suggestions.SuggestionsAdapter;
import org.lineageos.jelly.utils.PrefsUtils;
import org.lineageos.jelly.utils.UiUtils;
import org.lineageos.jelly.webview.WebViewCompat;
......@@ -122,27 +125,38 @@ public class MainActivity extends WebViewExtActivity implements View.OnTouchList
new Handler().postDelayed(() -> mSwipeRefreshLayout.setRefreshing(false), 1000);
});
mLoadingProgress = (ProgressBar) findViewById(R.id.load_progress);
EditTextExt editText = (EditTextExt) findViewById(R.id.url_bar);
editText.setOnEditorActionListener((v, actionId, event) -> {
AutoCompleteTextView autoCompleteTextView =
(AutoCompleteTextView) findViewById(R.id.url_bar);
autoCompleteTextView.setAdapter(new SuggestionsAdapter(this));
autoCompleteTextView.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
UiUtils.hideKeyboard(editText);
UiUtils.hideKeyboard(autoCompleteTextView);
mWebView.loadUrl(editText.getText().toString());
editText.clearFocus();
mWebView.loadUrl(autoCompleteTextView.getText().toString());
autoCompleteTextView.clearFocus();
return true;
}
return false;
});
editText.setOnKeyListener((v, keyCode, event) -> {
autoCompleteTextView.setOnKeyListener((v, keyCode, event) -> {
if (keyCode == KeyEvent.KEYCODE_ENTER) {
UiUtils.hideKeyboard(editText);
UiUtils.hideKeyboard(autoCompleteTextView);
mWebView.loadUrl(editText.getText().toString());
editText.clearFocus();
mWebView.loadUrl(autoCompleteTextView.getText().toString());
autoCompleteTextView.clearFocus();
return true;
}
return false;
});
autoCompleteTextView.setOnItemClickListener((adapterView, view, pos, l) -> {
CharSequence searchString = ((TextView) view.findViewById(R.id.title)).getText();
String url = searchString.toString();
UiUtils.hideKeyboard(autoCompleteTextView);
autoCompleteTextView.clearFocus();
mWebView.loadUrl(url);
});
Intent intent = getIntent();
String url = intent.getDataString();
......@@ -166,8 +180,9 @@ public class MainActivity extends WebViewExtActivity implements View.OnTouchList
incognitoIcon.setVisibility(mIncognito ? View.VISIBLE : View.GONE);
setupMenu();
mWebView = (WebViewExt) findViewById(R.id.web_view);
mWebView.init(this, editText, mLoadingProgress, mIncognito);
mWebView.init(this, autoCompleteTextView, mLoadingProgress, mIncognito);
mWebView.setDesktopMode(desktopMode);
mWebView.loadUrl(url == null ? PrefsUtils.getHomePage(this) : url);
......@@ -185,11 +200,24 @@ public class MainActivity extends WebViewExtActivity implements View.OnTouchList
mWebView.setOnScrollChangeListener(this);
applyThemeColor(mThemeColor);
try {
File httpCacheDir =
new File(getApplicationContext().getCacheDir(), "suggestion_responses");
long httpCacheSize = 1024 * 1024; // 1 MiB
HttpResponseCache.install(httpCacheDir, httpCacheSize);
} catch (IOException e) {
Log.i(TAG, "HTTP response cache installation failed:" + e);
}
}
@Override
protected void onStop() {
CookieManager.getInstance().flush();
HttpResponseCache cache = HttpResponseCache.getInstalled();
if (cache != null) {
cache.flush();
}
super.onStop();
}
......
/*
* Copyright (C) 2017 The LineageOS Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.lineageos.jelly.suggestions;
import android.support.annotation.NonNull;
/**
* Search suggestions provider for the Baidu search engine.
*/
class BaiduSuggestionProvider extends SuggestionProvider {
BaiduSuggestionProvider() {
super("GB2312");
}
@NonNull
protected String createQueryUrl(@NonNull String query,
@NonNull String language) {
return "http://suggestion.baidu.com/s?wd=" + query + "&action=opensearch";
}
}
/*
* Copyright (C) 2017 The LineageOS Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.lineageos.jelly.suggestions;
import android.support.annotation.NonNull;
/**
* Search suggestions provider for Bing search engine.
*/
class BingSuggestionProvider extends SuggestionProvider {
BingSuggestionProvider() {
super("UTF-8");
}
@NonNull
protected String createQueryUrl(@NonNull String query,
@NonNull String language) {
return "http://api.bing.com/osjson.aspx?query=" + query + "&language=" + language;
}
}
/*
* Copyright (C) 2017 The LineageOS Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.lineageos.jelly.suggestions;
import android.support.annotation.NonNull;
import org.json.JSONArray;
import org.json.JSONObject;
/**
* The search suggestions provider for the DuckDuckGo search engine.
*/
class DuckSuggestionProvider extends SuggestionProvider {
DuckSuggestionProvider() {
super("UTF-8");
}
@NonNull
@Override
protected String createQueryUrl(@NonNull String query,
@NonNull String language) {
return "https://duckduckgo.com/ac/?q=" + query;
}
@Override
protected void parseResults(@NonNull String content,
@NonNull ResultCallback callback) throws Exception {
JSONArray jsonArray = new JSONArray(content);
for (int n = 0, size = jsonArray.length(); n < size; n++) {
JSONObject object = jsonArray.getJSONObject(n);
String suggestion = object.getString("phrase");
if (!callback.addResult(suggestion)) {
break;
}
}
}
}
/*
* Copyright (C) 2017 The LineageOS Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.lineageos.jelly.suggestions;
import android.support.annotation.NonNull;
/**
* Search suggestions provider for Google search engine.
*/
class GoogleSuggestionProvider extends SuggestionProvider {
GoogleSuggestionProvider() {
super("UTF-8");
}
@NonNull
protected String createQueryUrl(@NonNull String query,
@NonNull String language) {
return "https://www.google.com/complete/search?client=android"
+ "&hl=" + language + "&q=" + query;
}
}
/*
* Copyright (C) 2017 The LineageOS Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.lineageos.jelly.suggestions;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import org.json.JSONArray;
import org.lineageos.jelly.utils.FileUtils;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
/**
* The base search suggestions API. Provides common
* fetching and caching functionality for each potential
* suggestions provider.
*/
abstract class SuggestionProvider {
private static final String TAG = "SuggestionProvider";
private static final long INTERVAL_DAY = TimeUnit.DAYS.toSeconds(1);
private static final String DEFAULT_LANGUAGE = "en";
@NonNull
private final String mEncoding;
@NonNull
private final String mLanguage;
SuggestionProvider(@NonNull String encoding) {
mEncoding = encoding;
mLanguage = getLanguage();
}
@NonNull
private static String getLanguage() {
String language = Locale.getDefault().getLanguage();
if (TextUtils.isEmpty(language)) {
language = DEFAULT_LANGUAGE;
}
return language;
}
/**
* Create a URL for the given query in the given language.
*
* @param query the query that was made.
* @param language the locale of the user.
* @return should return a URL that can be fetched using a GET.
*/
@NonNull
protected abstract String createQueryUrl(@NonNull String query,
@NonNull String language);
/**
* Parse the results of an input stream into a list of {@link String}.
*
* @param content the raw input to parse.
* @param callback the callback to invoke for each received suggestion
* @throws Exception throw an exception if anything goes wrong.
*/
void parseResults(@NonNull String content,
@NonNull ResultCallback callback) throws Exception {
JSONArray respArray = new JSONArray(content);
JSONArray jsonArray = respArray.getJSONArray(1);
for (int n = 0, size = jsonArray.length(); n < size; n++) {
String suggestion = jsonArray.getString(n);
if (!callback.addResult(suggestion)) {
break;
}
}
}
/**
* Retrieves the results for a query.
*
* @param rawQuery the raw query to retrieve the results for.
* @return a list of history items for the query.
*/
@NonNull
final List<String> fetchResults(@NonNull final String rawQuery) {
List<String> filter = new ArrayList<>(5);
String query;
try {
query = URLEncoder.encode(rawQuery, mEncoding);
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "Unable to encode the URL", e);
return filter;
}
String content = downloadSuggestionsForQuery(query, mLanguage);
if (content == null) {
// There are no suggestions for this query, return an empty list.
return filter;
}
try {
parseResults(content, (String suggestion) -> {
filter.add(suggestion);
return filter.size() < 5;
});
} catch (Exception e) {
Log.e(TAG, "Unable to parse results", e);
}
return filter;
}
/**
* This method downloads the search suggestions for the specific query.
* NOTE: This is a blocking operation, do not fetchResults on the UI thread.
*
* @param query the query to get suggestions for
* @return the cache file containing the suggestions
*/
@Nullable
private String downloadSuggestionsForQuery(@NonNull String query,
@NonNull String language) {
try {
URL url = new URL(createQueryUrl(query, language));
InputStream in = null;
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.addRequestProperty("Cache-Control",
"max-age=" + INTERVAL_DAY + ", max-stale=" + INTERVAL_DAY);
urlConnection.addRequestProperty("Accept-Charset", mEncoding);
try {
in = new BufferedInputStream(urlConnection.getInputStream());
String encoding = urlConnection.getContentEncoding();
return FileUtils.readStringFromStream(in, getEncoding(urlConnection));
} finally {
urlConnection.disconnect();
if (in != null) {
try {
in.close();
} catch (IOException e) {
// ignored
}
}
}
} catch (IOException e) {
Log.e(TAG, "Problem getting search suggestions", e);
}
return null;
}
private String getEncoding(HttpURLConnection connection) {
String contentEncoding = connection.getContentEncoding();
if (contentEncoding != null) {
return contentEncoding;
}
String contentType = connection.getContentType();
for (String value : contentType.split(";")) {
value = value.trim();
if (value.toLowerCase(Locale.US).startsWith("charset=")) {
return value.substring(8);
}
}
return mEncoding;
}
interface ResultCallback {
boolean addResult(String suggestion);
}
}
/*
* Copyright (C) 2017 The LineageOS Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.lineageos.jelly.suggestions;
import android.content.Context;
import android.graphics.Typeface;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.StyleSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Filter;
import android.widget.Filterable;
import android.widget.TextView;
import org.lineageos.jelly.R;
import org.lineageos.jelly.utils.PrefsUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public class SuggestionsAdapter extends BaseAdapter implements Filterable {
private final ArrayList<String> mItems = new ArrayList<>();
private final Context mContext;
private final LayoutInflater mInflater;
private final ItemFilter mFilter;
private String mQueryText;
public SuggestionsAdapter(Context context) {
super();
mContext = context;
mInflater = LayoutInflater.from(mContext);
mFilter = new ItemFilter();
}
@Override
public int getCount() {
return mItems.size();
}
@Override
public Object getItem(int position) {
return mItems.get(position);
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = mInflater.inflate(R.layout.item_suggestion, parent, false);
}
TextView title = (TextView) convertView.findViewById(R.id.title);
String suggestion = mItems.get(position);
if (mQueryText != null) {
SpannableStringBuilder spannable = new SpannableStringBuilder(suggestion);
String lcSuggestion = suggestion.toLowerCase(Locale.getDefault());
int queryTextPos = lcSuggestion.indexOf(mQueryText);
while (queryTextPos >= 0) {
spannable.setSpan(new StyleSpan(Typeface.BOLD),
queryTextPos, queryTextPos + mQueryText.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
queryTextPos = lcSuggestion.indexOf(mQueryText, queryTextPos + mQueryText.length());
}
title.setText(spannable);
} else {
title.setText(suggestion);
}
return convertView;
}
@Override
public Filter getFilter() {
return mFilter;
}
private class ItemFilter extends Filter {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults results = new FilterResults();
if (constraint == null || constraint.length() == 0) {
return results;
}
SuggestionProvider provider = getProvider();
String query = constraint.toString().toLowerCase(Locale.getDefault()).trim();
if (provider != null) {
List<String> items = provider.fetchResults(query);
results.count = items.size();
results.values = items;
}
return results;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
mItems.clear();
if (results.values != null) {
List<String> items = (List<String>) results.values;
mItems.addAll(items);
}
mQueryText = constraint != null
? constraint.toString().toLowerCase(Locale.getDefault()).trim() : null;
notifyDataSetChanged();
}
private SuggestionProvider getProvider() {
switch (PrefsUtils.getSuggestionProvider(mContext)) {
case BAIDU:
return new BaiduSuggestionProvider();
case BING:
return new BingSuggestionProvider();
case DUCK:
return new DuckSuggestionProvider();
case GOOGLE:
return new GoogleSuggestionProvider();
case YAHOO:
return new YahooSuggestionProvider();
}
return null;
}
}
}
\ No newline at end of file
/*
* Copyright (C) 2017 The LineageOS Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.lineageos.jelly.suggestions;
import android.support.annotation.NonNull;
/**
* Search suggestions provider for Yahoo search engine.
*/
class YahooSuggestionProvider extends SuggestionProvider {
YahooSuggestionProvider() {
super("UTF-8");
}
@NonNull
protected String createQueryUrl(@NonNull String query,
@NonNull String language) {
return "http://ff.search.yahoo.com/gossip?output=fxjson&command=" + query;
}
}
......@@ -21,22 +21,22 @@ import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Rect;
import android.graphics.Shader;
import android.support.v7.widget.AppCompatEditText;
import android.support.v7.widget.AppCompatAutoCompleteTextView;
import android.util.AttributeSet;
public class EditTextExt extends AppCompatEditText {
public class AutoCompleteTextViewExt extends AppCompatAutoCompleteTextView {
private int mPositionX;
public EditTextExt(Context context) {
public AutoCompleteTextViewExt(Context context) {
super(context);
}
public EditTextExt(Context context, AttributeSet attrs) {
public AutoCompleteTextViewExt(Context context, AttributeSet attrs) {
super(context, attrs);
}
public EditTextExt(Context context, AttributeSet attrs, int defStyle) {
public AutoCompleteTextViewExt(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
......
/*
* Copyright (C) 2017 The LineageOS Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0