blob: 30709f1fe8a943d006e2dd39c13d2fb256f2920e [file] [log] [blame]
// 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);
}