1 // Copyright 2013 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.android_webview;
6 
7 import android.annotation.SuppressLint;
8 import android.content.Context;
9 import android.media.AudioManager;
10 import android.net.Uri;
11 import android.os.Handler;
12 import android.os.Message;
13 import android.provider.MediaStore;
14 import android.text.TextUtils;
15 import android.util.Log;
16 import android.view.KeyEvent;
17 import android.view.View;
18 import android.webkit.URLUtil;
19 import android.widget.FrameLayout;
20 
21 import org.chromium.base.Callback;
22 import org.chromium.base.ContentUriUtils;
23 import org.chromium.base.ThreadUtils;
24 import org.chromium.base.task.AsyncTask;
25 import org.chromium.content_public.browser.InvalidateTypes;
26 import org.chromium.content_public.common.ContentUrlConstants;
27 import org.chromium.content_public.common.ResourceRequestBody;
28 
29 /**
30  * Adapts the AwWebContentsDelegate interface to the AwContentsClient interface.
31  * This class also serves a secondary function of routing certain callbacks from the content layer
32  * to specific listener interfaces.
33  */
34 class AwWebContentsDelegateAdapter extends AwWebContentsDelegate {
35     private static final String TAG = "AwWebContentsDelegateAdapter";
36 
37     private final AwContents mAwContents;
38     private final AwContentsClient mContentsClient;
39     private final AwSettings mAwSettings;
40     private final Context mContext;
41     private View mContainerView;
42     private FrameLayout mCustomView;
43 
AwWebContentsDelegateAdapter(AwContents awContents, AwContentsClient contentsClient, AwSettings settings, Context context, View containerView)44     public AwWebContentsDelegateAdapter(AwContents awContents, AwContentsClient contentsClient,
45             AwSettings settings, Context context, View containerView) {
46         mAwContents = awContents;
47         mContentsClient = contentsClient;
48         mAwSettings = settings;
49         mContext = context;
50         setContainerView(containerView);
51     }
52 
setContainerView(View containerView)53     public void setContainerView(View containerView) {
54         mContainerView = containerView;
55         mContainerView.setClickable(true);
56     }
57 
58     @Override
handleKeyboardEvent(KeyEvent event)59     public void handleKeyboardEvent(KeyEvent event) {
60         if (event.getAction() == KeyEvent.ACTION_DOWN) {
61             int direction;
62             switch (event.getKeyCode()) {
63                 case KeyEvent.KEYCODE_DPAD_DOWN:
64                     direction = View.FOCUS_DOWN;
65                     break;
66                 case KeyEvent.KEYCODE_DPAD_UP:
67                     direction = View.FOCUS_UP;
68                     break;
69                 case KeyEvent.KEYCODE_DPAD_LEFT:
70                     direction = View.FOCUS_LEFT;
71                     break;
72                 case KeyEvent.KEYCODE_DPAD_RIGHT:
73                     direction = View.FOCUS_RIGHT;
74                     break;
75                 default:
76                     direction = 0;
77                     break;
78             }
79             if (direction != 0 && tryToMoveFocus(direction)) return;
80         }
81         handleMediaKey(event);
82         mContentsClient.onUnhandledKeyEvent(event);
83     }
84 
85     /**
86      * Redispatches unhandled media keys. This allows bluetooth headphones with play/pause or
87      * other buttons to function correctly.
88      */
handleMediaKey(KeyEvent e)89     private void handleMediaKey(KeyEvent e) {
90         switch (e.getKeyCode()) {
91             case KeyEvent.KEYCODE_MUTE:
92             case KeyEvent.KEYCODE_HEADSETHOOK:
93             case KeyEvent.KEYCODE_MEDIA_PLAY:
94             case KeyEvent.KEYCODE_MEDIA_PAUSE:
95             case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
96             case KeyEvent.KEYCODE_MEDIA_STOP:
97             case KeyEvent.KEYCODE_MEDIA_NEXT:
98             case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
99             case KeyEvent.KEYCODE_MEDIA_REWIND:
100             case KeyEvent.KEYCODE_MEDIA_RECORD:
101             case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
102             case KeyEvent.KEYCODE_MEDIA_CLOSE:
103             case KeyEvent.KEYCODE_MEDIA_EJECT:
104             case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK:
105                 AudioManager am = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
106                 am.dispatchMediaKeyEvent(e);
107                 break;
108             default:
109                 break;
110         }
111     }
112 
113     @Override
takeFocus(boolean reverse)114     public boolean takeFocus(boolean reverse) {
115         int direction =
116                 (reverse == (mContainerView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL))
117                 ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
118         if (tryToMoveFocus(direction)) return true;
119         direction = reverse ? View.FOCUS_BACKWARD : View.FOCUS_FORWARD;
120         return tryToMoveFocus(direction);
121     }
122 
tryToMoveFocus(int direction)123     private boolean tryToMoveFocus(int direction) {
124         View focus = mContainerView.focusSearch(direction);
125         return focus != null && focus != mContainerView && focus.requestFocus();
126     }
127 
128     @Override
addMessageToConsole(int level, String message, int lineNumber, String sourceId)129     public boolean addMessageToConsole(int level, String message, int lineNumber,
130             String sourceId) {
131         @AwConsoleMessage.MessageLevel
132         int messageLevel = AwConsoleMessage.MESSAGE_LEVEL_DEBUG;
133         switch(level) {
134             case LOG_LEVEL_TIP:
135                 messageLevel = AwConsoleMessage.MESSAGE_LEVEL_TIP;
136                 break;
137             case LOG_LEVEL_LOG:
138                 messageLevel = AwConsoleMessage.MESSAGE_LEVEL_LOG;
139                 break;
140             case LOG_LEVEL_WARNING:
141                 messageLevel = AwConsoleMessage.MESSAGE_LEVEL_WARNING;
142                 break;
143             case LOG_LEVEL_ERROR:
144                 messageLevel = AwConsoleMessage.MESSAGE_LEVEL_ERROR;
145                 break;
146             default:
147                 Log.w(TAG, "Unknown message level, defaulting to DEBUG");
148                 break;
149         }
150         boolean result = mContentsClient.onConsoleMessage(
151                 new AwConsoleMessage(message, sourceId, lineNumber, messageLevel));
152         return result;
153     }
154 
155     @Override
onUpdateUrl(String url)156     public void onUpdateUrl(String url) {
157         // TODO: implement
158     }
159 
160     @Override
openNewTab(String url, String extraHeaders, ResourceRequestBody postData, int disposition, boolean isRendererInitiated)161     public void openNewTab(String url, String extraHeaders, ResourceRequestBody postData,
162             int disposition, boolean isRendererInitiated) {
163         // This is only called in chrome layers.
164         assert false;
165     }
166 
167     @Override
closeContents()168     public void closeContents() {
169         mContentsClient.onCloseWindow();
170     }
171 
172     @Override
173     @SuppressLint("HandlerLeak")
showRepostFormWarningDialog()174     public void showRepostFormWarningDialog() {
175         // TODO(mkosiba) We should be using something akin to the JsResultReceiver as the
176         // callback parameter (instead of WebContents) and implement a way of converting
177         // that to a pair of messages.
178         final int msgContinuePendingReload = 1;
179         final int msgCancelPendingReload = 2;
180 
181         // TODO(sgurun) Remember the URL to cancel the reload behavior
182         // if it is different than the most recent NavigationController entry.
183         final Handler handler = new Handler(ThreadUtils.getUiThreadLooper()) {
184             @Override
185             public void handleMessage(Message msg) {
186                 if (mAwContents.getNavigationController() == null) return;
187 
188                 switch(msg.what) {
189                     case msgContinuePendingReload: {
190                         mAwContents.getNavigationController().continuePendingReload();
191                         break;
192                     }
193                     case msgCancelPendingReload: {
194                         mAwContents.getNavigationController().cancelPendingReload();
195                         break;
196                     }
197                     default:
198                         throw new IllegalStateException(
199                                 "WebContentsDelegateAdapter: unhandled message " + msg.what);
200                 }
201             }
202         };
203 
204         Message resend = handler.obtainMessage(msgContinuePendingReload);
205         Message dontResend = handler.obtainMessage(msgCancelPendingReload);
206         mContentsClient.getCallbackHelper().postOnFormResubmission(dontResend, resend);
207     }
208 
209     @Override
runFileChooser(final int processId, final int renderId, final int modeFlags, String acceptTypes, String title, String defaultFilename, boolean capture)210     public void runFileChooser(final int processId, final int renderId, final int modeFlags,
211             String acceptTypes, String title, String defaultFilename, boolean capture) {
212         AwContentsClient.FileChooserParamsImpl params = new AwContentsClient.FileChooserParamsImpl(
213                 modeFlags, acceptTypes, title, defaultFilename, capture);
214 
215         mContentsClient.showFileChooser(new Callback<String[]>() {
216             boolean mCompleted;
217             @Override
218             public void onResult(String[] results) {
219                 if (mCompleted) {
220                     throw new IllegalStateException("Duplicate showFileChooser result");
221                 }
222                 mCompleted = true;
223                 if (results == null) {
224                     AwWebContentsDelegateJni.get().filesSelectedInChooser(
225                             processId, renderId, modeFlags, null, null);
226                     return;
227                 }
228                 GetDisplayNameTask task =
229                         new GetDisplayNameTask(mContext, processId, renderId, modeFlags, results);
230                 task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
231             }
232         }, params);
233     }
234 
235     @Override
addNewContents(boolean isDialog, boolean isUserGesture)236     public boolean addNewContents(boolean isDialog, boolean isUserGesture) {
237         return mContentsClient.onCreateWindow(isDialog, isUserGesture);
238     }
239 
240     @Override
activateContents()241     public void activateContents() {
242         mContentsClient.onRequestFocus();
243     }
244 
245     @Override
navigationStateChanged(int flags)246     public void navigationStateChanged(int flags) {
247         if ((flags & InvalidateTypes.URL) != 0
248                 && mAwContents.isPopupWindow()
249                 && mAwContents.hasAccessedInitialDocument()) {
250             // Hint the client to show the last committed url, as it may be unsafe to show
251             // the pending entry.
252             String url = mAwContents.getLastCommittedUrl();
253             url = TextUtils.isEmpty(url) ? ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL : url;
254             mContentsClient.getCallbackHelper().postSynthesizedPageLoadingForUrlBarUpdate(url);
255         }
256     }
257 
258     @Override
enterFullscreenModeForTab(boolean prefersNavigationBar)259     public void enterFullscreenModeForTab(boolean prefersNavigationBar) {
260         enterFullscreen();
261     }
262 
263     @Override
exitFullscreenModeForTab()264     public void exitFullscreenModeForTab() {
265         exitFullscreen();
266     }
267 
268     @Override
loadingStateChanged()269     public void loadingStateChanged() {
270         mContentsClient.updateTitle(mAwContents.getTitle(), false);
271     }
272 
273     /**
274      * Called to show the web contents in fullscreen mode.
275      *
276      * <p>If entering fullscreen on a video element the web contents will contain just
277      * the html5 video controls. {@link #enterFullscreenVideo(View)} will be called later
278      * once the ContentVideoView, which contains the hardware accelerated fullscreen video,
279      * is ready to be shown.
280      */
enterFullscreen()281     private void enterFullscreen() {
282         if (mAwContents.isFullScreen()) {
283             return;
284         }
285         View fullscreenView = mAwContents.enterFullScreen();
286         if (fullscreenView == null) {
287             return;
288         }
289         AwContentsClient.CustomViewCallback cb = () -> {
290             if (mCustomView != null) {
291                 mAwContents.requestExitFullscreen();
292             }
293         };
294         mCustomView = new FrameLayout(mContext);
295         mCustomView.addView(fullscreenView);
296         mContentsClient.onShowCustomView(mCustomView, cb);
297     }
298 
299     /**
300      * Called to show the web contents in embedded mode.
301      */
exitFullscreen()302     private void exitFullscreen() {
303         if (mCustomView != null) {
304             mCustomView = null;
305             mAwContents.exitFullScreen();
306             mContentsClient.onHideCustomView();
307         }
308     }
309 
310     @Override
shouldBlockMediaRequest(String url)311     public boolean shouldBlockMediaRequest(String url) {
312         return mAwSettings != null
313                 ? mAwSettings.getBlockNetworkLoads() && URLUtil.isNetworkUrl(url) : true;
314     }
315 
316     private static class GetDisplayNameTask extends AsyncTask<String[]> {
317         final int mProcessId;
318         final int mRenderId;
319         final int mModeFlags;
320         final String[] mFilePaths;
321 
322         // The task doesn't run long, so we don't gain anything from a weak ref.
323         @SuppressLint("StaticFieldLeak")
324         final Context mContext;
325 
GetDisplayNameTask( Context context, int processId, int renderId, int modeFlags, String[] filePaths)326         public GetDisplayNameTask(
327                 Context context, int processId, int renderId, int modeFlags, String[] filePaths) {
328             mProcessId = processId;
329             mRenderId = renderId;
330             mModeFlags = modeFlags;
331             mFilePaths = filePaths;
332             mContext = context;
333         }
334 
335         @Override
doInBackground()336         protected String[] doInBackground() {
337             String[] displayNames = new String[mFilePaths.length];
338             for (int i = 0; i < mFilePaths.length; i++) {
339                 displayNames[i] = resolveFileName(mFilePaths[i]);
340             }
341             return displayNames;
342         }
343 
344         @Override
onPostExecute(String[] result)345         protected void onPostExecute(String[] result) {
346             AwWebContentsDelegateJni.get().filesSelectedInChooser(
347                     mProcessId, mRenderId, mModeFlags, mFilePaths, result);
348         }
349 
350         /**
351          * @return the display name of a path if it is a content URI and is present in the database
352          * or an empty string otherwise.
353          */
resolveFileName(String filePath)354         private String resolveFileName(String filePath) {
355             if (filePath == null) return "";
356             Uri uri = Uri.parse(filePath);
357             return ContentUriUtils.getDisplayName(
358                     uri, mContext, MediaStore.MediaColumns.DISPLAY_NAME);
359         }
360     }
361 }
362