You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
483 lines
17 KiB
483 lines
17 KiB
package acr.browser.lightning.object; |
|
|
|
import android.app.Application; |
|
import android.content.Context; |
|
import android.graphics.Color; |
|
import android.graphics.drawable.Drawable; |
|
import android.net.ConnectivityManager; |
|
import android.net.NetworkInfo; |
|
import android.os.AsyncTask; |
|
import android.support.annotation.NonNull; |
|
import android.support.annotation.Nullable; |
|
import android.util.Log; |
|
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.ImageView; |
|
import android.widget.TextView; |
|
|
|
import org.xmlpull.v1.XmlPullParser; |
|
import org.xmlpull.v1.XmlPullParserFactory; |
|
|
|
import java.io.BufferedInputStream; |
|
import java.io.File; |
|
import java.io.FileInputStream; |
|
import java.io.FileOutputStream; |
|
import java.io.FilenameFilter; |
|
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.Collections; |
|
import java.util.Comparator; |
|
import java.util.Iterator; |
|
import java.util.List; |
|
import java.util.Locale; |
|
import java.util.regex.Pattern; |
|
|
|
import javax.inject.Inject; |
|
|
|
import acr.browser.lightning.R; |
|
import acr.browser.lightning.app.BrowserApp; |
|
import acr.browser.lightning.database.BookmarkManager; |
|
import acr.browser.lightning.database.HistoryDatabase; |
|
import acr.browser.lightning.database.HistoryItem; |
|
import acr.browser.lightning.preference.PreferenceManager; |
|
import acr.browser.lightning.utils.ThemeUtils; |
|
import acr.browser.lightning.utils.Utils; |
|
|
|
public class SearchAdapter extends BaseAdapter implements Filterable { |
|
|
|
private static final String TAG = SearchAdapter.class.getSimpleName(); |
|
private static final Pattern SPACE_PATTERN = Pattern.compile(" ", Pattern.LITERAL); |
|
private final List<HistoryItem> mHistory = new ArrayList<>(5); |
|
private final List<HistoryItem> mBookmarks = new ArrayList<>(5); |
|
private final List<HistoryItem> mSuggestions = new ArrayList<>(5); |
|
private final List<HistoryItem> mFilteredList = new ArrayList<>(5); |
|
private final List<HistoryItem> mAllBookmarks = new ArrayList<>(5); |
|
private final Object mLock = new Object(); |
|
@NonNull private final Context mContext; |
|
private boolean mUseGoogle = true; |
|
private boolean mIsExecuting = false; |
|
private final boolean mDarkTheme; |
|
private final boolean mIncognito; |
|
private static final String CACHE_FILE_TYPE = ".sgg"; |
|
private static final String ENCODING = "ISO-8859-1"; |
|
private static final String DEFAULT_LANGUAGE = "en"; |
|
private static final long INTERVAL_DAY = 86400000; |
|
private static final int MAX_SUGGESTIONS = 5; |
|
private static final SuggestionsComparator mComparator = new SuggestionsComparator(); |
|
@NonNull private final String mSearchSubtitle; |
|
private SearchFilter mFilter; |
|
@NonNull private final Drawable mSearchDrawable; |
|
@NonNull private final Drawable mHistoryDrawable; |
|
@NonNull private final Drawable mBookmarkDrawable; |
|
private String mLanguage; |
|
|
|
@Inject |
|
HistoryDatabase mDatabaseHandler; |
|
|
|
@Inject |
|
BookmarkManager mBookmarkManager; |
|
|
|
@Inject |
|
PreferenceManager mPreferenceManager; |
|
|
|
public SearchAdapter(@NonNull Context context, boolean dark, boolean incognito) { |
|
BrowserApp.getAppComponent().inject(this); |
|
mAllBookmarks.addAll(mBookmarkManager.getAllBookmarks(true)); |
|
mUseGoogle = mPreferenceManager.getGoogleSearchSuggestionsEnabled(); |
|
mContext = context; |
|
mSearchSubtitle = mContext.getString(R.string.suggestion); |
|
mDarkTheme = dark || incognito; |
|
mIncognito = incognito; |
|
Thread delete = new Thread(new ClearCacheRunnable(BrowserApp.get(context))); |
|
mSearchDrawable = ThemeUtils.getThemedDrawable(context, R.drawable.ic_search, mDarkTheme); |
|
mBookmarkDrawable = ThemeUtils.getThemedDrawable(context, R.drawable.ic_bookmark, mDarkTheme); |
|
mHistoryDrawable = ThemeUtils.getThemedDrawable(context, R.drawable.ic_history, mDarkTheme); |
|
delete.setPriority(Thread.MIN_PRIORITY); |
|
delete.start(); |
|
mLanguage = Locale.getDefault().getLanguage(); |
|
if (mLanguage.isEmpty()) { |
|
mLanguage = DEFAULT_LANGUAGE; |
|
} |
|
} |
|
|
|
private static class NameFilter implements FilenameFilter { |
|
|
|
@Override |
|
public boolean accept(File dir, @NonNull String filename) { |
|
return filename.endsWith(CACHE_FILE_TYPE); |
|
} |
|
|
|
} |
|
|
|
public void refreshPreferences() { |
|
mUseGoogle = mPreferenceManager.getGoogleSearchSuggestionsEnabled(); |
|
if (!mUseGoogle) { |
|
synchronized (mSuggestions) { |
|
mSuggestions.clear(); |
|
} |
|
} |
|
} |
|
|
|
public void refreshBookmarks() { |
|
synchronized (mLock) { |
|
mAllBookmarks.clear(); |
|
mAllBookmarks.addAll(mBookmarkManager.getAllBookmarks(true)); |
|
} |
|
} |
|
|
|
@Override |
|
public int getCount() { |
|
return mFilteredList.size(); |
|
} |
|
|
|
@Override |
|
public Object getItem(int position) { |
|
return mFilteredList.get(position); |
|
} |
|
|
|
@Override |
|
public long getItemId(int position) { |
|
return 0; |
|
} |
|
|
|
@Nullable |
|
@Override |
|
public View getView(int position, @Nullable View convertView, ViewGroup parent) { |
|
SuggestionHolder holder; |
|
|
|
if (convertView == null) { |
|
LayoutInflater inflater = LayoutInflater.from(mContext); |
|
convertView = inflater.inflate(R.layout.two_line_autocomplete, parent, false); |
|
|
|
holder = new SuggestionHolder(); |
|
holder.mTitle = (TextView) convertView.findViewById(R.id.title); |
|
holder.mUrl = (TextView) convertView.findViewById(R.id.url); |
|
holder.mImage = (ImageView) convertView.findViewById(R.id.suggestionIcon); |
|
convertView.setTag(holder); |
|
} else { |
|
holder = (SuggestionHolder) convertView.getTag(); |
|
} |
|
HistoryItem web; |
|
web = mFilteredList.get(position); |
|
holder.mTitle.setText(web.getTitle()); |
|
holder.mUrl.setText(web.getUrl()); |
|
|
|
Drawable image; |
|
switch (web.getImageId()) { |
|
case R.drawable.ic_bookmark: { |
|
if (mDarkTheme) |
|
holder.mTitle.setTextColor(Color.WHITE); |
|
image = mBookmarkDrawable; |
|
break; |
|
} |
|
case R.drawable.ic_search: { |
|
if (mDarkTheme) |
|
holder.mTitle.setTextColor(Color.WHITE); |
|
image = mSearchDrawable; |
|
break; |
|
} |
|
case R.drawable.ic_history: { |
|
if (mDarkTheme) |
|
holder.mTitle.setTextColor(Color.WHITE); |
|
image = mHistoryDrawable; |
|
break; |
|
} |
|
default: |
|
if (mDarkTheme) |
|
holder.mTitle.setTextColor(Color.WHITE); |
|
image = mSearchDrawable; |
|
break; |
|
} |
|
|
|
holder.mImage.setImageDrawable(image); |
|
|
|
return convertView; |
|
} |
|
|
|
@Override |
|
public Filter getFilter() { |
|
if (mFilter == null) { |
|
mFilter = new SearchFilter(); |
|
} |
|
return mFilter; |
|
} |
|
|
|
private static class ClearCacheRunnable implements Runnable { |
|
|
|
private final Application app; |
|
|
|
public ClearCacheRunnable(Application app) { |
|
this.app = app; |
|
} |
|
|
|
@Override |
|
public void run() { |
|
File dir = new File(app.getCacheDir().toString()); |
|
String[] fileList = dir.list(new NameFilter()); |
|
long earliestTimeAllowed = System.currentTimeMillis() - INTERVAL_DAY; |
|
for (String fileName : fileList) { |
|
File file = new File(dir.getPath() + fileName); |
|
if (earliestTimeAllowed > file.lastModified()) { |
|
file.delete(); |
|
} |
|
} |
|
} |
|
|
|
} |
|
|
|
private class SearchFilter extends Filter { |
|
|
|
@NonNull |
|
@Override |
|
protected FilterResults performFiltering(@Nullable CharSequence constraint) { |
|
FilterResults results = new FilterResults(); |
|
if (constraint == null) { |
|
return results; |
|
} |
|
String query = constraint.toString().toLowerCase(Locale.getDefault()); |
|
if (mUseGoogle && !mIncognito && !mIsExecuting) { |
|
new RetrieveSearchSuggestions().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, query); |
|
} |
|
|
|
int counter = 0; |
|
synchronized (mBookmarks) { |
|
mBookmarks.clear(); |
|
synchronized (mLock) { |
|
for (int n = 0; n < mAllBookmarks.size(); n++) { |
|
if (counter >= 5) { |
|
break; |
|
} |
|
if (mAllBookmarks.get(n).getTitle().toLowerCase(Locale.getDefault()) |
|
.startsWith(query)) { |
|
mBookmarks.add(mAllBookmarks.get(n)); |
|
counter++; |
|
} else if (mAllBookmarks.get(n).getUrl().contains(query)) { |
|
mBookmarks.add(mAllBookmarks.get(n)); |
|
counter++; |
|
} |
|
} |
|
} |
|
} |
|
|
|
List<HistoryItem> historyList = mDatabaseHandler.findItemsContaining(constraint.toString()); |
|
synchronized (mHistory) { |
|
mHistory.clear(); |
|
mHistory.addAll(historyList); |
|
} |
|
results.count = 1; |
|
return results; |
|
} |
|
|
|
@Override |
|
public CharSequence convertResultToString(@NonNull Object resultValue) { |
|
return ((HistoryItem) resultValue).getUrl(); |
|
} |
|
|
|
@Override |
|
protected void publishResults(CharSequence constraint, FilterResults results) { |
|
synchronized (mFilteredList) { |
|
mFilteredList.clear(); |
|
List<HistoryItem> filtered = getFilteredList(); |
|
Collections.sort(filtered, mComparator); |
|
mFilteredList.addAll(filtered); |
|
} |
|
notifyDataSetChanged(); |
|
} |
|
|
|
} |
|
|
|
private static class SuggestionHolder { |
|
ImageView mImage; |
|
TextView mTitle; |
|
TextView mUrl; |
|
} |
|
|
|
private class RetrieveSearchSuggestions extends AsyncTask<String, Void, List<HistoryItem>> { |
|
|
|
private XmlPullParserFactory mFactory; |
|
private XmlPullParser mXpp; |
|
|
|
@NonNull |
|
@Override |
|
protected List<HistoryItem> doInBackground(String... arg0) { |
|
mIsExecuting = true; |
|
|
|
List<HistoryItem> filter = new ArrayList<>(); |
|
String query = arg0[0]; |
|
try { |
|
query = SPACE_PATTERN.matcher(query).replaceAll("+"); |
|
URLEncoder.encode(query, ENCODING); |
|
} catch (UnsupportedEncodingException e) { |
|
e.printStackTrace(); |
|
} |
|
File cache = downloadSuggestionsForQuery(query, mLanguage, BrowserApp.get(mContext)); |
|
if (!cache.exists()) { |
|
return filter; |
|
} |
|
InputStream fileInput = null; |
|
try { |
|
fileInput = new BufferedInputStream(new FileInputStream(cache)); |
|
if (mFactory == null) { |
|
mFactory = XmlPullParserFactory.newInstance(); |
|
mFactory.setNamespaceAware(true); |
|
} |
|
if (mXpp == null) { |
|
mXpp = mFactory.newPullParser(); |
|
} |
|
mXpp.setInput(fileInput, ENCODING); |
|
int eventType = mXpp.getEventType(); |
|
int counter = 0; |
|
while (eventType != XmlPullParser.END_DOCUMENT) { |
|
if (eventType == XmlPullParser.START_TAG && "suggestion".equals(mXpp.getName())) { |
|
String suggestion = mXpp.getAttributeValue(null, "data"); |
|
filter.add(new HistoryItem(mSearchSubtitle + " \"" + suggestion + '"', |
|
suggestion, R.drawable.ic_search)); |
|
counter++; |
|
if (counter >= 5) { |
|
break; |
|
} |
|
} |
|
eventType = mXpp.next(); |
|
} |
|
} catch (Exception e) { |
|
return filter; |
|
} finally { |
|
Utils.close(fileInput); |
|
} |
|
return filter; |
|
} |
|
|
|
@Override |
|
protected void onPostExecute(@NonNull List<HistoryItem> result) { |
|
mIsExecuting = false; |
|
synchronized (mSuggestions) { |
|
mSuggestions.clear(); |
|
mSuggestions.addAll(result); |
|
} |
|
synchronized (mFilteredList) { |
|
mFilteredList.clear(); |
|
List<HistoryItem> filtered = getFilteredList(); |
|
Collections.sort(filtered, mComparator); |
|
mFilteredList.addAll(filtered); |
|
notifyDataSetChanged(); |
|
} |
|
} |
|
|
|
} |
|
|
|
/** |
|
* This method downloads the search suggestions for the specific query. |
|
* NOTE: This is a blocking operation, do not run on the UI thread. |
|
* |
|
* @param query the query to get suggestions for |
|
* @return the cache file containing the suggestions |
|
*/ |
|
@NonNull |
|
private static File downloadSuggestionsForQuery(@NonNull String query, String language, @NonNull Application app) { |
|
File cacheFile = new File(app.getCacheDir(), query.hashCode() + CACHE_FILE_TYPE); |
|
if (System.currentTimeMillis() - INTERVAL_DAY < cacheFile.lastModified()) { |
|
return cacheFile; |
|
} |
|
if (!isNetworkConnected(app)) { |
|
return cacheFile; |
|
} |
|
InputStream in = null; |
|
FileOutputStream fos = null; |
|
try { |
|
// Old API that doesn't support HTTPS |
|
// http://google.com/complete/search?q= + query + &output=toolbar&hl= + language |
|
URL url = new URL("https://suggestqueries.google.com/complete/search?output=toolbar&hl=" |
|
+ language + "&q=" + query); |
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); |
|
connection.setDoInput(true); |
|
connection.connect(); |
|
if (connection.getResponseCode() >= HttpURLConnection.HTTP_MULT_CHOICE || |
|
connection.getResponseCode() < HttpURLConnection.HTTP_OK) { |
|
Log.e(TAG, "Search API Responded with code: " + connection.getResponseCode()); |
|
return cacheFile; |
|
} |
|
in = connection.getInputStream(); |
|
|
|
if (in != null) { |
|
//noinspection IOResourceOpenedButNotSafelyClosed |
|
fos = new FileOutputStream(cacheFile); |
|
int buffer; |
|
while ((buffer = in.read()) != -1) { |
|
fos.write(buffer); |
|
} |
|
fos.flush(); |
|
} |
|
cacheFile.setLastModified(System.currentTimeMillis()); |
|
} catch (Exception e) { |
|
e.printStackTrace(); |
|
} finally { |
|
Utils.close(in); |
|
Utils.close(fos); |
|
} |
|
return cacheFile; |
|
} |
|
|
|
private static boolean isNetworkConnected(@NonNull Context context) { |
|
NetworkInfo networkInfo = getActiveNetworkInfo(context); |
|
return networkInfo != null && networkInfo.isConnected(); |
|
} |
|
|
|
private static NetworkInfo getActiveNetworkInfo(@NonNull Context context) { |
|
ConnectivityManager connectivity = (ConnectivityManager) context |
|
.getSystemService(Context.CONNECTIVITY_SERVICE); |
|
if (connectivity == null) { |
|
return null; |
|
} |
|
return connectivity.getActiveNetworkInfo(); |
|
} |
|
|
|
@NonNull |
|
private synchronized List<HistoryItem> getFilteredList() { |
|
List<HistoryItem> list = new ArrayList<>(5); |
|
synchronized (mBookmarks) { |
|
synchronized (mHistory) { |
|
synchronized (mSuggestions) { |
|
Iterator<HistoryItem> bookmark = mBookmarks.iterator(); |
|
Iterator<HistoryItem> history = mHistory.iterator(); |
|
Iterator<HistoryItem> suggestion = mSuggestions.listIterator(); |
|
while (list.size() < MAX_SUGGESTIONS) { |
|
if (!bookmark.hasNext() && !suggestion.hasNext() && !history.hasNext()) { |
|
return list; |
|
} |
|
if (bookmark.hasNext()) { |
|
list.add(bookmark.next()); |
|
} |
|
if (suggestion.hasNext() && list.size() < MAX_SUGGESTIONS) { |
|
list.add(suggestion.next()); |
|
} |
|
if (history.hasNext() && list.size() < MAX_SUGGESTIONS) { |
|
list.add(history.next()); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
return list; |
|
} |
|
|
|
private static class SuggestionsComparator implements Comparator<HistoryItem> { |
|
|
|
@Override |
|
public int compare(@NonNull HistoryItem lhs, @NonNull HistoryItem rhs) { |
|
if (lhs.getImageId() == rhs.getImageId()) return 0; |
|
if (lhs.getImageId() == R.drawable.ic_bookmark) return -1; |
|
if (rhs.getImageId() == R.drawable.ic_bookmark) return 1; |
|
if (lhs.getImageId() == R.drawable.ic_history) return -1; |
|
return 1; |
|
} |
|
} |
|
|
|
}
|
|
|