1 // Copyright 2019 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.feed.library.feedactionparser;
6 
7 import static org.chromium.chrome.browser.feed.library.common.Validators.checkState;
8 import static org.chromium.components.feed.core.proto.ui.action.FeedActionProto.FeedActionMetadata.Type.BLOCK_CONTENT;
9 import static org.chromium.components.feed.core.proto.ui.action.FeedActionProto.FeedActionMetadata.Type.DOWNLOAD;
10 import static org.chromium.components.feed.core.proto.ui.action.FeedActionProto.FeedActionMetadata.Type.LEARN_MORE;
11 import static org.chromium.components.feed.core.proto.ui.action.FeedActionProto.FeedActionMetadata.Type.MANAGE_INTERESTS;
12 import static org.chromium.components.feed.core.proto.ui.action.FeedActionProto.FeedActionMetadata.Type.OPEN_URL;
13 import static org.chromium.components.feed.core.proto.ui.action.FeedActionProto.FeedActionMetadata.Type.OPEN_URL_INCOGNITO;
14 import static org.chromium.components.feed.core.proto.ui.action.FeedActionProto.FeedActionMetadata.Type.OPEN_URL_NEW_TAB;
15 import static org.chromium.components.feed.core.proto.ui.action.FeedActionProto.FeedActionMetadata.Type.OPEN_URL_NEW_WINDOW;
16 import static org.chromium.components.feed.core.proto.ui.action.FeedActionProto.FeedActionMetadata.Type.REPORT_VIEW;
17 
18 import android.view.View;
19 
20 import org.chromium.base.Log;
21 import org.chromium.base.supplier.Supplier;
22 import org.chromium.chrome.browser.feed.library.api.client.knowncontent.ContentMetadata;
23 import org.chromium.chrome.browser.feed.library.api.host.action.StreamActionApi;
24 import org.chromium.chrome.browser.feed.library.api.host.logging.BasicLoggingApi;
25 import org.chromium.chrome.browser.feed.library.api.host.logging.InternalFeedError;
26 import org.chromium.chrome.browser.feed.library.api.internal.actionparser.ActionParser;
27 import org.chromium.chrome.browser.feed.library.api.internal.actionparser.ActionSource;
28 import org.chromium.chrome.browser.feed.library.api.internal.protocoladapter.ProtocolAdapter;
29 import org.chromium.chrome.browser.feed.library.common.logging.Logger;
30 import org.chromium.chrome.browser.feed.library.feedactionparser.internal.ActionTypesConverter;
31 import org.chromium.chrome.browser.feed.library.feedactionparser.internal.PietFeedActionPayloadRetriever;
32 import org.chromium.chrome.browser.feed.library.feedactionparser.internal.TooltipInfoImpl;
33 import org.chromium.components.feed.core.proto.ui.action.FeedActionPayloadProto.FeedActionPayload;
34 import org.chromium.components.feed.core.proto.ui.action.FeedActionProto.FeedAction;
35 import org.chromium.components.feed.core.proto.ui.action.FeedActionProto.FeedActionMetadata;
36 import org.chromium.components.feed.core.proto.ui.action.FeedActionProto.FeedActionMetadata.Type;
37 import org.chromium.components.feed.core.proto.ui.action.FeedActionProto.OpenUrlData;
38 import org.chromium.components.feed.core.proto.ui.action.FeedActionProto.ViewReportData;
39 import org.chromium.components.feed.core.proto.ui.piet.ActionsProto.Action;
40 import org.chromium.components.feed.core.proto.ui.piet.LogDataProto.LogData;
41 
42 /**
43  * Action parser which is able to parse Feed actions and notify clients about which action needs to
44  * be performed.
45  */
46 public final class FeedActionParser implements ActionParser {
47     private static final String TAG = "FeedActionParser";
48     static final String EXPECTED_MANAGE_INTERESTS_URL =
49             "https://www.google.com/preferences/interests";
50 
51     private final PietFeedActionPayloadRetriever mPietFeedActionPayloadRetriever;
52     private final ProtocolAdapter mProtocolAdapter;
53     private final Supplier<ContentMetadata> mContentMetadata;
54     private final BasicLoggingApi mBasicLoggingApi;
55 
FeedActionParser(ProtocolAdapter protocolAdapter, PietFeedActionPayloadRetriever pietFeedActionPayloadRetriever, Supplier<ContentMetadata> contentMetadata, BasicLoggingApi basicLoggingApi)56     FeedActionParser(ProtocolAdapter protocolAdapter,
57             PietFeedActionPayloadRetriever pietFeedActionPayloadRetriever,
58             Supplier<ContentMetadata> contentMetadata, BasicLoggingApi basicLoggingApi) {
59         this.mProtocolAdapter = protocolAdapter;
60         this.mPietFeedActionPayloadRetriever = pietFeedActionPayloadRetriever;
61         this.mContentMetadata = contentMetadata;
62         this.mBasicLoggingApi = basicLoggingApi;
63     }
64 
65     @Override
parseAction(Action action, StreamActionApi streamActionApi, View view, LogData logData, @ActionSource int actionSource)66     public void parseAction(Action action, StreamActionApi streamActionApi, View view,
67             LogData logData, @ActionSource int actionSource) {
68         FeedActionPayload feedActionPayload =
69                 mPietFeedActionPayloadRetriever.getFeedActionPayload(action);
70         if (feedActionPayload == null) {
71             Logger.w(TAG, "Unable to get FeedActionPayload from PietFeedActionPayloadRetriever");
72             return;
73         }
74         parseFeedActionPayload(feedActionPayload, streamActionApi, view, actionSource);
75     }
76 
77     @Override
parseFeedActionPayload(FeedActionPayload feedActionPayload, StreamActionApi streamActionApi, View view, @ActionSource int actionSource)78     public void parseFeedActionPayload(FeedActionPayload feedActionPayload,
79             StreamActionApi streamActionApi, View view, @ActionSource int actionSource) {
80         FeedActionMetadata feedActionMetadata =
81                 feedActionPayload.getExtension(FeedAction.feedActionExtension).getMetadata();
82         switch (feedActionMetadata.getType()) {
83             case OPEN_URL:
84             case OPEN_URL_NEW_WINDOW:
85             case OPEN_URL_INCOGNITO:
86             case OPEN_URL_NEW_TAB:
87                 // TODO(freedjm): Use a different action type for Manage Interests to handle it
88                 // separately from a simple OPEN_URL action.
89                 if (feedActionMetadata.getOpenUrlData().hasUrl()
90                         && feedActionMetadata.getOpenUrlData().getUrl().equals(
91                                 EXPECTED_MANAGE_INTERESTS_URL)) {
92                     streamActionApi.onClientAction(ActionTypesConverter.convert(MANAGE_INTERESTS));
93                 }
94                 handleOpenUrl(feedActionMetadata.getType(), feedActionMetadata.getOpenUrlData(),
95                         streamActionApi);
96                 break;
97             case OPEN_CONTEXT_MENU:
98                 if (!streamActionApi.canOpenContextMenu()) {
99                     Logger.e(TAG, "Cannot open context menu: StreamActionApi does not support it.");
100                     break;
101                 }
102 
103                 if (!feedActionMetadata.hasOpenContextMenuData()) {
104                     Logger.e(TAG, "Cannot open context menu: does not have context menu data.");
105                     break;
106                 }
107 
108                 streamActionApi.openContextMenu(feedActionMetadata.getOpenContextMenuData(), view);
109                 break;
110             case DISMISS:
111             case DISMISS_LOCAL:
112                 if (!streamActionApi.canDismiss()) {
113                     Logger.e(TAG, "Cannot dismiss: StreamActionApi does not support it.");
114                     return;
115                 }
116 
117                 if (!feedActionMetadata.getDismissData().hasContentId()) {
118                     Logger.e(TAG, "Cannot dismiss: no Content Id");
119                     return;
120                 }
121 
122                 // TODO: Once we start logging DISMISS via the feed action end point, DISMISS
123                 // and DISMISS_LOCAL should not be handled in the exact same way.
124                 streamActionApi.dismiss(mProtocolAdapter.getStreamContentId(
125                                                 feedActionMetadata.getDismissData().getContentId()),
126                         mProtocolAdapter.createOperations(
127                                 feedActionMetadata.getDismissData().getDataOperationsList()),
128                         feedActionMetadata.getDismissData().getUndoAction(),
129                         feedActionMetadata.getDismissData().getPayload());
130 
131                 break;
132             case NOT_INTERESTED_IN:
133                 if (!streamActionApi.canHandleNotInterestedIn()) {
134                     Logger.e(TAG,
135                             "Cannot preform action not interested in action: StreamActionApi does"
136                                     + " not support it.");
137                     return;
138                 }
139                 streamActionApi.handleNotInterestedIn(
140                         mProtocolAdapter.createOperations(
141                                 feedActionMetadata.getNotInterestedInData()
142                                         .getDataOperationsList()),
143                         feedActionMetadata.getNotInterestedInData().getUndoAction(),
144                         feedActionMetadata.getNotInterestedInData().getPayload(),
145                         feedActionMetadata.getNotInterestedInData().getInterestTypeValue());
146                 break;
147 
148             case DOWNLOAD:
149                 if (!streamActionApi.canDownloadUrl()) {
150                     Logger.e(TAG, "Cannot download: StreamActionApi does not support it");
151                     break;
152                 }
153                 ContentMetadata contentMetadata = this.mContentMetadata.get();
154                 if (contentMetadata == null) {
155                     Logger.e(TAG, " Cannot download: no ContentMetadata");
156                     break;
157                 }
158 
159                 streamActionApi.downloadUrl(contentMetadata);
160                 streamActionApi.onClientAction(ActionTypesConverter.convert(DOWNLOAD));
161                 break;
162             case LEARN_MORE:
163                 if (!streamActionApi.canLearnMore()) {
164                     Logger.e(TAG, "Cannot learn more: StreamActionApi does not support it");
165                     break;
166                 }
167 
168                 streamActionApi.learnMore();
169                 streamActionApi.onClientAction(ActionTypesConverter.convert(LEARN_MORE));
170                 break;
171             case VIEW_ELEMENT:
172                 if (!streamActionApi.canHandleElementView()) {
173                     Logger.e(TAG, "Cannot log Element View: StreamActionApi does not support it");
174                     break;
175                 } else if (!feedActionMetadata.hasElementTypeValue()) {
176                     Logger.e(TAG, "Cannot log ElementView : no Element Type");
177                     break;
178                 }
179                 streamActionApi.onElementView(feedActionMetadata.getElementTypeValue());
180                 break;
181             case HIDE_ELEMENT:
182                 if (!streamActionApi.canHandleElementHide()) {
183                     Logger.e(TAG, "Cannot log Element Hide: StreamActionApi does not support it");
184                     break;
185                 } else if (!feedActionMetadata.hasElementTypeValue()) {
186                     Logger.e(TAG, "Cannot log Element Hide : no Element Type");
187                     break;
188                 }
189                 streamActionApi.onElementHide(feedActionMetadata.getElementTypeValue());
190                 break;
191             case SHOW_TOOLTIP:
192                 if (!streamActionApi.canShowTooltip()) {
193                     Logger.e(
194                             TAG, "Cannot try to show tooltip: StreamActionApi does not support it");
195                     break;
196                 }
197                 streamActionApi.maybeShowTooltip(
198                         new TooltipInfoImpl(feedActionMetadata.getTooltipData()), view);
199                 break;
200             case SEND_FEEDBACK:
201                 Log.d(TAG, "SendFeedback menu item clicked.");
202                 streamActionApi.sendFeedback(this.mContentMetadata.get());
203                 break;
204             case BLOCK_CONTENT:
205                 streamActionApi.handleBlockContent(
206                         mProtocolAdapter.createOperations(
207                                 feedActionMetadata.getBlockContentData().getDataOperationsList()),
208                         feedActionMetadata.getBlockContentData().getPayload());
209                 streamActionApi.onClientAction(ActionTypesConverter.convert(BLOCK_CONTENT));
210                 break;
211             case REPORT_VIEW:
212                 ViewReportData viewReportData = feedActionMetadata.getViewReportData();
213                 String contentId =
214                         mProtocolAdapter.getStreamContentId(viewReportData.getContentId());
215                 switch (viewReportData.getVisibility()) {
216                     case SHOW:
217                         streamActionApi.reportViewVisible(
218                                 view, contentId, viewReportData.getPayload());
219                         break;
220                     case HIDE:
221                         streamActionApi.reportViewHidden(view, contentId);
222                         break;
223                     default:
224                         Log.d(TAG, "Unrecognized view report data visibility.");
225                 }
226                 break;
227             default:
228                 Logger.wtf(TAG, "Haven't implemented host handling of %s",
229                         feedActionMetadata.getType());
230         }
231         if (actionSource == ActionSource.CLICK) {
232             if (!streamActionApi.canHandleElementClick()) {
233                 Logger.e(TAG, "Cannot log Element Click: StreamActionApi does not support it");
234             } else if (!feedActionMetadata.hasElementTypeValue()) {
235                 Logger.e(TAG, "Cannot log Element Click: no Element Type");
236             } else {
237                 streamActionApi.onElementClick(feedActionMetadata.getElementTypeValue());
238             }
239         }
240     }
241 
handleOpenUrl( Type urlType, OpenUrlData openUrlData, StreamActionApi streamActionApi)242     private void handleOpenUrl(
243             Type urlType, OpenUrlData openUrlData, StreamActionApi streamActionApi) {
244         checkState(urlType.equals(OPEN_URL) || urlType.equals(OPEN_URL_NEW_WINDOW)
245                         || urlType.equals(OPEN_URL_INCOGNITO) || urlType.equals(OPEN_URL_NEW_TAB),
246                 "Attempting to handle URL that is not a URL type: %s", urlType);
247         if (!canPerformAction(urlType, streamActionApi)) {
248             Logger.e(TAG, "Cannot open URL action: %s, not supported.", urlType);
249             return;
250         }
251 
252         if (!openUrlData.hasUrl()) {
253             mBasicLoggingApi.onInternalError(InternalFeedError.NO_URL_FOR_OPEN);
254             Logger.e(TAG, "Cannot open URL action: %s, no URL available.", urlType);
255             return;
256         }
257 
258         if (urlType != OPEN_URL_INCOGNITO && openUrlData.hasContentId()
259                 && openUrlData.hasPayload()) {
260             streamActionApi.reportClickAction(
261                     mProtocolAdapter.getStreamContentId(openUrlData.getContentId()),
262                     openUrlData.getPayload());
263         }
264 
265         String url = openUrlData.getUrl();
266         switch (urlType) {
267             case OPEN_URL:
268                 if (openUrlData.hasConsistencyTokenQueryParamName()) {
269                     streamActionApi.openUrl(url, openUrlData.getConsistencyTokenQueryParamName());
270                 } else {
271                     streamActionApi.openUrl(url);
272                 }
273                 break;
274             case OPEN_URL_NEW_WINDOW:
275                 if (openUrlData.hasConsistencyTokenQueryParamName()) {
276                     streamActionApi.openUrlInNewWindow(
277                             url, openUrlData.getConsistencyTokenQueryParamName());
278                 } else {
279                     streamActionApi.openUrlInNewWindow(url);
280                 }
281                 break;
282             case OPEN_URL_INCOGNITO:
283                 if (openUrlData.hasConsistencyTokenQueryParamName()) {
284                     streamActionApi.openUrlInIncognitoMode(
285                             url, openUrlData.getConsistencyTokenQueryParamName());
286                 } else {
287                     streamActionApi.openUrlInIncognitoMode(url);
288                 }
289                 break;
290             case OPEN_URL_NEW_TAB:
291                 if (openUrlData.hasConsistencyTokenQueryParamName()) {
292                     streamActionApi.openUrlInNewTab(
293                             url, openUrlData.getConsistencyTokenQueryParamName());
294                 } else {
295                     streamActionApi.openUrlInNewTab(url);
296                 }
297                 break;
298             default:
299                 throw new AssertionError("Unhandled URL type: " + urlType);
300         }
301         streamActionApi.onClientAction(ActionTypesConverter.convert(urlType));
302     }
303 
304     @Override
canPerformAction( FeedActionPayload feedActionPayload, StreamActionApi streamActionApi)305     public boolean canPerformAction(
306             FeedActionPayload feedActionPayload, StreamActionApi streamActionApi) {
307         return canPerformAction(feedActionPayload.getExtension(FeedAction.feedActionExtension)
308                                         .getMetadata()
309                                         .getType(),
310                 streamActionApi);
311     }
312 
canPerformAction(Type type, StreamActionApi streamActionApi)313     private boolean canPerformAction(Type type, StreamActionApi streamActionApi) {
314         switch (type) {
315             case OPEN_URL:
316                 return streamActionApi.canOpenUrl();
317             case OPEN_URL_NEW_WINDOW:
318                 return streamActionApi.canOpenUrlInNewWindow();
319             case OPEN_URL_INCOGNITO:
320                 return streamActionApi.canOpenUrlInIncognitoMode();
321             case OPEN_URL_NEW_TAB:
322                 return streamActionApi.canOpenUrlInNewTab();
323             case OPEN_CONTEXT_MENU:
324                 return streamActionApi.canOpenContextMenu();
325             case DISMISS:
326             case DISMISS_LOCAL:
327                 // TODO: Once we start logging DISMISS via the feed action end point, DISMISS
328                 // and DISMISS_LOCAL should not be handled in the exact same way.
329                 return streamActionApi.canDismiss();
330             case DOWNLOAD:
331                 return mContentMetadata.get() != null && streamActionApi.canDownloadUrl();
332             case LEARN_MORE:
333                 return streamActionApi.canLearnMore();
334             case NOT_INTERESTED_IN:
335                 return streamActionApi.canHandleNotInterestedIn();
336             // Send Feedback for the feed is available in M81 and later.
337             case SEND_FEEDBACK:
338                 return true;
339             case UNKNOWN:
340             default:
341                 // TODO : Handle the action types introduced in [INTERNAL LINK]
342         }
343         Logger.e(TAG, "Unhandled feed action type: %s", type);
344         return false;
345     }
346 }
347