| // Copyright 2015 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.chrome.browser.tab; |
| |
| import android.annotation.TargetApi; |
| import android.app.Activity; |
| import android.app.ActivityManager; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.graphics.Rect; |
| import android.graphics.RectF; |
| import android.media.AudioManager; |
| import android.os.Build; |
| import android.os.Handler; |
| import android.util.Pair; |
| import android.view.KeyEvent; |
| import android.view.View; |
| |
| import org.chromium.base.ActivityState; |
| import org.chromium.base.ApplicationStatus; |
| import org.chromium.base.Log; |
| import org.chromium.base.ObserverList.RewindableIterator; |
| import org.chromium.base.annotations.CalledByNative; |
| import org.chromium.blink_public.platform.WebDisplayMode; |
| import org.chromium.chrome.R; |
| import org.chromium.chrome.browser.AppHooks; |
| import org.chromium.chrome.browser.ChromeActivity; |
| import org.chromium.chrome.browser.FullscreenActivity; |
| import org.chromium.chrome.browser.RepostFormWarningDialog; |
| import org.chromium.chrome.browser.document.DocumentUtils; |
| import org.chromium.chrome.browser.document.DocumentWebContentsDelegate; |
| import org.chromium.chrome.browser.findinpage.FindMatchRectsDetails; |
| import org.chromium.chrome.browser.findinpage.FindNotificationDetails; |
| import org.chromium.chrome.browser.fullscreen.FullscreenOptions; |
| import org.chromium.chrome.browser.media.MediaCaptureNotificationService; |
| import org.chromium.chrome.browser.policy.PolicyAuditor; |
| import org.chromium.chrome.browser.policy.PolicyAuditor.AuditEvent; |
| import org.chromium.chrome.browser.tabmodel.TabCreatorManager.TabCreator; |
| import org.chromium.chrome.browser.tabmodel.TabModel; |
| import org.chromium.chrome.browser.tabmodel.TabModel.TabLaunchType; |
| import org.chromium.chrome.browser.tabmodel.TabModelUtils; |
| import org.chromium.chrome.browser.tabmodel.TabWindowManager; |
| import org.chromium.components.embedder_support.delegate.WebContentsDelegateAndroid; |
| import org.chromium.content_public.browser.GestureListenerManager; |
| import org.chromium.content_public.browser.InvalidateTypes; |
| import org.chromium.content_public.browser.WebContents; |
| import org.chromium.content_public.common.ResourceRequestBody; |
| import org.chromium.ui.mojom.WindowOpenDisposition; |
| |
| /** |
| * A basic {@link TabWebContentsDelegateAndroid} that forwards some calls to the registered |
| * {@link TabObserver}s. |
| */ |
| public class TabWebContentsDelegateAndroid extends WebContentsDelegateAndroid { |
| /** |
| * Listener to be notified when a find result is received. |
| */ |
| public interface FindResultListener { |
| public void onFindResult(FindNotificationDetails result); |
| } |
| |
| /** |
| * Listener to be notified when the rects corresponding to find matches are received. |
| */ |
| public interface FindMatchRectsListener { |
| public void onFindMatchRects(FindMatchRectsDetails result); |
| } |
| |
| /** Used for logging. */ |
| private static final String TAG = "WebContentsDelegate"; |
| |
| protected final Tab mTab; |
| |
| private FindResultListener mFindResultListener; |
| |
| private FindMatchRectsListener mFindMatchRectsListener; |
| |
| private @WebDisplayMode int mDisplayMode = WebDisplayMode.BROWSER; |
| |
| protected Handler mHandler; |
| |
| private final Runnable mCloseContentsRunnable = new Runnable() { |
| @Override |
| public void run() { |
| boolean isSelected = mTab.getTabModelSelector().getCurrentTab() == mTab; |
| mTab.getTabModelSelector().closeTab(mTab); |
| |
| // If the parent Tab belongs to another Activity, fire the Intent to bring it back. |
| if (isSelected && mTab.getParentIntent() != null |
| && mTab.getActivity().getIntent() != mTab.getParentIntent()) { |
| mTab.getActivity().startActivity(mTab.getParentIntent()); |
| } |
| } |
| |
| /** If the API allows it, returns whether a Task still exists for the parent Activity. */ |
| @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| private boolean isParentInAndroidOverview() { |
| ActivityManager activityManager = (ActivityManager) mTab.getApplicationContext() |
| .getSystemService(Context.ACTIVITY_SERVICE); |
| for (ActivityManager.AppTask task : activityManager.getAppTasks()) { |
| Intent taskIntent = DocumentUtils.getBaseIntentFromTask(task); |
| if (taskIntent != null && taskIntent.filterEquals(mTab.getParentIntent())) { |
| return true; |
| } |
| } |
| return false; |
| } |
| }; |
| |
| public TabWebContentsDelegateAndroid(Tab tab) { |
| mTab = tab; |
| mHandler = new Handler(); |
| } |
| |
| /** |
| * Sets the current display mode which can be queried using media queries. |
| */ |
| public void setDisplayMode(@WebDisplayMode int displayMode) { |
| mDisplayMode = displayMode; |
| } |
| |
| @CalledByNative |
| private @WebDisplayMode int getDisplayMode() { |
| return mDisplayMode; |
| } |
| |
| @CalledByNative |
| private void onFindResultAvailable(FindNotificationDetails result) { |
| if (mFindResultListener != null) { |
| mFindResultListener.onFindResult(result); |
| } |
| } |
| |
| @CalledByNative |
| private void onFindMatchRectsAvailable(FindMatchRectsDetails result) { |
| if (mFindMatchRectsListener != null) { |
| mFindMatchRectsListener.onFindMatchRects(result); |
| } |
| } |
| |
| /** Register to receive the results of startFinding calls. */ |
| public void setFindResultListener(FindResultListener listener) { |
| mFindResultListener = listener; |
| } |
| |
| /** Register to receive the results of requestFindMatchRects calls. */ |
| public void setFindMatchRectsListener(FindMatchRectsListener listener) { |
| mFindMatchRectsListener = listener; |
| } |
| |
| // Helper functions used to create types that are part of the public interface |
| @CalledByNative |
| private static Rect createRect(int x, int y, int right, int bottom) { |
| return new Rect(x, y, right, bottom); |
| } |
| |
| @CalledByNative |
| private static RectF createRectF(float x, float y, float right, float bottom) { |
| return new RectF(x, y, right, bottom); |
| } |
| |
| @CalledByNative |
| private static FindNotificationDetails createFindNotificationDetails( |
| int numberOfMatches, Rect rendererSelectionRect, |
| int activeMatchOrdinal, boolean finalUpdate) { |
| return new FindNotificationDetails(numberOfMatches, rendererSelectionRect, |
| activeMatchOrdinal, finalUpdate); |
| } |
| |
| @CalledByNative |
| private static FindMatchRectsDetails createFindMatchRectsDetails( |
| int version, int numRects, RectF activeRect) { |
| return new FindMatchRectsDetails(version, numRects, activeRect); |
| } |
| |
| @CalledByNative |
| private static void setMatchRectByIndex( |
| FindMatchRectsDetails findMatchRectsDetails, int index, RectF rect) { |
| findMatchRectsDetails.rects[index] = rect; |
| } |
| |
| @Override |
| public void onLoadProgressChanged(int progress) { |
| if (!mTab.isLoading()) return; |
| mTab.notifyLoadProgress(mTab.getProgress()); |
| } |
| |
| @Override |
| public void loadingStateChanged(boolean toDifferentDocument) { |
| boolean isLoading = mTab.getWebContents() != null && mTab.getWebContents().isLoading(); |
| if (isLoading) { |
| mTab.onLoadStarted(toDifferentDocument); |
| } else { |
| mTab.onLoadStopped(); |
| } |
| } |
| |
| @Override |
| public void onUpdateUrl(String url) { |
| RewindableIterator<TabObserver> observers = mTab.getTabObservers(); |
| while (observers.hasNext()) { |
| observers.next().onUpdateUrl(mTab, url); |
| } |
| } |
| |
| @Override |
| public void showRepostFormWarningDialog() { |
| mTab.resetSwipeRefreshHandler(); |
| if (mTab.getActivity() == null) return; |
| RepostFormWarningDialog warningDialog = new RepostFormWarningDialog(mTab); |
| warningDialog.show(mTab.getActivity().getFragmentManager(), null); |
| } |
| |
| @Override |
| public void enterFullscreenModeForTab(boolean prefersNavigationBar) { |
| FullscreenOptions options = new FullscreenOptions(prefersNavigationBar); |
| if (FullscreenActivity.shouldUseFullscreenActivity(mTab)) { |
| FullscreenActivity.enterFullscreenMode(mTab, options); |
| } else { |
| mTab.enterFullscreenMode(options); |
| } |
| } |
| |
| @Override |
| public void exitFullscreenModeForTab() { |
| if (FullscreenActivity.shouldUseFullscreenActivity(mTab)) { |
| FullscreenActivity.exitFullscreenMode(mTab); |
| } else { |
| mTab.exitFullscreenMode(); |
| } |
| } |
| |
| @Override |
| public void navigationStateChanged(int flags) { |
| if ((flags & InvalidateTypes.TAB) != 0) { |
| int mediaType = MediaCaptureNotificationService.getMediaType( |
| isCapturingAudio(), isCapturingVideo(), isCapturingScreen()); |
| MediaCaptureNotificationService.updateMediaNotificationForTab( |
| mTab.getApplicationContext(), mTab.getId(), mediaType, mTab.getUrl()); |
| } |
| if ((flags & InvalidateTypes.TITLE) != 0) { |
| // Update cached title then notify observers. |
| mTab.updateTitle(); |
| } |
| if ((flags & InvalidateTypes.URL) != 0) { |
| RewindableIterator<TabObserver> observers = mTab.getTabObservers(); |
| while (observers.hasNext()) { |
| observers.next().onUrlUpdated(mTab); |
| } |
| } |
| } |
| |
| @Override |
| public void visibleSSLStateChanged() { |
| RewindableIterator<TabObserver> observers = mTab.getTabObservers(); |
| while (observers.hasNext()) { |
| observers.next().onSSLStateUpdated(mTab); |
| } |
| } |
| |
| @Override |
| public void webContentsCreated(WebContents sourceWebContents, long openerRenderProcessId, |
| long openerRenderFrameId, String frameName, String targetUrl, |
| WebContents newWebContents) { |
| RewindableIterator<TabObserver> observers = mTab.getTabObservers(); |
| while (observers.hasNext()) { |
| observers.next().webContentsCreated(mTab, sourceWebContents, openerRenderProcessId, |
| openerRenderFrameId, frameName, targetUrl, newWebContents); |
| } |
| // The URL can't be taken from the WebContents if it's paused. Save it for later. |
| assert mWebContentsUrlMapping == null; |
| mWebContentsUrlMapping = Pair.create(newWebContents, targetUrl); |
| |
| // TODO(dfalcantara): Re-remove this once crbug.com/508366 is fixed. |
| TabCreator tabCreator = mTab.getActivity().getTabCreator(mTab.isIncognito()); |
| |
| if (tabCreator != null && tabCreator.createsTabsAsynchronously()) { |
| DocumentWebContentsDelegate.getInstance().attachDelegate(newWebContents); |
| } |
| } |
| |
| @Override |
| public void rendererUnresponsive() { |
| super.rendererUnresponsive(); |
| if (mTab.getWebContents() != null) nativeOnRendererUnresponsive(mTab.getWebContents()); |
| mTab.handleRendererResponsiveStateChanged(false); |
| } |
| |
| @Override |
| public void rendererResponsive() { |
| super.rendererResponsive(); |
| if (mTab.getWebContents() != null) nativeOnRendererResponsive(mTab.getWebContents()); |
| mTab.handleRendererResponsiveStateChanged(true); |
| } |
| |
| @Override |
| public boolean isFullscreenForTabOrPending() { |
| return mTab.getFullscreenManager() == null |
| ? false : mTab.getFullscreenManager().getPersistentFullscreenMode(); |
| } |
| |
| @Override |
| public void openNewTab(String url, String extraHeaders, ResourceRequestBody postData, |
| int disposition, boolean isRendererInitiated) { |
| mTab.openNewTab(url, extraHeaders, postData, disposition, true, isRendererInitiated); |
| } |
| |
| private Pair<WebContents, String> mWebContentsUrlMapping; |
| |
| protected TabModel getTabModel() { |
| // TODO(dfalcantara): Remove this when DocumentActivity.getTabModelSelector() |
| // can return a TabModelSelector that activateContents() can use. |
| return mTab.getTabModelSelector().getModel(mTab.isIncognito()); |
| } |
| |
| @CalledByNative |
| public boolean shouldResumeRequestsForCreatedWindow() { |
| // Pause the WebContents if an Activity has to be created for it first. |
| TabCreator tabCreator = mTab.getActivity().getTabCreator(mTab.isIncognito()); |
| assert tabCreator != null; |
| return !tabCreator.createsTabsAsynchronously(); |
| } |
| |
| @CalledByNative |
| public boolean addNewContents(WebContents sourceWebContents, WebContents webContents, |
| int disposition, Rect initialPosition, boolean userGesture) { |
| assert mWebContentsUrlMapping.first == webContents; |
| |
| TabCreator tabCreator = mTab.getActivity().getTabCreator(mTab.isIncognito()); |
| assert tabCreator != null; |
| |
| // Grab the URL, which might not be available via the Tab. |
| String url = mWebContentsUrlMapping.second; |
| mWebContentsUrlMapping = null; |
| |
| // Skip opening a new Tab if it doesn't make sense. |
| if (mTab.isClosing()) return false; |
| |
| // Creating new Tabs asynchronously requires starting a new Activity to create the Tab, |
| // so the Tab returned will always be null. There's no way to know synchronously |
| // whether the Tab is created, so assume it's always successful. |
| boolean createdSuccessfully = tabCreator.createTabWithWebContents(mTab, |
| webContents, mTab.getId(), TabLaunchType.FROM_LONGPRESS_FOREGROUND, url); |
| boolean success = tabCreator.createsTabsAsynchronously() || createdSuccessfully; |
| if (success && disposition == WindowOpenDisposition.NEW_POPUP) { |
| PolicyAuditor auditor = AppHooks.get().getPolicyAuditor(); |
| auditor.notifyAuditEvent(mTab.getApplicationContext(), |
| AuditEvent.OPEN_POPUP_URL_SUCCESS, url, ""); |
| } |
| |
| return success; |
| } |
| |
| @Override |
| public void activateContents() { |
| ChromeActivity activity = mTab.getActivity(); |
| if (activity == null) { |
| Log.e(TAG, "Activity not set activateContents(). Bailing out."); |
| return; |
| } |
| if (activity.isActivityDestroyed()) { |
| Log.e(TAG, "Activity destroyed before calling activateContents(). Bailing out."); |
| return; |
| } |
| if (!mTab.isInitialized()) { |
| Log.e(TAG, "Tab not initialized before calling activateContents(). Bailing out."); |
| return; |
| } |
| |
| // Do nothing if the tab can currently be interacted with by the user. |
| if (mTab.isUserInteractable()) return; |
| |
| TabModel model = getTabModel(); |
| int index = model.indexOf(mTab); |
| if (index == TabModel.INVALID_TAB_INDEX) return; |
| TabModelUtils.setIndex(model, index); |
| |
| // Do nothing if the activity is visible (STOPPED is the only valid invisible state as we |
| // explicitly check isActivityDestroyed above). |
| if (ApplicationStatus.getStateForActivity(activity) == ActivityState.STOPPED) { |
| bringActivityToForeground(); |
| } |
| } |
| |
| /** |
| * Brings chrome's Activity to foreground, if it is not so. |
| */ |
| protected void bringActivityToForeground() { |
| // This intent is sent in order to get the activity back to the foreground if it was |
| // not already. The previous call will activate the right tab in the context of the |
| // TabModel but will only show the tab to the user if Chrome was already in the |
| // foreground. |
| // The intent is getting the tabId mostly because it does not cost much to do so. |
| // When receiving the intent, the tab associated with the tabId should already be |
| // active. |
| // Note that calling only the intent in order to activate the tab is slightly slower |
| // because it will change the tab when the intent is handled, which happens after |
| // Chrome gets back to the foreground. |
| Intent newIntent = Tab.createBringTabToFrontIntent(mTab.getId()); |
| if (newIntent != null) { |
| newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| mTab.getApplicationContext().startActivity(newIntent); |
| } |
| } |
| |
| @Override |
| public void closeContents() { |
| // Execute outside of callback, otherwise we end up deleting the native |
| // objects in the middle of executing methods on them. |
| mHandler.removeCallbacks(mCloseContentsRunnable); |
| mHandler.post(mCloseContentsRunnable); |
| } |
| |
| @Override |
| public boolean takeFocus(boolean reverse) { |
| Activity activity = mTab.getActivity(); |
| if (activity == null) return false; |
| if (reverse) { |
| View menuButton = activity.findViewById(R.id.menu_button); |
| if (menuButton != null && menuButton.isShown()) { |
| return menuButton.requestFocus(); |
| } |
| |
| View tabSwitcherButton = activity.findViewById(R.id.tab_switcher_button); |
| if (tabSwitcherButton != null && tabSwitcherButton.isShown()) { |
| return tabSwitcherButton.requestFocus(); |
| } |
| } else { |
| View urlBar = activity.findViewById(R.id.url_bar); |
| if (urlBar != null) return urlBar.requestFocus(); |
| } |
| return false; |
| } |
| |
| @Override |
| public void handleKeyboardEvent(KeyEvent event) { |
| if (event.getAction() == KeyEvent.ACTION_DOWN && mTab.getActivity() != null) { |
| if (mTab.getActivity().onKeyDown(event.getKeyCode(), event)) return; |
| |
| // Handle the Escape key here (instead of in KeyboardShortcuts.java), so it doesn't |
| // interfere with other parts of the activity (e.g. the URL bar). |
| if (event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE && event.hasNoModifiers()) { |
| WebContents wc = mTab.getWebContents(); |
| if (wc != null) wc.stop(); |
| return; |
| } |
| } |
| handleMediaKey(event); |
| } |
| |
| /** |
| * Redispatches unhandled media keys. This allows bluetooth headphones with play/pause or |
| * other buttons to function correctly. |
| */ |
| @TargetApi(19) |
| private void handleMediaKey(KeyEvent e) { |
| if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return; |
| switch (e.getKeyCode()) { |
| case KeyEvent.KEYCODE_MUTE: |
| case KeyEvent.KEYCODE_HEADSETHOOK: |
| case KeyEvent.KEYCODE_MEDIA_PLAY: |
| case KeyEvent.KEYCODE_MEDIA_PAUSE: |
| case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: |
| case KeyEvent.KEYCODE_MEDIA_STOP: |
| case KeyEvent.KEYCODE_MEDIA_NEXT: |
| case KeyEvent.KEYCODE_MEDIA_PREVIOUS: |
| case KeyEvent.KEYCODE_MEDIA_REWIND: |
| case KeyEvent.KEYCODE_MEDIA_RECORD: |
| case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: |
| case KeyEvent.KEYCODE_MEDIA_CLOSE: |
| case KeyEvent.KEYCODE_MEDIA_EJECT: |
| case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: |
| AudioManager am = (AudioManager) mTab.getApplicationContext().getSystemService( |
| Context.AUDIO_SERVICE); |
| am.dispatchMediaKeyEvent(e); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| /** |
| * @return Whether audio is being captured. |
| */ |
| private boolean isCapturingAudio() { |
| return !mTab.isClosing() && nativeIsCapturingAudio(mTab.getWebContents()); |
| } |
| |
| /** |
| * @return Whether video is being captured. |
| */ |
| private boolean isCapturingVideo() { |
| return !mTab.isClosing() && nativeIsCapturingVideo(mTab.getWebContents()); |
| } |
| |
| /** |
| * @return Whether screen is being captured. |
| */ |
| private boolean isCapturingScreen() { |
| return !mTab.isClosing() && nativeIsCapturingScreen(mTab.getWebContents()); |
| } |
| |
| /** |
| * When STOP button in the media capture notification is clicked, pass the event to native |
| * to stop the media capture. |
| */ |
| public static void notifyStopped(int tabId) { |
| final Tab tab = TabWindowManager.getInstance().getTabById(tabId); |
| if (tab != null) nativeNotifyStopped(tab.getWebContents()); |
| } |
| |
| @CalledByNative |
| private void setOverlayMode(boolean useOverlayMode) { |
| mTab.getActivity().setOverlayMode(useOverlayMode); |
| } |
| |
| @Override |
| public int getTopControlsHeight() { |
| return mTab.getTopControlsHeight(); |
| } |
| |
| @Override |
| public int getBottomControlsHeight() { |
| return mTab.getBottomControlsHeight(); |
| } |
| |
| @Override |
| public boolean controlsResizeView() { |
| return mTab.controlsResizeView(); |
| } |
| |
| private float getDipScale() { |
| return mTab.getWindowAndroid().getDisplay().getDipScale(); |
| } |
| |
| private void enableDoubleTap(boolean enable) { |
| WebContents wc = mTab.getWebContents(); |
| GestureListenerManager gestureManager = |
| wc != null ? GestureListenerManager.fromWebContents(wc) : null; |
| if (gestureManager != null) gestureManager.updateDoubleTapSupport(enable); |
| } |
| |
| public void showFramebustBlockInfobarForTesting(String url) { |
| nativeShowFramebustBlockInfoBar(mTab.getWebContents(), url); |
| } |
| |
| private static native void nativeOnRendererUnresponsive(WebContents webContents); |
| private static native void nativeOnRendererResponsive(WebContents webContents); |
| private static native boolean nativeIsCapturingAudio(WebContents webContents); |
| private static native boolean nativeIsCapturingVideo(WebContents webContents); |
| private static native boolean nativeIsCapturingScreen(WebContents webContents); |
| private static native void nativeNotifyStopped(WebContents webContents); |
| private static native void nativeShowFramebustBlockInfoBar(WebContents webContents, String url); |
| } |