Anthony Restaino
8 years ago
26 changed files with 820 additions and 588 deletions
@ -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 @@ |
|||||||
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 @@ |
|||||||
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 @@ |
|||||||
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 @@ |
|||||||
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 @@ |
|||||||
package acr.browser.lightning.react; |
|
||||||
|
|
||||||
public interface Subscription { |
|
||||||
|
|
||||||
void unsubscribe(); |
|
||||||
|
|
||||||
} |
|
@ -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 @@ |
|||||||
|
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 @@ |
|||||||
|
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(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
Loading…
Reference in new issue