package acr.browser.lightning.activity; 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.WebView; import com.squareup.otto.Bus; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; import acr.browser.lightning.R; import acr.browser.lightning.app.BrowserApp; import acr.browser.lightning.constant.BookmarkPage; import acr.browser.lightning.constant.Constants; import acr.browser.lightning.constant.HistoryPage; import acr.browser.lightning.constant.StartPage; import acr.browser.lightning.database.BookmarkManager; import acr.browser.lightning.database.HistoryDatabase; import acr.browser.lightning.dialog.BrowserDialog; 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.Schedulers; import com.anthonycr.bonsai.Subscriber; import acr.browser.lightning.utils.FileUtils; import acr.browser.lightning.utils.UrlUtils; import acr.browser.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.class.getSimpleName(); private static final String BUNDLE_KEY = "WEBVIEW_"; private static final String URL_KEY = "URL_KEY"; private static final String BUNDLE_STORAGE = "SAVED_TABS.parcel"; private final List mTabList = new ArrayList<>(1); @Nullable private LightningView mCurrentTab; @Nullable private TabNumberChangedListener mTabNumberListener; private boolean mIsInitialized = false; private final List mPostInitializationWorkList = new ArrayList<>(); @Inject PreferenceManager mPreferenceManager; @Inject BookmarkManager mBookmarkManager; @Inject HistoryDatabase mHistoryManager; @Inject Bus mEventBus; @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. */ public synchronized Observable initializeTabs(@NonNull final Activity activity, @Nullable final Intent intent, final boolean incognito) { return Observable.create(new Action() { @Override public void onSubscribe(@NonNull final Subscriber 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 Subscriber subscriber) { restoreState().subscribeOn(Schedulers.io()) .observeOn(Schedulers.main()).subscribe(new OnSubscribe() { @Override public void onNext(Bundle item) { LightningView tab = newTab(activity, "", false); String url = item.getString(URL_KEY); if (url != null && tab.getWebView() != null) { if (UrlUtils.isBookmarkUrl(url)) { new BookmarkPage(tab, activity, mBookmarkManager).load(); } else if (UrlUtils.isStartPageUrl(url)) { new StartPage(tab, mApp).load(); } else if (UrlUtils.isHistoryUrl(url)) { new HistoryPage(tab, mApp, mHistoryManager).load(); } } else if (tab.getWebView() != null) { tab.getWebView().restoreState(item); } } @Override public void onComplete() { if (url != null) { if (url.startsWith(Constants.FILE)) { 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(Constants.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 Observable restoreState() { return Observable.create(new Action() { @Override public void onSubscribe(@NonNull Subscriber subscriber) { Bundle savedState = FileUtils.readBundleFromStorage(mApp, BUNDLE_STORAGE); if (savedState != null) { Log.d(Constants.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(); } }); } /** * Return the {@link WebView} associated to the current tab, * or null if there is no current tab. * * @return a {@link WebView} or null if there is no current tab. */ @Nullable public synchronized WebView getCurrentWebView() { return mCurrentTab != null ? mCurrentTab.getWebView() : null; } /** * 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; } /** * 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; } } }