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