| // Copyright 2019 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.tasks.tab_management; |
| |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.graphics.Bitmap; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.graphics.Paint; |
| import android.graphics.PorterDuff; |
| import android.graphics.PorterDuffXfermode; |
| import android.graphics.Rect; |
| import android.graphics.RectF; |
| import android.graphics.drawable.Drawable; |
| |
| import org.chromium.base.ApiCompatibilityUtils; |
| import org.chromium.base.Callback; |
| import org.chromium.base.MathUtils; |
| import org.chromium.base.task.PostTask; |
| import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager; |
| import org.chromium.chrome.browser.profiles.Profile; |
| import org.chromium.chrome.browser.tabmodel.TabModel; |
| import org.chromium.chrome.browser.tabmodel.TabModelSelector; |
| import org.chromium.chrome.browser.tabmodel.TabModelSelectorObserver; |
| import org.chromium.chrome.browser.tasks.pseudotab.PseudoTab; |
| import org.chromium.chrome.tab_ui.R; |
| import org.chromium.content_public.browser.UiThreadTaskTraits; |
| import org.chromium.url.GURL; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.concurrent.atomic.AtomicInteger; |
| import java.util.concurrent.atomic.AtomicReference; |
| |
| /** |
| * A {@link TabListMediator.ThumbnailProvider} that will create a single Bitmap Thumbnail for all |
| * the related tabs for the given tabs. |
| */ |
| public class MultiThumbnailCardProvider implements TabListMediator.ThumbnailProvider { |
| private final TabContentManager mTabContentManager; |
| private final TabModelSelector mTabModelSelector; |
| private final TabModelSelectorObserver mTabModelSelectorObserver; |
| |
| private final float mRadius; |
| private final float mFaviconFrameCornerRadius; |
| private final int mThumbnailWidth; |
| private final int mThumbnailHeight; |
| private final Paint mEmptyThumbnailPaint; |
| private final Paint mThumbnailFramePaint; |
| private final Paint mThumbnailBasePaint; |
| private final Paint mTextPaint; |
| private final Paint mFaviconBackgroundPaint; |
| private final Paint mSelectedEmptyThumbnailPaint; |
| private final Paint mSelectedTextPaint; |
| private final int mFaviconBackgroundPaintColor; |
| private final List<Rect> mFaviconRects = new ArrayList<>(4); |
| private final List<RectF> mThumbnailRects = new ArrayList<>(4); |
| private final List<RectF> mFaviconBackgroundRects = new ArrayList<>(4); |
| private TabListFaviconProvider mTabListFaviconProvider; |
| private Context mContext; |
| |
| private class MultiThumbnailFetcher { |
| private final PseudoTab mInitialTab; |
| private final Callback<Bitmap> mFinalCallback; |
| private final boolean mForceUpdate; |
| private final boolean mWriteToCache; |
| private final boolean mIsTabSelected; |
| private final List<PseudoTab> mTabs = new ArrayList<>(4); |
| private final AtomicInteger mThumbnailsToFetch = new AtomicInteger(); |
| |
| private Canvas mCanvas; |
| private Bitmap mMultiThumbnailBitmap; |
| private String mText; |
| |
| /** |
| * Fetcher that get the thumbnail drawable depending on if the tab is selected. |
| * @see TabContentManager#getTabThumbnailWithCallback |
| * @param isTabSelected Whether the thumbnail is for a currently selected tab. |
| */ |
| MultiThumbnailFetcher(PseudoTab initialTab, Callback<Bitmap> finalCallback, |
| boolean forceUpdate, boolean writeToCache, boolean isTabSelected) { |
| mFinalCallback = finalCallback; |
| mInitialTab = initialTab; |
| mForceUpdate = forceUpdate; |
| mWriteToCache = writeToCache; |
| mIsTabSelected = isTabSelected; |
| } |
| |
| private void initializeAndStartFetching(PseudoTab tab) { |
| // Initialize mMultiThumbnailBitmap. |
| int width = mThumbnailWidth; |
| int height = mThumbnailHeight; |
| mMultiThumbnailBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); |
| mCanvas = new Canvas(mMultiThumbnailBitmap); |
| mCanvas.drawColor(Color.TRANSPARENT); |
| |
| // Initialize Tabs. |
| List<PseudoTab> relatedTabList = |
| PseudoTab.getRelatedTabs(mContext, tab, mTabModelSelector); |
| if (relatedTabList.size() <= 4) { |
| mThumbnailsToFetch.set(relatedTabList.size()); |
| |
| mTabs.add(tab); |
| relatedTabList.remove(tab); |
| |
| for (int i = 0; i < 3; i++) { |
| mTabs.add(i < relatedTabList.size() ? relatedTabList.get(i) : null); |
| } |
| } else { |
| mText = "+" + (relatedTabList.size() - 3); |
| mThumbnailsToFetch.set(3); |
| |
| mTabs.add(tab); |
| relatedTabList.remove(tab); |
| |
| mTabs.add(relatedTabList.get(0)); |
| mTabs.add(relatedTabList.get(1)); |
| mTabs.add(null); |
| } |
| |
| // Fetch and draw all. |
| for (int i = 0; i < 4; i++) { |
| if (mTabs.get(i) != null) { |
| final int index = i; |
| final GURL url = mTabs.get(i).getUrl(); |
| final boolean isIncognito = mTabs.get(i).isIncognito(); |
| // getTabThumbnailWithCallback() might call the callback up to twice, |
| // so use |lastFavicon| to avoid fetching the favicon the second time. |
| // Fetching the favicon after getting the live thumbnail would lead to |
| // visible flicker. |
| final AtomicReference<Drawable> lastFavicon = new AtomicReference<>(); |
| mTabContentManager.getTabThumbnailWithCallback( |
| mTabs.get(i).getId(), thumbnail -> { |
| drawThumbnailBitmapOnCanvasWithFrame(thumbnail, index); |
| if (lastFavicon.get() != null) { |
| drawFaviconThenMaybeSendBack(lastFavicon.get(), index); |
| } else { |
| mTabListFaviconProvider.getFaviconForUrlAsync( |
| url, isIncognito, (Drawable favicon) -> { |
| lastFavicon.set(favicon); |
| drawFaviconThenMaybeSendBack(favicon, index); |
| }); |
| } |
| }, mForceUpdate && i == 0, mWriteToCache && i == 0); |
| } else { |
| drawThumbnailBitmapOnCanvasWithFrame(null, i); |
| if (mText != null && i == 3) { |
| // Draw the text exactly centered on the thumbnail rect. |
| Paint textPaint = mIsTabSelected ? mSelectedTextPaint : mTextPaint; |
| mCanvas.drawText(mText, |
| (mThumbnailRects.get(i).left + mThumbnailRects.get(i).right) / 2, |
| (mThumbnailRects.get(i).top + mThumbnailRects.get(i).bottom) / 2 |
| - ((mTextPaint.descent() + mTextPaint.ascent()) / 2), |
| textPaint); |
| } |
| } |
| } |
| } |
| |
| private void drawThumbnailBitmapOnCanvasWithFrame(Bitmap thumbnail, int index) { |
| if (thumbnail == null) { |
| Paint emptyThumbnailPaint = |
| mIsTabSelected ? mSelectedEmptyThumbnailPaint : mEmptyThumbnailPaint; |
| mCanvas.drawRoundRect( |
| mThumbnailRects.get(index), mRadius, mRadius, emptyThumbnailPaint); |
| return; |
| } |
| |
| // Draw the base paint first and set the base for thumbnail to draw. Setting the xfer |
| // mode as SRC_OVER so the thumbnail can be drawn on top of this paint. See |
| // https://crbug.com/1227619. |
| mThumbnailBasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)); |
| mCanvas.drawRoundRect( |
| mThumbnailRects.get(index), mRadius, mRadius, mThumbnailBasePaint); |
| |
| thumbnail = |
| Bitmap.createScaledBitmap(thumbnail, (int) mThumbnailRects.get(index).width(), |
| (int) mThumbnailRects.get(index).height(), true); |
| mThumbnailBasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); |
| mCanvas.drawBitmap(thumbnail, |
| new Rect(0, 0, thumbnail.getWidth(), thumbnail.getHeight()), |
| mThumbnailRects.get(index), mThumbnailBasePaint); |
| thumbnail.recycle(); |
| |
| mCanvas.drawRoundRect( |
| mThumbnailRects.get(index), mRadius, mRadius, mThumbnailFramePaint); |
| } |
| |
| private void drawFaviconDrawableOnCanvasWithFrame(Drawable favicon, int index) { |
| mCanvas.drawRoundRect(mFaviconBackgroundRects.get(index), mFaviconFrameCornerRadius, |
| mFaviconFrameCornerRadius, mFaviconBackgroundPaint); |
| favicon.setBounds(mFaviconRects.get(index)); |
| favicon.draw(mCanvas); |
| } |
| |
| private void drawFaviconThenMaybeSendBack(Drawable favicon, int index) { |
| drawFaviconDrawableOnCanvasWithFrame(favicon, index); |
| if (mThumbnailsToFetch.decrementAndGet() == 0) { |
| PostTask.postTask(UiThreadTaskTraits.USER_VISIBLE, |
| mFinalCallback.bind(mMultiThumbnailBitmap)); |
| } |
| } |
| |
| private void fetch() { |
| initializeAndStartFetching(mInitialTab); |
| } |
| } |
| |
| MultiThumbnailCardProvider(Context context, TabContentManager tabContentManager, |
| TabModelSelector tabModelSelector) { |
| mContext = context; |
| Resources resource = context.getResources(); |
| float expectedThumbnailAspectRatio = |
| (float) TabUiFeatureUtilities.THUMBNAIL_ASPECT_RATIO.getValue(); |
| expectedThumbnailAspectRatio = MathUtils.clamp(expectedThumbnailAspectRatio, 0.5f, 2.0f); |
| |
| mThumbnailWidth = (int) resource.getDimension(R.dimen.tab_grid_thumbnail_card_default_size); |
| mThumbnailHeight = (int) (mThumbnailWidth / expectedThumbnailAspectRatio); |
| |
| mTabContentManager = tabContentManager; |
| mTabModelSelector = tabModelSelector; |
| mRadius = resource.getDimension(R.dimen.tab_list_mini_card_radius); |
| mFaviconFrameCornerRadius = |
| resource.getDimension(R.dimen.tab_grid_thumbnail_favicon_frame_corner_radius); |
| |
| mTabListFaviconProvider = new TabListFaviconProvider(context, false); |
| |
| // Initialize Paints to use. |
| mEmptyThumbnailPaint = new Paint(); |
| mEmptyThumbnailPaint.setStyle(Paint.Style.FILL); |
| mEmptyThumbnailPaint.setAntiAlias(true); |
| mEmptyThumbnailPaint.setColor( |
| TabUiThemeProvider.getMiniThumbnailPlaceHolderColor(context, false, false)); |
| |
| mSelectedEmptyThumbnailPaint = new Paint(mEmptyThumbnailPaint); |
| mSelectedEmptyThumbnailPaint.setColor( |
| TabUiThemeProvider.getMiniThumbnailPlaceHolderColor(context, false, true)); |
| |
| // Paint used to set base for thumbnails, in case mEmptyThumbnailPaint has transparency. |
| mThumbnailBasePaint = new Paint(mEmptyThumbnailPaint); |
| mThumbnailBasePaint.setColor(Color.BLACK); |
| mThumbnailBasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); |
| |
| mThumbnailFramePaint = new Paint(); |
| mThumbnailFramePaint.setStyle(Paint.Style.STROKE); |
| mThumbnailFramePaint.setStrokeWidth( |
| resource.getDimension(R.dimen.tab_list_mini_card_frame_size)); |
| mThumbnailFramePaint.setColor( |
| ApiCompatibilityUtils.getColor(resource, R.color.divider_line_bg_color)); |
| mThumbnailFramePaint.setAntiAlias(true); |
| |
| // TODO(996048): Use pre-defined styles to avoid style out of sync if any text/color styles |
| // changes. |
| mTextPaint = new Paint(); |
| mTextPaint.setTextSize(resource.getDimension(R.dimen.compositor_tab_title_text_size)); |
| mTextPaint.setFakeBoldText(true); |
| mTextPaint.setAntiAlias(true); |
| mTextPaint.setTextAlign(Paint.Align.CENTER); |
| mTextPaint.setColor(TabUiThemeProvider.getTabGroupNumberTextColor(context, false, false)); |
| |
| mSelectedTextPaint = new Paint(mTextPaint); |
| mSelectedTextPaint.setColor( |
| TabUiThemeProvider.getTabGroupNumberTextColor(context, false, true)); |
| |
| mFaviconBackgroundPaintColor = |
| ApiCompatibilityUtils.getColor(resource, R.color.favicon_background_color); |
| mFaviconBackgroundPaint = new Paint(); |
| mFaviconBackgroundPaint.setAntiAlias(true); |
| mFaviconBackgroundPaint.setColor(mFaviconBackgroundPaintColor); |
| mFaviconBackgroundPaint.setStyle(Paint.Style.FILL); |
| mFaviconBackgroundPaint.setShadowLayer( |
| resource.getDimension(R.dimen.tab_grid_thumbnail_favicon_background_radius), 0, |
| resource.getDimension(R.dimen.tab_grid_thumbnail_favicon_background_down_shift), |
| resource.getColor(R.color.modern_grey_800_alpha_38)); |
| |
| // Initialize Rects for thumbnails. |
| float thumbnailHorizontalPadding = resource.getDimension(R.dimen.tab_list_card_padding); |
| float thumbnailVerticalPadding = thumbnailHorizontalPadding / expectedThumbnailAspectRatio; |
| float thumbnailFaviconPaddingFromBackground = |
| resource.getDimension(R.dimen.tab_grid_thumbnail_favicon_padding_from_frame); |
| float centerX = mThumbnailWidth * 0.5f; |
| float centerY = mThumbnailHeight * 0.5f; |
| float halfThumbnailHorizontalPadding = thumbnailHorizontalPadding / 2; |
| float halfThumbnailVerticalPadding = thumbnailVerticalPadding / 2; |
| |
| mThumbnailRects.add(new RectF(thumbnailHorizontalPadding, thumbnailVerticalPadding, |
| centerX - halfThumbnailHorizontalPadding, centerY - halfThumbnailVerticalPadding)); |
| mThumbnailRects.add(new RectF(centerX + halfThumbnailHorizontalPadding, |
| thumbnailVerticalPadding, mThumbnailWidth - thumbnailHorizontalPadding, |
| centerY - halfThumbnailVerticalPadding)); |
| mThumbnailRects.add(new RectF(thumbnailHorizontalPadding, |
| centerY + halfThumbnailVerticalPadding, centerX - halfThumbnailHorizontalPadding, |
| mThumbnailHeight - thumbnailVerticalPadding)); |
| mThumbnailRects.add(new RectF(centerX + halfThumbnailHorizontalPadding, |
| centerY + halfThumbnailVerticalPadding, |
| mThumbnailWidth - thumbnailHorizontalPadding, |
| mThumbnailHeight - thumbnailVerticalPadding)); |
| |
| // Initialize Rects for favicons and favicon frame. |
| final float halfFaviconFrameSize = |
| resource.getDimension(R.dimen.tab_grid_thumbnail_favicon_frame_size) / 2f; |
| for (int i = 0; i < 4; i++) { |
| RectF thumbnailRect = mThumbnailRects.get(i); |
| |
| float thumbnailRectCenterX = thumbnailRect.centerX(); |
| float thumbnailRectCenterY = thumbnailRect.centerY(); |
| RectF faviconBackgroundRect = new RectF(thumbnailRectCenterX, thumbnailRectCenterY, |
| thumbnailRectCenterX, thumbnailRectCenterY); |
| faviconBackgroundRect.inset(-halfFaviconFrameSize, -halfFaviconFrameSize); |
| mFaviconBackgroundRects.add(faviconBackgroundRect); |
| |
| RectF faviconRectF = new RectF(faviconBackgroundRect); |
| faviconRectF.inset( |
| thumbnailFaviconPaddingFromBackground, thumbnailFaviconPaddingFromBackground); |
| Rect faviconRect = new Rect(); |
| faviconRectF.roundOut(faviconRect); |
| mFaviconRects.add(faviconRect); |
| } |
| |
| mTabModelSelectorObserver = new TabModelSelectorObserver() { |
| @Override |
| public void onTabModelSelected(TabModel newModel, TabModel oldModel) { |
| boolean isIncognito = newModel.isIncognito(); |
| mEmptyThumbnailPaint.setColor(TabUiThemeProvider.getMiniThumbnailPlaceHolderColor( |
| context, isIncognito, false)); |
| mTextPaint.setColor( |
| TabUiThemeProvider.getTabGroupNumberTextColor(context, isIncognito, false)); |
| mThumbnailFramePaint.setColor( |
| TabUiThemeProvider.getMiniThumbnailFrameColor(context, isIncognito)); |
| mFaviconBackgroundPaint.setColor( |
| TabUiThemeProvider.getFaviconBackgroundColor(context, isIncognito)); |
| |
| mSelectedEmptyThumbnailPaint.setColor( |
| TabUiThemeProvider.getMiniThumbnailPlaceHolderColor( |
| context, isIncognito, true)); |
| mSelectedTextPaint.setColor( |
| TabUiThemeProvider.getTabGroupNumberTextColor(context, isIncognito, true)); |
| } |
| }; |
| mTabModelSelector.addObserver(mTabModelSelectorObserver); |
| } |
| |
| public void initWithNative() { |
| // TODO (https://crbug.com/1048632): Use the current profile (i.e., regular profile or |
| // incognito profile) instead of always using regular profile. It works correctly now, but |
| // it is not safe. |
| mTabListFaviconProvider.initWithNative(Profile.getLastUsedRegularProfile()); |
| } |
| |
| /** |
| * Destroy any member that needs clean up. |
| */ |
| public void destroy() { |
| mTabModelSelector.removeObserver(mTabModelSelectorObserver); |
| } |
| |
| @Override |
| public void getTabThumbnailWithCallback( |
| int tabId, Callback<Bitmap> finalCallback, boolean forceUpdate, boolean writeToCache) { |
| PseudoTab tab = PseudoTab.fromTabId(tabId); |
| if (tab == null || PseudoTab.getRelatedTabs(mContext, tab, mTabModelSelector).size() == 1) { |
| mTabContentManager.getTabThumbnailWithCallback( |
| tabId, finalCallback, forceUpdate, writeToCache); |
| return; |
| } |
| boolean isSelected = tabId == mTabModelSelector.getCurrentTabId(); |
| new MultiThumbnailFetcher(tab, finalCallback, forceUpdate, writeToCache, isSelected) |
| .fetch(); |
| } |
| } |