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