1 // Copyright 2018 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.chrome.browser.omnibox.suggestions; 6 7 import android.content.Context; 8 import android.graphics.Bitmap; 9 import android.view.LayoutInflater; 10 import android.view.View; 11 import android.view.ViewGroup; 12 13 import org.chromium.base.Callback; 14 import org.chromium.base.task.PostTask; 15 import org.chromium.chrome.browser.image_fetcher.ImageFetcher; 16 import org.chromium.chrome.browser.image_fetcher.ImageFetcherConfig; 17 import org.chromium.chrome.browser.image_fetcher.ImageFetcherFactory; 18 import org.chromium.chrome.browser.profiles.Profile; 19 import org.chromium.components.browser_ui.util.ConversionUtils; 20 import org.chromium.components.browser_ui.util.GlobalDiscardableReferencePool; 21 import org.chromium.components.browser_ui.widget.image_tiles.ImageTile; 22 import org.chromium.components.browser_ui.widget.image_tiles.ImageTileCoordinator; 23 import org.chromium.components.browser_ui.widget.image_tiles.ImageTileCoordinatorFactory; 24 import org.chromium.components.browser_ui.widget.image_tiles.TileConfig; 25 import org.chromium.components.query_tiles.QueryTile; 26 import org.chromium.components.query_tiles.QueryTileConstants; 27 import org.chromium.components.query_tiles.TileUmaLogger; 28 import org.chromium.content_public.browser.UiThreadTaskTraits; 29 import org.chromium.ui.UiUtils; 30 import org.chromium.ui.modelutil.PropertyKey; 31 import org.chromium.ui.modelutil.PropertyModel; 32 33 import java.util.ArrayList; 34 import java.util.Arrays; 35 import java.util.List; 36 37 /** 38 * Wrapper around {@link ImageTileCoordinator} that shows query tiles in omnibox suggestions. 39 * Responsible for view creation, and wiring necessary dependencies for functioning of the widget. 40 */ 41 public class OmniboxQueryTileCoordinator { 42 private static final int MAX_IMAGE_CACHE_SIZE = 500 * ConversionUtils.BYTES_PER_KILOBYTE; 43 private static final String UMA_PREFIX = "QueryTiles.Omnibox"; 44 45 private final Context mContext; 46 private final Callback<QueryTile> mSelectionCallback; 47 private final TileUmaLogger mTileUmaLogger; 48 private ImageTileCoordinator mTileCoordinator; 49 private ImageFetcher mImageFetcher; 50 private Integer mTileWidth; 51 52 /** 53 * Constructor. 54 * @param context The associated {@link Context}. 55 * @param selectionCallback The callback to be invoked on tile selection. 56 */ OmniboxQueryTileCoordinator(Context context, Callback<QueryTile> selectionCallback)57 public OmniboxQueryTileCoordinator(Context context, Callback<QueryTile> selectionCallback) { 58 mContext = context; 59 mSelectionCallback = selectionCallback; 60 mTileUmaLogger = new TileUmaLogger(UMA_PREFIX); 61 } 62 63 /** Called to set the list of query tiles to be displayed in the suggestion. */ setTiles(List<QueryTile> tiles)64 public void setTiles(List<QueryTile> tiles) { 65 mTileUmaLogger.recordTilesLoaded(tiles); 66 getTileCoordinator().setTiles(tiles == null ? new ArrayList<>() : new ArrayList<>(tiles)); 67 } 68 69 /** Called to clean up resources used by this class. */ destroy()70 public void destroy() { 71 if (mImageFetcher != null) mImageFetcher.destroy(); 72 mImageFetcher = null; 73 } 74 75 /** @return The query tiles suggestion view to be shown in the autocomplete suggestions list. */ createView(Context context)76 public ViewGroup createView(Context context) { 77 LayoutInflater inflater = 78 (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 79 ViewGroup suggestionView = (ViewGroup) inflater.inflate( 80 org.chromium.chrome.R.layout.omnibox_query_tiles_suggestion, null); 81 82 View tilesView = getTileCoordinator().getView(); 83 if (tilesView.getParent() != null) UiUtils.removeViewFromParent(tilesView); 84 suggestionView.addView(tilesView); 85 return suggestionView; 86 } 87 88 /** 89 * A mechanism for binding query tile suggestion properties to its view. 90 * @see PropertyModelChangeProcessor.ViewBinder#bind(Object, Object, Object). 91 */ bind(PropertyModel model, View view, PropertyKey propertyKey)92 public void bind(PropertyModel model, View view, PropertyKey propertyKey) { 93 ViewGroup suggestionView = (ViewGroup) view; 94 View tilesView = getTileCoordinator().getView(); 95 if (tilesView.getParent() != null) UiUtils.removeViewFromParent(tilesView); 96 suggestionView.addView(tilesView); 97 } 98 99 /** 100 * Triggered when current user profile is changed. This method creates image fetcher using 101 * current user profile. 102 * @param profile Current user profile. 103 */ setProfile(Profile profile)104 public void setProfile(Profile profile) { 105 if (mImageFetcher != null) { 106 mImageFetcher.destroy(); 107 mImageFetcher = null; 108 } 109 110 mImageFetcher = createImageFetcher(profile); 111 } 112 113 /** @return A {@link ImageTileCoordinator} instance. Creates if it doesn't exist yet. */ getTileCoordinator()114 private ImageTileCoordinator getTileCoordinator() { 115 if (mTileCoordinator == null) { 116 TileConfig tileConfig = new TileConfig.Builder().setUmaPrefix(UMA_PREFIX).build(); 117 mTileCoordinator = ImageTileCoordinatorFactory.create( 118 mContext, tileConfig, this::onTileClicked, this::getVisuals); 119 } 120 return mTileCoordinator; 121 } 122 123 /** 124 * Method called by the query tiles widget to fetch the bitmap to be shown for a given tile. 125 * @param tile The associated query tile. 126 * @param callback The callback to be invoked by the backend when bitmap is available. 127 */ getVisuals(ImageTile tile, Callback<List<Bitmap>> callback)128 private void getVisuals(ImageTile tile, Callback<List<Bitmap>> callback) { 129 if (mTileWidth == null) { 130 mTileWidth = mContext.getResources().getDimensionPixelSize( 131 org.chromium.chrome.R.dimen.tile_ideal_width); 132 } 133 134 QueryTile queryTile = (QueryTile) tile; 135 if (queryTile.urls.isEmpty()) { 136 PostTask.postTask(UiThreadTaskTraits.DEFAULT, () -> callback.onResult(null)); 137 return; 138 } 139 140 ImageFetcher.Params params = ImageFetcher.Params.createWithExpirationInterval( 141 queryTile.urls.get(0), ImageFetcher.QUERY_TILE_UMA_CLIENT_NAME, mTileWidth, 142 mTileWidth, QueryTileConstants.IMAGE_EXPIRATION_INTERVAL_MINUTES); 143 getImageFetcher().fetchImage(params, bitmap -> callback.onResult(Arrays.asList(bitmap))); 144 } 145 146 /** @return {@link ImageFetcher} instance. Only creates if needed. */ getImageFetcher()147 private ImageFetcher getImageFetcher() { 148 if (mImageFetcher == null) { 149 // This will be called only if there is no tab. Using regular profile is safe, since 150 // mImageFetcher is updated, when switching to incognito mode. 151 mImageFetcher = createImageFetcher(Profile.getLastUsedRegularProfile()); 152 } 153 return mImageFetcher; 154 } 155 156 /** 157 * @param profile The profile to create image fetcher. 158 * @return an {@link ImageFetcher} instance for given profile. 159 */ createImageFetcher(Profile profile)160 private ImageFetcher createImageFetcher(Profile profile) { 161 return ImageFetcherFactory.createImageFetcher(ImageFetcherConfig.IN_MEMORY_WITH_DISK_CACHE, 162 profile, GlobalDiscardableReferencePool.getReferencePool(), MAX_IMAGE_CACHE_SIZE); 163 } 164 onTileClicked(ImageTile tile)165 private void onTileClicked(ImageTile tile) { 166 QueryTile queryTile = (QueryTile) tile; 167 mTileUmaLogger.recordTileClicked(queryTile); 168 mSelectionCallback.onResult(queryTile); 169 } 170 } 171