// Copyright 2015 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.bookmarks; import android.app.Activity; import android.app.ActivityManager; import android.content.Context; import android.support.v4.widget.DrawerLayout; import android.text.TextUtils; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.ViewSwitcher; import org.chromium.base.ContextUtils; import org.chromium.base.ObserverList; import org.chromium.base.metrics.RecordUserAction; import org.chromium.chrome.R; import org.chromium.chrome.browser.BasicNativePage; import org.chromium.chrome.browser.bookmarks.BookmarkBridge.BookmarkItem; import org.chromium.chrome.browser.bookmarks.BookmarkBridge.BookmarkModelObserver; import org.chromium.chrome.browser.favicon.LargeIconBridge; import org.chromium.chrome.browser.partnerbookmarks.PartnerBookmarksShim; import org.chromium.chrome.browser.profiles.Profile; import org.chromium.chrome.browser.snackbar.SnackbarManager.SnackbarManageable; import org.chromium.chrome.browser.widget.selection.SelectionDelegate; import org.chromium.components.bookmarks.BookmarkId; import java.util.Stack; /** * The new bookmark manager that is planned to replace the existing bookmark manager. It holds all * views and shared logics between tablet and phone. For tablet/phone specific logics, see * {@link BookmarkActivity} (phone) and {@link BookmarkPage} (tablet). */ public class BookmarkManager implements BookmarkDelegate { private static final int FAVICON_MAX_CACHE_SIZE_BYTES = 10 * 1024 * 1024; // 10MB private Activity mActivity; private ViewGroup mMainView; private BookmarkModel mBookmarkModel; private BookmarkUndoController mUndoController; private final ObserverList<BookmarkUIObserver> mUIObservers = new ObserverList<BookmarkUIObserver>(); private BasicNativePage mNativePage; private BookmarkContentView mContentView; private BookmarkSearchView mSearchView; private ViewSwitcher mViewSwitcher; private DrawerLayout mDrawer; private BookmarkDrawerListView mDrawerListView; private SelectionDelegate<BookmarkId> mSelectionDelegate; private final Stack<BookmarkUIState> mStateStack = new Stack<>(); private LargeIconBridge mLargeIconBridge; private String mInitialUrl; private boolean mIsDialogUi; private final BookmarkModelObserver mBookmarkModelObserver = new BookmarkModelObserver() { @Override public void bookmarkNodeRemoved(BookmarkItem parent, int oldIndex, BookmarkItem node, boolean isDoingExtensiveChanges) { // If the folder is removed in folder mode, show the parent folder or falls back to all // bookmarks mode. if (getCurrentState() == BookmarkUIState.STATE_FOLDER && node.getId().equals(mStateStack.peek().mFolder)) { if (mBookmarkModel.getTopLevelFolderIDs(true, true).contains( node.getId())) { openFolder(mBookmarkModel.getDefaultFolder()); } else { openFolder(parent.getId()); } } mSelectionDelegate.clearSelection(); } @Override public void bookmarkNodeMoved(BookmarkItem oldParent, int oldIndex, BookmarkItem newParent, int newIndex) { mSelectionDelegate.clearSelection(); } @Override public void bookmarkModelChanged() { // If the folder no longer exists in folder mode, we need to fall back. Relying on the // default behavior by setting the folder mode again. if (getCurrentState() == BookmarkUIState.STATE_FOLDER) { setState(mStateStack.peek()); } mSelectionDelegate.clearSelection(); } }; private final Runnable mModelLoadedRunnable = new Runnable() { @Override public void run() { mSearchView.onBookmarkDelegateInitialized(BookmarkManager.this); mDrawerListView.onBookmarkDelegateInitialized(BookmarkManager.this); mContentView.onBookmarkDelegateInitialized(BookmarkManager.this); if (!TextUtils.isEmpty(mInitialUrl)) { setState(BookmarkUIState.createStateFromUrl(mInitialUrl, mBookmarkModel)); } } }; /** * Creates an instance of {@link BookmarkManager}. It also initializes resources, * bookmark models and jni bridges. * @param activity The activity context to use. * @param isDialogUi Whether the main bookmarks UI will be shown in a dialog, not a NativePage. */ public BookmarkManager(Activity activity, boolean isDialogUi) { mActivity = activity; mIsDialogUi = isDialogUi; mSelectionDelegate = new SelectionDelegate<BookmarkId>() { @Override public boolean toggleSelectionForItem(BookmarkId bookmark) { if (!mBookmarkModel.getBookmarkById(bookmark).isEditable()) return false; return super.toggleSelectionForItem(bookmark); } }; mBookmarkModel = new BookmarkModel(); mMainView = (ViewGroup) mActivity.getLayoutInflater().inflate(R.layout.bookmark_main, null); mDrawer = (DrawerLayout) mMainView.findViewById(R.id.bookmark_drawer_layout); mDrawerListView = (BookmarkDrawerListView) mMainView.findViewById( R.id.bookmark_drawer_list); mContentView = (BookmarkContentView) mMainView.findViewById(R.id.bookmark_content_view); mViewSwitcher = (ViewSwitcher) mMainView.findViewById(R.id.bookmark_view_switcher); mUndoController = new BookmarkUndoController(activity, mBookmarkModel, ((SnackbarManageable) activity).getSnackbarManager()); mSearchView = (BookmarkSearchView) getView().findViewById(R.id.bookmark_search_view); mBookmarkModel.addObserver(mBookmarkModelObserver); initializeToLoadingState(); mBookmarkModel.runAfterBookmarkModelLoaded(mModelLoadedRunnable); // Load partner bookmarks explicitly. We load partner bookmarks in the deferred startup // code, but that might be executed much later. Especially on L, showing loading // progress bar blocks that so it won't be loaded. http://crbug.com/429383 PartnerBookmarksShim.kickOffReading(activity); mLargeIconBridge = new LargeIconBridge(Profile.getLastUsedProfile().getOriginalProfile()); ActivityManager activityManager = ((ActivityManager) ContextUtils .getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE)); int maxSize = Math.min(activityManager.getMemoryClass() / 4 * 1024 * 1024, FAVICON_MAX_CACHE_SIZE_BYTES); mLargeIconBridge.createCache(maxSize); RecordUserAction.record("MobileBookmarkManagerOpen"); if (!isDialogUi) { RecordUserAction.record("MobileBookmarkManagerPageOpen"); } } /** * Destroys and cleans up itself. This must be called after done using this class. */ public void destroy() { for (BookmarkUIObserver observer : mUIObservers) { observer.onDestroy(); } assert mUIObservers.size() == 0; if (mUndoController != null) { mUndoController.destroy(); mUndoController = null; } mBookmarkModel.removeObserver(mBookmarkModelObserver); mBookmarkModel.destroy(); mBookmarkModel = null; mLargeIconBridge.destroy(); mLargeIconBridge = null; } /** * Called when the user presses the back key. This is only going to be called on Phone. * @return True if manager handles this event, false if it decides to ignore. */ public boolean onBackPressed() { if (doesDrawerExist()) { if (mDrawer.isDrawerVisible(Gravity.START)) { mDrawer.closeDrawer(Gravity.START); return true; } } if (mContentView.onBackPressed()) return true; if (!mStateStack.empty()) { mStateStack.pop(); if (!mStateStack.empty()) { setState(mStateStack.pop()); return true; } } return false; } public View getView() { return mMainView; } /** * Sets the listener that reacts upon the change of the UI state of bookmark manager. */ public void setBasicNativePage(BasicNativePage nativePage) { mNativePage = nativePage; } /** * @return Current URL representing the UI state of bookmark manager. If no state has been shown * yet in this session, on phone return last used state stored in preference; on tablet * return the url previously set by {@link #updateForUrl(String)}. */ public String getCurrentUrl() { if (mStateStack.isEmpty()) return null; return mStateStack.peek().mUrl; } /** * Updates UI based on the new URL. If the bookmark model is not loaded yet, cache the url and * it will be picked up later when the model is loaded. This method is supposed to align with * {@link BookmarkPage#updateForUrl(String)} * <p> * @param url The url to navigate to. */ public void updateForUrl(String url) { // Bookmark model is null if the manager has been destroyed. if (mBookmarkModel == null) return; if (mBookmarkModel.isBookmarkModelLoaded()) { setState(BookmarkUIState.createStateFromUrl(url, mBookmarkModel)); } else { mInitialUrl = url; } } /** * Puts all UI elements to loading state. This state might be overridden synchronously by * {@link #updateForUrl(String)}, if the bookmark model is already loaded. */ private void initializeToLoadingState() { mContentView.showLoadingUi(); mDrawerListView.showLoadingUi(); mContentView.showLoadingUi(); assert mStateStack.isEmpty(); setState(BookmarkUIState.createLoadingState()); } /** * This is the ultimate internal method that updates UI and controls backstack. And it is the * only method that pushes states to {@link #mStateStack}. * <p> * If the given state is not valid, all_bookmark state will be shown. Afterwards, this method * checks the current state: if currently in loading state, it pops it out and adds the new * state to the back stack. It also notifies the {@link #mNativePage} (if any) that the * url has changed. * <p> * Also note that even if we store states to {@link #mStateStack}, on tablet the back navigation * and back button are not controlled by the manager: the tab handles back key and backstack * navigation. */ private void setState(BookmarkUIState state) { if (!state.isValid(mBookmarkModel)) { state = BookmarkUIState.createFolderState(mBookmarkModel.getDefaultFolder(), mBookmarkModel); } if (!mStateStack.isEmpty() && mStateStack.peek().equals(state)) return; // The loading state is not persisted in history stack and once we have a valid state it // shall be removed. if (!mStateStack.isEmpty() && mStateStack.peek().mState == BookmarkUIState.STATE_LOADING) { mStateStack.pop(); } mStateStack.push(state); if (state.mState != BookmarkUIState.STATE_LOADING) { // Loading state may be pushed to the stack but should never be stored in preferences. BookmarkUtils.setLastUsedUrl(mActivity, state.mUrl); // If a loading state is replaced by another loading state, do not notify this change. if (mNativePage != null) { mNativePage.onStateChange(state.mUrl); } } mSelectionDelegate.clearSelection(); for (BookmarkUIObserver observer : mUIObservers) { notifyStateChange(observer); } } // BookmarkDelegate implementations. @Override public boolean isDialogUi() { return mIsDialogUi; } @Override public void openFolder(BookmarkId folder) { closeSearchUI(); setState(BookmarkUIState.createFolderState(folder, mBookmarkModel)); } @Override public SelectionDelegate<BookmarkId> getSelectionDelegate() { return mSelectionDelegate; } @Override public void notifyStateChange(BookmarkUIObserver observer) { int state = getCurrentState(); switch (state) { case BookmarkUIState.STATE_FOLDER: observer.onFolderStateSet(mStateStack.peek().mFolder); break; case BookmarkUIState.STATE_LOADING: // In loading state, onBookmarkDelegateInitialized() is not called for all // UIObservers, which means that there will be no observers at the time. Do nothing. assert mUIObservers.isEmpty(); break; default: assert false : "State not valid"; break; } } @Override public boolean doesDrawerExist() { return mDrawer != null; } @Override public void closeDrawer() { if (!doesDrawerExist()) return; mDrawer.closeDrawer(Gravity.START); } @Override public DrawerLayout getDrawerLayout() { return mDrawer; } @Override public void openBookmark(BookmarkId bookmark, int launchLocation) { mSelectionDelegate.clearSelection(); if (BookmarkUtils.openBookmark( mBookmarkModel, mActivity, bookmark, launchLocation)) { BookmarkUtils.finishActivityOnPhone(mActivity); } } @Override public void openSearchUI() { // Give search view focus, because it needs to handle back key event. mViewSwitcher.showNext(); } @Override public void closeSearchUI() { if (mSearchView.getVisibility() != View.VISIBLE) return; mViewSwitcher.showPrevious(); } @Override public void addUIObserver(BookmarkUIObserver observer) { mUIObservers.addObserver(observer); } @Override public void removeUIObserver(BookmarkUIObserver observer) { mUIObservers.removeObserver(observer); } @Override public BookmarkModel getModel() { return mBookmarkModel; } @Override public int getCurrentState() { if (mStateStack.isEmpty()) return BookmarkUIState.STATE_LOADING; return mStateStack.peek().mState; } @Override public LargeIconBridge getLargeIconBridge() { return mLargeIconBridge; } }