Anthony Restaino
9 years ago
26 changed files with 820 additions and 588 deletions
@ -1,16 +0,0 @@
@@ -1,16 +0,0 @@
|
||||
package acr.browser.lightning.react; |
||||
|
||||
import android.support.annotation.NonNull; |
||||
|
||||
public interface Action<T> { |
||||
/** |
||||
* Should be overridden to send the subscriber |
||||
* events such as {@link Subscriber#onNext(Object)} |
||||
* or {@link Subscriber#onComplete()}. |
||||
* |
||||
* @param subscriber the subscriber that is sent in |
||||
* when the user of the Observable |
||||
* subscribes. |
||||
*/ |
||||
void onSubscribe(@NonNull Subscriber<T> subscriber); |
||||
} |
@ -1,254 +0,0 @@
@@ -1,254 +0,0 @@
|
||||
package acr.browser.lightning.react; |
||||
|
||||
import android.os.Looper; |
||||
import android.support.annotation.NonNull; |
||||
import android.support.annotation.Nullable; |
||||
import android.util.Log; |
||||
|
||||
import java.util.concurrent.Executor; |
||||
|
||||
import acr.browser.lightning.utils.Preconditions; |
||||
|
||||
/** |
||||
* An RxJava implementation. This class allows work |
||||
* to be done on a certain thread and then allows |
||||
* items to be emitted on a different thread. It is |
||||
* a replacement for {@link android.os.AsyncTask}. |
||||
* |
||||
* @param <T> the type that the Observable will emit. |
||||
*/ |
||||
public class Observable<T> { |
||||
|
||||
private static final String TAG = Observable.class.getSimpleName(); |
||||
|
||||
@NonNull private final Action<T> mAction; |
||||
@Nullable private Executor mSubscriberThread; |
||||
@Nullable private Executor mObserverThread; |
||||
@NonNull private final Executor mDefault; |
||||
|
||||
private Observable(@NonNull Action<T> action) { |
||||
mAction = action; |
||||
Looper looper = Looper.myLooper(); |
||||
Preconditions.checkNonNull(looper); |
||||
mDefault = new ThreadExecutor(looper); |
||||
} |
||||
|
||||
/** |
||||
* Static creator method that creates an Observable from the |
||||
* {@link Action} that is passed in as the parameter. Action |
||||
* must not be null. |
||||
* |
||||
* @param action the Action to perform |
||||
* @param <T> the type that will be emitted to the onSubscribe |
||||
* @return a valid non-null Observable. |
||||
*/ |
||||
@NonNull |
||||
public static <T> Observable<T> create(@NonNull Action<T> action) { |
||||
Preconditions.checkNonNull(action); |
||||
return new Observable<>(action); |
||||
} |
||||
|
||||
/** |
||||
* Tells the Observable what Executor that the onSubscribe |
||||
* work should run on. |
||||
* |
||||
* @param subscribeExecutor the Executor to run the work on. |
||||
* @return returns this so that calls can be conveniently chained. |
||||
*/ |
||||
public Observable<T> subscribeOn(@NonNull Executor subscribeExecutor) { |
||||
mSubscriberThread = subscribeExecutor; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Tells the Observable what Executor the onSubscribe should observe |
||||
* the work on. |
||||
* |
||||
* @param observerExecutor the Executor to run to callback on. |
||||
* @return returns this so that calls can be conveniently chained. |
||||
*/ |
||||
public Observable<T> observeOn(@NonNull Executor observerExecutor) { |
||||
mObserverThread = observerExecutor; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Subscribes immediately to the Observable and ignores |
||||
* all onComplete and onNext calls. |
||||
*/ |
||||
public void subscribe() { |
||||
executeOnSubscriberThread(new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
mAction.onSubscribe(new Subscriber<T>() { |
||||
@Override |
||||
public void unsubscribe() {} |
||||
|
||||
@Override |
||||
public void onComplete() {} |
||||
|
||||
@Override |
||||
public void onStart() {} |
||||
|
||||
@Override |
||||
public void onError(@NonNull Throwable throwable) {} |
||||
|
||||
@Override |
||||
public void onNext(T item) {} |
||||
}); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Immediately subscribes to the Observable and starts |
||||
* sending events from the Observable to the {@link OnSubscribe}. |
||||
* |
||||
* @param onSubscribe the class that wishes to receive onNext and |
||||
* onComplete callbacks from the Observable. |
||||
*/ |
||||
public Subscription subscribe(@NonNull OnSubscribe<T> onSubscribe) { |
||||
|
||||
Preconditions.checkNonNull(onSubscribe); |
||||
|
||||
final Subscriber<T> subscriber = new SubscriberImpl<>(onSubscribe, this); |
||||
|
||||
subscriber.onStart(); |
||||
|
||||
executeOnSubscriberThread(new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
mAction.onSubscribe(subscriber); |
||||
} |
||||
}); |
||||
|
||||
return subscriber; |
||||
} |
||||
|
||||
private void executeOnObserverThread(@NonNull Runnable runnable) { |
||||
if (mObserverThread != null) { |
||||
mObserverThread.execute(runnable); |
||||
} else { |
||||
mDefault.execute(runnable); |
||||
} |
||||
} |
||||
|
||||
private void executeOnSubscriberThread(@NonNull Runnable runnable) { |
||||
if (mSubscriberThread != null) { |
||||
mSubscriberThread.execute(runnable); |
||||
} else { |
||||
mDefault.execute(runnable); |
||||
} |
||||
} |
||||
|
||||
private static class SubscriberImpl<T> implements Subscriber<T> { |
||||
|
||||
@Nullable private volatile OnSubscribe<T> mOnSubscribe; |
||||
@NonNull private final Observable<T> mObservable; |
||||
private boolean mOnCompleteExecuted = false; |
||||
private boolean mOnError = false; |
||||
|
||||
public SubscriberImpl(@NonNull OnSubscribe<T> onSubscribe, @NonNull Observable<T> observable) { |
||||
mOnSubscribe = onSubscribe; |
||||
mObservable = observable; |
||||
} |
||||
|
||||
@Override |
||||
public void unsubscribe() { |
||||
mOnSubscribe = null; |
||||
} |
||||
|
||||
@Override |
||||
public void onComplete() { |
||||
OnSubscribe<T> onSubscribe = mOnSubscribe; |
||||
if (!mOnCompleteExecuted && onSubscribe != null && !mOnError) { |
||||
mOnCompleteExecuted = true; |
||||
mObservable.executeOnObserverThread(new OnCompleteRunnable<>(onSubscribe)); |
||||
} else if (!mOnError) { |
||||
Log.e(TAG, "onComplete called more than once"); |
||||
throw new RuntimeException("onComplete called more than once"); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void onStart() { |
||||
OnSubscribe<T> onSubscribe = mOnSubscribe; |
||||
if (onSubscribe != null) { |
||||
mObservable.executeOnObserverThread(new OnStartRunnable<>(onSubscribe)); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void onError(@NonNull final Throwable throwable) { |
||||
OnSubscribe<T> onSubscribe = mOnSubscribe; |
||||
if (onSubscribe != null) { |
||||
mOnError = true; |
||||
mObservable.executeOnObserverThread(new OnErrorRunnable<>(onSubscribe, throwable)); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void onNext(final T item) { |
||||
OnSubscribe<T> onSubscribe = mOnSubscribe; |
||||
if (!mOnCompleteExecuted && onSubscribe != null) { |
||||
mObservable.executeOnObserverThread(new OnNextRunnable<>(onSubscribe, item)); |
||||
} else { |
||||
Log.e(TAG, "onComplete has been already called, onNext should not be called"); |
||||
throw new RuntimeException("onNext should not be called after onComplete has been called"); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private static class OnCompleteRunnable<T> implements Runnable { |
||||
private final OnSubscribe<T> onSubscribe; |
||||
|
||||
public OnCompleteRunnable(@NonNull OnSubscribe<T> onSubscribe) {this.onSubscribe = onSubscribe;} |
||||
|
||||
@Override |
||||
public void run() { |
||||
onSubscribe.onComplete(); |
||||
} |
||||
} |
||||
|
||||
private static class OnNextRunnable<T> implements Runnable { |
||||
private final OnSubscribe<T> onSubscribe; |
||||
private final T item; |
||||
|
||||
public OnNextRunnable(@NonNull OnSubscribe<T> onSubscribe, T item) { |
||||
this.onSubscribe = onSubscribe; |
||||
this.item = item; |
||||
} |
||||
|
||||
@Override |
||||
public void run() { |
||||
onSubscribe.onNext(item); |
||||
} |
||||
} |
||||
|
||||
private static class OnErrorRunnable<T> implements Runnable { |
||||
private final OnSubscribe<T> onSubscribe; |
||||
private final Throwable throwable; |
||||
|
||||
public OnErrorRunnable(@NonNull OnSubscribe<T> onSubscribe, @NonNull Throwable throwable) { |
||||
this.onSubscribe = onSubscribe; |
||||
this.throwable = throwable; |
||||
} |
||||
|
||||
@Override |
||||
public void run() { |
||||
onSubscribe.onError(throwable); |
||||
} |
||||
} |
||||
|
||||
private static class OnStartRunnable<T> implements Runnable { |
||||
private final OnSubscribe<T> onSubscribe; |
||||
|
||||
public OnStartRunnable(@NonNull OnSubscribe<T> onSubscribe) {this.onSubscribe = onSubscribe;} |
||||
|
||||
@Override |
||||
public void run() { |
||||
onSubscribe.onStart(); |
||||
} |
||||
} |
||||
} |
||||
|
@ -1,47 +0,0 @@
@@ -1,47 +0,0 @@
|
||||
package acr.browser.lightning.react; |
||||
|
||||
import android.support.annotation.NonNull; |
||||
import android.support.annotation.Nullable; |
||||
|
||||
public abstract class OnSubscribe<T> { |
||||
|
||||
/** |
||||
* Called when the observable |
||||
* runs into an error that will |
||||
* cause it to abort and not finish. |
||||
* Receiving this callback means that |
||||
* the observable is dead and no |
||||
* {@link #onComplete()} or {@link #onNext(Object)} |
||||
* callbacks will be called. |
||||
* |
||||
* @param throwable an optional throwable that could |
||||
* be sent. |
||||
*/ |
||||
public void onError(@NonNull Throwable throwable) {} |
||||
|
||||
/** |
||||
* Called before the observer begins |
||||
* to process and emit items or complete. |
||||
*/ |
||||
public void onStart() {} |
||||
|
||||
/** |
||||
* Called when the Observer emits an |
||||
* item. It can be called multiple times. |
||||
* It cannot be called after onComplete |
||||
* has been called. |
||||
* |
||||
* @param item the item that has been emitted, |
||||
* can be null. |
||||
*/ |
||||
public void onNext(@Nullable T item) {} |
||||
|
||||
/** |
||||
* This method is called when the observer is |
||||
* finished sending the subscriber events. It |
||||
* is guaranteed that no other methods will be |
||||
* called on the OnSubscribe after this method |
||||
* has been called. |
||||
*/ |
||||
public void onComplete() {} |
||||
} |
@ -1,46 +0,0 @@
@@ -1,46 +0,0 @@
|
||||
package acr.browser.lightning.react; |
||||
|
||||
import android.os.Looper; |
||||
import android.support.annotation.NonNull; |
||||
|
||||
import java.util.concurrent.Executor; |
||||
import java.util.concurrent.Executors; |
||||
|
||||
public class Schedulers { |
||||
private static final Executor sWorker = Executors.newFixedThreadPool(4); |
||||
private static final Executor sIOWorker = Executors.newSingleThreadExecutor(); |
||||
private static final Executor sMain = new ThreadExecutor(Looper.getMainLooper()); |
||||
|
||||
/** |
||||
* The worker thread executor, will |
||||
* execute work on any one of multiple |
||||
* threads. |
||||
* |
||||
* @return a non-null executor. |
||||
*/ |
||||
@NonNull |
||||
public static Executor worker() { |
||||
return sWorker; |
||||
} |
||||
|
||||
/** |
||||
* The main thread. |
||||
* |
||||
* @return a non-null executor that does work on the main thread. |
||||
*/ |
||||
@NonNull |
||||
public static Executor main() { |
||||
return sMain; |
||||
} |
||||
|
||||
/** |
||||
* The io thread. |
||||
* |
||||
* @return a non-null executor that does |
||||
* work on a single thread off the main thread. |
||||
*/ |
||||
@NonNull |
||||
public static Executor io() { |
||||
return sIOWorker; |
||||
} |
||||
} |
@ -1,51 +0,0 @@
@@ -1,51 +0,0 @@
|
||||
package acr.browser.lightning.react; |
||||
|
||||
import android.support.annotation.NonNull; |
||||
import android.support.annotation.Nullable; |
||||
|
||||
public interface Subscriber<T> extends Subscription { |
||||
|
||||
/** |
||||
* Called immediately upon subscribing |
||||
* and before the Observable begins |
||||
* emitting items. This should not be |
||||
* called by the creator of the Observable |
||||
* and is rather called internally by the |
||||
* Observable class itself. |
||||
*/ |
||||
void onStart(); |
||||
|
||||
/** |
||||
* Called when the observable |
||||
* runs into an error that will |
||||
* cause it to abort and not finish. |
||||
* Receiving this callback means that |
||||
* the observable is dead and no |
||||
* {@link #onComplete()} or {@link #onNext(Object)} |
||||
* callbacks will be called. |
||||
* |
||||
* @param throwable an optional throwable that could |
||||
* be sent. |
||||
*/ |
||||
void onError(@NonNull Throwable throwable); |
||||
|
||||
/** |
||||
* Called when the Observer emits an |
||||
* item. It can be called multiple times. |
||||
* It cannot be called after onComplete |
||||
* has been called. |
||||
* |
||||
* @param item the item that has been emitted, |
||||
* can be null. |
||||
*/ |
||||
void onNext(@Nullable T item); |
||||
|
||||
/** |
||||
* This method is called when the observer is |
||||
* finished sending the subscriber events. It |
||||
* is guaranteed that no other methods will be |
||||
* called on the OnSubscribe after this method |
||||
* has been called. |
||||
*/ |
||||
void onComplete(); |
||||
} |
@ -1,7 +0,0 @@
@@ -1,7 +0,0 @@
|
||||
package acr.browser.lightning.react; |
||||
|
||||
public interface Subscription { |
||||
|
||||
void unsubscribe(); |
||||
|
||||
} |
@ -1,21 +0,0 @@
@@ -1,21 +0,0 @@
|
||||
package acr.browser.lightning.react; |
||||
|
||||
import android.os.Handler; |
||||
import android.os.Looper; |
||||
import android.support.annotation.NonNull; |
||||
|
||||
import java.util.concurrent.Executor; |
||||
|
||||
class ThreadExecutor implements Executor { |
||||
|
||||
private final Handler mHandler; |
||||
|
||||
public ThreadExecutor(@NonNull Looper looper) { |
||||
mHandler = new Handler(looper); |
||||
} |
||||
|
||||
@Override |
||||
public void execute(@NonNull Runnable command) { |
||||
mHandler.post(command); |
||||
} |
||||
} |
@ -0,0 +1,418 @@
@@ -0,0 +1,418 @@
|
||||
package acr.browser.lightning.search; |
||||
|
||||
import android.app.Application; |
||||
import android.content.Context; |
||||
import android.graphics.Color; |
||||
import android.graphics.drawable.Drawable; |
||||
import android.support.annotation.NonNull; |
||||
import android.support.annotation.Nullable; |
||||
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 java.io.File; |
||||
import java.io.FilenameFilter; |
||||
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.concurrent.Executor; |
||||
import java.util.concurrent.TimeUnit; |
||||
|
||||
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 com.anthonycr.bonsai.Action; |
||||
import com.anthonycr.bonsai.Observable; |
||||
import com.anthonycr.bonsai.OnSubscribe; |
||||
import com.anthonycr.bonsai.Scheduler; |
||||
import com.anthonycr.bonsai.Schedulers; |
||||
import com.anthonycr.bonsai.Subscriber; |
||||
import acr.browser.lightning.utils.ThemeUtils; |
||||
|
||||
public class Suggestions extends BaseAdapter implements Filterable { |
||||
|
||||
private static final Scheduler FILTER_SCHEDULER = Schedulers.newSingleThreadedScheduler(); |
||||
|
||||
public static final String CACHE_FILE_TYPE = ".sgg"; |
||||
|
||||
private final List<HistoryItem> mFilteredList = new ArrayList<>(5); |
||||
|
||||
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 static final int MAX_SUGGESTIONS = 5; |
||||
|
||||
@NonNull private final Drawable mSearchDrawable; |
||||
@NonNull private final Drawable mHistoryDrawable; |
||||
@NonNull private final Drawable mBookmarkDrawable; |
||||
|
||||
private final Comparator<HistoryItem> mFilterComparator = new SuggestionsComparator(); |
||||
|
||||
@Inject HistoryDatabase mDatabaseHandler; |
||||
@Inject BookmarkManager mBookmarkManager; |
||||
@Inject PreferenceManager mPreferenceManager; |
||||
|
||||
private final List<HistoryItem> mAllBookmarks = new ArrayList<>(5); |
||||
|
||||
private boolean mDarkTheme; |
||||
private boolean mUseGoogle = true; |
||||
private boolean mIsIncognito = true; |
||||
@NonNull private Context mContext; |
||||
|
||||
public Suggestions(@NonNull Context context, boolean dark, boolean incognito) { |
||||
super(); |
||||
BrowserApp.getAppComponent().inject(this); |
||||
mContext = context; |
||||
mDarkTheme = dark || incognito; |
||||
mIsIncognito = incognito; |
||||
|
||||
mUseGoogle = mPreferenceManager.getGoogleSearchSuggestionsEnabled(); |
||||
|
||||
refreshBookmarks(); |
||||
|
||||
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); |
||||
|
||||
clearCache(); |
||||
} |
||||
|
||||
public void refreshPreferences() { |
||||
mUseGoogle = mPreferenceManager.getGoogleSearchSuggestionsEnabled(); |
||||
} |
||||
|
||||
private void clearCache() { |
||||
BrowserApp.getTaskThread().execute(new ClearCacheRunnable(BrowserApp.get(mContext))); |
||||
} |
||||
|
||||
public void refreshBookmarks() { |
||||
mAllBookmarks.clear(); |
||||
mAllBookmarks.addAll(mBookmarkManager.getAllBookmarks(true)); |
||||
} |
||||
|
||||
@Override |
||||
public int getCount() { |
||||
return mFilteredList.size(); |
||||
} |
||||
|
||||
@Override |
||||
public Object getItem(int position) { |
||||
if (position > mFilteredList.size() || position < 0) { |
||||
return null; |
||||
} |
||||
return mFilteredList.get(position); |
||||
} |
||||
|
||||
@Override |
||||
public long getItemId(int position) { |
||||
return 0; |
||||
} |
||||
|
||||
private static class SuggestionHolder { |
||||
|
||||
public SuggestionHolder(View view) { |
||||
mTitle = (TextView) view.findViewById(R.id.title); |
||||
mUrl = (TextView) view.findViewById(R.id.url); |
||||
mImage = (ImageView) view.findViewById(R.id.suggestionIcon); |
||||
} |
||||
|
||||
ImageView mImage; |
||||
TextView mTitle; |
||||
TextView mUrl; |
||||
|
||||
} |
||||
|
||||
@Override |
||||
public View getView(int position, 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(convertView); |
||||
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() { |
||||
return new SearchFilter(this); |
||||
} |
||||
|
||||
private synchronized void publishResults(List<HistoryItem> list) { |
||||
mFilteredList.clear(); |
||||
mFilteredList.addAll(list); |
||||
notifyDataSetChanged(); |
||||
} |
||||
|
||||
private void clearSuggestions() { |
||||
Observable.create(new Action<Void>() { |
||||
@Override |
||||
public void onSubscribe(@NonNull Subscriber<Void> subscriber) { |
||||
mBookmarks.clear(); |
||||
mHistory.clear(); |
||||
mSuggestions.clear(); |
||||
subscriber.onComplete(); |
||||
} |
||||
}).subscribeOn(FILTER_SCHEDULER) |
||||
.observeOn(Schedulers.main()) |
||||
.subscribe(); |
||||
} |
||||
|
||||
private void combineResults(final @Nullable List<HistoryItem> bookmarkList, |
||||
final @Nullable List<HistoryItem> historyList, |
||||
final @Nullable List<HistoryItem> suggestionList) { |
||||
Observable.create(new Action<List<HistoryItem>>() { |
||||
@Override |
||||
public void onSubscribe(@NonNull Subscriber<List<HistoryItem>> subscriber) { |
||||
List<HistoryItem> list = new ArrayList<>(5); |
||||
if (bookmarkList != null) { |
||||
mBookmarks.clear(); |
||||
mBookmarks.addAll(bookmarkList); |
||||
} |
||||
if (historyList != null) { |
||||
mHistory.clear(); |
||||
mHistory.addAll(historyList); |
||||
} |
||||
if (suggestionList != null) { |
||||
mSuggestions.clear(); |
||||
mSuggestions.addAll(suggestionList); |
||||
} |
||||
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()) { |
||||
break; |
||||
} |
||||
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()); |
||||
} |
||||
} |
||||
|
||||
Collections.sort(list, mFilterComparator); |
||||
subscriber.onNext(list); |
||||
subscriber.onComplete(); |
||||
} |
||||
}).subscribeOn(FILTER_SCHEDULER) |
||||
.observeOn(Schedulers.main()) |
||||
.subscribe(new OnSubscribe<List<HistoryItem>>() { |
||||
@Override |
||||
public void onNext(@Nullable List<HistoryItem> item) { |
||||
publishResults(item); |
||||
} |
||||
}); |
||||
|
||||
} |
||||
|
||||
@NonNull |
||||
private Observable<List<HistoryItem>> getBookmarksForQuery(@NonNull final String query) { |
||||
return Observable.create(new Action<List<HistoryItem>>() { |
||||
@Override |
||||
public void onSubscribe(@NonNull Subscriber<List<HistoryItem>> subscriber) { |
||||
List<HistoryItem> bookmarks = new ArrayList<>(5); |
||||
int counter = 0; |
||||
for (int n = 0; n < mAllBookmarks.size(); n++) { |
||||
if (counter >= 5) { |
||||
break; |
||||
} |
||||
if (mAllBookmarks.get(n).getTitle().toLowerCase(Locale.getDefault()) |
||||
.startsWith(query)) { |
||||
bookmarks.add(mAllBookmarks.get(n)); |
||||
counter++; |
||||
} else if (mAllBookmarks.get(n).getUrl().contains(query)) { |
||||
bookmarks.add(mAllBookmarks.get(n)); |
||||
counter++; |
||||
} |
||||
} |
||||
subscriber.onNext(bookmarks); |
||||
subscriber.onComplete(); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
@NonNull |
||||
private Observable<List<HistoryItem>> getSuggestionsForQuery(@NonNull final String query) { |
||||
return SuggestionsTask.getObservable(query, mContext); |
||||
} |
||||
|
||||
@NonNull |
||||
private Observable<List<HistoryItem>> getHistoryForQuery(@NonNull final String query) { |
||||
return Observable.create(new Action<List<HistoryItem>>() { |
||||
@Override |
||||
public void onSubscribe(@NonNull Subscriber<List<HistoryItem>> subscriber) { |
||||
List<HistoryItem> historyList = mDatabaseHandler.findItemsContaining(query); |
||||
subscriber.onNext(historyList); |
||||
subscriber.onComplete(); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
private boolean shouldRequestNetwork() { |
||||
return mUseGoogle && !mIsIncognito; |
||||
} |
||||
|
||||
private static class SearchFilter extends Filter { |
||||
|
||||
@NonNull private Suggestions mSuggestions; |
||||
|
||||
public SearchFilter(@NonNull Suggestions suggestions) { |
||||
mSuggestions = suggestions; |
||||
} |
||||
|
||||
@Override |
||||
protected FilterResults performFiltering(CharSequence constraint) { |
||||
FilterResults results = new FilterResults(); |
||||
if (constraint == null || constraint.length() == 0) { |
||||
mSuggestions.clearSuggestions(); |
||||
return results; |
||||
} |
||||
String query = constraint.toString().toLowerCase(Locale.getDefault()); |
||||
|
||||
if (mSuggestions.shouldRequestNetwork() && !SuggestionsTask.isRequestInProgress()) { |
||||
mSuggestions.getSuggestionsForQuery(query) |
||||
.subscribeOn(Schedulers.worker()) |
||||
.observeOn(Schedulers.main()) |
||||
.subscribe(new OnSubscribe<List<HistoryItem>>() { |
||||
@Override |
||||
public void onNext(@Nullable List<HistoryItem> item) { |
||||
mSuggestions.combineResults(null, null, item); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
mSuggestions.getBookmarksForQuery(query) |
||||
.subscribeOn(Schedulers.io()) |
||||
.observeOn(Schedulers.main()) |
||||
.subscribe(new OnSubscribe<List<HistoryItem>>() { |
||||
@Override |
||||
public void onNext(@Nullable List<HistoryItem> item) { |
||||
mSuggestions.combineResults(item, null, null); |
||||
} |
||||
}); |
||||
|
||||
mSuggestions.getHistoryForQuery(query) |
||||
.subscribeOn(Schedulers.io()) |
||||
.observeOn(Schedulers.main()) |
||||
.subscribe(new OnSubscribe<List<HistoryItem>>() { |
||||
@Override |
||||
public void onNext(@Nullable List<HistoryItem> item) { |
||||
mSuggestions.combineResults(null, item, null); |
||||
} |
||||
}); |
||||
results.count = 1; |
||||
return results; |
||||
} |
||||
|
||||
@Override |
||||
public CharSequence convertResultToString(Object resultValue) { |
||||
return ((HistoryItem) resultValue).getUrl(); |
||||
} |
||||
|
||||
@Override |
||||
protected void publishResults(CharSequence constraint, FilterResults results) { |
||||
mSuggestions.combineResults(null, null, null); |
||||
} |
||||
} |
||||
|
||||
private static class ClearCacheRunnable implements Runnable { |
||||
|
||||
@NonNull |
||||
private final Application app; |
||||
|
||||
public ClearCacheRunnable(@NonNull 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() - TimeUnit.DAYS.toMillis(1); |
||||
for (String fileName : fileList) { |
||||
File file = new File(dir.getPath() + fileName); |
||||
if (earliestTimeAllowed > file.lastModified()) { |
||||
file.delete(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private static class NameFilter implements FilenameFilter { |
||||
|
||||
@Override |
||||
public boolean accept(File dir, @NonNull String filename) { |
||||
return filename.endsWith(CACHE_FILE_TYPE); |
||||
} |
||||
} |
||||
} |
||||
|
||||
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; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,223 @@
@@ -0,0 +1,223 @@
|
||||
package acr.browser.lightning.search; |
||||
|
||||
import android.app.Application; |
||||
import android.content.Context; |
||||
import android.net.ConnectivityManager; |
||||
import android.net.NetworkInfo; |
||||
import android.support.annotation.NonNull; |
||||
import android.support.annotation.Nullable; |
||||
import android.text.TextUtils; |
||||
import android.util.Log; |
||||
|
||||
import org.xmlpull.v1.XmlPullParser; |
||||
import org.xmlpull.v1.XmlPullParserException; |
||||
import org.xmlpull.v1.XmlPullParserFactory; |
||||
|
||||
import java.io.BufferedInputStream; |
||||
import java.io.File; |
||||
import java.io.FileInputStream; |
||||
import java.io.FileOutputStream; |
||||
import java.io.InputStream; |
||||
import java.io.UnsupportedEncodingException; |
||||
import java.lang.ref.WeakReference; |
||||
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; |
||||
import java.util.regex.Pattern; |
||||
|
||||
import acr.browser.lightning.R; |
||||
import acr.browser.lightning.app.BrowserApp; |
||||
import acr.browser.lightning.database.HistoryItem; |
||||
import com.anthonycr.bonsai.Action; |
||||
import com.anthonycr.bonsai.Observable; |
||||
import com.anthonycr.bonsai.Subscriber; |
||||
import acr.browser.lightning.utils.Utils; |
||||
|
||||
public class SuggestionsTask { |
||||
|
||||
private static final String TAG = RetrieveSuggestionsTask.class.getSimpleName(); |
||||
|
||||
private static final Pattern SPACE_PATTERN = Pattern.compile(" ", Pattern.LITERAL); |
||||
private static final String ENCODING = "ISO-8859-1"; |
||||
private static final long INTERVAL_DAY = TimeUnit.DAYS.toMillis(1); |
||||
private static final String DEFAULT_LANGUAGE = "en"; |
||||
@Nullable private static XmlPullParser sXpp; |
||||
@Nullable private static String sLanguage; |
||||
@NonNull private final SuggestionsResult mResultCallback; |
||||
@NonNull private final Application mApplication; |
||||
@NonNull private final String mSearchSubtitle; |
||||
@NonNull private String mQuery; |
||||
|
||||
private static volatile boolean sIsTaskExecuting = false; |
||||
|
||||
public static boolean isRequestInProgress() { |
||||
return sIsTaskExecuting; |
||||
} |
||||
|
||||
public static Observable<List<HistoryItem>> getObservable(@NonNull final String query, @NonNull final Context context) { |
||||
return Observable.create(new Action<List<HistoryItem>>() { |
||||
@Override |
||||
public void onSubscribe(@NonNull final Subscriber<List<HistoryItem>> subscriber) { |
||||
sIsTaskExecuting = true; |
||||
new SuggestionsTask(query, BrowserApp.get(context), new SuggestionsResult() { |
||||
@Override |
||||
public void resultReceived(@NonNull List<HistoryItem> searchResults) { |
||||
subscriber.onNext(searchResults); |
||||
subscriber.onComplete(); |
||||
} |
||||
}).run(); |
||||
sIsTaskExecuting = false; |
||||
} |
||||
}); |
||||
} |
||||
|
||||
private SuggestionsTask(@NonNull String query, |
||||
@NonNull Application application, |
||||
@NonNull SuggestionsResult callback) { |
||||
mQuery = query; |
||||
mResultCallback = callback; |
||||
mApplication = application; |
||||
mSearchSubtitle = mApplication.getString(R.string.suggestion); |
||||
} |
||||
|
||||
@NonNull |
||||
private static synchronized String getLanguage() { |
||||
if (sLanguage == null) { |
||||
sLanguage = Locale.getDefault().getLanguage(); |
||||
} |
||||
if (TextUtils.isEmpty(sLanguage)) { |
||||
sLanguage = DEFAULT_LANGUAGE; |
||||
} |
||||
return sLanguage; |
||||
} |
||||
|
||||
@NonNull |
||||
private static synchronized XmlPullParser getParser() throws XmlPullParserException { |
||||
if (sXpp == null) { |
||||
XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); |
||||
factory.setNamespaceAware(true); |
||||
sXpp = factory.newPullParser(); |
||||
} |
||||
return sXpp; |
||||
} |
||||
|
||||
private void run() { |
||||
List<HistoryItem> filter = new ArrayList<>(5); |
||||
try { |
||||
mQuery = SPACE_PATTERN.matcher(mQuery).replaceAll("+"); |
||||
URLEncoder.encode(mQuery, ENCODING); |
||||
} catch (UnsupportedEncodingException e) { |
||||
e.printStackTrace(); |
||||
} |
||||
File cache = downloadSuggestionsForQuery(mQuery, getLanguage(), mApplication); |
||||
if (!cache.exists()) { |
||||
post(filter); |
||||
return; |
||||
} |
||||
InputStream fileInput = null; |
||||
try { |
||||
fileInput = new BufferedInputStream(new FileInputStream(cache)); |
||||
XmlPullParser parser = getParser(); |
||||
parser.setInput(fileInput, ENCODING); |
||||
int eventType = parser.getEventType(); |
||||
int counter = 0; |
||||
while (eventType != XmlPullParser.END_DOCUMENT) { |
||||
if (eventType == XmlPullParser.START_TAG && "suggestion".equals(parser.getName())) { |
||||
String suggestion = parser.getAttributeValue(null, "data"); |
||||
filter.add(new HistoryItem(mSearchSubtitle + " \"" + suggestion + '"', |
||||
suggestion, R.drawable.ic_search)); |
||||
counter++; |
||||
if (counter >= 5) { |
||||
break; |
||||
} |
||||
} |
||||
eventType = parser.next(); |
||||
} |
||||
} catch (Exception e) { |
||||
post(filter); |
||||
return; |
||||
} finally { |
||||
Utils.close(fileInput); |
||||
} |
||||
post(filter); |
||||
} |
||||
|
||||
private void post(@NonNull List<HistoryItem> result) { |
||||
mResultCallback.resultReceived(result); |
||||
} |
||||
|
||||
/** |
||||
* 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() + Suggestions.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()); |
||||
connection.disconnect(); |
||||
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(); |
||||
} |
||||
connection.disconnect(); |
||||
cacheFile.setLastModified(System.currentTimeMillis()); |
||||
} catch (Exception e) { |
||||
Log.w(TAG, "Problem getting search suggestions", e); |
||||
} 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(); |
||||
} |
||||
|
||||
@Nullable |
||||
private static NetworkInfo getActiveNetworkInfo(@NonNull Context context) { |
||||
ConnectivityManager connectivity = (ConnectivityManager) context |
||||
.getApplicationContext() |
||||
.getSystemService(Context.CONNECTIVITY_SERVICE); |
||||
if (connectivity == null) { |
||||
return null; |
||||
} |
||||
return connectivity.getActiveNetworkInfo(); |
||||
} |
||||
|
||||
} |
@ -1,49 +1,57 @@
@@ -1,49 +1,57 @@
|
||||
<!-- Copyright 2014 ACR Development --> |
||||
<LinearLayout |
||||
android:id="@+id/main_layout" |
||||
<android.support.design.widget.CoordinatorLayout |
||||
android:id="@+id/coordinator_layout" |
||||
xmlns:android="http://schemas.android.com/apk/res/android" |
||||
xmlns:tools="http://schemas.android.com/tools" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent" |
||||
android:background="@null" |
||||
android:orientation="vertical"> |
||||
tools:context=".activity.BrowserActivity"> |
||||
|
||||
<android.support.v4.widget.DrawerLayout |
||||
android:id="@+id/drawer_layout" |
||||
<LinearLayout |
||||
android:id="@+id/main_layout" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent" |
||||
android:fitsSystemWindows="true"> |
||||
android:background="@null" |
||||
android:orientation="vertical"> |
||||
|
||||
<LinearLayout |
||||
android:id="@+id/ui_layout" |
||||
<android.support.v4.widget.DrawerLayout |
||||
android:id="@+id/drawer_layout" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent" |
||||
android:orientation="vertical"> |
||||
|
||||
<include layout="@layout/toolbar"/> |
||||
|
||||
<include layout="@layout/browser_content"/> |
||||
|
||||
<include layout="@layout/search_interface"/> |
||||
</LinearLayout> |
||||
|
||||
<FrameLayout |
||||
android:id="@+id/left_drawer" |
||||
android:layout_width="@dimen/navigation_width" |
||||
android:layout_height="match_parent" |
||||
android:layout_gravity="start" |
||||
android:background="?attr/drawerBackground" |
||||
android:fitsSystemWindows="true" |
||||
android:weightSum="1"/> |
||||
|
||||
<FrameLayout |
||||
android:id="@+id/right_drawer" |
||||
android:layout_width="@dimen/navigation_width" |
||||
android:layout_height="match_parent" |
||||
android:layout_gravity="end" |
||||
android:background="?attr/drawerBackground" |
||||
android:fitsSystemWindows="true" |
||||
android:weightSum="1"/> |
||||
|
||||
</android.support.v4.widget.DrawerLayout> |
||||
|
||||
</LinearLayout> |
||||
android:fitsSystemWindows="true"> |
||||
|
||||
<LinearLayout |
||||
android:id="@+id/ui_layout" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent" |
||||
android:orientation="vertical"> |
||||
|
||||
<include layout="@layout/toolbar"/> |
||||
|
||||
<include layout="@layout/browser_content"/> |
||||
|
||||
<include layout="@layout/search_interface"/> |
||||
</LinearLayout> |
||||
|
||||
<FrameLayout |
||||
android:id="@+id/left_drawer" |
||||
android:layout_width="@dimen/navigation_width" |
||||
android:layout_height="match_parent" |
||||
android:layout_gravity="start" |
||||
android:background="?attr/drawerBackground" |
||||
android:fitsSystemWindows="true" |
||||
android:weightSum="1"/> |
||||
|
||||
<FrameLayout |
||||
android:id="@+id/right_drawer" |
||||
android:layout_width="@dimen/navigation_width" |
||||
android:layout_height="match_parent" |
||||
android:layout_gravity="end" |
||||
android:background="?attr/drawerBackground" |
||||
android:fitsSystemWindows="true" |
||||
android:weightSum="1"/> |
||||
|
||||
</android.support.v4.widget.DrawerLayout> |
||||
|
||||
</LinearLayout> |
||||
</android.support.design.widget.CoordinatorLayout> |
Loading…
Reference in new issue