Lightning browser with I2P configuration
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.
 
 

611 lines
22 KiB

package org.purplei2p.lightning.browser;
import android.app.Activity;
import android.app.Application;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AlertDialog;
import android.text.TextUtils;
import android.util.Log;
import android.webkit.URLUtil;
import android.webkit.WebView;
import com.anthonycr.bonsai.Completable;
import com.anthonycr.bonsai.CompletableAction;
import com.anthonycr.bonsai.CompletableSubscriber;
import com.anthonycr.bonsai.Schedulers;
import com.anthonycr.bonsai.SingleOnSubscribe;
import com.anthonycr.bonsai.Stream;
import com.anthonycr.bonsai.StreamAction;
import com.anthonycr.bonsai.StreamOnSubscribe;
import com.anthonycr.bonsai.StreamSubscriber;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import org.purplei2p.lightning.R;
import org.purplei2p.lightning.BrowserApp;
import org.purplei2p.lightning.constant.BookmarkPage;
import org.purplei2p.lightning.constant.DownloadsPage;
import org.purplei2p.lightning.constant.HistoryPage;
import org.purplei2p.lightning.constant.StartPage;
import org.purplei2p.lightning.dialog.BrowserDialog;
import org.purplei2p.lightning.preference.PreferenceManager;
import org.purplei2p.lightning.utils.FileUtils;
import org.purplei2p.lightning.utils.Preconditions;
import org.purplei2p.lightning.utils.UrlUtils;
import org.purplei2p.lightning.view.LightningView;
/**
* A manager singleton that holds all the {@link LightningView}
* and tracks the current tab. It handles creation, deletion,
* restoration, state saving, and switching of tabs.
*/
public class TabsManager {
private static final String TAG = "TabsManager";
private static final String BUNDLE_KEY = "WEBVIEW_";
private static final String URL_KEY = "URL_KEY";
private static final String BUNDLE_STORAGE = "SAVED_TABS.parcel";
@NonNull private final List<LightningView> mTabList = new ArrayList<>(1);
@Nullable private LightningView mCurrentTab;
@Nullable private TabNumberChangedListener mTabNumberListener;
private boolean mIsInitialized = false;
@NonNull private final List<Runnable> mPostInitializationWorkList = new ArrayList<>();
@Inject PreferenceManager mPreferenceManager;
@Inject Application mApp;
public TabsManager() {
BrowserApp.getAppComponent().inject(this);
}
// TODO remove and make presenter call new tab methods so it always knows
@Deprecated
public interface TabNumberChangedListener {
void tabNumberChanged(int newNumber);
}
public void setTabNumberChangedListener(@Nullable TabNumberChangedListener listener) {
mTabNumberListener = listener;
}
public void cancelPendingWork() {
mPostInitializationWorkList.clear();
}
public synchronized void doAfterInitialization(@NonNull Runnable runnable) {
if (mIsInitialized) {
runnable.run();
} else {
mPostInitializationWorkList.add(runnable);
}
}
private synchronized void finishInitialization() {
mIsInitialized = true;
for (Runnable runnable : mPostInitializationWorkList) {
runnable.run();
}
}
/**
* Restores old tabs that were open before the browser
* was closed. Handles the intent used to open the browser.
*
* @param activity the activity needed to create tabs.
* @param intent the intent that started the browser activity.
* @param incognito whether or not we are in incognito mode.
*/
@NonNull
public synchronized Completable initializeTabs(@NonNull final Activity activity,
@Nullable final Intent intent,
final boolean incognito) {
return Completable.create(new CompletableAction() {
@Override
public void onSubscribe(@NonNull CompletableSubscriber subscriber) {
// Make sure we start with a clean tab list
shutdown();
String url = null;
if (intent != null) {
url = intent.getDataString();
}
// If incognito, only create one tab
if (incognito) {
newTab(activity, url, true);
subscriber.onComplete();
return;
}
Log.d(TAG, "URL from intent: " + url);
mCurrentTab = null;
if (mPreferenceManager.getRestoreLostTabsEnabled()) {
restoreLostTabs(url, activity, subscriber);
} else {
if (!TextUtils.isEmpty(url)) {
newTab(activity, url, false);
} else {
newTab(activity, null, false);
}
finishInitialization();
subscriber.onComplete();
}
}
});
}
private void restoreLostTabs(@Nullable final String url, @NonNull final Activity activity,
@NonNull final CompletableSubscriber subscriber) {
restoreState()
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.main())
.subscribe(new StreamOnSubscribe<Bundle>() {
@Override
public void onNext(@Nullable Bundle item) {
final LightningView tab = newTab(activity, "", false);
Preconditions.checkNonNull(item);
String url = item.getString(URL_KEY);
if (url != null && tab.getWebView() != null) {
if (UrlUtils.isBookmarkUrl(url)) {
new BookmarkPage(activity).getBookmarkPage()
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.main())
.subscribe(new SingleOnSubscribe<String>() {
@Override
public void onItem(@Nullable String item) {
Preconditions.checkNonNull(item);
tab.loadUrl(item);
}
});
} else if (UrlUtils.isDownloadsUrl(url)) {
new DownloadsPage().getDownloadsPage()
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.main())
.subscribe(new SingleOnSubscribe<String>() {
@Override
public void onItem(@Nullable String item) {
Preconditions.checkNonNull(item);
tab.loadUrl(item);
}
});
} else if (UrlUtils.isStartPageUrl(url)) {
new StartPage().getHomepage()
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.main())
.subscribe(new SingleOnSubscribe<String>() {
@Override
public void onItem(@Nullable String item) {
Preconditions.checkNonNull(item);
tab.loadUrl(item);
}
});
} else if (UrlUtils.isHistoryUrl(url)) {
new HistoryPage().getHistoryPage()
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.main())
.subscribe(new SingleOnSubscribe<String>() {
@Override
public void onItem(@Nullable String item) {
Preconditions.checkNonNull(item);
tab.loadUrl(item);
}
});
}
} else if (tab.getWebView() != null) {
tab.getWebView().restoreState(item);
}
}
@Override
public void onComplete() {
if (url != null) {
if (URLUtil.isFileUrl(url)) {
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
Dialog dialog = builder.setCancelable(true)
.setTitle(R.string.title_warning)
.setMessage(R.string.message_blocked_local)
.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
if (mTabList.isEmpty()) {
newTab(activity, null, false);
}
finishInitialization();
subscriber.onComplete();
}
})
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.action_open, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
newTab(activity, url, false);
}
}).show();
BrowserDialog.setDialogSize(activity, dialog);
} else {
newTab(activity, url, false);
if (mTabList.isEmpty()) {
newTab(activity, null, false);
}
finishInitialization();
subscriber.onComplete();
}
} else {
if (mTabList.isEmpty()) {
newTab(activity, null, false);
}
finishInitialization();
subscriber.onComplete();
}
}
});
}
/**
* Method used to resume all the tabs in the browser.
* This is necessary because we cannot pause the
* WebView when the app is open currently due to a
* bug in the WebView, where calling onResume doesn't
* consistently resume it.
*
* @param context the context needed to initialize
* the LightningView preferences.
*/
public void resumeAll(@NonNull Context context) {
LightningView current = getCurrentTab();
if (current != null) {
current.resumeTimers();
}
for (LightningView tab : mTabList) {
if (tab != null) {
tab.onResume();
tab.initializePreferences(context);
}
}
}
/**
* Method used to pause all the tabs in the browser.
* This is necessary because we cannot pause the
* WebView when the app is open currently due to a
* bug in the WebView, where calling onResume doesn't
* consistently resume it.
*/
public void pauseAll() {
LightningView current = getCurrentTab();
if (current != null) {
current.pauseTimers();
}
for (LightningView tab : mTabList) {
if (tab != null) {
tab.onPause();
}
}
}
/**
* Return the tab at the given position in tabs list, or
* null if position is not in tabs list range.
*
* @param position the index in tabs list
* @return the corespondent {@link LightningView},
* or null if the index is invalid
*/
@Nullable
public synchronized LightningView getTabAtPosition(final int position) {
if (position < 0 || position >= mTabList.size()) {
return null;
}
return mTabList.get(position);
}
/**
* Frees memory for each tab in the
* manager. Note: this will only work
* on API < KITKAT as on KITKAT onward
* the WebViews manage their own
* memory correctly.
*/
public synchronized void freeMemory() {
for (LightningView tab : mTabList) {
//noinspection deprecation
tab.freeMemory();
}
}
/**
* Shutdown the manager. This destroys
* all tabs and clears the references
* to those tabs. Current tab is also
* released for garbage collection.
*/
public synchronized void shutdown() {
for (LightningView tab : mTabList) {
tab.onDestroy();
}
mTabList.clear();
mIsInitialized = false;
mCurrentTab = null;
}
/**
* Forwards network connection status to the WebViews.
*
* @param isConnected whether there is a network
* connection or not.
*/
public synchronized void notifyConnectionStatus(final boolean isConnected) {
for (LightningView tab : mTabList) {
final WebView webView = tab.getWebView();
if (webView != null) {
webView.setNetworkAvailable(isConnected);
}
}
}
/**
* The current number of tabs in the manager.
*
* @return the number of tabs in the list.
*/
public synchronized int size() {
return mTabList.size();
}
/**
* The index of the last tab in the manager.
*
* @return the last tab in the list or -1 if there are no tabs.
*/
public synchronized int last() {
return mTabList.size() - 1;
}
/**
* The last tab in the tab manager.
*
* @return the last tab, or null if
* there are no tabs.
*/
@Nullable
public synchronized LightningView lastTab() {
if (last() < 0) {
return null;
}
return mTabList.get(last());
}
/**
* Create and return a new tab. The tab is
* automatically added to the tabs list.
*
* @param activity the activity needed to create the tab.
* @param url the URL to initialize the tab with.
* @param isIncognito whether the tab is an incognito
* tab or not.
* @return a valid initialized tab.
*/
@NonNull
public synchronized LightningView newTab(@NonNull final Activity activity,
@Nullable final String url,
final boolean isIncognito) {
Log.d(TAG, "New tab");
final LightningView tab = new LightningView(activity, url, isIncognito);
mTabList.add(tab);
if (mTabNumberListener != null) {
mTabNumberListener.tabNumberChanged(size());
}
return tab;
}
/**
* Removes a tab from the list and destroys the tab.
* If the tab removed is the current tab, the reference
* to the current tab will be nullified.
*
* @param position The position of the tab to remove.
*/
private synchronized void removeTab(final int position) {
if (position >= mTabList.size()) {
return;
}
final LightningView tab = mTabList.remove(position);
if (mCurrentTab == tab) {
mCurrentTab = null;
}
tab.onDestroy();
}
/**
* Deletes a tab from the manager. If the tab
* being deleted is the current tab, this method
* will switch the current tab to a new valid tab.
*
* @param position the position of the tab to delete.
* @return returns true if the current tab
* was deleted, false otherwise.
*/
public synchronized boolean deleteTab(int position) {
Log.d(TAG, "Delete tab: " + position);
final LightningView currentTab = getCurrentTab();
int current = positionOf(currentTab);
if (current == position) {
if (size() == 1) {
mCurrentTab = null;
} else if (current < size() - 1) {
// There is another tab after this one
switchToTab(current + 1);
} else {
switchToTab(current - 1);
}
}
removeTab(position);
if (mTabNumberListener != null) {
mTabNumberListener.tabNumberChanged(size());
}
return current == position;
}
/**
* Return the position of the given tab.
*
* @param tab the tab to look for.
* @return the position of the tab or -1
* if the tab is not in the list.
*/
public synchronized int positionOf(final LightningView tab) {
return mTabList.indexOf(tab);
}
/**
* Saves the state of the current WebViews,
* to a bundle which is then stored in persistent
* storage and can be unparceled.
*/
public void saveState() {
Bundle outState = new Bundle(ClassLoader.getSystemClassLoader());
Log.d(TAG, "Saving tab state");
for (int n = 0; n < mTabList.size(); n++) {
LightningView tab = mTabList.get(n);
if (TextUtils.isEmpty(tab.getUrl())) {
continue;
}
Bundle state = new Bundle(ClassLoader.getSystemClassLoader());
if (tab.getWebView() != null && !UrlUtils.isSpecialUrl(tab.getUrl())) {
tab.getWebView().saveState(state);
outState.putBundle(BUNDLE_KEY + n, state);
} else if (tab.getWebView() != null) {
state.putString(URL_KEY, tab.getUrl());
outState.putBundle(BUNDLE_KEY + n, state);
}
}
FileUtils.writeBundleToStorage(mApp, outState, BUNDLE_STORAGE);
}
/**
* Use this method to clear the saved
* state if you do not wish it to be
* restored when the browser next starts.
*/
public void clearSavedState() {
FileUtils.deleteBundleInStorage(mApp, BUNDLE_STORAGE);
}
/**
* Restores the previously saved tabs from the
* bundle stored in peristent file storage.
* It will create new tabs for each tab saved
* and will delete the saved instance file when
* restoration is complete.
*/
private Stream<Bundle> restoreState() {
return Stream.create(new StreamAction<Bundle>() {
@Override
public void onSubscribe(@NonNull StreamSubscriber<Bundle> subscriber) {
Bundle savedState = FileUtils.readBundleFromStorage(mApp, BUNDLE_STORAGE);
if (savedState != null) {
Log.d(TAG, "Restoring previous WebView state now");
for (String key : savedState.keySet()) {
if (key.startsWith(BUNDLE_KEY)) {
subscriber.onNext(savedState.getBundle(key));
}
}
}
FileUtils.deleteBundleInStorage(mApp, BUNDLE_STORAGE);
subscriber.onComplete();
}
});
}
/**
* Returns the index of the current tab.
*
* @return Return the index of the current tab, or -1 if the
* current tab is null.
*/
public synchronized int indexOfCurrentTab() {
return mTabList.indexOf(mCurrentTab);
}
/**
* Returns the index of the tab.
*
* @return Return the index of the tab, or -1 if the tab isn't in the list.
*/
public synchronized int indexOfTab(LightningView tab) {
return mTabList.indexOf(tab);
}
/**
* Return the current {@link LightningView} or null if
* no current tab has been set.
*
* @return a {@link LightningView} or null if there
* is no current tab.
*/
@Nullable
public synchronized LightningView getCurrentTab() {
return mCurrentTab;
}
/**
* Returns the {@link LightningView} with
* the provided hash, or null if there is
* no tab with the hash.
*
* @param hashCode the hashcode.
* @return the tab with an identical hash, or null.
*/
@Nullable
public synchronized LightningView getTabForHashCode(int hashCode) {
for (LightningView tab : mTabList) {
if (tab.getWebView() != null) {
if (tab.getWebView().hashCode() == hashCode) {
return tab;
}
}
}
return null;
}
/**
* Switch the current tab to the one at the given position.
* It returns the selected tab that has been switced to.
*
* @return the selected tab or null if position is out of tabs range.
*/
@Nullable
public synchronized LightningView switchToTab(final int position) {
Log.d(TAG, "switch to tab: " + position);
if (position < 0 || position >= mTabList.size()) {
Log.e(TAG, "Returning a null LightningView requested for position: " + position);
return null;
} else {
final LightningView tab = mTabList.get(position);
if (tab != null) {
mCurrentTab = tab;
}
return tab;
}
}
}