// 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.ntp; import android.app.Activity; import android.graphics.Canvas; import android.graphics.Color; import android.os.SystemClock; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ExpandableListView; import org.chromium.base.ActivityState; import org.chromium.base.ApiCompatibilityUtils; import org.chromium.base.ApplicationStatus; import org.chromium.base.metrics.RecordHistogram; import org.chromium.chrome.R; import org.chromium.chrome.browser.NativePage; import org.chromium.chrome.browser.UrlConstants; import org.chromium.chrome.browser.compositor.layouts.content.InvalidationAwareThumbnailProvider; import org.chromium.chrome.browser.metrics.StartupMetrics; import org.chromium.chrome.browser.util.ViewUtils; import java.util.concurrent.TimeUnit; /** * The native recent tabs page. Lists recently closed tabs, open windows and tabs from the user's * synced devices, and snapshot documents sent from Chrome to Mobile in an expandable list view. */ public class RecentTabsPage implements NativePage, ApplicationStatus.ActivityStateListener, ExpandableListView.OnChildClickListener, ExpandableListView.OnGroupCollapseListener, ExpandableListView.OnGroupExpandListener, RecentTabsManager.UpdatedCallback, View.OnAttachStateChangeListener, View.OnCreateContextMenuListener, InvalidationAwareThumbnailProvider { private final Activity mActivity; private final ExpandableListView mListView; private final String mTitle; private final ViewGroup mView; private RecentTabsManager mRecentTabsManager; private RecentTabsRowAdapter mAdapter; private boolean mSnapshotContentChanged; private int mSnapshotListPosition; private int mSnapshotListTop; private int mSnapshotWidth; private int mSnapshotHeight; private final int mThemeColor; /** * Whether the page is in the foreground and is visible. */ private boolean mInForeground; /** * Whether {@link #mView} is attached to the application window. */ private boolean mIsAttachedToWindow; /** * The time, whichever is most recent, that the page: * - Moved to the foreground * - Became visible */ private long mForegroundTimeMs; /** * Constructor returns an instance of RecentTabsPage. * * @param activity The activity this view belongs to. * @param recentTabsManager The RecentTabsManager which provides the model data. */ public RecentTabsPage(Activity activity, RecentTabsManager recentTabsManager) { mActivity = activity; mRecentTabsManager = recentTabsManager; mTitle = activity.getResources().getString(R.string.recent_tabs); mThemeColor = ApiCompatibilityUtils.getColor( activity.getResources(), R.color.default_primary_color); mRecentTabsManager.setUpdatedCallback(this); LayoutInflater inflater = LayoutInflater.from(activity); mView = (ViewGroup) inflater.inflate(R.layout.recent_tabs_page, null); mListView = (ExpandableListView) mView.findViewById(R.id.odp_listview); mAdapter = buildAdapter(activity, recentTabsManager); mListView.setAdapter(mAdapter); mListView.setOnChildClickListener(this); mListView.setGroupIndicator(null); mListView.setOnGroupCollapseListener(this); mListView.setOnGroupExpandListener(this); mListView.setOnCreateContextMenuListener(this); mView.addOnAttachStateChangeListener(this); ApplicationStatus.registerStateListenerForActivity(this, activity); // {@link #mInForeground} will be updated once the view is attached to the window. onUpdated(); } private static RecentTabsRowAdapter buildAdapter(Activity activity, RecentTabsManager recentTabsManager) { return new RecentTabsRowAdapter(activity, recentTabsManager); } /** * Updates whether the page is in the foreground based on whether the application is in the * foreground and whether {@link #mView} is attached to the application window. If the page is * no longer in the foreground, records the time that the page spent in the foreground to UMA. */ private void updateForegroundState() { boolean inForeground = mIsAttachedToWindow && ApplicationStatus.getStateForActivity(mActivity) == ActivityState.RESUMED; if (mInForeground == inForeground) { return; } mInForeground = inForeground; if (mInForeground) { mForegroundTimeMs = SystemClock.elapsedRealtime(); StartupMetrics.getInstance().recordOpenedRecents(); } else { RecordHistogram.recordLongTimesHistogram("NewTabPage.RecentTabsPage.TimeVisibleAndroid", SystemClock.elapsedRealtime() - mForegroundTimeMs, TimeUnit.MILLISECONDS); } } // NativePage overrides @Override public String getUrl() { return UrlConstants.RECENT_TABS_URL; } @Override public String getTitle() { return mTitle; } @Override public int getBackgroundColor() { return Color.WHITE; } @Override public int getThemeColor() { return mThemeColor; } @Override public boolean needsToolbarShadow() { return true; } @Override public View getView() { return mView; } @Override public String getHost() { return UrlConstants.RECENT_TABS_HOST; } @Override public void destroy() { assert getView().getParent() == null : "Destroy called before removed from window"; mRecentTabsManager.destroy(); mRecentTabsManager = null; mAdapter.notifyDataSetInvalidated(); mAdapter = null; mListView.setAdapter((RecentTabsRowAdapter) null); mView.removeOnAttachStateChangeListener(this); ApplicationStatus.unregisterActivityStateListener(this); } @Override public void updateForUrl(String url) { } // ApplicationStatus.ActivityStateListener @Override public void onActivityStateChange(Activity activity, int state) { // Called when the user locks the screen or moves Chrome to the background via the task // switcher. updateForegroundState(); } // View.OnAttachStateChangeListener @Override public void onViewAttachedToWindow(View view) { // Called when the user opens the RecentTabsPage or switches back to the RecentTabsPage from // another tab. mIsAttachedToWindow = true; updateForegroundState(); // Work around a bug on Samsung devices where the recent tabs page does not appear after // toggling the Sync quick setting. For some reason, the layout is being dropped on the // flow and we need to force a root level layout to get the UI to appear. view.getRootView().requestLayout(); } @Override public void onViewDetachedFromWindow(View view) { // Called when the user navigates from the RecentTabsPage or switches to another tab. mIsAttachedToWindow = false; updateForegroundState(); } // ExpandableListView.OnChildClickedListener @Override public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) { return mAdapter.getGroup(groupPosition).onChildClick(childPosition); } // ExpandableListView.OnGroupExpandedListener @Override public void onGroupExpand(int groupPosition) { mAdapter.getGroup(groupPosition).setCollapsed(false); mSnapshotContentChanged = true; } // ExpandableListView.OnGroupCollapsedListener @Override public void onGroupCollapse(int groupPosition) { mAdapter.getGroup(groupPosition).setCollapsed(true); mSnapshotContentChanged = true; } // RecentTabsManager.UpdatedCallback @Override public void onUpdated() { mAdapter.notifyDataSetChanged(); for (int i = 0; i < mAdapter.getGroupCount(); i++) { if (mAdapter.getGroup(i).isCollapsed()) { mListView.collapseGroup(i); } else { mListView.expandGroup(i); } } mSnapshotContentChanged = true; } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { // Would prefer to have this context menu view managed internal to RecentTabsGroupView // Unfortunately, setting either onCreateContextMenuListener or onLongClickListener // disables the native onClick (expand/collapse) behaviour of the group view. ExpandableListView.ExpandableListContextMenuInfo info = (ExpandableListView.ExpandableListContextMenuInfo) menuInfo; int type = ExpandableListView.getPackedPositionType(info.packedPosition); int groupPosition = ExpandableListView.getPackedPositionGroup(info.packedPosition); if (type == ExpandableListView.PACKED_POSITION_TYPE_GROUP) { mAdapter.getGroup(groupPosition).onCreateContextMenuForGroup(menu, mActivity); } else if (type == ExpandableListView.PACKED_POSITION_TYPE_CHILD) { int childPosition = ExpandableListView.getPackedPositionChild(info.packedPosition); mAdapter.getGroup(groupPosition).onCreateContextMenuForChild(childPosition, menu, mActivity); } } // InvalidationAwareThumbnailProvider @Override public boolean shouldCaptureThumbnail() { if (mView.getWidth() == 0 || mView.getHeight() == 0) return false; View topItem = mListView.getChildAt(0); return mSnapshotContentChanged || mSnapshotListPosition != mListView.getFirstVisiblePosition() || mSnapshotListTop != (topItem == null ? 0 : topItem.getTop()) || mView.getWidth() != mSnapshotWidth || mView.getHeight() != mSnapshotHeight; } @Override public void captureThumbnail(Canvas canvas) { ViewUtils.captureBitmap(mView, canvas); mSnapshotContentChanged = false; mSnapshotListPosition = mListView.getFirstVisiblePosition(); View topItem = mListView.getChildAt(0); mSnapshotListTop = topItem == null ? 0 : topItem.getTop(); mSnapshotWidth = mView.getWidth(); mSnapshotHeight = mView.getHeight(); } }