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