From 1f8dc216df5ca11615cc5f5ae49bbd286ac73dba Mon Sep 17 00:00:00 2001 From: dfalcantara Date: Tue, 16 Dec 2014 14:16:41 -0800 Subject: [PATCH] Upstream DocumentTabModelImpl and related classes Moves the DocumentTabModelImpl and its associated tests/utility classes upstream from the downstream directories. Updates the findbugs to remove obsolete ones and add temporary new ones (until more is upstreamed). Setting NOTRY to true to get around findbugs changes. BUG=415747 TEST=DocumentTabModelImplTest, OffTheRecordDocumentTabModelTest NOTRY=true Review URL: https://codereview.chromium.org/802343003 Cr-Commit-Position: refs/heads/master@{#308680} --- .../findbugs_filter/findbugs_known_bugs.txt | 13 +- chrome/android/BUILD.gn | 1 + .../chrome/browser/document/DocumentMetricIds.java | 46 ++ .../org/chromium/chrome/browser/document/OWNERS | 1 + .../browser/document/PendingDocumentData.java | 33 + .../tabmodel/document/ActivityDelegate.java | 151 ++++ .../tabmodel/document/DocumentTabModelImpl.java | 903 +++++++++++++++++++++ .../chrome/browser/tabmodel/document/OWNERS | 1 + .../document/OffTheRecordDocumentTabModel.java | 141 ++++ .../browser/tabmodel/document/StorageDelegate.java | 154 ++++ .../browser/tabmodel/document/TabDelegate.java | 45 + .../document/DocumentTabModelImplTest.java | 434 ++++++++++ .../chrome/browser/tabmodel/document/OWNERS | 1 + .../document/OffTheRecordDocumentTabModelTest.java | 227 ++++++ chrome/chrome.gyp | 1 + .../tabmodel/document/MockActivityDelegate.java | 70 ++ .../tabmodel/document/MockStorageDelegate.java | 109 +++ .../browser/tabmodel/document/MockTabDelegate.java | 36 + .../test/util/browser/tabmodel/document/OWNERS | 1 + .../document/TestInitializationObserver.java | 62 ++ 20 files changed, 2423 insertions(+), 7 deletions(-) create mode 100644 chrome/android/java/src/org/chromium/chrome/browser/document/DocumentMetricIds.java create mode 100644 chrome/android/java/src/org/chromium/chrome/browser/document/OWNERS create mode 100644 chrome/android/java/src/org/chromium/chrome/browser/document/PendingDocumentData.java create mode 100644 chrome/android/java/src/org/chromium/chrome/browser/tabmodel/document/ActivityDelegate.java create mode 100644 chrome/android/java/src/org/chromium/chrome/browser/tabmodel/document/DocumentTabModelImpl.java create mode 100644 chrome/android/java/src/org/chromium/chrome/browser/tabmodel/document/OWNERS create mode 100644 chrome/android/java/src/org/chromium/chrome/browser/tabmodel/document/OffTheRecordDocumentTabModel.java create mode 100644 chrome/android/java/src/org/chromium/chrome/browser/tabmodel/document/StorageDelegate.java create mode 100644 chrome/android/java/src/org/chromium/chrome/browser/tabmodel/document/TabDelegate.java create mode 100644 chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/document/DocumentTabModelImplTest.java create mode 100644 chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/document/OWNERS create mode 100644 chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/document/OffTheRecordDocumentTabModelTest.java create mode 100644 chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/tabmodel/document/MockActivityDelegate.java create mode 100644 chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/tabmodel/document/MockStorageDelegate.java create mode 100644 chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/tabmodel/document/MockTabDelegate.java create mode 100644 chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/tabmodel/document/OWNERS create mode 100644 chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/tabmodel/document/TestInitializationObserver.java diff --git a/build/android/findbugs_filter/findbugs_known_bugs.txt b/build/android/findbugs_filter/findbugs_known_bugs.txt index cbc5e3081145..c92700c89549 100644 --- a/build/android/findbugs_filter/findbugs_known_bugs.txt +++ b/build/android/findbugs_filter/findbugs_known_bugs.txt @@ -23,10 +23,9 @@ M M LI: Incorrect lazy initialization of static field org.chromium.chrome.browse M V EI2: org.chromium.content_public.browser.LoadUrlParams.setPostData(byte[]) may expose internal representation by storing an externally mutable object into LoadUrlParams.mPostData At LoadUrlParams.java M V EI: org.chromium.content_public.browser.LoadUrlParams.getPostData() may expose internal representation by returning LoadUrlParams.mPostData At LoadUrlParams.java M V EI2: org.chromium.net.ChromiumUrlRequest.setUploadData(String, byte[]) may expose internal representation by storing an externally mutable object into ChromiumUrlRequest.mUploadData At ChromiumUrlRequest.java -M D UrF: Unread public/protected field: org.chromium.chrome.browser.tabmodel.document.DocumentTabModel$Entry.initialUrl At DocumentTabModel.java -M D UrF: Unread public/protected field: org.chromium.chrome.browser.tabmodel.document.DocumentTabModel$Entry.isTabStateReady At DocumentTabModel.java -M D UrF: Unread public/protected field: org.chromium.chrome.browser.tabmodel.document.DocumentTabModel$Entry.tabState At DocumentTabModel.java -M D UuF: Unused public or protected field: org.chromium.chrome.browser.tabmodel.document.DocumentTabModel$Entry.canGoBack In DocumentTabModel.java -M D UuF: Unused public or protected field: org.chromium.chrome.browser.tabmodel.document.DocumentTabModel$Entry.currentUrl In DocumentTabModel.java -M D UuF: Unused public or protected field: org.chromium.chrome.browser.tabmodel.document.DocumentTabModel$Entry.isDirty In DocumentTabModel.java -M D UuF: Unused public or protected field: org.chromium.chrome.browser.tabmodel.document.DocumentTabModel$Entry.placeholderTab In DocumentTabModel.java +M D UuF: Unused public or protected field: org.chromium.chrome.browser.document.PendingDocumentData.extraHeaders In PendingDocumentData.java +M D UuF: Unused public or protected field: org.chromium.chrome.browser.document.PendingDocumentData.nativeWebContents In PendingDocumentData.java +M D UuF: Unused public or protected field: org.chromium.chrome.browser.document.PendingDocumentData.originalIntent In PendingDocumentData.java +M D UuF: Unused public or protected field: org.chromium.chrome.browser.document.PendingDocumentData.postData In PendingDocumentData.java +M D UuF: Unused public or protected field: org.chromium.chrome.browser.document.PendingDocumentData.referrer In PendingDocumentData.java +M D UuF: Unused public or protected field: org.chromium.chrome.browser.document.PendingDocumentData.url In PendingDocumentData.java diff --git a/chrome/android/BUILD.gn b/chrome/android/BUILD.gn index d4aa47e27880..c63bf0d35f62 100644 --- a/chrome/android/BUILD.gn +++ b/chrome/android/BUILD.gn @@ -89,6 +89,7 @@ android_library("chrome_java") { "//content/public/android:content_java", "//printing:printing_java", "//sync/android:sync_java", + "//third_party/android_protobuf:protobuf_nano_javalib", "//third_party/android_tools:android_support_v13_java", "//third_party/android_tools:android_support_v7_appcompat_java", "//third_party/cacheinvalidation:cacheinvalidation_javalib", diff --git a/chrome/android/java/src/org/chromium/chrome/browser/document/DocumentMetricIds.java b/chrome/android/java/src/org/chromium/chrome/browser/document/DocumentMetricIds.java new file mode 100644 index 000000000000..50a11e198ca0 --- /dev/null +++ b/chrome/android/java/src/org/chromium/chrome/browser/document/DocumentMetricIds.java @@ -0,0 +1,46 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chrome.browser.document; + +/** + * IDs for metrics tracking Document-mode actions. + */ +public class DocumentMetricIds { + // DocumentActivity.HomeExitAction (enumerated) + public static final int HOME_EXIT_ACTION_OTHER = 0; + public static final int HOME_EXIT_ACTION_MOST_VISITED_ITEM = 1; + public static final int HOME_EXIT_ACTION_LAST_VIEWED_ITEM = 2; + public static final int HOME_EXIT_ACTION_BOOKMARKS_BUTTON = 3; + public static final int HOME_EXIT_ACTION_RECENT_TABS_BUTTON = 4; + public static final int HOME_EXIT_ACTION_SEARCHBOX = 5; + public static final int HOME_EXIT_ACTION_COUNT = 6; + + // DocumentActivity.StartedBy (sparse) + public static final int STARTED_BY_UNKNOWN = 0; + public static final int STARTED_BY_LAUNCHER = 1; + public static final int STARTED_BY_ACTIVITY_RESTARTED = 2; + public static final int STARTED_BY_ACTIVITY_BROUGHT_TO_FOREGROUND = 3; + public static final int STARTED_BY_CHROME_HOME_MOST_VISITED = 100; + public static final int STARTED_BY_CHROME_HOME_BOOKMARK = 101; + public static final int STARTED_BY_CHROME_HOME_RECENT_TABS = 102; + public static final int STARTED_BY_WINDOW_OPEN = 200; + public static final int STARTED_BY_CONTEXT_MENU = 201; + public static final int STARTED_BY_OPTIONS_MENU = 202; + public static final int STARTED_BY_SEARCH_RESULT_PAGE = 300; + public static final int STARTED_BY_SEARCH_SUGGESTION_EXTERNAL = 301; + public static final int STARTED_BY_SEARCH_SUGGESTION_CHROME = 302; + public static final int STARTED_BY_EXTERNAL_APP_GMAIL = 400; + public static final int STARTED_BY_EXTERNAL_APP_FACEBOOK = 401; + public static final int STARTED_BY_EXTERNAL_APP_PLUS = 402; + public static final int STARTED_BY_EXTERNAL_APP_TWITTER = 403; + public static final int STARTED_BY_EXTERNAL_APP_CHROME = 404; + public static final int STARTED_BY_EXTERNAL_APP_OTHER = 405; + public static final int STARTED_BY_CONTEXTUAL_SEARCH = 500; + + // DocumentActivity.OptOutDecision (enumerated) + public static final int OPT_OUT_CLICK_GOT_IT = 0; + public static final int OPT_OUT_CLICK_SETTINGS = 1; + public static final int OPT_OUT_CLICK_COUNT = 2; +} diff --git a/chrome/android/java/src/org/chromium/chrome/browser/document/OWNERS b/chrome/android/java/src/org/chromium/chrome/browser/document/OWNERS new file mode 100644 index 000000000000..79becd27e526 --- /dev/null +++ b/chrome/android/java/src/org/chromium/chrome/browser/document/OWNERS @@ -0,0 +1 @@ +dfalcantara@chromium.org diff --git a/chrome/android/java/src/org/chromium/chrome/browser/document/PendingDocumentData.java b/chrome/android/java/src/org/chromium/chrome/browser/document/PendingDocumentData.java new file mode 100644 index 000000000000..01e0b1e59e93 --- /dev/null +++ b/chrome/android/java/src/org/chromium/chrome/browser/document/PendingDocumentData.java @@ -0,0 +1,33 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chrome.browser.document; + +import android.content.Intent; + +import org.chromium.content_public.common.Referrer; + +/** + * Data that will be used later when a tab is opened via an intent. Often only the necessary + * subset of the data will be set. All data is removed once the tab finishes initializing. + */ +public class PendingDocumentData { + /** Pending native web contents object to initialize with. */ + public long nativeWebContents; + + /** The url to load in the current tab. */ + public String url; + + /** Data to send with a POST request. */ + public byte[] postData; + + /** Extra HTTP headers to send. */ + public String extraHeaders; + + /** HTTP "referer". */ + public Referrer referrer; + + /** The original intent */ + public Intent originalIntent; +} \ No newline at end of file diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/document/ActivityDelegate.java b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/document/ActivityDelegate.java new file mode 100644 index 000000000000..61d3ac583512 --- /dev/null +++ b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/document/ActivityDelegate.java @@ -0,0 +1,151 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chrome.browser.tabmodel.document; + +import android.annotation.TargetApi; +import android.app.ActivityManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.Build; +import android.text.TextUtils; + +import org.chromium.base.ApplicationStatus; +import org.chromium.chrome.browser.Tab; +import org.chromium.chrome.browser.UrlConstants; +import org.chromium.chrome.browser.tabmodel.document.DocumentTabModel.Entry; + +import java.util.ArrayList; +import java.util.List; + +/** + * Interfaces with the ActivityManager to identify Tabs/Tasks that are being tracked by + * Android's Recents list. + */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class ActivityDelegate { + private final String mRegularActivityName; + private final String mIncognitoActivityName; + + /** + * Creates a ActivityDelegate. + * @param regularName Name of the regular DocumentActivity. + * @param incognitoName Name of the Incognito DocumentActivity. + */ + public ActivityDelegate(String regularName, String incognitoName) { + mRegularActivityName = regularName; + mIncognitoActivityName = incognitoName; + } + + /** + * Checks whether or not the Intent corresponds to an Activity that should be tracked. + * @param isIncognito Whether or not the TabModel is managing incognito tabs. + * @param intent Intent used to launch the Activity. + * @return Whether or not to track the Activity. + */ + public boolean isValidActivity(boolean isIncognito, Intent intent) { + if (intent == null) return false; + String desiredClassName = isIncognito ? mIncognitoActivityName : mRegularActivityName; + String className = null; + if (intent.getComponent() == null) { + Context context = ApplicationStatus.getApplicationContext(); + PackageManager pm = context.getPackageManager(); + ResolveInfo resolveInfo = pm.resolveActivity(intent, 0); + if (resolveInfo != null) className = resolveInfo.activityInfo.name; + } else { + className = intent.getComponent().getClassName(); + } + + return TextUtils.equals(className, desiredClassName); + } + + /** + * Get a map of the Chrome tasks displayed by Android's Recents. + * @param isIncognito Whether or not the TabList is managing incognito tabs. + */ + public List getTasksFromRecents(boolean isIncognito) { + List entries = new ArrayList(); + Context context = ApplicationStatus.getApplicationContext(); + ActivityManager activityManager = + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + for (ActivityManager.AppTask task : activityManager.getAppTasks()) { + Intent intent = task.getTaskInfo().baseIntent; + if (!isValidActivity(isIncognito, intent)) continue; + + int tabId = getTabIdFromIntent(intent); + String initialUrl = getInitialUrlForDocument(intent); + if (tabId == Tab.INVALID_TAB_ID || initialUrl == null) continue; + entries.add(new Entry(tabId, initialUrl)); + } + return entries; + } + + /** + * Moves the given task to the front, if it exists. + * @param isIncognito Whether or not the TabList is managing incognito tabs. + * @param tabId ID of the tab to move to front. + */ + public void moveTaskToFront(boolean isIncognito, int tabId) { + ActivityManager.AppTask task = getTask(isIncognito, tabId); + if (task != null) task.moveToFront(); + } + + /** + * Finishes and removes the task. + * @param isIncognito Whether or not the TabList is managing incognito tabs. + * @param tabId ID of the tab to move to front. + */ + public void finishAndRemoveTask(boolean isIncognito, int tabId) { + ActivityManager.AppTask task = getTask(isIncognito, tabId); + if (task != null) task.finishAndRemoveTask(); + } + + private final ActivityManager.AppTask getTask(boolean isIncognito, int tabId) { + Context context = ApplicationStatus.getApplicationContext(); + ActivityManager activityManager = + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + for (ActivityManager.AppTask task : activityManager.getAppTasks()) { + Intent intent = task.getTaskInfo().baseIntent; + int taskId = getTabIdFromIntent(intent); + if (taskId == tabId && isValidActivity(isIncognito, intent)) return task; + } + return null; + } + + /** + * Check whether or not the Intent contains an ID for document mode. + * @param intent Intent to check. + * @return ID for the document that has the given intent as base intent, or + * {@link Tab.INVALID_TAB_ID} if it couldn't be retrieved. + */ + public static int getTabIdFromIntent(Intent intent) { + if (intent == null || intent.getData() == null) return Tab.INVALID_TAB_ID; + + Uri data = intent.getData(); + if (!TextUtils.equals(data.getScheme(), UrlConstants.DOCUMENT_SCHEME)) { + return Tab.INVALID_TAB_ID; + } + + try { + return Integer.parseInt(data.getHost()); + } catch (NumberFormatException e) { + return Tab.INVALID_TAB_ID; + } + } + + /** + * Parse out the URL for a document Intent. + * @param intent Intent to check. + * @return The URL that the Intent was fired to display, or null if it couldn't be retrieved. + */ + public static String getInitialUrlForDocument(Intent intent) { + if (intent == null || intent.getData() == null) return null; + Uri data = intent.getData(); + return TextUtils.equals(data.getScheme(), UrlConstants.DOCUMENT_SCHEME) + ? data.getQuery() : null; + } +} \ No newline at end of file diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/document/DocumentTabModelImpl.java b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/document/DocumentTabModelImpl.java new file mode 100644 index 000000000000..1a04499d6ac6 --- /dev/null +++ b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/document/DocumentTabModelImpl.java @@ -0,0 +1,903 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chrome.browser.tabmodel.document; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.AsyncTask; +import android.os.Build; +import android.text.TextUtils; +import android.util.Log; +import android.util.Pair; +import android.util.SparseArray; + +import com.google.protobuf.nano.MessageNano; + +import org.chromium.base.ApplicationStatus; +import org.chromium.base.ObserverList; +import org.chromium.base.ThreadUtils; +import org.chromium.base.VisibleForTesting; +import org.chromium.chrome.browser.Tab; +import org.chromium.chrome.browser.TabState; +import org.chromium.chrome.browser.tabmodel.TabList; +import org.chromium.chrome.browser.tabmodel.TabModel; +import org.chromium.chrome.browser.tabmodel.TabModelJniBridge; +import org.chromium.chrome.browser.tabmodel.TabModelObserver; +import org.chromium.chrome.browser.tabmodel.TabModelUtils; +import org.chromium.chrome.browser.tabmodel.document.DocumentTabModelInfo.DocumentEntry; +import org.chromium.chrome.browser.tabmodel.document.DocumentTabModelInfo.DocumentList; +import org.chromium.chrome.browser.util.MathUtils; + +import java.io.File; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Maintains a list of Tabs displayed when Chrome is running in document-mode. + */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class DocumentTabModelImpl extends TabModelJniBridge implements DocumentTabModel { + private static final String TAG = "DocumentTabModel"; + + @VisibleForTesting + public static final String PREF_PACKAGE = "com.google.android.apps.chrome.document"; + + @VisibleForTesting + public static final String PREF_LAST_SHOWN_TAB_ID_REGULAR = "last_shown_tab_id.regular"; + + public static final String PREF_LAST_SHOWN_TAB_ID_INCOGNITO = "last_shown_tab_id.incognito"; + + /** TabModel is uninitialized. */ + public static final int STATE_UNINITIALIZED = 0; + + /** Begin parsing the tasks from Recents and loading persisted state. */ + public static final int STATE_READ_RECENT_TASKS_START = 1; + + /** Done parsing the tasks from Recents and loading persisted state. */ + public static final int STATE_READ_RECENT_TASKS_END = 2; + + /** Begin loading the current/prioritized tab state synchronously. */ + public static final int STATE_LOAD_CURRENT_TAB_STATE_START = 3; + + /** Finish loading the current/prioritized tab state synchronously. */ + public static final int STATE_LOAD_CURRENT_TAB_STATE_END = 4; + + /** Begin reading TabStates from storage for background tabs. */ + public static final int STATE_LOAD_TAB_STATE_BG_START = 5; + + /** Done reading TabStates from storage for background tabs. */ + public static final int STATE_LOAD_TAB_STATE_BG_END = 6; + + /** Begin deserializing the TabState. Requires the native library. */ + public static final int STATE_DESERIALIZE_START = 7; + + /** Done deserializing the TabState. */ + public static final int STATE_DESERIALIZE_END = 8; + + /** Begin parsing the historical tabs. */ + public static final int STATE_DETERMINE_HISTORICAL_TABS_START = 9; + + /** Done parsing the historical tabs. */ + public static final int STATE_DETERMINE_HISTORICAL_TABS_END = 10; + + /** Clean out old TabState files. */ + public static final int STATE_CLEAN_UP_OBSOLETE_TABS = 11; + + /** TabModel is fully ready to use. */ + public static final int STATE_FULLY_LOADED = 12; + + /** List of known tabs. */ + private final ArrayList mTabIdList; + + /** Stores an entry for each DocumentActivity that is alive. Keys are document IDs. */ + private final SparseArray mEntryMap; + + /** + * Stores tabIds which have been removed from the ActivityManager while Chrome was not alive. + * It is cleared after restoration has been finished. + */ + private final List mHistoricalTabs; + + /** Delegate for working with the ActivityManager. */ + private final ActivityDelegate mActivityDelegate; + + /** Delegate for working with the filesystem. */ + private final StorageDelegate mStorageDelegate; + + /** Delegate that provides Tabs to the DocumentTabModel. */ + private final TabDelegate mTabDelegate; + + /** ID of a Tab whose state should be loaded immediately, if it belongs to this TabList. */ + private final int mPrioritizedTabId; + + /** List of observers watching for a particular loading state. */ + private final ObserverList mInitializationObservers; + + /** List of observers watching the TabModel. */ + private final ObserverList mObservers; + + /** Context to use. */ + private final Context mContext; + + /** Current loading status. */ + private int mCurrentState; + + /** ID of the last tab that was shown to the user. */ + private int mLastShownTabId = Tab.INVALID_TAB_ID; + + /** + * Construct a DocumentTabModelImpl. + * @param activityDelegate Used to interact with DocumentActivities. + * @param tabDelegate Used to create/get Tabs. + * @param isIncognito Whether or not the TabList is managing incognito tabs. + * @param prioritizedTabId ID of the tab to prioritize when loading. + */ + public DocumentTabModelImpl(ActivityDelegate activityDelegate, TabDelegate tabDelegate, + boolean isIncognito, int prioritizedTabId) { + this(activityDelegate, new StorageDelegate(isIncognito), tabDelegate, isIncognito, + prioritizedTabId, ApplicationStatus.getApplicationContext()); + } + + /** + * Construct a DocumentTabModel. + * @param activityDelegate Delegate to use for accessing the ActivityManager. + * @param storageDelegate Delegate to use for accessing persistent storage. + * @param tabDelegate Used to create/get Tabs. + * @param isIncognito Whether or not the TabList is managing incognito tabs. + * @param prioritizedTabId ID of the tab to prioritize when loading. + * @param context Context to use for accessing SharedPreferences. + * + * TODO(dfalcantara): Reduce visibility once DocumentMigrationHelper is upstreamed. + */ + public DocumentTabModelImpl(ActivityDelegate activityDelegate, StorageDelegate storageDelegate, + TabDelegate tabDelegate, boolean isIncognito, int prioritizedTabId, Context context) { + super(isIncognito); + mActivityDelegate = activityDelegate; + mStorageDelegate = storageDelegate; + mTabDelegate = tabDelegate; + mPrioritizedTabId = prioritizedTabId; + mContext = context; + + mCurrentState = STATE_UNINITIALIZED; + mTabIdList = new ArrayList(); + mEntryMap = new SparseArray(); + mHistoricalTabs = new ArrayList(); + mInitializationObservers = new ObserverList(); + mObservers = new ObserverList(); + + SharedPreferences prefs = mContext.getSharedPreferences(PREF_PACKAGE, Context.MODE_PRIVATE); + mLastShownTabId = prefs.getInt( + isIncognito() ? PREF_LAST_SHOWN_TAB_ID_INCOGNITO : PREF_LAST_SHOWN_TAB_ID_REGULAR, + Tab.INVALID_TAB_ID); + + initializeTabList(); + } + + @Override + public void initializeNative() { + if (!isNativeInitialized()) super.initializeNative(); + deserializeTabStatesAsync(); + } + + public StorageDelegate getStorageDelegate() { + return mStorageDelegate; + } + + /** + * Finds the index of the given Tab ID. + * @param tabId ID of the Tab to find. + * @return Index of the tab, or -1 if it couldn't be found. + */ + private int indexOf(int tabId) { + return mTabIdList.indexOf(tabId); + } + + @Override + public int index() { + if (getCount() == 0) return TabList.INVALID_TAB_INDEX; + int indexOfLastId = indexOf(mLastShownTabId); + if (indexOfLastId != -1) return indexOfLastId; + + // The previous Tab is gone; select a Tab based on MRU ordering. + List tasks = mActivityDelegate.getTasksFromRecents(isIncognito()); + if (tasks.size() == 0) return TabList.INVALID_TAB_INDEX; + + for (int i = 0; i < tasks.size(); i++) { + int lastKnownId = tasks.get(i).tabId; + int indexOfMostRecentlyUsedId = indexOf(lastKnownId); + if (indexOfMostRecentlyUsedId != -1) return indexOfMostRecentlyUsedId; + } + + return TabList.INVALID_TAB_INDEX; + } + + @Override + public void setLastShownId(int id) { + mLastShownTabId = id; + + String prefName = + isIncognito() ? PREF_LAST_SHOWN_TAB_ID_INCOGNITO : PREF_LAST_SHOWN_TAB_ID_REGULAR; + SharedPreferences prefs = mContext.getSharedPreferences(PREF_PACKAGE, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + editor.putInt(prefName, id); + editor.apply(); + } + + @Override + public int indexOf(Tab tab) { + if (tab == null) return Tab.INVALID_TAB_ID; + return indexOf(tab.getId()); + } + + @Override + public int getCount() { + return mTabIdList.size(); + } + + @Override + public boolean isClosurePending(int tabId) { + return false; + } + + @Override + public Tab getTabAt(int index) { + if (index < 0 || index >= getCount()) return null; + + // Return a live tab if the corresponding Activity is currently alive. + int tabId = mTabIdList.get(index); + List> activities = ApplicationStatus.getRunningActivities(); + for (WeakReference activityRef : activities) { + Tab tab = mTabDelegate.getActivityTab( + isIncognito(), mActivityDelegate, activityRef.get()); + int documentId = tab == null ? Tab.INVALID_TAB_ID : tab.getId(); + if (documentId == tabId) return tab; + } + + // Try to create a Tab that will hold the Tab's info. + Entry entry = mEntryMap.get(tabId); + if (entry == null) return null; + + // If a tab has already been initialized, use that. + if (entry.placeholderTab != null && entry.placeholderTab.isInitialized()) { + return entry.placeholderTab; + } + + // Create a frozen Tab if we are capable, or if the previous Tab is just a placeholder. + if (entry.tabState != null && isNativeInitialized() + && (entry.placeholderTab == null || !entry.placeholderTab.isInitialized())) { + entry.placeholderTab = mTabDelegate.createFrozenTab(entry); + entry.placeholderTab.initialize(); + } + + // Create a placeholder Tab that just has the ID. + if (entry.placeholderTab == null) { + entry.placeholderTab = new Tab(tabId, isIncognito(), null, null); + } + + return entry.placeholderTab; + } + + @Override + public void setIndex(int index, TabSelectionType type) { + if (index < 0 || index >= getCount()) return; + int tabId = mTabIdList.get(index); + mActivityDelegate.moveTaskToFront(isIncognito(), tabId); + setLastShownId(tabId); + } + + @Override + public boolean closeTabAt(int index) { + ThreadUtils.assertOnUiThread(); + if (index < 0 || index >= getCount()) return false; + + int tabId = mTabIdList.get(index); + mActivityDelegate.finishAndRemoveTask(isIncognito(), tabId); + mTabIdList.remove(index); + mEntryMap.remove(tabId); + return true; + } + + @Override + public boolean closeTab(Tab tab) { + return closeTab(tab, false, false, false); + } + + @Override + public boolean closeTab(Tab tabToClose, boolean animate, boolean uponExit, boolean canUndo) { + // The tab should be destroyed by the DocumentActivity that owns it. + return closeTabAt(indexOf(tabToClose.getId())); + } + + @Override + protected Tab createTabWithNativeContents( + boolean isIncognito, long webContentsPtr, int parentTabId) { + mTabDelegate.createTabWithNativeContents(isIncognito, webContentsPtr, parentTabId); + return null; + } + + @Override + protected Tab createNewTabForDevTools(String url) { + mTabDelegate.createTabForDevTools(url); + return null; + } + + @Override + protected boolean isSessionRestoreInProgress() { + return mCurrentState < STATE_FULLY_LOADED; + } + + /** + * Add the tab ID to the end of the list. + * @param tabId ID to add. + */ + private void addTabId(int tabId) { + addTabId(mTabIdList.size(), tabId); + } + + /** + * Adds the Tab ID at the given index. + * @param index Where to add the ID. + * @param tabId ID to add. + */ + private void addTabId(int index, int tabId) { + if (mTabIdList.contains(tabId)) return; + mTabIdList.add(index, tabId); + } + + @Override + public String getInitialUrlForDocument(int tabId) { + Entry entry = mEntryMap.get(tabId); + return entry == null ? null : entry.initialUrl; + } + + @Override + public String getCurrentUrlForDocument(int tabId) { + Entry entry = mEntryMap.get(tabId); + return entry == null ? null : entry.currentUrl; + } + + @Override + public boolean isTabStateReady(int tabId) { + Entry entry = mEntryMap.get(tabId); + return entry == null ? true : entry.isTabStateReady; + } + + @Override + public TabState getTabStateForDocument(int tabId) { + Entry entry = mEntryMap.get(tabId); + return entry == null ? null : entry.tabState; + } + + @Override + public boolean hasEntryForTabId(int tabId) { + return mEntryMap.get(tabId) != null; + } + + @Override + public boolean isRetargetable(int tabId) { + Entry entry = mEntryMap.get(tabId); + return entry == null ? false : !entry.canGoBack; + } + + @Override + public void addInitializationObserver(InitializationObserver observer) { + ThreadUtils.assertOnUiThread(); + mInitializationObservers.addObserver(observer); + } + + @Override + public void updateRecentlyClosed() { + ThreadUtils.assertOnUiThread(); + List current = mActivityDelegate.getTasksFromRecents(isIncognito()); + Set removed = new HashSet(); + for (int i = 0; i < mEntryMap.size(); i++) { + int tabId = mEntryMap.keyAt(i); + if (!isTabIdInEntryList(current, tabId)) { + Entry entry = mEntryMap.get(tabId); + if (!isIncognito() && entry.tabState != null + && entry.tabState.contentsState != null) { + entry.tabState.contentsState.createHistoricalTab(); + } + removed.add(tabId); + } + } + + for (Integer tabId : removed) { + closeTabAt(indexOf(tabId)); + } + } + + @Override + public void updateEntry(Intent intent, Tab tab) { + if (!mActivityDelegate.isValidActivity(isIncognito(), intent)) return; + + int id = ActivityDelegate.getTabIdFromIntent(intent); + if (id == Tab.INVALID_TAB_ID) return; + + Entry currentEntry = mEntryMap.get(id); + String currentUrl = tab.getUrl(); + boolean canGoBack = tab.canGoBack(); + TabState state = tab.getState(); + if (currentEntry != null + && currentEntry.tabId == id + && TextUtils.equals(currentEntry.currentUrl, currentUrl) + && currentEntry.canGoBack == canGoBack + && currentEntry.tabState == state + && !tab.isTabStateDirty()) { + return; + } + + if (currentEntry == null) { + currentEntry = new Entry(id, ActivityDelegate.getInitialUrlForDocument(intent)); + mEntryMap.put(id, currentEntry); + } + currentEntry.isDirty = true; + currentEntry.currentUrl = currentUrl; + currentEntry.canGoBack = canGoBack; + currentEntry.tabState = state; + + // TODO(dfalcantara): This is different from how the normal Tab determines when to save its + // state, but this can't be fixed because we cann't hold onto Tabs in this class. + tab.setIsTabStateDirty(false); + + if (currentEntry.placeholderTab != null) { + if (currentEntry.placeholderTab.isInitialized()) currentEntry.placeholderTab.destroy(); + currentEntry.placeholderTab = null; + } + + writeGeneralDataToStorageAsync(); + writeTabStatesToStorageAsync(); + } + + @Override + public int getCurrentInitializationStage() { + return mCurrentState; + } + + /** + * Add an entry to the entry map for migration purposes. + * @param entry The entry to be added. + * + * TODO(dfalcantara): Reduce visibility once DocumentMigrationHelper is upstreamed. + */ + public void addEntryForMigration(Entry entry) { + addTabId(getCount(), entry.tabId); + if (mEntryMap.indexOfKey(entry.tabId) >= 0) return; + mEntryMap.put(entry.tabId, entry); + } + + private void initializeTabList() { + setCurrentState(STATE_READ_RECENT_TASKS_START); + + // Run through Recents to see what tasks exist. Prevent them from being retargeted until we + // have had the opportunity to load more information about them. + List entries = mActivityDelegate.getTasksFromRecents(isIncognito()); + for (Entry entry : entries) { + entry.canGoBack = true; + mEntryMap.put(entry.tabId, entry); + } + + // Read the file, which saved out the task IDs in regular order. + byte[] tabFileBytes = mStorageDelegate.readTaskFileBytes(); + if (tabFileBytes != null) { + try { + DocumentList list = MessageNano.mergeFrom(new DocumentList(), tabFileBytes); + for (int i = 0; i < list.entries.length; i++) { + DocumentEntry savedEntry = list.entries[i]; + int tabId = savedEntry.tabId; + + if (mEntryMap.indexOfKey(tabId) < 0) { + mHistoricalTabs.add(tabId); + continue; + } + + addTabId(getCount(), tabId); + mEntryMap.get(tabId).canGoBack = savedEntry.canGoBack; + } + } catch (IOException e) { + Log.e(TAG, "I/O exception", e); + } + } + + // Add any missing tasks to the list. + for (int i = 0; i < mEntryMap.size(); i++) { + int id = mEntryMap.keyAt(i); + if (mTabIdList.contains(id)) continue; + addTabId(id); + } + + setCurrentState(STATE_READ_RECENT_TASKS_END); + } + + // TODO(mariakhomenko): we no longer need prioritized tab id in constructor, shift it here. + @Override + public void startTabStateLoad() { + if (mCurrentState != STATE_READ_RECENT_TASKS_END) return; + setCurrentState(STATE_LOAD_CURRENT_TAB_STATE_START); + // Immediately try loading the requested tab. + if (mPrioritizedTabId != Tab.INVALID_TAB_ID) { + Entry entry = mEntryMap.get(mPrioritizedTabId); + if (entry != null) { + entry.tabState = mStorageDelegate.restoreTabState(mPrioritizedTabId); + entry.isTabStateReady = true; + } + } + setCurrentState(STATE_LOAD_CURRENT_TAB_STATE_END); + loadTabStatesAsync(); + } + + private void loadTabStatesAsync() { + new AsyncTask() { + private final List mEntries = new ArrayList(getCount()); + + @Override + public void onPreExecute() { + setCurrentState(STATE_LOAD_TAB_STATE_BG_START); + for (int i = 0; i < getCount(); i++) { + mEntries.add(new Entry(getTabAt(i).getId())); + } + } + + @Override + public Void doInBackground(Void... params) { + for (Entry entry : mEntries) { + if (mPrioritizedTabId == entry.tabId) continue; + entry.tabState = mStorageDelegate.restoreTabState(entry.tabId); + entry.isTabStateReady = true; + } + + return null; + } + + @Override + public void onPostExecute(Void result) { + for (Entry pair : mEntries) { + Entry entry = mEntryMap.get(pair.tabId); + if (entry == null) continue; + + if (entry.tabState == null) entry.tabState = pair.tabState; + entry.isTabStateReady = true; + } + + setCurrentState(STATE_LOAD_TAB_STATE_BG_END); + deserializeTabStatesAsync(); + } + }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); + } + + private void deserializeTabStatesAsync() { + if (!shouldStartDeserialization(mCurrentState)) return; + + new AsyncTask() { + private final List mCachedEntries = new ArrayList(mEntryMap.size()); + + @Override + public void onPreExecute() { + setCurrentState(STATE_DESERIALIZE_START); + + for (int i = 0; i < mEntryMap.size(); i++) { + Entry entry = mEntryMap.valueAt(i); + if (entry.tabState == null) continue; + mCachedEntries.add(new Entry(entry.tabId, entry.tabState)); + } + } + + @Override + public Void doInBackground(Void... params) { + for (Entry entry : mCachedEntries) { + TabState tabState = entry.tabState; + updateEntryInfoFromTabState(entry, tabState); + } + return null; + } + + @Override + public void onPostExecute(Void result) { + for (Entry pair : mCachedEntries) { + Entry realEntry = mEntryMap.get(pair.tabId); + if (realEntry == null || realEntry.currentUrl != null) continue; + realEntry.currentUrl = pair.currentUrl; + } + + setCurrentState(STATE_DESERIALIZE_END); + if (isNativeInitialized()) { + broadcastSessionRestoreComplete(); + loadHistoricalTabsAsync(); + } + } + }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); + } + + /** + * Call for extending classes to override for getting additional information for an entry from + * the tab state when it is deserialized. + * @param entry The {@link Entry} currently being processed + * @param tabState The {@link TabState} that has been deserialized for the entry. + */ + protected void updateEntryInfoFromTabState(Entry entry, TabState tabState) { + entry.currentUrl = tabState.getVirtualUrlFromState(); + } + + /** + * Checks whether initialization should move to the deserialization step. + * @param currentState Current initialization stage. + * @return Whether to proceed or not. + */ + protected boolean shouldStartDeserialization(int currentState) { + return isNativeInitialized() && currentState == STATE_LOAD_TAB_STATE_BG_END; + } + + private void loadHistoricalTabsAsync() { + new AsyncTask() { + private Set mHistoricalTabsForBackgroundThread; + private List mEntries; + + @Override + public void onPreExecute() { + setCurrentState(STATE_DETERMINE_HISTORICAL_TABS_START); + mHistoricalTabsForBackgroundThread = new HashSet(mHistoricalTabs.size()); + mHistoricalTabsForBackgroundThread.addAll(mHistoricalTabs); + mEntries = new ArrayList(mHistoricalTabsForBackgroundThread.size()); + } + + @Override + public Void doInBackground(Void... params) { + for (Integer tabId : mHistoricalTabsForBackgroundThread) { + // Read the saved state, then delete the file. + TabState state = mStorageDelegate.restoreTabState(tabId); + mEntries.add(new Entry(tabId, state)); + mStorageDelegate.deleteTabStateFile(tabId); + } + + return null; + } + + @Override + public void onPostExecute(Void result) { + for (Entry entry : mEntries) { + if (entry.tabState == null || entry.tabState.contentsState == null) continue; + entry.tabState.contentsState.createHistoricalTab(); + } + mHistoricalTabs.clear(); + setCurrentState(STATE_DETERMINE_HISTORICAL_TABS_END); + cleanUpObsoleteTabStatesAsync(); + } + }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); + } + + /** + * Clears the folder of TabStates that correspond to missing tasks. + */ + private void cleanUpObsoleteTabStatesAsync() { + new AsyncTask() { + private List mCurrentTabs; + + @Override + protected void onPreExecute() { + setCurrentState(STATE_CLEAN_UP_OBSOLETE_TABS); + mCurrentTabs = mActivityDelegate.getTasksFromRecents(isIncognito()); + } + + @Override + protected Void doInBackground(Void... voids) { + File stateDirectory = mStorageDelegate.getStateDirectory(); + String[] files = stateDirectory.list(); + for (final String fileName : files) { + Pair tabInfo = TabState.parseInfoFromFilename(fileName); + if (tabInfo == null) continue; + + int tabId = tabInfo.first; + boolean incognito = tabInfo.second; + if (incognito != isIncognito() || isTabIdInEntryList(mCurrentTabs, tabId)) { + continue; + } + + boolean success = new File(stateDirectory, fileName).delete(); + if (!success) Log.w(TAG, "Failed to delete: " + fileName); + } + + return null; + } + + @Override + protected void onPostExecute(Void result) { + setCurrentState(STATE_FULLY_LOADED); + } + }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); + } + + /** + * Save out a tiny file with minimal information required for retargeting. + */ + private void writeGeneralDataToStorageAsync() { + if (isIncognito()) return; + + new AsyncTask() { + private DocumentList mList; + + @Override + protected void onPreExecute() { + List entriesList = new ArrayList(); + for (int i = 0; i < getCount(); i++) { + Entry entry = mEntryMap.get(getTabAt(i).getId()); + if (entry == null) continue; + + DocumentEntry docEntry = new DocumentEntry(); + docEntry.tabId = entry.tabId; + docEntry.canGoBack = entry.canGoBack; + + entriesList.add(docEntry); + } + mList = new DocumentList(); + mList.entries = entriesList.toArray(new DocumentEntry[entriesList.size()]); + } + + @Override + protected Void doInBackground(Void... params) { + mStorageDelegate.writeTaskFileBytes(MessageNano.toByteArray(mList)); + return null; + } + }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); + } + + /** + * Write out all of the TabStates. + */ + private void writeTabStatesToStorageAsync() { + new AsyncTask() { + private final SparseArray mStatesToWrite = new SparseArray(); + + @Override + protected void onPreExecute() { + for (int i = 0; i < mEntryMap.size(); i++) { + Entry entry = mEntryMap.valueAt(i); + if (!entry.isDirty || entry.tabState == null) continue; + mStatesToWrite.put(entry.tabId, entry.tabState); + } + } + + @Override + protected Void doInBackground(Void... voids) { + for (int i = 0; i < mStatesToWrite.size(); i++) { + int tabId = mStatesToWrite.keyAt(i); + mStorageDelegate.saveTabState(tabId, mStatesToWrite.valueAt(i)); + } + return null; + } + + @Override + protected void onPostExecute(Void v) { + for (int i = 0; i < mStatesToWrite.size(); i++) { + int tabId = mStatesToWrite.keyAt(i); + Entry entry = mEntryMap.get(tabId); + if (entry == null) continue; + entry.isDirty = false; + } + } + }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); + } + + private void setCurrentState(int newState) { + ThreadUtils.assertOnUiThread(); + assert mCurrentState == newState - 1; + mCurrentState = newState; + + for (InitializationObserver observer : mInitializationObservers) { + if (observer.isCanceled()) { + Log.w(TAG, "Observer alerted after canceled: " + observer); + mInitializationObservers.removeObserver(observer); + } else if (observer.isSatisfied(mCurrentState)) { + observer.runWhenReady(); + mInitializationObservers.removeObserver(observer); + } + } + } + + @Override + public Tab getNextTabIfClosed(int id) { + // Tab may not necessarily exist. + return null; + } + + @Override + public void closeAllTabs() { + closeAllTabs(true, false); + } + + @Override + public void closeAllTabs(boolean allowDelegation, boolean uponExit) { + for (int i = getCount() - 1; i >= 0; i--) closeTabAt(i); + } + + @Override + public void moveTab(int id, int newIndex) { + newIndex = MathUtils.clamp(newIndex, 0, getCount()); + int curIndex = TabModelUtils.getTabIndexById(this, id); + if (curIndex == INVALID_TAB_INDEX || curIndex == newIndex || curIndex + 1 == newIndex) { + return; + } + + mTabIdList.remove(curIndex); + addTabId(newIndex, id); + + Tab tab = getTabAt(curIndex); + if (tab == null) return; + for (TabModelObserver obs : mObservers) obs.didMoveTab(tab, newIndex, curIndex); + } + + @Override + public void destroy() { + super.destroy(); + mInitializationObservers.clear(); + mObservers.clear(); + } + + @Override + public void addTab(Tab tab) { + int parentIndex = indexOf(tab.getParentId()); + int index = parentIndex == -1 ? getCount() : parentIndex + 1; + addTab(tab, index, tab.getLaunchType()); + } + + @Override + public void addTab(Tab tab, int index, TabLaunchType type) { + for (TabModelObserver obs : mObservers) obs.willAddTab(tab, type); + + if (index == TabModel.INVALID_TAB_INDEX) { + addTabId(getCount(), tab.getId()); + } else { + addTabId(index, tab.getId()); + } + + tabAddedToModel(tab); + for (TabModelObserver obs : mObservers) obs.didAddTab(tab, type); + } + + @Override + public boolean supportsPendingClosures() { + return false; + } + + @Override + public void commitAllTabClosures() { + } + + @Override + public void commitTabClosure(int tabId) { + } + + @Override + public void cancelTabClosure(int tabId) { + } + + @Override + public TabList getComprehensiveModel() { + return this; + } + + @Override + public void addObserver(TabModelObserver observer) { + mObservers.addObserver(observer); + } + + @Override + public void removeObserver(TabModelObserver observer) { + mObservers.removeObserver(observer); + } + + private static boolean isTabIdInEntryList(List entries, int tabId) { + for (int i = 0; i < entries.size(); i++) { + if (entries.get(i).tabId == tabId) return true; + } + return false; + } +} diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/document/OWNERS b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/document/OWNERS new file mode 100644 index 000000000000..79becd27e526 --- /dev/null +++ b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/document/OWNERS @@ -0,0 +1 @@ +dfalcantara@chromium.org diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/document/OffTheRecordDocumentTabModel.java b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/document/OffTheRecordDocumentTabModel.java new file mode 100644 index 000000000000..d9d29c09aa68 --- /dev/null +++ b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/document/OffTheRecordDocumentTabModel.java @@ -0,0 +1,141 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chrome.browser.tabmodel.document; + +import android.content.Intent; + +import org.chromium.base.VisibleForTesting; +import org.chromium.chrome.browser.Tab; +import org.chromium.chrome.browser.TabState; +import org.chromium.chrome.browser.tabmodel.EmptyTabModel; +import org.chromium.chrome.browser.tabmodel.OffTheRecordTabModel; +import org.chromium.chrome.browser.tabmodel.TabModel; + +/** + * Implements an OffTheRecord version of the DocumentTabModel. Timing is a little bit different for + * profile deletion because we don't get all the signals we'd expect when Tabs are closed. More + * specifically, Android doesn't fire signals when tasks are swiped away from the Recents menu if + * the Activity is dead when it occurs. + */ +public class OffTheRecordDocumentTabModel extends OffTheRecordTabModel implements DocumentTabModel { + public OffTheRecordDocumentTabModel(OffTheRecordTabModelDelegate tabModelCreator, + ActivityDelegate delegate) { + super(tabModelCreator); + if (delegate.getTasksFromRecents(true).size() > 0) { + ensureTabModelImpl(); + } + } + + @VisibleForTesting + public boolean isDocumentTabModelImplCreated() { + return !(getDelegateModel() instanceof EmptyTabModel); + } + + private DocumentTabModel getDelegateDocumentTabModel() { + TabModel delegate = getDelegateModel(); + return isDocumentTabModelImplCreated() ? (DocumentTabModel) delegate : null; + } + + @Override + public void initializeNative() { + if (!isDocumentTabModelImplCreated()) return; + getDelegateDocumentTabModel().initializeNative(); + } + + @Override + public TabState getTabStateForDocument(int tabId) { + if (!isDocumentTabModelImplCreated()) return null; + return getDelegateDocumentTabModel().getTabStateForDocument(tabId); + } + + @Override + public boolean isRetargetable(int tabId) { + if (!isDocumentTabModelImplCreated()) return false; + return getDelegateDocumentTabModel().isRetargetable(tabId); + } + + @Override + public void updateRecentlyClosed() { + if (!isDocumentTabModelImplCreated()) return; + getDelegateDocumentTabModel().updateRecentlyClosed(); + destroyIncognitoIfNecessary(); + } + + @Override + public boolean hasEntryForTabId(int tabId) { + if (!isDocumentTabModelImplCreated()) return false; + return getDelegateDocumentTabModel().hasEntryForTabId(tabId); + } + + @Override + public void updateEntry(Intent intent, Tab tab) { + if (!isDocumentTabModelImplCreated()) return; + getDelegateDocumentTabModel().updateEntry(intent, tab); + } + + @Override + public String getCurrentUrlForDocument(int tabId) { + if (!isDocumentTabModelImplCreated()) return null; + return getDelegateDocumentTabModel().getCurrentUrlForDocument(tabId); + } + + @Override + public boolean isTabStateReady(int tabId) { + if (!isDocumentTabModelImplCreated()) return false; + return getDelegateDocumentTabModel().isTabStateReady(tabId); + } + + @Override + public String getInitialUrlForDocument(int tabId) { + if (!isDocumentTabModelImplCreated()) return null; + return getDelegateDocumentTabModel().getInitialUrlForDocument(tabId); + } + + @Override + public void addTab(Tab tab) { + ensureTabModelImpl(); + getDelegateDocumentTabModel().addTab(tab); + } + + @Override + public boolean closeTabAt(int index) { + boolean success = false; + if (isDocumentTabModelImplCreated()) { + success = getDelegateDocumentTabModel().closeTabAt(index); + } + destroyIncognitoIfNecessary(); + return success; + } + + @Override + public int getCurrentInitializationStage() { + if (!isDocumentTabModelImplCreated()) return DocumentTabModelImpl.STATE_UNINITIALIZED; + return getDelegateDocumentTabModel().getCurrentInitializationStage(); + } + + @Override + public boolean isNativeInitialized() { + if (!isDocumentTabModelImplCreated()) return false; + return getDelegateDocumentTabModel().isNativeInitialized(); + } + + @Override + public void addInitializationObserver(InitializationObserver observer) { + ensureTabModelImpl(); + getDelegateDocumentTabModel().addInitializationObserver(observer); + } + + @Override + public void setLastShownId(int id) { + ensureTabModelImpl(); + getDelegateDocumentTabModel().setLastShownId(id); + } + + @Override + public void startTabStateLoad() { + ensureTabModelImpl(); + getDelegateDocumentTabModel().startTabStateLoad(); + } +} \ No newline at end of file diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/document/StorageDelegate.java b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/document/StorageDelegate.java new file mode 100644 index 000000000000..2d2f7fe15bbf --- /dev/null +++ b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/document/StorageDelegate.java @@ -0,0 +1,154 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chrome.browser.tabmodel.document; + +import android.content.Context; +import android.util.Log; + +import org.chromium.base.ApplicationStatus; +import org.chromium.chrome.browser.TabState; +import org.chromium.chrome.browser.util.StreamUtil; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * Contains functions for interacting with the file system. + */ +public class StorageDelegate { + private static final String TAG = "StorageDelegate"; + + /** Filename to use for the DocumentTabModel that stores regular tabs. */ + private static final String REGULAR_FILE_NAME = "chrome_document_activity.store"; + + /** Directory to store TabState files in. */ + private static final String STATE_DIRECTORY = "ChromeDocumentActivity"; + + /** The buffer size to use when reading the DocumentTabModel file, set to 4k bytes. */ + private static final int BUF_SIZE = 0x1000; + + /** Whether this is dealing with incognito state. */ + protected final boolean mIsIncognito; + + public StorageDelegate(boolean isIncognito) { + mIsIncognito = isIncognito; + } + + /** + * Reads the file containing the minimum info required to restore the state of the + * {@link DocumentTabModel}. + * @return Byte buffer containing the task file's data, or null if it wasn't read. + */ + public byte[] readTaskFileBytes() { + // Incognito mode doesn't save its state out. + if (mIsIncognito) return null; + + // Read in the file. + byte[] bytes = null; + FileInputStream streamIn = null; + try { + String filename = getFilename(); + streamIn = ApplicationStatus.getApplicationContext().openFileInput(filename); + + // Read the file from the file into the out stream. + ByteArrayOutputStream streamOut = new ByteArrayOutputStream(); + byte[] buf = new byte[BUF_SIZE]; + int r; + while ((r = streamIn.read(buf)) != -1) { + streamOut.write(buf, 0, r); + } + bytes = streamOut.toByteArray(); + } catch (FileNotFoundException e) { + Log.e(TAG, "DocumentTabModel file not found."); + } catch (IOException e) { + Log.e(TAG, "I/O exception", e); + } finally { + StreamUtil.closeQuietly(streamIn); + } + + return bytes; + } + + /** + * Writes the file containing the minimum info required to restore the state of the + * {@link DocumentTabModel}. + * @param isIncognito Whether the TabModel is incognito. + * @param bytes Byte buffer containing the tab's data. + */ + public void writeTaskFileBytes(byte[] bytes) { + // Incognito mode doesn't save its state out. + if (mIsIncognito) return; + + FileOutputStream outputStream = null; + try { + outputStream = ApplicationStatus.getApplicationContext().openFileOutput( + getFilename(), Context.MODE_PRIVATE); + outputStream.write(bytes); + } catch (FileNotFoundException e) { + Log.e(TAG, "DocumentTabModel file not found", e); + } catch (IOException e) { + Log.e(TAG, "I/O exception", e); + } finally { + StreamUtil.closeQuietly(outputStream); + } + } + + /** @return The directory that stores the TabState files. */ + public File getStateDirectory() { + return ApplicationStatus.getApplicationContext().getDir( + STATE_DIRECTORY, Context.MODE_PRIVATE); + } + + /** + * Restores the TabState with the given ID. + * @param tabId ID of the Tab. + * @return TabState for the Tab. + */ + public TabState restoreTabState(int tabId) { + return TabState.restoreTabState(getTabFile(tabId), mIsIncognito); + } + + /** + * Saves the TabState with the given ID. + * @param tabId ID of the Tab. + * @param state TabState for the Tab. + */ + public void saveTabState(int tabId, TabState state) { + FileOutputStream stream = null; + try { + stream = new FileOutputStream(getTabFile(tabId)); + TabState.saveState(stream, state, mIsIncognito); + } catch (FileNotFoundException exception) { + Log.e(TAG, "Failed to save out tab state for tab " + tabId, exception); + } catch (IOException exception) { + Log.e(TAG, "Failed to save out tab state.", exception); + } finally { + StreamUtil.closeQuietly(stream); + } + } + + /** + * Deletes the TabState file for the given ID. + * @param tabId ID of the TabState file to delete. + */ + public void deleteTabStateFile(int tabId) { + boolean success = getTabFile(tabId).delete(); + if (!success) Log.w(TAG, "Failed to delete file for tab " + tabId); + } + + private File getTabFile(int tabId) { + String tabStateFilename = TabState.getTabStateFilename(tabId, mIsIncognito); + return new File(getStateDirectory(), tabStateFilename); + } + + /** @return the filename of the persisted TabModel state. */ + private String getFilename() { + return mIsIncognito ? null : REGULAR_FILE_NAME; + } +} \ No newline at end of file diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/document/TabDelegate.java b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/document/TabDelegate.java new file mode 100644 index 000000000000..f152845dd886 --- /dev/null +++ b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/document/TabDelegate.java @@ -0,0 +1,45 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chrome.browser.tabmodel.document; + +import android.app.Activity; + +import org.chromium.chrome.browser.Tab; +import org.chromium.chrome.browser.tabmodel.document.DocumentTabModel.Entry; + +/** + * Provides Tabs to a DocumentTabModel. + */ +public interface TabDelegate { + /** + * Returns the Tab for the given Activity. + * @param incognito Whether the Activity is supposed to hold an incognito Tab. + * @param delgate Sotres information about DocumentActivities. + * @param activity Activity to grab the Tab of. + * @return Tab for the DocumentActivity, if it is a valid DocumentActivity. Null otherwise. + */ + Tab getActivityTab(boolean incognito, ActivityDelegate delgate, Activity activity); + + /** + * Creates a frozen Tab for the Entry. + * @param entry Entry containing TabState. + * @return A frozen Tab. + */ + Tab createFrozenTab(Entry entry); + + /** + * Creates a new Activity for the pre-created WebContents. + * @param isIncognito Whether the Activity is supposed to hold an incognito Tab. + * @param webContentsPtr Native-side WebContents pointer to use for the new Tab. + * @param parentTabId ID of the spawning Tab. + */ + void createTabWithNativeContents(boolean isIncognito, long webContentsPtr, int parentTabId); + + /** + * Creates a new Tab for a URL typed into DevTools. + * @param url URL to spawn a Tab for. + */ + void createTabForDevTools(String url); +} \ No newline at end of file diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/document/DocumentTabModelImplTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/document/DocumentTabModelImplTest.java new file mode 100644 index 000000000000..348fc1ca14f3 --- /dev/null +++ b/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/document/DocumentTabModelImplTest.java @@ -0,0 +1,434 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chrome.browser.tabmodel.document; + +import android.os.Build; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.ArrayMap; + +import org.chromium.base.CommandLine; +import org.chromium.base.ThreadUtils; +import org.chromium.base.test.util.AdvancedMockContext; +import org.chromium.base.test.util.MinAndroidSdkLevel; +import org.chromium.chrome.browser.tabmodel.TabModel; +import org.chromium.chrome.browser.tabmodel.TabModelUtils; +import org.chromium.chrome.test.util.browser.tabmodel.document.MockActivityDelegate; +import org.chromium.chrome.test.util.browser.tabmodel.document.MockStorageDelegate; +import org.chromium.chrome.test.util.browser.tabmodel.document.MockTabDelegate; +import org.chromium.chrome.test.util.browser.tabmodel.document.TestInitializationObserver; +import org.chromium.content.browser.test.NativeLibraryTestBase; + +import java.util.Map; + +/** + * Tests the functionality of the DocumentTabModel. + */ +@MinAndroidSdkLevel(Build.VERSION_CODES.LOLLIPOP) +public class DocumentTabModelImplTest extends NativeLibraryTestBase { + private static final String MODEL_STATE_WITH_1010_1011 = "CgUgACjyBwoFIAEo8wc="; + + private static final String TAB_STATE_1010_ERFWORLD_RETARGETABLE = + "AAABSVhnsswAAAFkYAEAAAAAAAABAAAAAAAAAFABAABMAQAAAAAAACcAAABodHRwOi8vd3d3LmVyZndvcmxkLm" + + "NvbS9lcmZfc3RyZWFtL3ZpZXcAAAAAAMQAAADAAAAAFQAAAAAAAABOAAAAaAB0AHQAcAA6AC8ALwB3AHcAdw" + + "AuAGUAcgBmAHcAbwByAGwAZAAuAGMAbwBtAC8AZQByAGYAXwBzAHQAcgBlAGEAbQAvAHYAaQBlAHcAAAD///" + + "//AAAAAAAAAAD/////AAAAAAgAAAAAAAAAAAAAAM2oGVWBBgUAzqgZVYEGBQDPqBlVgQYFAAEAAAAIAAAAAA" + + "AAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAP////8AAAAAAAAACAAAAAAAAAAAAQAAACcAAABodHRwOi8vd3" + + "d3LmVyZndvcmxkLmNvbS9lcmZfc3RyZWFtL3ZpZXcAAAAAAOrxn50XZS4AAAAAAMgAAAD/////AAAAAAACAA" + + "AAAAAAAAAA"; + + private static final String TAB_STATE_1011_REDDIT = + "AAABSVhw+HkAAAJkYAIAAAAAAAACAAAAAQAAACABAAAcAQAAAAAAABcAAABjaHJvbWUtbmF0aXZlOi8vbmV3" + + "dGFiLwAHAAAATgBlAHcAIAB0AGEAYgAAAKQAAACgAAAAFQAAAAAAAAAuAAAAYwBoAHIAbwBtAGUALQBuAGEA" + + "dABpAHYAZQA6AC8ALwBuAGUAdwB0AGEAYgAvAAAA/////wAAAAAAAAAA/////wAAAAAIAAAAAAAAAAAA8D9M" + + "Bk15gQYFAE0GTXmBBgUATgZNeYEGBQABAAAACAAAAAAAAAAAAPC/CAAAAAAAAAAAAPC/AAAAAAAAAAD/////" + + "AAAAAAYAAAAAAAAAAAAAAAEAAAAXAAAAY2hyb21lLW5hdGl2ZTovL25ld3RhYi8AAAAAAN5f08EXZS4AAAAA" + + "AAAAAAAsAQAAKAEAAAEAAAAfAAAAaHR0cDovL3d3dy5yZWRkaXQuY29tL3IvYW5kcm9pZAAAAAAAtAAAALAA" + + "AAAVAAAAAAAAAD4AAABoAHQAdABwADoALwAvAHcAdwB3AC4AcgBlAGQAZABpAHQALgBjAG8AbQAvAHIALwBh" + + "AG4AZAByAG8AaQBkAAAA/////wAAAAAAAAAA/////wAAAAAIAAAAAAAAAAAAAABPBk15gQYFAFAGTXmBBgUA" + + "TgZNeYEGBQABAAAACAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAAEAAAIAAAAAAAAA" + + "AAEAAAAbAAAAaHR0cDovL3JlZGRpdC5jb20vci9hbmRyb2lkAAAAAAAB65zCF2UuAAAAAADIAAAA/////wAA" + + "AAAAAgAAAAAAAAAAAA=="; + + private static class CloseRunnable implements Runnable { + final DocumentTabModel mTabModel; + final int mIndex; + boolean mSucceeded; + + public CloseRunnable(DocumentTabModel model, int index) { + mTabModel = model; + mIndex = index; + } + + @Override + public void run() { + mSucceeded = mTabModel.closeTabAt(mIndex); + } + + static boolean closeTabAt(DocumentTabModel model, int index) throws Exception { + CloseRunnable runnable = new CloseRunnable(model, index); + ThreadUtils.runOnUiThreadBlocking(runnable); + return runnable.mSucceeded; + } + } + + private MockActivityDelegate mActivityDelegate; + private MockStorageDelegate mStorageDelegate; + private MockTabDelegate mTabDelegate; + private DocumentTabModel mTabModel; + private AdvancedMockContext mContext; + + @Override + protected void setUp() throws Exception { + super.setUp(); + CommandLine.init(null); + loadNativeLibraryAndInitBrowserProcess(); + + mActivityDelegate = + new MockActivityDelegate("DocumentActivity", "IncognitoDocumentActivity"); + mTabDelegate = new MockTabDelegate(); + mContext = new AdvancedMockContext(getInstrumentation().getTargetContext()); + } + + @Override + protected void tearDown() throws Exception { + mStorageDelegate.ensureDirectoryDestroyed(); + + ThreadUtils.runOnUiThreadBlocking(new Runnable() { + @Override + public void run() { + if (mTabModel.isNativeInitialized()) mTabModel.destroy(); + } + }); + + super.tearDown(); + } + + private void setupDocumentTabModel() { + ThreadUtils.runOnUiThreadBlocking(new Runnable() { + @Override + public void run() { + mTabModel = new DocumentTabModelImpl( + mActivityDelegate, mStorageDelegate, mTabDelegate, false, 1010, mContext); + mTabModel.startTabStateLoad(); + } + }); + } + + private void initializeNativeTabModel() throws Exception { + ThreadUtils.runOnUiThreadBlocking(new Runnable() { + @Override + public void run() { + mTabModel.initializeNative(); + } + }); + TestInitializationObserver.waitUntilState( + mTabModel, DocumentTabModelImpl.STATE_DESERIALIZE_END); + } + + @SmallTest + public void testBasic() throws Exception { + mActivityDelegate.addTask(false, 1010, "http://erfworld.com"); + mActivityDelegate.addTask(false, 1011, "http://reddit.com/r/android"); + + mStorageDelegate = new MockStorageDelegate(mContext.getCacheDir(), false); + mStorageDelegate.setTaskFileBytesFromEncodedString(MODEL_STATE_WITH_1010_1011); + mStorageDelegate.addEncodedTabState(1010, TAB_STATE_1010_ERFWORLD_RETARGETABLE); + mStorageDelegate.addEncodedTabState(1011, TAB_STATE_1011_REDDIT); + + setupDocumentTabModel(); + + // Confirm the data from the task file is restored correctly. + assertEquals(2, mTabModel.getCount()); + assertEquals(1010, mTabModel.getTabAt(0).getId()); + assertEquals(1011, mTabModel.getTabAt(1).getId()); + assertEquals("http://erfworld.com", mTabModel.getInitialUrlForDocument(1010)); + assertEquals("http://reddit.com/r/android", mTabModel.getInitialUrlForDocument(1011)); + assertEquals(true, mTabModel.isRetargetable(1010)); + assertEquals(false, mTabModel.isRetargetable(1011)); + + // State of the tabs. + assertTrue(mTabModel.isTabStateReady(1010)); + assertNotNull(mTabModel.getTabStateForDocument(1010)); + assertNull(mTabModel.getCurrentUrlForDocument(1010)); + assertNull(mTabModel.getCurrentUrlForDocument(1011)); + + // Wait until the tab states are loaded. + TestInitializationObserver.waitUntilState( + mTabModel, DocumentTabModelImpl.STATE_LOAD_TAB_STATE_BG_END); + assertNotNull(mTabModel.getTabStateForDocument(1011)); + + // Load the native library, wait until the states are deserialized, then check their values. + initializeNativeTabModel(); + assertEquals("http://www.erfworld.com/erf_stream/view", + mTabModel.getCurrentUrlForDocument(1010)); + assertEquals("http://www.reddit.com/r/android", mTabModel.getCurrentUrlForDocument(1011)); + } + + @SmallTest + public void testIncognitoIgnored() throws Exception { + mActivityDelegate.addTask(false, 1010, "http://erfworld.com"); + mActivityDelegate.addTask(false, 1011, "http://reddit.com/r/android"); + mActivityDelegate.addTask(true, 1012, "http://incognito.com/ignored"); + + mStorageDelegate = new MockStorageDelegate(mContext.getCacheDir(), false); + mStorageDelegate.setTaskFileBytesFromEncodedString(MODEL_STATE_WITH_1010_1011); + mStorageDelegate.addEncodedTabState(1010, TAB_STATE_1010_ERFWORLD_RETARGETABLE); + mStorageDelegate.addEncodedTabState(1011, TAB_STATE_1011_REDDIT); + + setupDocumentTabModel(); + + // Confirm the data from the task file is restored correctly. + assertEquals(2, mTabModel.getCount()); + assertEquals(1010, mTabModel.getTabAt(0).getId()); + assertEquals(1011, mTabModel.getTabAt(1).getId()); + assertEquals("http://erfworld.com", mTabModel.getInitialUrlForDocument(1010)); + assertEquals("http://reddit.com/r/android", mTabModel.getInitialUrlForDocument(1011)); + assertEquals(true, mTabModel.isRetargetable(1010)); + assertEquals(false, mTabModel.isRetargetable(1011)); + + // State of the tabs. + assertTrue(mTabModel.isTabStateReady(1010)); + assertNotNull(mTabModel.getTabStateForDocument(1010)); + assertNull(mTabModel.getCurrentUrlForDocument(1010)); + assertNull(mTabModel.getCurrentUrlForDocument(1011)); + + // Wait until the tab states are loaded. + TestInitializationObserver.waitUntilState( + mTabModel, DocumentTabModelImpl.STATE_LOAD_TAB_STATE_BG_END); + assertNotNull(mTabModel.getTabStateForDocument(1011)); + + // Load the native library, wait until the states are deserialized, then check their values. + initializeNativeTabModel(); + assertEquals("http://www.erfworld.com/erf_stream/view", + mTabModel.getCurrentUrlForDocument(1010)); + assertEquals("http://www.reddit.com/r/android", mTabModel.getCurrentUrlForDocument(1011)); + } + + /** + * Tasks found in Android's Recents and not in the DocumentTabModel's task file should be + * added to the DocumentTabModel. + */ + @SmallTest + public void testMissingTaskAddedAndUnretargetable() throws Exception { + mActivityDelegate.addTask(false, 1012, "http://digg.com"); + mActivityDelegate.addTask(false, 1010, "http://erfworld.com"); + mActivityDelegate.addTask(false, 1011, "http://reddit.com/r/android"); + + mStorageDelegate = new MockStorageDelegate(mContext.getCacheDir(), false); + mStorageDelegate.setTaskFileBytesFromEncodedString(MODEL_STATE_WITH_1010_1011); + + setupDocumentTabModel(); + + assertEquals(3, mTabModel.getCount()); + assertEquals(1010, mTabModel.getTabAt(0).getId()); + assertEquals(1011, mTabModel.getTabAt(1).getId()); + assertEquals(1012, mTabModel.getTabAt(2).getId()); + + assertEquals("http://erfworld.com", mTabModel.getInitialUrlForDocument(1010)); + assertEquals("http://reddit.com/r/android", mTabModel.getInitialUrlForDocument(1011)); + assertEquals("http://digg.com", mTabModel.getInitialUrlForDocument(1012)); + + assertEquals(true, mTabModel.isRetargetable(1010)); + assertEquals(false, mTabModel.isRetargetable(1011)); + assertEquals(false, mTabModel.isRetargetable(1012)); + } + + /** + * If a TabState file is missing, we won't be able to get a current URL for it but should still + * get notification that the TabState was loaded. + */ + @SmallTest + public void testMissingTabState() throws Exception { + mActivityDelegate.addTask(false, 1010, "http://erfworld.com"); + mActivityDelegate.addTask(false, 1011, "http://reddit.com/r/android"); + + mStorageDelegate = new MockStorageDelegate(mContext.getCacheDir(), false); + mStorageDelegate.setTaskFileBytesFromEncodedString(MODEL_STATE_WITH_1010_1011); + mStorageDelegate.addEncodedTabState(1011, TAB_STATE_1011_REDDIT); + + setupDocumentTabModel(); + + assertEquals(2, mTabModel.getCount()); + assertTrue(mTabModel.isTabStateReady(1010)); + assertNull(mTabModel.getTabStateForDocument(1010)); + + assertNull(mTabModel.getCurrentUrlForDocument(1010)); + assertNull(mTabModel.getCurrentUrlForDocument(1011)); + + // After the DocumentTabModel has progressed far enough, confirm that the other available + // TabState has been loaded. + TestInitializationObserver.waitUntilState( + mTabModel, DocumentTabModelImpl.STATE_LOAD_TAB_STATE_BG_END); + assertTrue(mTabModel.isTabStateReady(1010)); + assertTrue(mTabModel.isTabStateReady(1011)); + + assertNull(mTabModel.getTabStateForDocument(1010)); + assertNotNull(mTabModel.getTabStateForDocument(1011)); + + // Load the native library, wait until the states are deserialized, then check their values. + initializeNativeTabModel(); + assertNull(null, mTabModel.getCurrentUrlForDocument(1010)); + assertEquals("http://www.reddit.com/r/android", mTabModel.getCurrentUrlForDocument(1011)); + } + + @SmallTest + public void testTasksSwipedAwayBeforeTabModelCreation() throws Exception { + mActivityDelegate.addTask(false, 1010, "http://erfworld.com"); + + mStorageDelegate = new MockStorageDelegate(mContext.getCacheDir(), false); + mStorageDelegate.setTaskFileBytesFromEncodedString(MODEL_STATE_WITH_1010_1011); + setupDocumentTabModel(); + + assertEquals(1, mTabModel.getCount()); + assertEquals(1010, mTabModel.getTabAt(0).getId()); + assertEquals("http://erfworld.com", mTabModel.getInitialUrlForDocument(1010)); + } + + /** + * Tasks swiped away in Android's Recents should be reflected as closed tabs in the + * DocumentTabModel. + */ + @SmallTest + public void testTasksSwipedAwayAfterTabModelCreation() throws Exception { + mActivityDelegate.addTask(false, 1010, "http://erfworld.com"); + mActivityDelegate.addTask(false, 1011, "http://reddit.com/r/android"); + + mStorageDelegate = new MockStorageDelegate(mContext.getCacheDir(), false); + mStorageDelegate.setTaskFileBytesFromEncodedString(MODEL_STATE_WITH_1010_1011); + + setupDocumentTabModel(); + assertEquals(2, mTabModel.getCount()); + assertEquals(1010, mTabModel.getTabAt(0).getId()); + assertEquals(1011, mTabModel.getTabAt(1).getId()); + + mActivityDelegate.removeTask(false, 1010); + ThreadUtils.runOnUiThreadBlocking(new Runnable() { + @Override + public void run() { + mTabModel.updateRecentlyClosed(); + } + }); + assertEquals(1, mTabModel.getCount()); + assertEquals(1011, mTabModel.getTabAt(0).getId()); + } + + /** + * DocumentTabModel#closeAllTabs() should remove all the tabs from the TabModel. + */ + @SmallTest + public void testCloseAllTabs() throws Exception { + mActivityDelegate.addTask(false, 1010, "http://erfworld.com"); + mActivityDelegate.addTask(false, 1011, "http://reddit.com/r/android"); + + mStorageDelegate = new MockStorageDelegate(mContext.getCacheDir(), false); + mStorageDelegate.setTaskFileBytesFromEncodedString(MODEL_STATE_WITH_1010_1011); + + setupDocumentTabModel(); + assertEquals(2, mTabModel.getCount()); + assertEquals(1010, mTabModel.getTabAt(0).getId()); + assertEquals(1011, mTabModel.getTabAt(1).getId()); + + ThreadUtils.runOnUiThreadBlocking(new Runnable() { + @Override + public void run() { + mTabModel.closeAllTabs(); + } + }); + assertEquals(0, mTabModel.getCount()); + } + + /** + * DocumentTabModel#closeTabAt() should close a tab and slide the other ones in to fill the gap. + * This test also relies on the DocumentTabModel adding tasks it finds in Android's Recents to + * pad out the test. + */ + @SmallTest + public void testCloseTabAt() throws Exception { + mActivityDelegate.addTask(false, 1010, "http://erfworld.com"); + mActivityDelegate.addTask(false, 1011, "http://reddit.com/r/android"); + mActivityDelegate.addTask(false, 1012, "http://digg.com"); + mActivityDelegate.addTask(false, 1013, "http://slashdot.org"); + + mStorageDelegate = new MockStorageDelegate(mContext.getCacheDir(), false); + + setupDocumentTabModel(); + assertEquals(4, mTabModel.getCount()); + assertEquals(1010, mTabModel.getTabAt(0).getId()); + assertEquals(1011, mTabModel.getTabAt(1).getId()); + assertEquals(1012, mTabModel.getTabAt(2).getId()); + assertEquals(1013, mTabModel.getTabAt(3).getId()); + + assertTrue(CloseRunnable.closeTabAt(mTabModel, 1)); + assertEquals(3, mTabModel.getCount()); + assertEquals(1010, mTabModel.getTabAt(0).getId()); + assertEquals(1012, mTabModel.getTabAt(1).getId()); + assertEquals(1013, mTabModel.getTabAt(2).getId()); + + assertTrue(CloseRunnable.closeTabAt(mTabModel, 2)); + assertEquals(2, mTabModel.getCount()); + assertEquals(1010, mTabModel.getTabAt(0).getId()); + assertEquals(1012, mTabModel.getTabAt(1).getId()); + + assertTrue(CloseRunnable.closeTabAt(mTabModel, 0)); + assertEquals(1, mTabModel.getCount()); + assertEquals(1012, mTabModel.getTabAt(0).getId()); + + assertTrue(CloseRunnable.closeTabAt(mTabModel, 0)); + assertEquals(0, mTabModel.getCount()); + + assertFalse(CloseRunnable.closeTabAt(mTabModel, 0)); + } + + /** + * Test that the DocumentTabModel.index() function works as expected as Tabs are selected and + * closed. + */ + @SmallTest + public void testIndex() throws Exception { + mActivityDelegate.addTask(false, 1010, "http://erfworld.com"); + mActivityDelegate.addTask(false, 1011, "http://reddit.com/r/android"); + mActivityDelegate.addTask(false, 1012, "http://digg.com"); + mActivityDelegate.addTask(false, 1013, "http://slashdot.org"); + + mStorageDelegate = new MockStorageDelegate(mContext.getCacheDir(), false); + + Map data = new ArrayMap(); + data.put(DocumentTabModelImpl.PREF_LAST_SHOWN_TAB_ID_REGULAR, 1011); + mContext.addSharedPreferences(DocumentTabModelImpl.PREF_PACKAGE, data); + + // The ID stored in the SharedPreferences points at index 1. + setupDocumentTabModel(); + assertEquals(1, mTabModel.index()); + + // Pick a different Tab. + ThreadUtils.runOnUiThreadBlocking(new Runnable() { + @Override + public void run() { + TabModelUtils.setIndex(mTabModel, 3); + } + }); + assertEquals(3, mTabModel.index()); + assertEquals(1013, mTabModel.getTabAt(3).getId()); + assertEquals(1013, data.get(DocumentTabModelImpl.PREF_LAST_SHOWN_TAB_ID_REGULAR)); + + // Select the MRU tab since the last known Tab was closed. The last shown ID isn't updated + // when the new Tab is selected; it's the job of the DocumentActivity to alert the + // DocumentTabModel about shown Tab changes. + assertTrue(CloseRunnable.closeTabAt(mTabModel, 3)); + assertEquals(3, mTabModel.getCount()); + assertEquals(0, mTabModel.index()); + assertEquals(1010, mTabModel.getTabAt(0).getId()); + + // Close everything; index should be invalid. + ThreadUtils.runOnUiThreadBlocking(new Runnable() { + @Override + public void run() { + mTabModel.closeAllTabs(); + } + }); + assertEquals(0, mTabModel.getCount()); + assertEquals(TabModel.INVALID_TAB_INDEX, mTabModel.index()); + } +} diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/document/OWNERS b/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/document/OWNERS new file mode 100644 index 000000000000..79becd27e526 --- /dev/null +++ b/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/document/OWNERS @@ -0,0 +1 @@ +dfalcantara@chromium.org diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/document/OffTheRecordDocumentTabModelTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/document/OffTheRecordDocumentTabModelTest.java new file mode 100644 index 000000000000..682c0a072036 --- /dev/null +++ b/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/document/OffTheRecordDocumentTabModelTest.java @@ -0,0 +1,227 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chrome.browser.tabmodel.document; + +import android.content.Context; +import android.os.Build; +import android.test.suitebuilder.annotation.SmallTest; + +import org.chromium.base.CommandLine; +import org.chromium.base.ThreadUtils; +import org.chromium.base.test.util.MinAndroidSdkLevel; +import org.chromium.chrome.browser.Tab; +import org.chromium.chrome.browser.profiles.Profile; +import org.chromium.chrome.browser.tabmodel.OffTheRecordTabModel.OffTheRecordTabModelDelegate; +import org.chromium.chrome.browser.tabmodel.TabModel; +import org.chromium.chrome.test.util.browser.tabmodel.document.MockActivityDelegate; +import org.chromium.chrome.test.util.browser.tabmodel.document.MockStorageDelegate; +import org.chromium.chrome.test.util.browser.tabmodel.document.MockTabDelegate; +import org.chromium.chrome.test.util.browser.tabmodel.document.TestInitializationObserver; +import org.chromium.content.browser.test.NativeLibraryTestBase; + +/** + * Tests the functionality of the OffTheRecordDocumentTabModel. + * These tests rely on the DocumentTabModelImpl adding tasks it finds via the ActivityManager to its + * internal list, instead of loading a task file containing an explicit list like the regular + * DocumentTabModelTests do. + */ +@MinAndroidSdkLevel(Build.VERSION_CODES.LOLLIPOP) +public class OffTheRecordDocumentTabModelTest extends NativeLibraryTestBase { + private MockActivityDelegate mActivityDelegate; + private MockStorageDelegate mStorageDelegate; + private MockTabDelegate mTabDelegate; + + private OffTheRecordDocumentTabModel mTabModel; + private Profile mProfile; + private Context mContext; + + static final String INIT_SWITCHES[] = { "chrome_shell" }; + + @Override + protected void setUp() throws Exception { + super.setUp(); + CommandLine.init(null); + loadNativeLibraryAndInitBrowserProcess(); + + mContext = getInstrumentation().getTargetContext(); + mStorageDelegate = new MockStorageDelegate(mContext.getCacheDir(), true); + mActivityDelegate = + new MockActivityDelegate("DocumentActivity", "IncognitoDocumentActivity"); + mTabDelegate = new MockTabDelegate(); + } + + private void createTabModel() { + OffTheRecordTabModelDelegate delegate = new OffTheRecordTabModelDelegate() { + private DocumentTabModel mModel; + + @Override + public TabModel createTabModel() { + mModel = new DocumentTabModelImpl(mActivityDelegate, mStorageDelegate, mTabDelegate, + true, Tab.INVALID_TAB_ID, mContext); + return mModel; + } + + @Override + public int getOffTheRecordTabCount() { + return mModel == null ? 0 : mModel.getCount(); + } + + }; + mTabModel = new OffTheRecordDocumentTabModel(delegate, mActivityDelegate); + } + + private void initializeNativeTabModel() throws Exception { + ThreadUtils.runOnUiThreadBlocking(new Runnable() { + @Override + public void run() { + mTabModel.initializeNative(); + mProfile = mTabModel.getProfile(); + assertTrue(mProfile.isNativeInitialized()); + } + }); + TestInitializationObserver.waitUntilState( + mTabModel, DocumentTabModelImpl.STATE_DESERIALIZE_END); + } + + @SmallTest + public void testNotCreatedWithoutIncognitoTask() throws Exception { + mActivityDelegate.addTask(false, 11684, "http://regular.tab"); + createTabModel(); + assertEquals(0, mTabModel.getCount()); + assertFalse(mTabModel.isDocumentTabModelImplCreated()); + } + + @SmallTest + public void testCreatedIfIncognitoTaskExists() throws Exception { + mActivityDelegate.addTask(true, 11684, "http://incognito.tab"); + createTabModel(); + assertEquals(1, mTabModel.getCount()); + assertTrue(mTabModel.isDocumentTabModelImplCreated()); + } + + @SmallTest + public void testTabModelDestructionWhenSwipedAway() throws Exception { + mActivityDelegate.addTask(false, 11683, "http://regular.tab"); + mActivityDelegate.addTask(true, 11684, "http://incognito.tab"); + mActivityDelegate.addTask(true, 11685, "http://second.incognito.tab"); + createTabModel(); + assertEquals(2, mTabModel.getCount()); + assertTrue(mTabModel.isDocumentTabModelImplCreated()); + mTabModel.startTabStateLoad(); + initializeNativeTabModel(); + + mActivityDelegate.removeTask(true, 11684); + ThreadUtils.runOnUiThreadBlocking(new Runnable() { + @Override + public void run() { + mTabModel.updateRecentlyClosed(); + } + }); + assertEquals(1, mTabModel.getCount()); + assertTrue(mTabModel.isDocumentTabModelImplCreated()); + assertTrue(mProfile.isNativeInitialized()); + + mActivityDelegate.removeTask(true, 11685); + ThreadUtils.runOnUiThreadBlocking(new Runnable() { + @Override + public void run() { + mTabModel.updateRecentlyClosed(); + } + }); + assertEquals(0, mTabModel.getCount()); + assertFalse(mTabModel.isDocumentTabModelImplCreated()); + assertFalse(mProfile.isNativeInitialized()); + } + + @SmallTest + public void testTabModelDestructionWhenNativeNotInitialized() throws Exception { + mActivityDelegate.addTask(false, 11683, "http://regular.tab"); + mActivityDelegate.addTask(true, 11684, "http://incognito.tab"); + mActivityDelegate.addTask(true, 11685, "http://second.incognito.tab"); + createTabModel(); + assertEquals(2, mTabModel.getCount()); + assertTrue(mTabModel.isDocumentTabModelImplCreated()); + + mActivityDelegate.removeTask(true, 11684); + ThreadUtils.runOnUiThreadBlocking(new Runnable() { + @Override + public void run() { + mTabModel.updateRecentlyClosed(); + } + }); + assertEquals(1, mTabModel.getCount()); + assertTrue(mTabModel.isDocumentTabModelImplCreated()); + + mActivityDelegate.removeTask(true, 11685); + ThreadUtils.runOnUiThreadBlocking(new Runnable() { + @Override + public void run() { + mTabModel.updateRecentlyClosed(); + } + }); + assertEquals(0, mTabModel.getCount()); + assertFalse(mTabModel.isDocumentTabModelImplCreated()); + } + + @SmallTest + public void testTabModelDestructionAfterCloseTabAt() throws Exception { + mActivityDelegate.addTask(false, 11683, "http://regular.tab"); + mActivityDelegate.addTask(true, 11684, "http://incognito.tab"); + mActivityDelegate.addTask(true, 11685, "http://second.incognito.tab"); + createTabModel(); + assertEquals(2, mTabModel.getCount()); + assertTrue(mTabModel.isDocumentTabModelImplCreated()); + mTabModel.startTabStateLoad(); + initializeNativeTabModel(); + + ThreadUtils.runOnUiThreadBlocking(new Runnable() { + @Override + public void run() { + mTabModel.closeTabAt(0); + } + }); + assertEquals(1, mActivityDelegate.getTasksFromRecents(false).size()); + assertEquals(1, mActivityDelegate.getTasksFromRecents(true).size()); + assertEquals(1, mTabModel.getCount()); + assertTrue(mTabModel.isDocumentTabModelImplCreated()); + assertTrue(mProfile.isNativeInitialized()); + + ThreadUtils.runOnUiThreadBlocking(new Runnable() { + @Override + public void run() { + mTabModel.closeTabAt(0); + } + }); + assertEquals(1, mActivityDelegate.getTasksFromRecents(false).size()); + assertEquals(0, mActivityDelegate.getTasksFromRecents(true).size()); + assertEquals(0, mTabModel.getCount()); + assertFalse(mTabModel.isDocumentTabModelImplCreated()); + assertFalse(mProfile.isNativeInitialized()); + } + + @SmallTest + public void testTabModelDestructionAfterCloseAllTabs() throws Exception { + mActivityDelegate.addTask(false, 11683, "http://regular.tab"); + mActivityDelegate.addTask(true, 11684, "http://incognito.tab"); + mActivityDelegate.addTask(true, 11685, "http://second.incognito.tab"); + createTabModel(); + assertEquals(2, mTabModel.getCount()); + assertTrue(mTabModel.isDocumentTabModelImplCreated()); + mTabModel.startTabStateLoad(); + initializeNativeTabModel(); + + ThreadUtils.runOnUiThreadBlocking(new Runnable() { + @Override + public void run() { + mTabModel.closeAllTabs(); + } + }); + assertEquals(1, mActivityDelegate.getTasksFromRecents(false).size()); + assertEquals(0, mActivityDelegate.getTasksFromRecents(true).size()); + assertEquals(0, mTabModel.getCount()); + assertFalse(mTabModel.isDocumentTabModelImplCreated()); + assertFalse(mProfile.isNativeInitialized()); + } +} diff --git a/chrome/chrome.gyp b/chrome/chrome.gyp index 2615a6289e1d..ad1b2402d67e 100644 --- a/chrome/chrome.gyp +++ b/chrome/chrome.gyp @@ -606,6 +606,7 @@ 'chrome_resources.gyp:chrome_strings', 'chrome_strings_grd', 'chrome_version_java', + 'document_tab_model_info_proto_java', 'profile_account_management_metrics_java', 'content_setting_java', 'content_settings_type_java', diff --git a/chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/tabmodel/document/MockActivityDelegate.java b/chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/tabmodel/document/MockActivityDelegate.java new file mode 100644 index 000000000000..523fee3f7f28 --- /dev/null +++ b/chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/tabmodel/document/MockActivityDelegate.java @@ -0,0 +1,70 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chrome.test.util.browser.tabmodel.document; + +import static junit.framework.Assert.assertTrue; + +import org.chromium.chrome.browser.tabmodel.document.ActivityDelegate; +import org.chromium.chrome.browser.tabmodel.document.DocumentTabModel.Entry; + +import java.util.ArrayList; +import java.util.List; + +/** + * Mocks out calls to the ActivityManager by a DocumentTabModel. + */ +public class MockActivityDelegate extends ActivityDelegate { + private final List mRegularTasks = new ArrayList(); + private final List mIncognitoTasks = new ArrayList(); + + /** + * Creates a MockActivityDelegate. + * @param regularName Name of the regular DocumentActivity. + * @param incognitoName Name of the Incognito DocumentActivity. + */ + public MockActivityDelegate(String regularName, String incognitoName) { + super(regularName, incognitoName); + } + + @Override + public List getTasksFromRecents(boolean isIncognito) { + return isIncognito ? mIncognitoTasks : mRegularTasks; + } + + @Override + public void finishAndRemoveTask(boolean isIncognito, int tabId) { + List tasks = getTasksFromRecents(isIncognito); + for (int i = 0; i < tasks.size(); i++) { + if (tasks.get(i).tabId == tabId) { + tasks.remove(i); + return; + } + } + } + + /** + * Adds a task to the recents list. + * @param isIncognito Whether the task is an incognito task. + * @param tabId ID of the task. + * @param initialUrl Initial URL for the task. + */ + public void addTask(boolean isIncognito, int tabId, String initialUrl) { + getTasksFromRecents(isIncognito).add(new Entry(tabId, initialUrl)); + } + + /** + * Removes a task from the recents list. + * @param tabId ID of the task. + */ + public void removeTask(boolean isIncognito, int tabId) { + boolean found = false; + List tasks = getTasksFromRecents(isIncognito); + for (int i = 0; i < tasks.size() && !found; i++) { + if (tasks.get(i).tabId == tabId) found = true; + } + assertTrue(found); + finishAndRemoveTask(isIncognito, tabId); + } +} \ No newline at end of file diff --git a/chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/tabmodel/document/MockStorageDelegate.java b/chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/tabmodel/document/MockStorageDelegate.java new file mode 100644 index 000000000000..fd93aac5028d --- /dev/null +++ b/chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/tabmodel/document/MockStorageDelegate.java @@ -0,0 +1,109 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chrome.test.util.browser.tabmodel.document; + +import android.util.Base64; +import android.util.Log; + +import junit.framework.Assert; + +import org.chromium.chrome.browser.TabState; +import org.chromium.chrome.browser.tabmodel.document.StorageDelegate; +import org.chromium.chrome.browser.util.StreamUtil; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * Mocks out a directory on the file system for TabState storage. Because of the way that + * {@link TabState} relies on real files and because it's not possible to mock out a + * {@link FileInputStream}, we manage real files in a temporary directory that get written when + * {@link #addEncodedTabState} is called. + */ +public class MockStorageDelegate extends StorageDelegate { + private static final String TAG = "MockStorageDelegate"; + + private byte[] mTaskFileBytes; + private final File mStateDirectory; + + public MockStorageDelegate(File cacheDirectory, boolean isIncognito) { + super(isIncognito); + mStateDirectory = new File(cacheDirectory, "DocumentTabModelTest"); + ensureDirectoryDestroyed(); + } + + @Override + public byte[] readTaskFileBytes() { + if (mIsIncognito) return null; + return mTaskFileBytes == null ? null : mTaskFileBytes.clone(); + } + + @Override + public void writeTaskFileBytes(byte[] bytes) { + if (mIsIncognito) return; + mTaskFileBytes = bytes.clone(); + } + + @Override + public File getStateDirectory() { + if (!mStateDirectory.exists() && !mStateDirectory.mkdir()) { + Assert.fail("Failed to create state directory. Tests should fail."); + } + return mStateDirectory; + } + + /** + * Sets the task file byte buffer to be the decoded format of the given string. + * @param encoded Base64 encoded task file. + */ + public void setTaskFileBytesFromEncodedString(String encoded) { + mTaskFileBytes = Base64.decode(encoded, Base64.DEFAULT); + } + + /** + * Adds a TabState to the file system. + * @param tabId ID of the Tab. + * @param encodedState Base64 encoded TabState. + * @return Whether or not the TabState was successfully read. + */ + public boolean addEncodedTabState(int tabId, String encodedState) { + String filename = TabState.getTabStateFilename(tabId, mIsIncognito); + File tabStateFile = new File(getStateDirectory(), filename); + FileOutputStream outputStream = null; + try { + outputStream = new FileOutputStream(tabStateFile); + outputStream.write(Base64.decode(encodedState, 0)); + } catch (FileNotFoundException e) { + assert false : "Failed to create " + filename; + return false; + } catch (IOException e) { + assert false : "IO exception " + filename; + return false; + } finally { + StreamUtil.closeQuietly(outputStream); + } + + return true; + } + + /** + * Ensures that the state directory and its contents are all wiped from storage. + */ + public void ensureDirectoryDestroyed() { + File states = getStateDirectory(); + if (!states.exists()) return; + + File[] files = states.listFiles(); + if (files != null) { + for (File file : files) { + if (!file.delete()) Log.e(TAG, "Failed to delete: " + file.getName()); + } + } + if (!states.delete()) Log.e(TAG, "Failed to delete: " + states.getName()); + } +} diff --git a/chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/tabmodel/document/MockTabDelegate.java b/chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/tabmodel/document/MockTabDelegate.java new file mode 100644 index 000000000000..d9bdaf31dc20 --- /dev/null +++ b/chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/tabmodel/document/MockTabDelegate.java @@ -0,0 +1,36 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chrome.test.util.browser.tabmodel.document; + +import android.app.Activity; + +import org.chromium.chrome.browser.Tab; +import org.chromium.chrome.browser.tabmodel.document.ActivityDelegate; +import org.chromium.chrome.browser.tabmodel.document.DocumentTabModel.Entry; +import org.chromium.chrome.browser.tabmodel.document.TabDelegate; + +/** + * Mocks out calls to get Tabs for the DocumentTabModel. + */ +public class MockTabDelegate implements TabDelegate { + @Override + public Tab getActivityTab(boolean incognito, ActivityDelegate delgate, Activity activity) { + return null; + } + + @Override + public Tab createFrozenTab(Entry entry) { + return null; + } + + @Override + public void createTabWithNativeContents(boolean isIncognito, long webContentsPtr, + int parentTabId) { + } + + @Override + public void createTabForDevTools(String url) { + } +} \ No newline at end of file diff --git a/chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/tabmodel/document/OWNERS b/chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/tabmodel/document/OWNERS new file mode 100644 index 000000000000..79becd27e526 --- /dev/null +++ b/chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/tabmodel/document/OWNERS @@ -0,0 +1 @@ +dfalcantara@chromium.org diff --git a/chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/tabmodel/document/TestInitializationObserver.java b/chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/tabmodel/document/TestInitializationObserver.java new file mode 100644 index 000000000000..7f20edccb82d --- /dev/null +++ b/chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/tabmodel/document/TestInitializationObserver.java @@ -0,0 +1,62 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chrome.test.util.browser.tabmodel.document; + +import static junit.framework.Assert.assertTrue; + +import org.chromium.base.ThreadUtils; +import org.chromium.chrome.browser.tabmodel.document.DocumentTabModel; +import org.chromium.content.browser.test.util.Criteria; +import org.chromium.content.browser.test.util.CriteriaHelper; + +/** + * Monitors the DocumentTabModel until it has progressed far enough along initialization. + */ +public class TestInitializationObserver extends DocumentTabModel.InitializationObserver { + public boolean mIsReady; + private final int mState; + + public TestInitializationObserver(DocumentTabModel model, int state) { + super(model); + mState = state; + } + + @Override + protected void runImmediately() { + mIsReady = true; + } + + @Override + public boolean isSatisfied(int currentState) { + return currentState >= mState; + } + + @Override + public boolean isCanceled() { + return false; + } + + /** + * Wait until the DocumentTabModel has reached the given state. + * @param model DocumentTabModel to monitor. + * @param state State to wait for. + */ + public static void waitUntilState(final DocumentTabModel model, int state) throws Exception { + final TestInitializationObserver observer = new TestInitializationObserver(model, state); + ThreadUtils.runOnUiThread(new Runnable() { + @Override + public void run() { + observer.runWhenReady(); + } + }); + + assertTrue(CriteriaHelper.pollForCriteria(new Criteria() { + @Override + public boolean isSatisfied() { + return observer.mIsReady; + } + })); + } +} \ No newline at end of file -- 2.11.4.GIT