diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-12 14:27:29 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-13 09:35:20 +0000 |
commit | c30a6232df03e1efbd9f3b226777b07e087a1122 (patch) | |
tree | e992f45784689f373bcc38d1b79a239ebe17ee23 /chromium/weblayer/browser/java/org/chromium/weblayer_private | |
parent | 7b5b123ac58f58ffde0f4f6e488bcd09aa4decd3 (diff) | |
download | qtwebengine-chromium-c30a6232df03e1efbd9f3b226777b07e087a1122.tar.gz |
BASELINE: Update Chromium to 85.0.4183.14085-based
Change-Id: Iaa42f4680837c57725b1344f108c0196741f6057
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/weblayer/browser/java/org/chromium/weblayer_private')
60 files changed, 5484 insertions, 859 deletions
diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/AccessibilityUtil.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/AccessibilityUtil.java deleted file mode 100644 index 5382c726ad7..00000000000 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/AccessibilityUtil.java +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright 2014 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. - -// TODO(sky): this is a forked copy of that from src/chrome, refactor and share. - -package org.chromium.weblayer_private; - -import android.accessibilityservice.AccessibilityServiceInfo; -import android.content.Context; -import android.content.res.Configuration; -import android.os.Build; -import android.view.accessibility.AccessibilityManager; -import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; -import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener; - -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; - -import org.chromium.base.ContextUtils; -import org.chromium.base.ObserverList; -import org.chromium.base.TraceEvent; -import org.chromium.base.task.PostTask; -import org.chromium.content_public.browser.UiThreadTaskTraits; - -import java.util.List; - -/** - * Exposes information about the current accessibility state. - */ -public class AccessibilityUtil { - /** - * An observer to be notified of accessibility status changes. - */ - public interface Observer { - /** - * @param enabled Whether a touch exploration or an accessibility service that performs can - * perform gestures is enabled. Indicates that the UI must be fully navigable using - * the accessibility view tree. - */ - void onAccessibilityModeChanged(boolean enabled); - } - - private Boolean mIsAccessibilityEnabled; - private ObserverList<Observer> mObservers; - private final class ModeChangeHandler - implements AccessibilityStateChangeListener, TouchExplorationStateChangeListener { - // AccessibilityStateChangeListener - - @Override - public final void onAccessibilityStateChanged(boolean enabled) { - updateIsAccessibilityEnabledAndNotify(); - } - - // TouchExplorationStateChangeListener - - @Override - public void onTouchExplorationStateChanged(boolean enabled) { - updateIsAccessibilityEnabledAndNotify(); - } - } - - private ModeChangeHandler mModeChangeHandler; - - protected AccessibilityUtil() {} - - /** - * Checks to see that this device has accessibility and touch exploration enabled. - * @return Whether or not accessibility and touch exploration are enabled. - */ - public boolean isAccessibilityEnabled() { - if (mModeChangeHandler == null) registerModeChangeListeners(); - if (mIsAccessibilityEnabled != null) return mIsAccessibilityEnabled; - - TraceEvent.begin("AccessibilityManager::isAccessibilityEnabled"); - - AccessibilityManager manager = getAccessibilityManager(); - boolean accessibilityEnabled = - manager != null && manager.isEnabled() && manager.isTouchExplorationEnabled(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && manager != null - && manager.isEnabled() && !accessibilityEnabled) { - List<AccessibilityServiceInfo> services = manager.getEnabledAccessibilityServiceList( - AccessibilityServiceInfo.FEEDBACK_ALL_MASK); - for (AccessibilityServiceInfo service : services) { - if (canPerformGestures(service)) { - accessibilityEnabled = true; - break; - } - } - } - - mIsAccessibilityEnabled = accessibilityEnabled; - - TraceEvent.end("AccessibilityManager::isAccessibilityEnabled"); - return mIsAccessibilityEnabled; - } - - /** - * Add {@link Observer} object. The observer will be notified of the current accessibility - * mode immediately. - * @param observer Observer object monitoring a11y mode change. - */ - public void addObserver(Observer observer) { - getObservers().addObserver(observer); - - // Notify mode change to a new observer so things are initialized correctly when Chrome - // has been re-started after closing due to the last tab being closed when homepage is - // enabled. See crbug.com/541546. - observer.onAccessibilityModeChanged(isAccessibilityEnabled()); - } - - /** - * Remove {@link Observer} object. - * @param observer Observer object monitoring a11y mode change. - */ - public void removeObserver(Observer observer) { - getObservers().removeObserver(observer); - } - - /** - * @return True if a hardware keyboard is detected. - */ - public static boolean isHardwareKeyboardAttached(Configuration c) { - return c.keyboard != Configuration.KEYBOARD_NOKEYS; - } - - private AccessibilityManager getAccessibilityManager() { - return (AccessibilityManager) ContextUtils.getApplicationContext().getSystemService( - Context.ACCESSIBILITY_SERVICE); - } - - private void registerModeChangeListeners() { - assert mModeChangeHandler == null; - mModeChangeHandler = new ModeChangeHandler(); - AccessibilityManager manager = getAccessibilityManager(); - manager.addAccessibilityStateChangeListener(mModeChangeHandler); - manager.addTouchExplorationStateChangeListener(mModeChangeHandler); - } - - /** - * Removes all global state tracking observers/listeners as well as any observers added to this. - * As this removes all observers, be very careful in calling. In general, only call when the - * application is going to be destroyed. - */ - protected void stopTrackingStateAndRemoveObservers() { - if (mObservers != null) mObservers.clear(); - if (mModeChangeHandler == null) return; - AccessibilityManager manager = getAccessibilityManager(); - manager.removeAccessibilityStateChangeListener(mModeChangeHandler); - manager.removeTouchExplorationStateChangeListener(mModeChangeHandler); - } - - /** - * Forces recalculating the value of isAccessibilityEnabled(). If the value has changed observer - * are notified. - */ - protected void updateIsAccessibilityEnabledAndNotify() { - boolean oldIsAccessibilityEnabled = isAccessibilityEnabled(); - // Setting to null forces the next call to isAccessibilityEnabled() to update the value. - mIsAccessibilityEnabled = null; - if (oldIsAccessibilityEnabled != isAccessibilityEnabled()) notifyModeChange(); - } - - private ObserverList<Observer> getObservers() { - if (mObservers == null) mObservers = new ObserverList<>(); - return mObservers; - } - - /** - * Notify all the observers of the mode change. - */ - private void notifyModeChange() { - boolean enabled = isAccessibilityEnabled(); - for (Observer observer : getObservers()) { - observer.onAccessibilityModeChanged(enabled); - } - } - - /** - * Checks whether the given {@link AccessibilityServiceInfo} can perform gestures. - * @param service The service to check. - * @return Whether the {@code service} can perform gestures. On N+, this relies on the - * capabilities the service can perform. On L & M, this looks specifically for - * Switch Access. - */ - private boolean canPerformGestures(AccessibilityServiceInfo service) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return (service.getCapabilities() - & AccessibilityServiceInfo.CAPABILITY_CAN_PERFORM_GESTURES) - != 0; - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - return service.getResolveInfo() != null - && service.getResolveInfo().toString().contains("switchaccess"); - } - return false; - } - - /** - * Set whether the device has accessibility enabled. Should be reset back to null after the test - * has finished. - * @param isEnabled whether the device has accessibility enabled. - */ - @VisibleForTesting - public void setAccessibilityEnabledForTesting(@Nullable Boolean isEnabled) { - mIsAccessibilityEnabled = isEnabled; - PostTask.runOrPostTask(UiThreadTaskTraits.DEFAULT, this::notifyModeChange); - } -} diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/AutofillView.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/AutofillView.java index 1b2992d55b4..ca8217eae14 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/AutofillView.java +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/AutofillView.java @@ -12,9 +12,12 @@ import android.view.ViewStructure; import android.view.autofill.AutofillValue; import android.widget.FrameLayout; +import org.chromium.base.annotations.VerifiesOnO; + /** * View which handles autofill support for a tab. */ +@VerifiesOnO public class AutofillView extends FrameLayout { private TabImpl mTab; diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/BrowserControlsContainerView.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/BrowserControlsContainerView.java index d860d7cdef4..ce10368c02c 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/BrowserControlsContainerView.java +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/BrowserControlsContainerView.java @@ -267,15 +267,32 @@ class BrowserControlsContainerView extends FrameLayout { if (mView == null) return; int width = right - left; int height = bottom - top; - if (height != mLastHeight || width != mLastWidth) { - mLastWidth = width; - mLastHeight = height; - if (mLastWidth > 0 && mLastHeight > 0) { - if (mViewResourceAdapter == null) { - createAdapterAndLayer(); + boolean heightChanged = height != mLastHeight; + if (!heightChanged && width == mLastWidth) return; + + mLastWidth = width; + mLastHeight = height; + if (mLastWidth > 0 && mLastHeight > 0 && mViewResourceAdapter == null) { + createAdapterAndLayer(); + } else if (mViewResourceAdapter != null) { + BrowserControlsContainerViewJni.get().setControlsSize( + mNativeBrowserControlsContainerView, mLastWidth, mLastHeight); + if (mWebContents != null) mWebContents.notifyBrowserControlsHeightChanged(); + if (heightChanged) { + // When the height changes cc doesn't generate a new frame, which means this code + // must process the change now. If cc generated a new frame, it would likely be at + // the wrong size. + if (mControlsOffset == 0) { + // The controls are completely visible. + onOffsetsChanged(0, height); } else { - BrowserControlsContainerViewJni.get().setControlsSize( - mNativeBrowserControlsContainerView, mLastWidth, mLastHeight); + // The controls are partially (and possibly completely) hidden. Snap to + // completely hidden. + if (mIsTop) { + onOffsetsChanged(-height, height); + } else { + onOffsetsChanged(height, 0); + } } } } @@ -333,7 +350,11 @@ class BrowserControlsContainerView extends FrameLayout { private void finishScroll(int contentOffsetY) { mInScroll = false; setControlsOffset(0, contentOffsetY); - mContentViewRenderView.postOnAnimation(() -> showControls()); + if (BrowserControlsContainerViewJni.get().shouldDelayVisibilityChange()) { + mContentViewRenderView.postOnAnimation(() -> showControls()); + } else { + showControls(); + } } private void setControlsOffset(int controlsOffsetY, int contentOffsetY) { @@ -350,16 +371,20 @@ class BrowserControlsContainerView extends FrameLayout { } if (mIsTop) { BrowserControlsContainerViewJni.get().setTopControlsOffset( - mNativeBrowserControlsContainerView, mControlsOffset, mContentOffset); + mNativeBrowserControlsContainerView, mContentOffset); } else { BrowserControlsContainerViewJni.get().setBottomControlsOffset( - mNativeBrowserControlsContainerView, mControlsOffset); + mNativeBrowserControlsContainerView); } } private void prepareForScroll() { mInScroll = true; - mContentViewRenderView.postOnAnimation(() -> hideControls()); + if (BrowserControlsContainerViewJni.get().shouldDelayVisibilityChange()) { + mContentViewRenderView.postOnAnimation(() -> hideControls()); + } else { + hideControls(); + } } private void hideControls() { @@ -371,6 +396,11 @@ class BrowserControlsContainerView extends FrameLayout { } @CalledByNative + private int getControlsOffset() { + return mControlsOffset; + } + + @CalledByNative private void didToggleFullscreenModeForTab(final boolean isFullscreen) { // Delay hiding until after the animation. This comes from Chrome code. if (mSystemUiFullscreenResizeRunnable != null) { @@ -410,11 +440,11 @@ class BrowserControlsContainerView extends FrameLayout { void deleteBrowserControlsContainerView(long nativeBrowserControlsContainerView); void createControlsLayer(long nativeBrowserControlsContainerView, int id); void deleteControlsLayer(long nativeBrowserControlsContainerView); - void setTopControlsOffset( - long nativeBrowserControlsContainerView, int controlsOffsetY, int contentOffsetY); - void setBottomControlsOffset(long nativeBrowserControlsContainerView, int controlsOffsetY); + void setTopControlsOffset(long nativeBrowserControlsContainerView, int contentOffsetY); + void setBottomControlsOffset(long nativeBrowserControlsContainerView); void setControlsSize(long nativeBrowserControlsContainerView, int width, int height); void updateControlsResource(long nativeBrowserControlsContainerView); void setWebContents(long nativeBrowserControlsContainerView, WebContents webContents); + boolean shouldDelayVisibilityChange(); } } diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/BrowserFragmentImpl.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/BrowserFragmentImpl.java index 33a8af8ce37..9ccb13e1704 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/BrowserFragmentImpl.java +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/BrowserFragmentImpl.java @@ -4,6 +4,7 @@ package org.chromium.weblayer_private; +import android.app.Activity; import android.content.Context; import android.content.Intent; import android.os.Bundle; @@ -127,7 +128,8 @@ public class BrowserFragmentImpl extends RemoteFragmentImpl { @Override public void onStop() { super.onStop(); - mBrowser.onFragmentStop(); + Activity activity = getActivity(); + mBrowser.onFragmentStop(activity != null && activity.getChangingConfigurations() != 0); } @Override diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/BrowserImpl.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/BrowserImpl.java index fc12eccb248..66433bde80a 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/BrowserImpl.java +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/BrowserImpl.java @@ -63,6 +63,7 @@ public class BrowserImpl extends IBrowser.Stub { private final UrlBarControllerImpl mUrlBarController; private boolean mFragmentStarted; private boolean mFragmentResumed; + private boolean mFragmentStoppedForConfigurationChange; // Cache the value instead of querying system every time. private Boolean mPasswordEchoEnabled; private Boolean mDarkThemeEnabled; @@ -106,6 +107,8 @@ public class BrowserImpl extends IBrowser.Stub { ? savedInstanceState.getByteArray(SAVED_STATE_MINIMAL_PERSISTENCE_STATE_KEY) : null; + windowAndroid.restoreInstanceState(savedInstanceState); + createAttachmentState(embedderAppContext, windowAndroid); mNativeBrowser = BrowserImplJni.get().createBrowser(profile.getNativeProfile(), this); mUrlBarController = new UrlBarControllerImpl(this, mNativeBrowser); @@ -160,6 +163,10 @@ public class BrowserImpl extends IBrowser.Stub { outState.putByteArray(SAVED_STATE_MINIMAL_PERSISTENCE_STATE_KEY, BrowserImplJni.get().getMinimalPersistenceState(mNativeBrowser)); } + + if (mWindowAndroid != null) { + mWindowAndroid.saveInstanceState(outState); + } } public void onActivityResult(int requestCode, int resultCode, Intent data) { @@ -188,6 +195,13 @@ public class BrowserImpl extends IBrowser.Stub { } @Override + public TabImpl createTab() { + TabImpl tab = new TabImpl(mProfile, mWindowAndroid); + addTab(tab); + return tab; + } + + @Override public void setSupportsEmbedding(boolean enable, IObjectWrapper valueCallback) { StrictModeWorkaround.apply(); getViewController().setSupportsEmbedding(enable, @@ -230,7 +244,7 @@ public class BrowserImpl extends IBrowser.Stub { } @CalledByNative - private void createTabForSessionRestore(long nativeTab) { + private void createJavaTabForNativeTab(long nativeTab) { new TabImpl(mProfile, mWindowAndroid, nativeTab); } @@ -309,7 +323,7 @@ public class BrowserImpl extends IBrowser.Stub { @CalledByNative private void onActiveTabChanged(TabImpl tab) { - mViewController.setActiveTab(tab); + if (mViewController != null) mViewController.setActiveTab(tab); if (mInDestroy) return; try { if (mClient != null) { @@ -388,10 +402,8 @@ public class BrowserImpl extends IBrowser.Stub { updateAllTabsAndSetActive(); } else if (persistenceInfo.mPersistenceId == null || persistenceInfo.mPersistenceId.isEmpty()) { - TabImpl tab = new TabImpl(mProfile, mWindowAndroid); - addTab(tab); - boolean set_active_result = setActiveTab(tab); - assert set_active_result; + boolean setActiveResult = setActiveTab(createTab()); + assert setActiveResult; } // else case is session restore, which will asynchronously create tabs. } @@ -404,7 +416,6 @@ public class BrowserImpl extends IBrowser.Stub { } private void destroyTabImpl(TabImpl tab) { - BrowserImplJni.get().removeTab(mNativeBrowser, tab.getNativeTab()); tab.destroy(); } @@ -438,24 +449,31 @@ public class BrowserImpl extends IBrowser.Stub { } public void onFragmentStart() { + mFragmentStoppedForConfigurationChange = false; mFragmentStarted = true; BrowserImplJni.get().onFragmentStart(mNativeBrowser); updateAllTabs(); checkPreferences(); } - public void onFragmentStop() { + public void onFragmentStop(boolean forConfigurationChange) { + mFragmentStoppedForConfigurationChange = forConfigurationChange; mFragmentStarted = false; + if (mFragmentStoppedForConfigurationChange) { + destroyAttachmentState(); + } updateAllTabs(); } public void onFragmentResume() { mFragmentResumed = true; WebLayerAccessibilityUtil.get().onBrowserResumed(); + BrowserImplJni.get().onFragmentResume(mNativeBrowser); } public void onFragmentPause() { mFragmentResumed = false; + BrowserImplJni.get().onFragmentPause(mNativeBrowser); } public boolean isStarted() { @@ -466,6 +484,10 @@ public class BrowserImpl extends IBrowser.Stub { return mFragmentResumed; } + public boolean isFragmentStoppedForConfigurationChange() { + return mFragmentStoppedForConfigurationChange; + } + private void destroyAttachmentState() { if (mLocaleReceiver != null) { mLocaleReceiver.destroy(); @@ -515,5 +537,7 @@ public class BrowserImpl extends IBrowser.Stub { byte[] persistenceCryptoKey, byte[] minimalPersistenceState); void webPreferencesChanged(long nativeBrowserImpl); void onFragmentStart(long nativeBrowserImpl); + void onFragmentResume(long nativeBrowserImpl); + void onFragmentPause(long nativeBrowserImpl); } } diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/BrowserViewController.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/BrowserViewController.java index c9ae777bd63..436113adde3 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/BrowserViewController.java +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/BrowserViewController.java @@ -17,6 +17,7 @@ import android.widget.RelativeLayout; import org.chromium.base.annotations.JNINamespace; import org.chromium.components.browser_ui.modaldialog.AppModalPresenter; +import org.chromium.components.embedder_support.view.ContentView; import org.chromium.content_public.browser.WebContents; import org.chromium.ui.modaldialog.DialogDismissalCause; import org.chromium.ui.modaldialog.ModalDialogManager; @@ -79,7 +80,7 @@ public final class BrowserViewController new BrowserControlsContainerView(context, mContentViewRenderView, this, false); mBottomControlsContainerView.setId(View.generateViewId()); mContentView = ContentView.createContentView( - context, mTopControlsContainerView.getEventOffsetHandler()); + context, mTopControlsContainerView.getEventOffsetHandler(), null /* webContents */); mContentViewRenderView.addView(mContentView, new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)); @@ -125,6 +126,11 @@ public final class BrowserViewController return mContentViewRenderView; } + /** Returns the ViewGroup into which the InfoBarContainer should be parented. **/ + public ViewGroup getInfoBarContainerParentView() { + return mContentViewRenderView; + } + public ViewGroup getContentView() { return mContentView; } @@ -137,6 +143,14 @@ public final class BrowserViewController return mAutofillView; } + // Returns the index at which the infobar container view should be inserted. + public int getDesiredInfoBarContainerViewIndex() { + // Ensure that infobars are positioned behind WebContents overlays in z-order. + // TODO(blundell): Should infobars instead be hidden while a WebContents overlay is + // presented? + return mContentViewRenderView.indexOfChild(mWebContentsOverlayView) - 1; + } + public void setActiveTab(TabImpl tab) { if (tab == mTab) return; @@ -160,8 +174,8 @@ public final class BrowserViewController new WebContentsGestureStateTracker(mContentView, webContents, this); } mAutofillView.setTab(mTab); - mContentView.setTab(mTab); + mContentView.setWebContents(webContents); mContentViewRenderView.setWebContents(webContents); mTopControlsContainerView.setWebContents(webContents); mBottomControlsContainerView.setWebContents(webContents); @@ -184,6 +198,10 @@ public final class BrowserViewController mBottomControlsContainerView.setView(view); } + public int getBottomContentHeightDelta() { + return mBottomControlsContainerView.getContentHeightDelta(); + } + public boolean compositorHasSurface() { return mContentViewRenderView.hasSurface(); } @@ -210,19 +228,24 @@ public final class BrowserViewController } @Override - public void onDialogShown(PropertyModel model) { + public void onDialogAdded(PropertyModel model) { onDialogVisibilityChanged(true); } @Override - public void onDialogHidden(PropertyModel model) { + public void onLastDialogDismissed() { onDialogVisibilityChanged(false); } private void onDialogVisibilityChanged(boolean showing) { if (WebLayerFactoryImpl.getClientMajorVersion() < 82) return; - if (mModalDialogManager.getCurrentType() == ModalDialogType.TAB) { + // ModalDialogManager.onLastDialogDismissed() may be called if |mTab| is currently null. + // This is because in some situations ModalDialogManager calls onLastDialogDismissed() even + // if there were no dialogs present and dismissDialog() is called. This matters as + // dismissDialog() may be called when |mTab| is null. + // TODO(sky): fix ModalDialogManager and remove mTab conditional. + if (mModalDialogManager.getCurrentType() == ModalDialogType.TAB && mTab != null) { try { mTab.getClient().onTabModalStateChanged(showing); } catch (RemoteException e) { diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/ConfirmInfoBar.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/ConfirmInfoBar.java new file mode 100644 index 00000000000..4b49a4e8ebf --- /dev/null +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/ConfirmInfoBar.java @@ -0,0 +1,81 @@ +// Copyright 2020 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.weblayer_private; + +import android.graphics.Bitmap; + +import androidx.annotation.ColorRes; + +import org.chromium.base.annotations.CalledByNative; +import org.chromium.components.infobars.InfoBarLayout; + +/** + * An infobar that presents the user with several buttons. + * + * TODO(newt): merge this into InfoBar.java. + */ +public class ConfirmInfoBar extends InfoBar { + /** Text shown on the primary button, e.g. "OK". */ + private final String mPrimaryButtonText; + + /** Text shown on the secondary button, e.g. "Cancel".*/ + private final String mSecondaryButtonText; + + /** Text shown on the link, e.g. "Learn more". */ + private final String mLinkText; + + protected ConfirmInfoBar(int iconDrawableId, @ColorRes int iconTintId, Bitmap iconBitmap, + String message, String linkText, String primaryButtonText, String secondaryButtonText) { + super(iconDrawableId, iconTintId, message, iconBitmap); + mPrimaryButtonText = primaryButtonText; + mSecondaryButtonText = secondaryButtonText; + mLinkText = linkText; + } + + @Override + public void createContent(InfoBarLayout layout) { + setButtons(layout, mPrimaryButtonText, mSecondaryButtonText); + if (mLinkText != null && !mLinkText.isEmpty()) layout.appendMessageLinkText(mLinkText); + } + + /** + * If your custom infobar overrides this function, YOU'RE PROBABLY DOING SOMETHING WRONG. + * + * Adds buttons to the infobar. This should only be overridden in cases where an infobar + * requires adding something other than a button for its secondary View on the bottom row + * (almost never). + * + * @param primaryText Text to display on the primary button. + * @param secondaryText Text to display on the secondary button. May be null. + */ + protected void setButtons(InfoBarLayout layout, String primaryText, String secondaryText) { + layout.setButtons(primaryText, secondaryText); + } + + @Override + public void onButtonClicked(final boolean isPrimaryButton) { + int action = isPrimaryButton ? ActionType.OK : ActionType.CANCEL; + onButtonClicked(action); + } + + /** + * Creates and begins the process for showing a ConfirmInfoBar. + * @param iconId ID corresponding to the icon that will be shown for the infobar. + * @param iconBitmap Bitmap to use if there is no equivalent Java resource for + * iconId. + * @param message Message to display to the user indicating what the infobar is for. + * @param linkText Link text to display in addition to the message. + * @param buttonOk String to display on the OK button. + * @param buttonCancel String to display on the Cancel button. + */ + @CalledByNative + private static ConfirmInfoBar create(int iconId, Bitmap iconBitmap, String message, + String linkText, String buttonOk, String buttonCancel) { + ConfirmInfoBar infoBar = new ConfirmInfoBar( + iconId, 0, iconBitmap, message, linkText, buttonOk, buttonCancel); + + return infoBar; + } +} diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/ContentView.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/ContentView.java deleted file mode 100644 index 44accce66d8..00000000000 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/ContentView.java +++ /dev/null @@ -1,519 +0,0 @@ -// Copyright 2012 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.weblayer_private; - -import android.content.Context; -import android.content.res.Configuration; -import android.graphics.Rect; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.view.DragEvent; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.view.View; -import android.view.View.OnSystemUiVisibilityChangeListener; -import android.view.ViewGroup.OnHierarchyChangeListener; -import android.view.ViewStructure; -import android.view.accessibility.AccessibilityNodeProvider; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputConnection; -import android.widget.RelativeLayout; - -import org.chromium.base.ObserverList; -import org.chromium.base.TraceEvent; -import org.chromium.base.compat.ApiHelperForO; -import org.chromium.content_public.browser.ImeAdapter; -import org.chromium.content_public.browser.RenderCoordinates; -import org.chromium.content_public.browser.SmartClipProvider; -import org.chromium.content_public.browser.ViewEventSink; -import org.chromium.content_public.browser.WebContents; -import org.chromium.content_public.browser.WebContentsAccessibility; -import org.chromium.ui.base.EventForwarder; -import org.chromium.ui.base.EventOffsetHandler; - -/** - * The containing view for {@link WebContents} that exists in the Android UI hierarchy and exposes - * the various {@link View} functionality to it. - */ -public class ContentView extends RelativeLayout - implements ViewEventSink.InternalAccessDelegate, SmartClipProvider, - OnHierarchyChangeListener, OnSystemUiVisibilityChangeListener { - private static final String TAG = "ContentView"; - - // Default value to signal that the ContentView's size need not be overridden. - public static final int DEFAULT_MEASURE_SPEC = - MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); - - private TabImpl mTab; - private WebContents mWebContents; - private boolean mIsObscuredForAccessibility; - private final ObserverList<OnHierarchyChangeListener> mHierarchyChangeListeners = - new ObserverList<>(); - private final ObserverList<OnSystemUiVisibilityChangeListener> mSystemUiChangeListeners = - new ObserverList<>(); - - /** - * The desired size of this view in {@link MeasureSpec}. Set by the host - * when it should be different from that of the parent. - */ - private int mDesiredWidthMeasureSpec = DEFAULT_MEASURE_SPEC; - private int mDesiredHeightMeasureSpec = DEFAULT_MEASURE_SPEC; - - private EventOffsetHandler mEventOffsetHandler; - - /** - * Constructs a new ContentView for the appropriate Android version. - * @param context The Context the view is running in, through which it can - * access the current theme, resources, etc. - * @param webContents The WebContents managing this content view. - * @return an instance of a ContentView. - */ - public static ContentView createContentView( - Context context, EventOffsetHandler eventOffsetHandler) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - return new ContentViewApi23(context, eventOffsetHandler); - } - return new ContentView(context, eventOffsetHandler); - } - - /** - * Creates an instance of a ContentView. - * @param context The Context the view is running in, through which it can - * access the current theme, resources, etc. - * @param webContents A pointer to the WebContents managing this content view. - */ - ContentView(Context context, EventOffsetHandler eventOffsetHandler) { - super(context, null, android.R.attr.webViewStyle); - - if (getScrollBarStyle() == View.SCROLLBARS_INSIDE_OVERLAY) { - setHorizontalScrollBarEnabled(false); - setVerticalScrollBarEnabled(false); - } - - mEventOffsetHandler = eventOffsetHandler; - - setFocusable(true); - setFocusableInTouchMode(true); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - ApiHelperForO.setDefaultFocusHighlightEnabled(this, false); - } - - setOnHierarchyChangeListener(this); - setOnSystemUiVisibilityChangeListener(this); - } - - protected WebContentsAccessibility getWebContentsAccessibility() { - return mWebContents != null && !mWebContents.isDestroyed() - ? WebContentsAccessibility.fromWebContents(mWebContents) - : null; - } - - protected TabImpl getTab() { - return mTab; - } - - public void setTab(TabImpl tab) { - mTab = tab; - boolean wasFocused = isFocused(); - boolean wasWindowFocused = hasWindowFocus(); - boolean wasAttached = isAttachedToWindow(); - boolean wasObscured = mIsObscuredForAccessibility; - if (wasFocused) onFocusChanged(false, View.FOCUS_FORWARD, null); - if (wasWindowFocused) onWindowFocusChanged(false); - if (wasAttached) onDetachedFromWindow(); - if (wasObscured) setIsObscuredForAccessibility(false); - mWebContents = mTab != null ? mTab.getWebContents() : null; - if (wasFocused) onFocusChanged(true, View.FOCUS_FORWARD, null); - if (wasWindowFocused) onWindowFocusChanged(true); - if (wasAttached) onAttachedToWindow(); - if (wasObscured) setIsObscuredForAccessibility(true); - } - - /** - * Control whether WebContentsAccessibility will respond to accessibility requests. - */ - public void setIsObscuredForAccessibility(boolean isObscured) { - if (mIsObscuredForAccessibility == isObscured) return; - mIsObscuredForAccessibility = isObscured; - WebContentsAccessibility wcax = getWebContentsAccessibility(); - if (wcax == null) return; - wcax.setObscuredByAnotherView(mIsObscuredForAccessibility); - } - - @Override - public boolean performAccessibilityAction(int action, Bundle arguments) { - WebContentsAccessibility wcax = getWebContentsAccessibility(); - return wcax != null && wcax.supportsAction(action) - ? wcax.performAction(action, arguments) - : super.performAccessibilityAction(action, arguments); - } - - /** - * Set the desired size of the view. The values are in {@link MeasureSpec}. - * @param width The width of the content view. - * @param height The height of the content view. - */ - public void setDesiredMeasureSpec(int width, int height) { - mDesiredWidthMeasureSpec = width; - mDesiredHeightMeasureSpec = height; - } - - @Override - public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) { - assert listener == this : "Use add/removeOnHierarchyChangeListener instead."; - super.setOnHierarchyChangeListener(listener); - } - - /** - * Registers the given listener to receive state changes for the content view hierarchy. - * @param listener Listener to receive view hierarchy state changes. - */ - public void addOnHierarchyChangeListener(OnHierarchyChangeListener listener) { - mHierarchyChangeListeners.addObserver(listener); - } - - /** - * Unregisters the given listener from receiving state changes for the content view hierarchy. - * @param listener Listener that doesn't want to receive view hierarchy state changes. - */ - public void removeOnHierarchyChangeListener(OnHierarchyChangeListener listener) { - mHierarchyChangeListeners.removeObserver(listener); - } - - @Override - public void setOnSystemUiVisibilityChangeListener(OnSystemUiVisibilityChangeListener listener) { - assert listener == this : "Use add/removeOnSystemUiVisibilityChangeListener instead."; - super.setOnSystemUiVisibilityChangeListener(listener); - } - - /** - * Registers the given listener to receive system UI visibility state changes. - * @param listener Listener to receive system UI visibility state changes. - */ - public void addOnSystemUiVisibilityChangeListener(OnSystemUiVisibilityChangeListener listener) { - mSystemUiChangeListeners.addObserver(listener); - } - - /** - * Unregisters the given listener from receiving system UI visibility state changes. - * @param listener Listener that doesn't want to receive state changes. - */ - public void removeOnSystemUiVisibilityChangeListener( - OnSystemUiVisibilityChangeListener listener) { - mSystemUiChangeListeners.removeObserver(listener); - } - - // View.OnHierarchyChangeListener implementation - - @Override - public void onChildViewRemoved(View parent, View child) { - for (OnHierarchyChangeListener listener : mHierarchyChangeListeners) { - listener.onChildViewRemoved(parent, child); - } - } - - @Override - public void onChildViewAdded(View parent, View child) { - for (OnHierarchyChangeListener listener : mHierarchyChangeListeners) { - listener.onChildViewAdded(parent, child); - } - } - - // View.OnHierarchyChangeListener implementation - - @Override - public void onSystemUiVisibilityChange(int visibility) { - for (OnSystemUiVisibilityChangeListener listener : mSystemUiChangeListeners) { - listener.onSystemUiVisibilityChange(visibility); - } - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - if (mDesiredWidthMeasureSpec != DEFAULT_MEASURE_SPEC) { - widthMeasureSpec = mDesiredWidthMeasureSpec; - } - if (mDesiredHeightMeasureSpec != DEFAULT_MEASURE_SPEC) { - heightMeasureSpec = mDesiredHeightMeasureSpec; - } - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - } - - @Override - public AccessibilityNodeProvider getAccessibilityNodeProvider() { - WebContentsAccessibility wcax = getWebContentsAccessibility(); - AccessibilityNodeProvider provider = - (wcax != null) ? wcax.getAccessibilityNodeProvider() : null; - return (provider != null) ? provider : super.getAccessibilityNodeProvider(); - } - - // Needed by ViewEventSink.InternalAccessDelegate - @Override - public void onScrollChanged(int l, int t, int oldl, int oldt) { - super.onScrollChanged(l, t, oldl, oldt); - } - - @Override - public InputConnection onCreateInputConnection(EditorInfo outAttrs) { - // Calls may come while/after WebContents is destroyed. See https://crbug.com/821750#c8. - if (mWebContents == null || mWebContents.isDestroyed()) return null; - return ImeAdapter.fromWebContents(mWebContents).onCreateInputConnection(outAttrs); - } - - @Override - public boolean onCheckIsTextEditor() { - if (mWebContents == null || mWebContents.isDestroyed()) return false; - return ImeAdapter.fromWebContents(mWebContents).onCheckIsTextEditor(); - } - - @Override - protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { - try { - TraceEvent.begin("ContentView.onFocusChanged"); - super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); - if (mWebContents != null) { - getViewEventSink().setHideKeyboardOnBlur(true); - getViewEventSink().onViewFocusChanged(gainFocus); - } - } finally { - TraceEvent.end("ContentView.onFocusChanged"); - } - } - - @Override - public void onWindowFocusChanged(boolean hasWindowFocus) { - super.onWindowFocusChanged(hasWindowFocus); - if (mWebContents != null) { - getViewEventSink().onWindowFocusChanged(hasWindowFocus); - } - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - EventForwarder forwarder = getEventForwarder(); - return forwarder != null ? forwarder.onKeyUp(keyCode, event) : false; - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - if (!isFocused()) return super.dispatchKeyEvent(event); - EventForwarder forwarder = getEventForwarder(); - return forwarder != null ? forwarder.dispatchKeyEvent(event) : false; - } - - @Override - public boolean onDragEvent(DragEvent event) { - EventForwarder forwarder = getEventForwarder(); - return forwarder != null ? forwarder.onDragEvent(event, this) : false; - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent e) { - boolean ret = super.onInterceptTouchEvent(e); - mEventOffsetHandler.onInterceptTouchEvent(e); - return ret; - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - EventForwarder forwarder = getEventForwarder(); - boolean ret = forwarder != null ? forwarder.onTouchEvent(event) : false; - mEventOffsetHandler.onTouchEvent(event); - return ret; - } - - @Override - public boolean onInterceptHoverEvent(MotionEvent e) { - mEventOffsetHandler.onInterceptHoverEvent(e); - return super.onInterceptHoverEvent(e); - } - - @Override - public boolean dispatchDragEvent(DragEvent e) { - mEventOffsetHandler.onPreDispatchDragEvent(e.getAction()); - boolean ret = super.dispatchDragEvent(e); - mEventOffsetHandler.onPostDispatchDragEvent(e.getAction()); - return ret; - } - - /** - * Mouse move events are sent on hover enter, hover move and hover exit. - * They are sent on hover exit because sometimes it acts as both a hover - * move and hover exit. - */ - @Override - public boolean onHoverEvent(MotionEvent event) { - EventForwarder forwarder = getEventForwarder(); - boolean consumed = forwarder != null ? forwarder.onHoverEvent(event) : false; - WebContentsAccessibility wcax = getWebContentsAccessibility(); - if (wcax != null && !wcax.isTouchExplorationEnabled()) super.onHoverEvent(event); - return consumed; - } - - @Override - public boolean onGenericMotionEvent(MotionEvent event) { - EventForwarder forwarder = getEventForwarder(); - return forwarder != null ? forwarder.onGenericMotionEvent(event) : false; - } - - private EventForwarder getEventForwarder() { - return mWebContents != null ? mWebContents.getEventForwarder() : null; - } - - private ViewEventSink getViewEventSink() { - return mWebContents != null ? ViewEventSink.from(mWebContents) : null; - } - - @Override - public boolean performLongClick() { - return false; - } - - @Override - protected void onConfigurationChanged(Configuration newConfig) { - if (mWebContents != null) { - getViewEventSink().onConfigurationChanged(newConfig); - } - super.onConfigurationChanged(newConfig); - } - - /** - * Currently the ContentView scrolling happens in the native side. In - * the Java view system, it is always pinned at (0, 0). scrollBy() and scrollTo() - * are overridden, so that View's mScrollX and mScrollY will be unchanged at - * (0, 0). This is critical for drawing ContentView correctly. - */ - @Override - public void scrollBy(int x, int y) { - EventForwarder forwarder = getEventForwarder(); - if (forwarder != null) forwarder.scrollBy(x, y); - } - - @Override - public void scrollTo(int x, int y) { - EventForwarder forwarder = getEventForwarder(); - if (forwarder != null) forwarder.scrollTo(x, y); - } - - @Override - protected int computeHorizontalScrollExtent() { - RenderCoordinates rc = getRenderCoordinates(); - return rc != null ? rc.getLastFrameViewportWidthPixInt() : 0; - } - - @Override - protected int computeHorizontalScrollOffset() { - RenderCoordinates rc = getRenderCoordinates(); - return rc != null ? rc.getScrollXPixInt() : 0; - } - - @Override - protected int computeHorizontalScrollRange() { - RenderCoordinates rc = getRenderCoordinates(); - return rc != null ? rc.getContentWidthPixInt() : 0; - } - - @Override - protected int computeVerticalScrollExtent() { - RenderCoordinates rc = getRenderCoordinates(); - return rc != null ? rc.getLastFrameViewportHeightPixInt() : 0; - } - - @Override - protected int computeVerticalScrollOffset() { - RenderCoordinates rc = getRenderCoordinates(); - return rc != null ? rc.getScrollYPixInt() : 0; - } - - @Override - protected int computeVerticalScrollRange() { - RenderCoordinates rc = getRenderCoordinates(); - return rc != null ? rc.getContentHeightPixInt() : 0; - } - - private RenderCoordinates getRenderCoordinates() { - return mWebContents != null ? RenderCoordinates.fromWebContents(mWebContents) : null; - } - - // End RelativeLayout overrides. - - @Override - public boolean awakenScrollBars(int startDelay, boolean invalidate) { - // For the default implementation of ContentView which draws the scrollBars on the native - // side, calling this function may get us into a bad state where we keep drawing the - // scrollBars, so disable it by always returning false. - if (getScrollBarStyle() == View.SCROLLBARS_INSIDE_OVERLAY) return false; - return super.awakenScrollBars(startDelay, invalidate); - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - if (mWebContents != null) { - getViewEventSink().onAttachedToWindow(); - } - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - if (mWebContents != null) { - getViewEventSink().onDetachedFromWindow(); - } - } - - // Implements SmartClipProvider - @Override - public void extractSmartClipData(int x, int y, int width, int height) { - if (mWebContents != null) { - mWebContents.requestSmartClipExtract(x, y, width, height); - } - } - - // Implements SmartClipProvider - @Override - public void setSmartClipResultHandler(final Handler resultHandler) { - if (mWebContents != null) { - mWebContents.setSmartClipResultHandler(resultHandler); - } - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // Start Implementation of ViewEventSink.InternalAccessDelegate // - /////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public boolean super_onKeyUp(int keyCode, KeyEvent event) { - return super.onKeyUp(keyCode, event); - } - - @Override - public boolean super_dispatchKeyEvent(KeyEvent event) { - return super.dispatchKeyEvent(event); - } - - @Override - public boolean super_onGenericMotionEvent(MotionEvent event) { - return super.onGenericMotionEvent(event); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // End Implementation of ViewEventSink.InternalAccessDelegate // - /////////////////////////////////////////////////////////////////////////////////////////////// - - private static class ContentViewApi23 extends ContentView { - public ContentViewApi23(Context context, EventOffsetHandler eventOffsetHandler) { - super(context, eventOffsetHandler); - } - - @Override - public void onProvideVirtualStructure(final ViewStructure structure) { - WebContentsAccessibility wcax = getWebContentsAccessibility(); - if (wcax != null) wcax.onProvideVirtualStructure(structure, false); - } - } -} diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/ContentViewRenderView.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/ContentViewRenderView.java index b74f5266590..ac0235eaeb2 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/ContentViewRenderView.java +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/ContentViewRenderView.java @@ -86,6 +86,7 @@ public class ContentViewRenderView extends RelativeLayout { int width, int height); // |cacheBackBuffer| will delay destroying the EGLSurface until after the next swap. void surfaceDestroyed(boolean cacheBackBuffer); + void surfaceRedrawNeededAsync(Runnable drawingFinished); } private final ArrayList<TrackedRunnable> mPendingRunnables = new ArrayList<>(); @@ -160,6 +161,11 @@ public class ContentViewRenderView extends RelativeLayout { mNativeContentViewRenderView, cacheBackBuffer); mCompositorHasSurface = false; } + + @Override + public void surfaceRedrawNeededAsync(Runnable drawingFinished) { + assert false; // NOTREACHED. + } } // Abstract differences between SurfaceView and TextureView behind this class. @@ -220,6 +226,7 @@ public class ContentViewRenderView extends RelativeLayout { private final TextureViewSurfaceTextureListener mSurfaceTextureListener; private final ArrayList<ValueCallback<Boolean>> mModeCallbacks = new ArrayList<>(); + private ArrayList<Runnable> mSurfaceRedrawNeededCallbacks; public SurfaceData(@Mode int mode, FrameLayout parent, SurfaceEventListener listener, int backgroundColor, Runnable evict) { @@ -302,6 +309,7 @@ public class ContentViewRenderView extends RelativeLayout { mListener.surfaceDestroyed(mCachedSurfaceNeedsEviction); mNeedsOnSurfaceDestroyed = false; } + runSurfaceRedrawNeededCallbacks(); if (mMode == MODE_SURFACE_VIEW) { mSurfaceView.getHolder().removeCallback(mSurfaceCallback); @@ -403,6 +411,15 @@ public class ContentViewRenderView extends RelativeLayout { return false; } + public void runSurfaceRedrawNeededCallbacks() { + ArrayList<Runnable> callbacks = mSurfaceRedrawNeededCallbacks; + mSurfaceRedrawNeededCallbacks = null; + if (callbacks == null) return; + for (Runnable r : callbacks) { + r.run(); + } + } + private void destroyPreviousData() { if (mPrevSurfaceDataNeedsDestroy != null) { mPrevSurfaceDataNeedsDestroy.destroy(); @@ -445,6 +462,22 @@ public class ContentViewRenderView extends RelativeLayout { assert mNeedsOnSurfaceDestroyed; mListener.surfaceDestroyed(cacheBackBuffer); mNeedsOnSurfaceDestroyed = false; + runSurfaceRedrawNeededCallbacks(); + } + + @Override + public void surfaceRedrawNeededAsync(Runnable drawingFinished) { + if (mMarkedForDestroy) { + drawingFinished.run(); + return; + } + assert mNativeContentViewRenderView != 0; + assert this == ContentViewRenderView.this.mCurrent; + if (mSurfaceRedrawNeededCallbacks == null) { + mSurfaceRedrawNeededCallbacks = new ArrayList<>(); + } + mSurfaceRedrawNeededCallbacks.add(drawingFinished); + ContentViewRenderViewJni.get().setNeedsRedraw(mNativeContentViewRenderView); } private void runCallbacks() { @@ -470,7 +503,7 @@ public class ContentViewRenderView extends RelativeLayout { } // Adapter for SurfaceHoolder.Callback. - private static class SurfaceHolderCallback implements SurfaceHolder.Callback { + private static class SurfaceHolderCallback implements SurfaceHolder.Callback2 { private final SurfaceEventListener mListener; public SurfaceHolderCallback(SurfaceEventListener listener) { @@ -491,6 +524,16 @@ public class ContentViewRenderView extends RelativeLayout { public void surfaceDestroyed(SurfaceHolder holder) { mListener.surfaceDestroyed(false /* cacheBackBuffer */); } + + @Override + public void surfaceRedrawNeeded(SurfaceHolder holder) { + // Intentionally not implemented. + } + + @Override + public void surfaceRedrawNeededAsync(SurfaceHolder holder, Runnable drawingFinished) { + mListener.surfaceRedrawNeededAsync(drawingFinished); + } } // Adapter for TextureView.SurfaceTextureListener. @@ -697,7 +740,9 @@ public class ContentViewRenderView extends RelativeLayout { mWebContents = webContents; if (webContents != null) { - updateWebContentsSize(); + if (getWidth() != 0 && getHeight() != 0) { + updateWebContentsSize(); + } ContentViewRenderViewJni.get().onPhysicalBackingSizeChanged( mNativeContentViewRenderView, webContents, mPhysicalWidth, mPhysicalHeight); } @@ -719,6 +764,13 @@ public class ContentViewRenderView extends RelativeLayout { return mCurrent.didSwapFrame(); } + @CalledByNative + private void didSwapBuffers(boolean sizeMatches) { + assert mCurrent != null; + if (!sizeMatches) return; + mCurrent.runSurfaceRedrawNeededCallbacks(); + } + private void evictCachedSurface() { if (mNativeContentViewRenderView == 0) return; ContentViewRenderViewJni.get().evictCachedSurface(mNativeContentViewRenderView); @@ -752,6 +804,7 @@ public class ContentViewRenderView extends RelativeLayout { void surfaceDestroyed(long nativeContentViewRenderView, boolean cacheBackBuffer); void surfaceChanged(long nativeContentViewRenderView, boolean canBeUsedWithSurfaceControl, int format, int width, int height, Surface surface); + void setNeedsRedraw(long nativeContentViewRenderView); void evictCachedSurface(long nativeContentViewRenderView); ResourceManager getResourceManager(long nativeContentViewRenderView); } diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/CrashReporterControllerImpl.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/CrashReporterControllerImpl.java index 1f147c0467c..5318bcc20d0 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/CrashReporterControllerImpl.java +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/CrashReporterControllerImpl.java @@ -204,6 +204,7 @@ public final class CrashReporterControllerImpl extends ICrashReporterController. private String[] processNewMinidumpsOnBackgroundThread() { Map<String, Map<String, String>> crashesInfoMap = getCrashFileManager().importMinidumpsCrashKeys(); + if (crashesInfoMap == null) return new String[0]; ArrayList<String> localIds = new ArrayList<>(crashesInfoMap.size()); for (Map.Entry<String, Map<String, String>> entry : crashesInfoMap.entrySet()) { JSONObject crashKeysJson = new JSONObject(entry.getValue()); diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/DownloadImpl.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/DownloadImpl.java index 694e8839498..b1046065bc3 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/DownloadImpl.java +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/DownloadImpl.java @@ -24,7 +24,6 @@ import org.chromium.components.browser_ui.notifications.NotificationManagerProxy import org.chromium.components.browser_ui.notifications.NotificationManagerProxyImpl; import org.chromium.components.browser_ui.notifications.NotificationMetadata; import org.chromium.components.browser_ui.notifications.PendingIntentProvider; -import org.chromium.components.browser_ui.notifications.channels.ChannelsInitializer; import org.chromium.components.browser_ui.util.DownloadUtils; import org.chromium.weblayer_private.interfaces.APICallException; import org.chromium.weblayer_private.interfaces.DownloadError; @@ -342,18 +341,14 @@ public final class DownloadImpl extends IDownload.Stub { PendingIntentProvider deletePendingIntent = PendingIntentProvider.getBroadcast(context, mNotificationId, deleteIntent, 0); - ChannelsInitializer channelsInitializer = new ChannelsInitializer(notificationManager, - WebLayerNotificationChannels.getInstance(), context.getResources()); - @DownloadState int state = getState(); String channelId = state == DownloadState.COMPLETE ? WebLayerNotificationChannels.ChannelId.COMPLETED_DOWNLOADS : WebLayerNotificationChannels.ChannelId.ACTIVE_DOWNLOADS; - WebLayerNotificationBuilder builder = - new WebLayerNotificationBuilder(context, channelId, channelsInitializer, - new NotificationMetadata(0, NOTIFICATION_TAG, mNotificationId)); + WebLayerNotificationBuilder builder = WebLayerNotificationBuilder.create( + channelId, new NotificationMetadata(0, NOTIFICATION_TAG, mNotificationId)); builder.setOngoing(true) .setDeleteIntent(deletePendingIntent) .setPriorityBeforeO(NotificationCompat.PRIORITY_DEFAULT); diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/ExternalNavigationDelegateImpl.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/ExternalNavigationDelegateImpl.java index 6cd383bebbf..253c35212d0 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/ExternalNavigationDelegateImpl.java +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/ExternalNavigationDelegateImpl.java @@ -8,6 +8,8 @@ import android.app.Activity; import android.content.Intent; import android.content.pm.ResolveInfo; +import androidx.annotation.Nullable; + import org.chromium.base.ContextUtils; import org.chromium.base.PackageManagerUtils; import org.chromium.components.embedder_support.util.UrlUtilities; @@ -16,14 +18,18 @@ import org.chromium.components.external_intents.ExternalNavigationDelegate.Start import org.chromium.components.external_intents.ExternalNavigationHandler; import org.chromium.components.external_intents.ExternalNavigationHandler.OverrideUrlLoadingResult; import org.chromium.components.external_intents.ExternalNavigationParams; +import org.chromium.components.webapk.lib.client.ChromeWebApkHostSignature; +import org.chromium.components.webapk.lib.client.WebApkValidator; import org.chromium.content_public.browser.LoadUrlParams; import org.chromium.content_public.browser.WebContents; import org.chromium.ui.base.WindowAndroid; +import org.chromium.url.Origin; /** * WebLayer's implementation of the {@link ExternalNavigationDelegate}. */ public class ExternalNavigationDelegateImpl implements ExternalNavigationDelegate { + private static boolean sWebApkValidatorInitialized; private final TabImpl mTab; private boolean mTabDestroyed; @@ -154,7 +160,8 @@ public class ExternalNavigationDelegateImpl implements ExternalNavigationDelegat @Override // This is relevant only if the intent ends up being handled by this app, which does not happen // for WebLayer. - public void maybeSetUserGesture(Intent intent) {} + public void maybeSetRequestMetadata(Intent intent, boolean hasUserGesture, + boolean isRendererInitiated, @Nullable Origin initiatorOrigin) {} @Override // This is relevant only if the intent ends up being handled by this app, which does not happen @@ -205,8 +212,12 @@ public class ExternalNavigationDelegateImpl implements ExternalNavigationDelegat @Override public boolean isValidWebApk(String packageName) { - // TODO(crbug.com/1063874): Determine whether to refine this. - return false; + if (!sWebApkValidatorInitialized) { + WebApkValidator.init(ChromeWebApkHostSignature.EXPECTED_SIGNATURE, + ChromeWebApkHostSignature.PUBLIC_KEY); + sWebApkValidatorInitialized = true; + } + return WebApkValidator.isValidWebApk(ContextUtils.getApplicationContext(), packageName); } @Override diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/InfoBar.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/InfoBar.java new file mode 100644 index 00000000000..69e4754d98b --- /dev/null +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/InfoBar.java @@ -0,0 +1,331 @@ +// Copyright 2020 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.weblayer_private; + +import android.content.Context; +import android.graphics.Bitmap; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.ColorRes; +import androidx.annotation.Nullable; + +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.annotations.NativeMethods; +import org.chromium.chrome.browser.infobar.InfoBarIdentifier; +import org.chromium.components.infobars.InfoBarInteractionHandler; +import org.chromium.components.infobars.InfoBarLayout; +import org.chromium.ui.modelutil.PropertyModel; + +/** + * The base class for all InfoBar classes. + * Note that infobars expire by default when a new navigation occurs. + * Make sure to use setExpireOnNavigation(false) if you want an infobar to be sticky. + */ +@JNINamespace("weblayer") +public abstract class InfoBar implements InfoBarInteractionHandler, InfoBarUiItem { + private static final String TAG = "InfoBar"; + + /** + * Interface for InfoBar to interact with its container. + */ + public interface Container { + /** + * @return True if the infobar is in front. + */ + boolean isFrontInfoBar(InfoBar infoBar); + + /** + * Remove the infobar from its container. + * @param infoBar InfoBar to remove from the View hierarchy. + */ + void removeInfoBar(InfoBar infoBar); + + /** + * Notifies that an infobar's View ({@link InfoBar#getView}) has changed. + */ + void notifyInfoBarViewChanged(); + + /** + * @return True if the container's destroy() method has been called. + */ + boolean isDestroyed(); + } + + private final int mIconDrawableId; + private final Bitmap mIconBitmap; + private final @ColorRes int mIconTintId; + private final CharSequence mMessage; + + private @Nullable Container mContainer; + private @Nullable View mView; + private @Nullable Context mContext; + + private boolean mIsDismissed; + private boolean mControlsEnabled = true; + + private @Nullable PropertyModel mModel; + + // This points to the InfoBarAndroid class not any of its subclasses. + private long mNativeInfoBarPtr; + + /** + * Constructor for regular infobars. + * @param iconDrawableId ID of the resource to use for the Icon. If 0, no icon will be shown. + * @param iconTintId The {@link ColorRes} used as tint for the {@code iconDrawableId}. + * @param message The message to show in the infobar. + * @param iconBitmap Icon to draw, in bitmap form. Used mainly for generated icons. + */ + public InfoBar( + int iconDrawableId, @ColorRes int iconTintId, CharSequence message, Bitmap iconBitmap) { + mIconDrawableId = iconDrawableId; + mIconBitmap = iconBitmap; + mIconTintId = iconTintId; + mMessage = message; + } + + /** + * Stores a pointer to the native-side counterpart of this InfoBar. + * @param nativeInfoBarPtr Pointer to the native InfoBarAndroid, not to its subclass. + */ + @CalledByNative + private final void setNativeInfoBar(long nativeInfoBarPtr) { + mNativeInfoBarPtr = nativeInfoBarPtr; + } + + @CalledByNative + protected void onNativeDestroyed() { + mNativeInfoBarPtr = 0; + } + + /** + * Sets the Context used when creating the InfoBar. + */ + public void setContext(Context context) { + mContext = context; + } + + /** + * @return The {@link Context} used to create the InfoBar. This will be null before the InfoBar + * is added to an {@link InfoBarContainer}, or after the InfoBar is closed. + */ + @Nullable + protected Context getContext() { + return mContext; + } + + /** + * Creates the View that represents the InfoBar. + * @return The View representing the InfoBar. + */ + public final View createView() { + assert mContext != null; + + if (usesCompactLayout()) { + InfoBarCompactLayout layout = new InfoBarCompactLayout( + mContext, this, mIconDrawableId, mIconTintId, mIconBitmap); + createCompactLayoutContent(layout); + mView = layout; + } else { + InfoBarLayout layout = new InfoBarLayout( + mContext, this, mIconDrawableId, mIconTintId, mIconBitmap, mMessage); + createContent(layout); + layout.onContentCreated(); + mView = layout; + } + + return mView; + } + + /** + * @return The model for this infobar if one was created. + */ + @Nullable + PropertyModel getModel() { + return mModel; + } + + /** + * If this returns true, the infobar contents will be replaced with a one-line layout. + * When overriding this, also override {@link #getAccessibilityMessage}. + */ + protected boolean usesCompactLayout() { + return false; + } + + /** + * Prepares the InfoBar for display and adds InfoBar-specific controls to the layout. + * @param layout Layout containing all of the controls. + */ + protected void createContent(InfoBarLayout layout) {} + + /** + * Prepares and inserts views into an {@link InfoBarCompactLayout}. + * {@link #usesCompactLayout} must return 'true' for this function to be called. + * @param layout Layout to plug views into. + */ + protected void createCompactLayoutContent(InfoBarCompactLayout layout) {} + + /** + * Replaces the View currently shown in the infobar with the given View. Triggers the swap + * animation via the InfoBarContainer. + */ + protected void replaceView(View newView) { + mView = newView; + mContainer.notifyInfoBarViewChanged(); + } + + /** + * Returns the View shown in this infobar. Only valid after createView() has been called. + */ + @Override + public View getView() { + return mView; + } + + /** + * Returns the accessibility message to announce when this infobar is first shown. + * Override this if the InfoBar doesn't have {@link R.id.infobar_message}. It is usually the + * case when it is in CompactLayout. + */ + protected CharSequence getAccessibilityMessage(CharSequence defaultTitle) { + return defaultTitle == null ? "" : defaultTitle; + } + + @Override + public CharSequence getAccessibilityText() { + if (mView == null) return ""; + + CharSequence title = null; + TextView messageView = (TextView) mView.findViewById(R.id.infobar_message); + if (messageView != null) { + title = messageView.getText(); + } + title = getAccessibilityMessage(title); + if (title.length() > 0) { + title = title + " "; + } + // TODO(crbug/773717): Avoid string concatenation due to i18n. + return title + mContext.getString(R.string.weblayer_bottom_bar_screen_position); + } + + @Override + public int getPriority() { + return InfoBarPriority.PAGE_TRIGGERED; + } + + @Override + @InfoBarIdentifier + public int getInfoBarIdentifier() { + if (mNativeInfoBarPtr == 0) return InfoBarIdentifier.INVALID; + return InfoBarJni.get().getInfoBarIdentifier(mNativeInfoBarPtr, InfoBar.this); + } + + /** + * @return whether the infobar actually needed closing. + */ + @CalledByNative + private boolean closeInfoBar() { + if (!mIsDismissed) { + mIsDismissed = true; + if (!mContainer.isDestroyed()) { + // If the container was destroyed, it's already been emptied of all its infobars. + onStartedHiding(); + mContainer.removeInfoBar(this); + } + mContainer = null; + mView = null; + mContext = null; + return true; + } + return false; + } + + /** + * @return If the infobar is the front infobar (i.e. visible and not hidden behind other + * infobars). + */ + public boolean isFrontInfoBar() { + return mContainer.isFrontInfoBar(this); + } + + /** + * Called just before the Java infobar has begun hiding. Give the chance to clean up any child + * UI that may remain open. + */ + protected void onStartedHiding() {} + + /** + * Returns pointer to native InfoBarAndroid instance. + * TODO(crbug/1056346): The function is used in subclasses typically to get Tab reference. When + * Tab is modularized, replace this function with the one that returns Tab reference. + */ + protected long getNativeInfoBarPtr() { + return mNativeInfoBarPtr; + } + + /** + * Sets the Container that displays the InfoBar. + */ + public void setContainer(Container container) { + mContainer = container; + } + + /** + * @return Whether or not this InfoBar is already dismissed (i.e. closed). + */ + protected boolean isDismissed() { + return mIsDismissed; + } + + @Override + public boolean areControlsEnabled() { + return mControlsEnabled; + } + + @Override + public void setControlsEnabled(boolean state) { + mControlsEnabled = state; + } + + @Override + public void onClick() { + setControlsEnabled(false); + } + + @Override + public void onButtonClicked(boolean isPrimaryButton) {} + + @Override + public void onLinkClicked() { + if (mNativeInfoBarPtr != 0) InfoBarJni.get().onLinkClicked(mNativeInfoBarPtr, InfoBar.this); + } + + /** + * Performs some action related to the button being clicked. + * @param action The type of action defined in {@link ActionType} in this class. + */ + protected void onButtonClicked(@ActionType int action) { + if (mNativeInfoBarPtr != 0) { + InfoBarJni.get().onButtonClicked(mNativeInfoBarPtr, InfoBar.this, action); + } + } + + @Override + public void onCloseButtonClicked() { + if (mNativeInfoBarPtr != 0 && !mIsDismissed) { + InfoBarJni.get().onCloseButtonClicked(mNativeInfoBarPtr, InfoBar.this); + } + } + + @NativeMethods + interface Natives { + int getInfoBarIdentifier(long nativeInfoBarAndroid, InfoBar caller); + void onLinkClicked(long nativeInfoBarAndroid, InfoBar caller); + void onButtonClicked(long nativeInfoBarAndroid, InfoBar caller, int action); + void onCloseButtonClicked(long nativeInfoBarAndroid, InfoBar caller); + } +} diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/InfoBarCompactLayout.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/InfoBarCompactLayout.java new file mode 100644 index 00000000000..c2303eaea6b --- /dev/null +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/InfoBarCompactLayout.java @@ -0,0 +1,238 @@ +// Copyright 2017 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.weblayer_private; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.method.LinkMovementMethod; +import android.view.Gravity; +import android.view.View; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.ColorRes; +import androidx.annotation.StringRes; +import androidx.appcompat.content.res.AppCompatResources; + +import org.chromium.base.ApiCompatibilityUtils; +import org.chromium.base.Callback; +import org.chromium.components.infobars.InfoBarInteractionHandler; +import org.chromium.components.infobars.InfoBarLayout; +import org.chromium.components.infobars.InfoBarMessageView; +import org.chromium.ui.text.NoUnderlineClickableSpan; +import org.chromium.ui.widget.ChromeImageButton; + +/** + * Lays out controls along a line, sandwiched between an (optional) icon and close button. + * This should only be used by the {@link InfoBar} class, and is created when the InfoBar subclass + * declares itself to be using a compact layout via {@link InfoBar#usesCompactLayout}. + */ +public class InfoBarCompactLayout extends LinearLayout implements View.OnClickListener { + private final InfoBarInteractionHandler mInfoBar; + private final int mCompactInfoBarSize; + private final int mIconWidth; + private final View mCloseButton; + + /** + * Constructs a compat layout for the specified infobar. + * @param context The context used to render. + * @param infoBar {@link InfoBarInteractionHandler} that listens to events. + * @param iconResourceId Resource ID of the icon to use for the infobar. + * @param iconTintId The {@link ColorRes} used as tint for {@code iconResourceId}. + * @param iconBitmap Bitmap for the icon to use, if {@code iconResourceId} is not set. + */ + // TODO(crbug/1056346): ctor is made public to allow access from InfoBar. Once + // InfoBar is modularized, restore access to package private. + public InfoBarCompactLayout(Context context, InfoBarInteractionHandler infoBar, + int iconResourceId, @ColorRes int iconTintId, Bitmap iconBitmap) { + super(context); + mInfoBar = infoBar; + mCompactInfoBarSize = + context.getResources().getDimensionPixelOffset(R.dimen.infobar_compact_size); + mIconWidth = context.getResources().getDimensionPixelOffset(R.dimen.infobar_big_icon_size); + + setOrientation(LinearLayout.HORIZONTAL); + setGravity(Gravity.CENTER_VERTICAL); + + prepareIcon(iconResourceId, iconTintId, iconBitmap); + mCloseButton = prepareCloseButton(); + } + + @Override + public void onClick(View view) { + if (view.getId() == R.id.infobar_close_button) { + mInfoBar.onCloseButtonClicked(); + } else { + assert false; + } + } + + /** + * Inserts a view before the close button. + * @param view View to insert. + * @param weight Weight to assign to it. + */ + // TODO(crbug/1056346): addContent is made public to allow access from InfoBar. Once + // InfoBar is modularized, restore access to protected. + public void addContent(View view, float weight) { + LinearLayout.LayoutParams params; + if (weight <= 0.0f) { + params = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, mCompactInfoBarSize); + } else { + params = new LinearLayout.LayoutParams(0, LayoutParams.WRAP_CONTENT, weight); + } + view.setMinimumHeight(mCompactInfoBarSize); + params.gravity = Gravity.BOTTOM; + addView(view, indexOfChild(mCloseButton), params); + } + + /** + * Adds an icon to the start of the infobar, if the infobar requires one. + * @param iconResourceId Resource ID of the icon to use. + * @param iconTintId The {@link ColorRes} used as tint for {@code iconResourceId}. + * @param iconBitmap Raw {@link Bitmap} to use instead of a resource. + */ + private void prepareIcon(int iconResourceId, @ColorRes int iconTintId, Bitmap iconBitmap) { + ImageView iconView = + InfoBarLayout.createIconView(getContext(), iconResourceId, iconTintId, iconBitmap); + if (iconView != null) { + LinearLayout.LayoutParams iconParams = + new LinearLayout.LayoutParams(mIconWidth, mCompactInfoBarSize); + addView(iconView, iconParams); + } + } + + /** + * Creates a close button that can be inserted into an infobar. + * NOTE: This was forked from //chrome's InfoBarLayout.java, as WebLayer supports only compact + * infobars and does not have a corresponding InfoBarLayout.java. + * @param context Context to grab resources from. + * @return {@link ImageButton} that represents a close button. + */ + static ImageButton createCloseButton(Context context) { + final ColorStateList tint = + AppCompatResources.getColorStateList(context, R.color.default_icon_color); + TypedArray a = + context.obtainStyledAttributes(new int[] {android.R.attr.selectableItemBackground}); + Drawable closeButtonBackground = a.getDrawable(0); + a.recycle(); + + ChromeImageButton closeButton = new ChromeImageButton(context); + closeButton.setId(R.id.infobar_close_button); + closeButton.setImageResource(R.drawable.btn_close); + ApiCompatibilityUtils.setImageTintList(closeButton, tint); + closeButton.setBackground(closeButtonBackground); + closeButton.setContentDescription(context.getString(R.string.weblayer_infobar_close)); + closeButton.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + + return closeButton; + } + + /** Adds a close button to the end of the infobar. */ + private View prepareCloseButton() { + ImageButton closeButton = createCloseButton(getContext()); + closeButton.setOnClickListener(this); + LinearLayout.LayoutParams closeParams = + new LinearLayout.LayoutParams(mCompactInfoBarSize, mCompactInfoBarSize); + addView(closeButton, closeParams); + return closeButton; + } + + /** + * Helps building a standard message to display in a compact InfoBar. The message can feature + * a link to perform and action from this infobar. + */ + public static class MessageBuilder { + private final InfoBarCompactLayout mLayout; + private CharSequence mMessage; + private CharSequence mLink; + + /** @param layout The layout we are building a message view for. */ + public MessageBuilder(InfoBarCompactLayout layout) { + mLayout = layout; + } + + public MessageBuilder withText(CharSequence message) { + assert mMessage == null; + mMessage = message; + + return this; + } + + public MessageBuilder withText(@StringRes int messageResId) { + assert mMessage == null; + mMessage = mLayout.getResources().getString(messageResId); + + return this; + } + + /** Appends a link after the main message, its displayed text being the specified string. */ + public MessageBuilder withLink(CharSequence label, Callback<View> onTapCallback) { + assert mLink == null; + + final Resources resources = mLayout.getResources(); + SpannableString link = new SpannableString(label); + link.setSpan(new NoUnderlineClickableSpan(resources, onTapCallback), 0, label.length(), + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + mLink = link; + + return this; + } + + /** + * Appends a link after the main message, its displayed text being constructed from the + * given resource ID. + */ + public MessageBuilder withLink(@StringRes int textResId, Callback<View> onTapCallback) { + final Resources resources = mLayout.getResources(); + String label = resources.getString(textResId); + return withLink(label, onTapCallback); + } + + /** Finalizes the message view as set up in the builder and inserts it into the layout. */ + public void buildAndInsert() { + mLayout.addContent(build(), 1f); + } + + /** + * Finalizes the message view as set up in the builder. The caller is responsible for adding + * it to the parent layout. + */ + public View build() { + // TODO(dgn): Should be able to handle ReaderMode and Survey infobars but they have non + // standard interaction models (no button/link, whole bar is a button) or style (large + // rather than default text). Revisit after snowflake review. + + assert mMessage != null; + + final int messagePadding = mLayout.getResources().getDimensionPixelOffset( + R.dimen.infobar_compact_message_vertical_padding); + + SpannableStringBuilder builder = new SpannableStringBuilder(); + builder.append(mMessage); + if (mLink != null) builder.append(" ").append(mLink); + + TextView prompt = new InfoBarMessageView(mLayout.getContext()); + ApiCompatibilityUtils.setTextAppearance( + prompt, R.style.TextAppearance_TextMedium_Primary); + prompt.setText(builder); + prompt.setGravity(Gravity.CENTER_VERTICAL); + prompt.setPadding(0, messagePadding, 0, messagePadding); + + if (mLink != null) prompt.setMovementMethod(LinkMovementMethod.getInstance()); + + return prompt; + } + } +} diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/InfoBarContainer.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/InfoBarContainer.java new file mode 100644 index 00000000000..15037971602 --- /dev/null +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/InfoBarContainer.java @@ -0,0 +1,486 @@ +// Copyright 2013 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.weblayer_private; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import org.chromium.base.ObserverList; +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.annotations.NativeMethods; +import org.chromium.chrome.browser.infobar.InfoBarIdentifier; +import org.chromium.content_public.browser.NavigationHandle; +import org.chromium.content_public.browser.WebContents; +import org.chromium.content_public.browser.WebContentsObserver; +import org.chromium.ui.KeyboardVisibilityDelegate.KeyboardVisibilityListener; +import org.chromium.ui.util.AccessibilityUtil; + +import java.util.ArrayList; + +/** + * A container for all the infobars of a specific tab. + * Note that infobars creation can be initiated from Java or from native code. + * When initiated from native code, special code is needed to keep the Java and native infobar in + * sync, see NativeInfoBar. + */ +@JNINamespace("weblayer") +public class InfoBarContainer implements KeyboardVisibilityListener, InfoBar.Container { + private static final String TAG = "InfoBarContainer"; + + // Number of instances that have not been destroyed. + private static int sInstanceCount; + + // InfoBarContainer's handling of accessibility is a global toggle, and thus a static observer + // suffices. However, observing accessibility events has the wrinkle that all accessibility + // observers are removed when there are no more Browsers and are not re-added if a new Browser + // is subsequently created. To handle this wrinkle, |sAccessibilityObserver| is added as an + // observer whenever the number of non-destroyed InfoBarContainers becomes non-zero and removed + // whenever that number flips to zero. + private static final AccessibilityUtil.Observer sAccessibilityObserver; + static { + sAccessibilityObserver = (enabled) -> setIsAllowedToAutoHide(!enabled); + } + + /** + * A listener for the InfoBar animations. + */ + public interface InfoBarAnimationListener { + public static final int ANIMATION_TYPE_SHOW = 0; + public static final int ANIMATION_TYPE_SWAP = 1; + public static final int ANIMATION_TYPE_HIDE = 2; + + /** + * Notifies the subscriber when an animation is completed. + */ + void notifyAnimationFinished(int animationType); + + /** + * Notifies the subscriber when all animations are finished. + * @param frontInfoBar The frontmost infobar or {@code null} if none are showing. + */ + void notifyAllAnimationsFinished(InfoBarUiItem frontInfoBar); + } + + /** + * An observer that is notified of changes to a {@link InfoBarContainer} object. + */ + public interface InfoBarContainerObserver { + /** + * Called when an {@link InfoBar} is about to be added (before the animation). + * @param container The notifying {@link InfoBarContainer} + * @param infoBar An {@link InfoBar} being added + * @param isFirst Whether the infobar container was empty + */ + void onAddInfoBar(InfoBarContainer container, InfoBar infoBar, boolean isFirst); + + /** + * Called when an {@link InfoBar} is about to be removed (before the animation). + * @param container The notifying {@link InfoBarContainer} + * @param infoBar An {@link InfoBar} being removed + * @param isLast Whether the infobar container is going to be empty + */ + void onRemoveInfoBar(InfoBarContainer container, InfoBar infoBar, boolean isLast); + + /** + * Called when the InfobarContainer is attached to the window. + * @param hasInfobars True if infobar container has infobars to show. + */ + void onInfoBarContainerAttachedToWindow(boolean hasInfobars); + + /** + * A notification that the shown ratio of the infobar container has changed. + * @param container The notifying {@link InfoBarContainer} + * @param shownRatio The shown ratio of the infobar container. + */ + void onInfoBarContainerShownRatioChanged(InfoBarContainer container, float shownRatio); + } + + /** + * Resets the visibility of the InfoBarContainer when the user navigates, following Chrome's + * behavior. In particular in Chrome some features hide the infobar container. This hiding is + * always on a per-URL basis that should be undone on navigation. While no feature in WebLayer + * yet does this, we put this * defensive behavior in place so that any such added features + * don't end up inadvertently hiding the infobar container "forever" in a given tab. + */ + private final WebContentsObserver mWebContentsObserver = new WebContentsObserver() { + @Override + public void didFinishNavigation(NavigationHandle navigation) { + if (navigation.hasCommitted() && navigation.isInMainFrame()) { + setHidden(false); + } + } + }; + + public void onTabDidGainActive() { + initializeContainerView(mTab.getBrowser().getContext()); + updateWebContents(); + mInfoBarContainerView.addToParentView(); + } + + public void onTabDidLoseActive() { + mInfoBarContainerView.removeFromParentView(); + destroyContainerView(); + } + + /** The list of all InfoBars in this container, regardless of whether they've been shown yet. */ + private final ArrayList<InfoBar> mInfoBars = new ArrayList<>(); + + private final ObserverList<InfoBarContainerObserver> mObservers = new ObserverList<>(); + private final ObserverList<InfoBarAnimationListener> mAnimationListeners = new ObserverList<>(); + + private final InfoBarContainerView.ContainerViewObserver mContainerViewObserver = + new InfoBarContainerView.ContainerViewObserver() { + @Override + public void notifyAnimationFinished(int animationType) { + for (InfoBarAnimationListener listener : mAnimationListeners) { + listener.notifyAnimationFinished(animationType); + } + } + + @Override + public void notifyAllAnimationsFinished(InfoBarUiItem frontInfoBar) { + for (InfoBarAnimationListener listener : mAnimationListeners) { + listener.notifyAllAnimationsFinished(frontInfoBar); + } + } + + @Override + public void onShownRatioChanged(float shownFraction) { + for (InfoBarContainer.InfoBarContainerObserver observer : mObservers) { + observer.onInfoBarContainerShownRatioChanged( + InfoBarContainer.this, shownFraction); + } + } + }; + + /** The tab that hosts this infobar container. */ + private final TabImpl mTab; + + /** Native InfoBarContainer pointer which will be set by InfoBarContainerJni.get().init(). */ + private long mNativeInfoBarContainer; + + /** True when this container has been emptied and its native counterpart has been destroyed. */ + private boolean mDestroyed; + + /** Whether or not this View should be hidden. */ + private boolean mIsHidden; + + /** + * The view for this {@link InfoBarContainer}. It will be null when the {@link Tab} is detached + * from a {@link ChromeActivity}. + */ + private @Nullable InfoBarContainerView mInfoBarContainerView; + + InfoBarContainer(TabImpl tab) { + if (++sInstanceCount == 1) { + WebLayerAccessibilityUtil.get().addObserver(sAccessibilityObserver); + } + + mTab = tab; + mTab.getWebContents().addObserver(mWebContentsObserver); + + // Chromium's InfoBarContainer may add an InfoBar immediately during this initialization + // call, so make sure everything in the InfoBarContainer is completely ready beforehand. + mNativeInfoBarContainer = InfoBarContainerJni.get().init(InfoBarContainer.this); + } + + /** + * Adds an {@link InfoBarContainerObserver}. + * @param observer The {@link InfoBarContainerObserver} to add. + */ + public void addObserver(InfoBarContainerObserver observer) { + mObservers.addObserver(observer); + } + + /** + * Removes a {@link InfoBarContainerObserver}. + * @param observer The {@link InfoBarContainerObserver} to remove. + */ + public void removeObserver(InfoBarContainerObserver observer) { + mObservers.removeObserver(observer); + } + + /** + * Sets the parent {@link ViewGroup} that contains the {@link InfoBarContainer}. + */ + public void setParentView(ViewGroup parent) { + assert mTab.getBrowser().getActiveTab() == mTab; + if (mInfoBarContainerView != null) mInfoBarContainerView.setParentView(parent); + } + + @VisibleForTesting + public void addAnimationListener(InfoBarAnimationListener listener) { + mAnimationListeners.addObserver(listener); + } + + /** + * Removes the passed in {@link InfoBarAnimationListener} from the {@link InfoBarContainer}. + */ + public void removeAnimationListener(InfoBarAnimationListener listener) { + mAnimationListeners.removeObserver(listener); + } + + /** + * Adds an InfoBar to the view hierarchy. + * @param infoBar InfoBar to add to the View hierarchy. + */ + @CalledByNative + private void addInfoBar(InfoBar infoBar) { + assert !mDestroyed; + if (infoBar == null) { + return; + } + if (mInfoBars.contains(infoBar)) { + assert false : "Trying to add an info bar that has already been added."; + return; + } + + infoBar.setContext(mInfoBarContainerView.getContext()); + infoBar.setContainer(this); + + // We notify observers immediately (before the animation starts). + for (InfoBarContainerObserver observer : mObservers) { + observer.onAddInfoBar(this, infoBar, mInfoBars.isEmpty()); + } + + assert mInfoBarContainerView != null : "The container view is null when adding an InfoBar"; + + // We add the infobar immediately to mInfoBars but we wait for the animation to end to + // notify it's been added, as tests rely on this notification but expects the infobar view + // to be available when they get the notification. + mInfoBars.add(infoBar); + + mInfoBarContainerView.addInfoBar(infoBar); + } + + @VisibleForTesting + public View getViewForTesting() { + return mInfoBarContainerView; + } + + /** + * Adds an InfoBar to the view hierarchy. + * @param infoBar InfoBar to add to the View hierarchy. + */ + @VisibleForTesting + public void addInfoBarForTesting(InfoBar infoBar) { + addInfoBar(infoBar); + } + + @Override + public void notifyInfoBarViewChanged() { + assert !mDestroyed; + if (mInfoBarContainerView != null) mInfoBarContainerView.notifyInfoBarViewChanged(); + } + + /** + * Sets the visibility for the {@link InfoBarContainerView}. + * @param visibility One of {@link View#GONE}, {@link View#INVISIBLE}, or {@link View#VISIBLE}. + */ + public void setVisibility(int visibility) { + if (mInfoBarContainerView != null) mInfoBarContainerView.setVisibility(visibility); + } + + /** + * @return The visibility of the {@link InfoBarContainerView}. + */ + public int getVisibility() { + return mInfoBarContainerView != null ? mInfoBarContainerView.getVisibility() : View.GONE; + } + + @Override + public void removeInfoBar(InfoBar infoBar) { + assert !mDestroyed; + + if (!mInfoBars.remove(infoBar)) { + assert false : "Trying to remove an InfoBar that is not in this container."; + return; + } + + // Notify observers immediately, before any animations begin. + for (InfoBarContainerObserver observer : mObservers) { + observer.onRemoveInfoBar(this, infoBar, mInfoBars.isEmpty()); + } + + assert mInfoBarContainerView + != null : "The container view is null when removing an InfoBar."; + mInfoBarContainerView.removeInfoBar(infoBar); + } + + @Override + public boolean isDestroyed() { + return mDestroyed; + } + + public void destroy() { + mTab.getWebContents().removeObserver(mWebContentsObserver); + + if (--sInstanceCount == 0) { + WebLayerAccessibilityUtil.get().removeObserver(sAccessibilityObserver); + } + + if (mInfoBarContainerView != null) destroyContainerView(); + if (mNativeInfoBarContainer != 0) { + InfoBarContainerJni.get().destroy(mNativeInfoBarContainer, InfoBarContainer.this); + mNativeInfoBarContainer = 0; + } + mDestroyed = true; + } + + /** + * @return all of the InfoBars held in this container. + */ + @VisibleForTesting + public ArrayList<InfoBar> getInfoBarsForTesting() { + return mInfoBars; + } + + /** + * @return True if the container has any InfoBars. + */ + @CalledByNative + public boolean hasInfoBars() { + return !mInfoBars.isEmpty(); + } + + /** + * @return InfoBarIdentifier of the InfoBar which is currently at the top of the infobar stack, + * or InfoBarIdentifier.INVALID if there are no infobars. + */ + @CalledByNative + private @InfoBarIdentifier int getTopInfoBarIdentifier() { + if (!hasInfoBars()) return InfoBarIdentifier.INVALID; + return mInfoBars.get(0).getInfoBarIdentifier(); + } + + /** + * Hides or stops hiding this View. + * + * @param isHidden Whether this View is should be hidden. + */ + public void setHidden(boolean isHidden) { + mIsHidden = isHidden; + if (mInfoBarContainerView == null) return; + mInfoBarContainerView.setHidden(isHidden); + } + + /** + * Sets whether the InfoBarContainer is allowed to auto-hide when the user scrolls the page. + * Expected to be called when Touch Exploration is enabled. + * @param isAllowed Whether auto-hiding is allowed. + */ + private static void setIsAllowedToAutoHide(boolean isAllowed) { + InfoBarContainerView.setIsAllowedToAutoHide(isAllowed); + } + + // KeyboardVisibilityListener implementation. + @Override + public void keyboardVisibilityChanged(boolean isKeyboardShowing) { + assert mInfoBarContainerView != null; + boolean isShowing = (mInfoBarContainerView.getVisibility() == View.VISIBLE); + if (isKeyboardShowing) { + if (isShowing) { + mInfoBarContainerView.setVisibility(View.INVISIBLE); + } + } else { + if (!isShowing && !mIsHidden) { + mInfoBarContainerView.setVisibility(View.VISIBLE); + } + } + } + + private void updateWebContents() { + // When the tab is detached, we don't update the InfoBarContainer web content so that it + // stays null until the tab is attached to some ChromeActivity. + if (mInfoBarContainerView == null) return; + WebContents webContents = mTab.getWebContents(); + + if (webContents != null && webContents != mInfoBarContainerView.getWebContents()) { + mInfoBarContainerView.setWebContents(webContents); + if (mNativeInfoBarContainer != 0) { + InfoBarContainerJni.get().setWebContents( + mNativeInfoBarContainer, InfoBarContainer.this, webContents); + } + } + } + + private void initializeContainerView(Context chromeActivity) { + assert chromeActivity + != null + : "ChromeActivity should not be null when initializing InfoBarContainerView"; + mInfoBarContainerView = new InfoBarContainerView(chromeActivity, mContainerViewObserver, + mTab, /*isTablet=*/!mTab.getBrowser().isWindowOnSmallDevice()); + + mInfoBarContainerView.addOnAttachStateChangeListener( + new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View view) { + for (InfoBarContainer.InfoBarContainerObserver observer : mObservers) { + observer.onInfoBarContainerAttachedToWindow(!mInfoBars.isEmpty()); + } + } + + @Override + public void onViewDetachedFromWindow(View view) {} + }); + + mInfoBarContainerView.setHidden(mIsHidden); + setParentView(mTab.getBrowser().getViewController().getInfoBarContainerParentView()); + + mTab.getBrowser().getWindowAndroid().getKeyboardDelegate().addKeyboardVisibilityListener( + this); + } + + private void destroyContainerView() { + if (mInfoBarContainerView != null) { + mInfoBarContainerView.setWebContents(null); + if (mNativeInfoBarContainer != 0) { + InfoBarContainerJni.get().setWebContents( + mNativeInfoBarContainer, InfoBarContainer.this, null); + } + mInfoBarContainerView.destroy(); + mInfoBarContainerView = null; + } + + mTab.getBrowser().getWindowAndroid().getKeyboardDelegate().removeKeyboardVisibilityListener( + this); + } + + @Override + public boolean isFrontInfoBar(InfoBar infoBar) { + if (mInfoBars.isEmpty()) return false; + return mInfoBars.get(0) == infoBar; + } + + /** + * Returns true if any animations are pending or in progress. + */ + @VisibleForTesting + public boolean isAnimating() { + assert mInfoBarContainerView != null; + return mInfoBarContainerView.isAnimating(); + } + + /** + * @return The {@link InfoBarContainerView} this class holds. + */ + @VisibleForTesting + public InfoBarContainerView getContainerViewForTesting() { + return mInfoBarContainerView; + } + + @NativeMethods + interface Natives { + long init(InfoBarContainer caller); + void setWebContents(long nativeInfoBarContainerAndroid, InfoBarContainer caller, + WebContents webContents); + void destroy(long nativeInfoBarContainerAndroid, InfoBarContainer caller); + } +} diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/InfoBarContainerLayout.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/InfoBarContainerLayout.java new file mode 100644 index 00000000000..4f91d6f437d --- /dev/null +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/InfoBarContainerLayout.java @@ -0,0 +1,852 @@ +// 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.weblayer_private; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.Resources; +import android.text.TextUtils; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; + +import org.chromium.ui.widget.OptimizedFrameLayout; +import org.chromium.weblayer_private.InfoBarContainer.InfoBarAnimationListener; + +import java.util.ArrayList; + +/** + * Layout that displays infobars in a stack. Handles all the animations when adding or removing + * infobars and when swapping infobar contents. + * + * The first infobar to be added is visible at the front of the stack. Later infobars peek up just + * enough behind the front infobar to signal their existence; their contents aren't visible at all. + * The stack has a max depth of three infobars. If additional infobars are added beyond this, they + * won't be visible at all until infobars in front of them are dismissed. + * + * Animation details: + * - Newly added infobars slide up from the bottom and then their contents fade in. + * - Disappearing infobars slide down and away. The remaining infobars, if any, resize to the + * new front infobar's size, then the content of the new front infobar fades in. + * - When swapping the front infobar's content, the old content fades out, the infobar resizes to + * the new content's size, then the new content fades in. + * - Only a single animation happens at a time. If several infobars are added and/or removed in + * quick succession, the animations will be queued and run sequentially. + * + * Note: this class depends only on Android view code; it intentionally does not depend on any other + * infobar code. This is an explicit design decision and should remain this way. + * + * TODO(newt): what happens when detached from window? Do animations run? Do animations jump to end + * values? Should they jump to end values? Does requestLayout() get called when detached + * from window? Probably not; it probably just gets called later when reattached. + * + * TODO(newt): use hardware acceleration? See + * http://blog.danlew.net/2015/10/20/using-hardware-layers-to-improve-animation-performance/ + * and http://developer.android.com/guide/topics/graphics/hardware-accel.html#layers + * + * TODO(newt): handle tall infobars on small devices. Use a ScrollView inside the InfoBarWrapper? + * Make sure InfoBarContainerLayout doesn't extend into tabstrip on tablet. + * + * TODO(newt): Disable key events during animations, perhaps by overriding dispatchKeyEvent(). + * Or can we just call setEnabled() false on the infobar wrapper? Will this cause the buttons + * visual state to change (i.e. to turn gray)? + * + * TODO(newt): finalize animation timings and interpolators. + */ +public class InfoBarContainerLayout extends OptimizedFrameLayout { + /** + * Creates an empty InfoBarContainerLayout. + */ + InfoBarContainerLayout(Context context, Runnable makeContainerVisibleRunnable, + InfoBarAnimationListener animationListener) { + super(context, null); + Resources res = context.getResources(); + mBackInfobarHeight = res.getDimensionPixelSize(R.dimen.infobar_peeking_height); + mFloatingBehavior = new FloatingBehavior(this); + mAnimationListener = animationListener; + mMakeContainerVisibleRunnable = makeContainerVisibleRunnable; + } + + /** + * Adds an infobar to the container. The infobar appearing animation will happen after the + * current animation, if any, finishes. + */ + void addInfoBar(InfoBarUiItem item) { + mItems.add(findInsertIndex(item), item); + processPendingAnimations(); + } + + /** + * Finds the appropriate index in the infobar stack for inserting this item. + * @param item The infobar to be inserted. + */ + private int findInsertIndex(InfoBarUiItem item) { + for (int i = 0; i < mItems.size(); ++i) { + if (item.getPriority() < mItems.get(i).getPriority()) { + return i; + } + } + + return mItems.size(); + } + + /** + * Removes an infobar from the container. The infobar will be animated off the screen if it's + * currently visible. + */ + void removeInfoBar(InfoBarUiItem item) { + mItems.remove(item); + processPendingAnimations(); + } + + /** + * Notifies that an infobar's View ({@link InfoBarUiItem#getView}) has changed. If the infobar + * is visible in the front of the stack, the infobar will fade out the old contents, resize, + * then fade in the new contents. + */ + void notifyInfoBarViewChanged() { + processPendingAnimations(); + } + + /** + * Returns true if any animations are pending or in progress. + */ + boolean isAnimating() { + return mAnimation != null; + } + + ///////////////////////////////////////// + // Implementation details + ///////////////////////////////////////// + + /** The maximum number of infobars visible at any time. */ + private static final int MAX_STACK_DEPTH = 3; + + // Animation durations. + private static final int DURATION_SLIDE_UP_MS = 250; + private static final int DURATION_SLIDE_DOWN_MS = 250; + private static final int DURATION_FADE_MS = 100; + private static final int DURATION_FADE_OUT_MS = 200; + + /** + * Base class for animations inside the InfoBarContainerLayout. + * + * Provides a standardized way to prepare for, run, and clean up after animations. Each subclass + * should implement prepareAnimation(), createAnimator(), and onAnimationEnd() as needed. + */ + private abstract class InfoBarAnimation { + private Animator mAnimator; + + final boolean isStarted() { + return mAnimator != null; + } + + final void start() { + Animator.AnimatorListener listener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + InfoBarAnimation.this.onAnimationEnd(); + mAnimation = null; + mAnimationListener.notifyAnimationFinished(getAnimationType()); + processPendingAnimations(); + } + }; + + mAnimator = createAnimator(); + mAnimator.addListener(listener); + mAnimator.start(); + } + + /** + * Returns an animator that animates an InfoBarWrapper's y-translation from its current + * value to endValue and updates the side shadow positions on each frame. + */ + ValueAnimator createTranslationYAnimator(final InfoBarWrapper wrapper, float endValue) { + ValueAnimator animator = ValueAnimator.ofFloat(wrapper.getTranslationY(), endValue); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + wrapper.setTranslationY((float) animation.getAnimatedValue()); + mFloatingBehavior.updateShadowPosition(); + } + }); + return animator; + } + + /** + * Called before the animation begins. This is the time to add views to the hierarchy and + * adjust layout parameters. + */ + void prepareAnimation() {} + + /** + * Called to create an Animator which will control the animation. Called after + * prepareAnimation() and after a subsequent layout has happened. + */ + abstract Animator createAnimator(); + + /** + * Called after the animation completes. This is the time to do post-animation cleanup, such + * as removing views from the hierarchy. + */ + void onAnimationEnd() {} + + /** + * Returns the InfoBarAnimationListener.ANIMATION_TYPE_* constant that corresponds to this + * type of animation (showing, swapping, etc). + */ + abstract int getAnimationType(); + } + + /** + * The animation to show the first infobar. The infobar slides up from the bottom; then its + * content fades in. + */ + private class FirstInfoBarAppearingAnimation extends InfoBarAnimation { + private InfoBarUiItem mFrontItem; + private InfoBarWrapper mFrontWrapper; + private View mFrontContents; + + FirstInfoBarAppearingAnimation(InfoBarUiItem frontItem) { + mFrontItem = frontItem; + } + + @Override + void prepareAnimation() { + mFrontContents = mFrontItem.getView(); + mFrontWrapper = new InfoBarWrapper(getContext(), mFrontItem); + mFrontWrapper.addView(mFrontContents); + addWrapper(mFrontWrapper); + } + + @Override + Animator createAnimator() { + mFrontWrapper.setTranslationY(mFrontWrapper.getHeight()); + mFrontContents.setAlpha(0f); + + AnimatorSet animator = new AnimatorSet(); + animator.playSequentially( + createTranslationYAnimator(mFrontWrapper, 0f).setDuration(DURATION_SLIDE_UP_MS), + ObjectAnimator.ofFloat(mFrontContents, View.ALPHA, 1f) + .setDuration(DURATION_FADE_MS)); + return animator; + } + + @Override + void onAnimationEnd() { + announceForAccessibility(mFrontItem.getAccessibilityText()); + } + + @Override + int getAnimationType() { + return InfoBarAnimationListener.ANIMATION_TYPE_SHOW; + } + } + + /** + * The animation to show the a new front-most infobar in front of existing visible infobars. The + * infobar slides up from the bottom; then its content fades in. The previously visible infobars + * will be resized simulatenously to the new desired size. + */ + private class FrontInfoBarAppearingAnimation extends InfoBarAnimation { + private InfoBarUiItem mFrontItem; + private InfoBarWrapper mFrontWrapper; + private InfoBarWrapper mOldFrontWrapper; + private View mFrontContents; + + FrontInfoBarAppearingAnimation(InfoBarUiItem frontItem) { + mFrontItem = frontItem; + } + + @Override + void prepareAnimation() { + mOldFrontWrapper = mInfoBarWrappers.get(0); + + mFrontContents = mFrontItem.getView(); + mFrontWrapper = new InfoBarWrapper(getContext(), mFrontItem); + mFrontWrapper.addView(mFrontContents); + addWrapperToFront(mFrontWrapper); + } + + @Override + Animator createAnimator() { + // After adding the new wrapper, the new front item's view, and the old front item's + // view are both in their wrappers, and the height of the stack as determined by + // FrameLayout will take both into account. This means the height of the container will + // be larger than it needs to be, if the previous old front item is larger than the sum + // of the new front item and mBackInfobarHeight. + // + // First work out how much the container will grow or shrink by. + int heightDelta = + mFrontWrapper.getHeight() + mBackInfobarHeight - mOldFrontWrapper.getHeight(); + + // Now work out where to animate the new front item to / from. + int newFrontStart = mFrontWrapper.getHeight(); + int newFrontEnd = 0; + if (heightDelta < 0) { + // If the container is shrinking, this won't be reflected in the layout just yet. + // The layout will have extra space in it for the previous front infobar, which the + // animation of the new front infobar has to take into account. + newFrontStart -= heightDelta; + newFrontEnd -= heightDelta; + } + mFrontWrapper.setTranslationY(newFrontStart); + mFrontContents.setAlpha(0f); + + // Since we are adding the infobar to the top of the stack, make the container fully + // visible since it could be at hidden or partially hidden state. + mMakeContainerVisibleRunnable.run(); + + AnimatorSet animator = new AnimatorSet(); + animator.play(createTranslationYAnimator(mFrontWrapper, newFrontEnd) + .setDuration(DURATION_SLIDE_UP_MS)); + + // If the container is shrinking, the back infobars need to animate down (from 0 to the + // positive delta). Otherwise they have to animate up (from the negative delta to 0). + int backStart = Math.max(0, heightDelta); + int backEnd = Math.max(-heightDelta, 0); + for (int i = 1; i < mInfoBarWrappers.size(); i++) { + mInfoBarWrappers.get(i).setTranslationY(backStart); + animator.play(createTranslationYAnimator(mInfoBarWrappers.get(i), backEnd) + .setDuration(DURATION_SLIDE_UP_MS)); + } + + animator.play(ObjectAnimator.ofFloat(mFrontContents, View.ALPHA, 1f) + .setDuration(DURATION_FADE_MS)) + .after(DURATION_SLIDE_UP_MS); + + return animator; + } + + @Override + void onAnimationEnd() { + // Remove the old front wrappers view so it won't affect the height of the container any + // more. + mOldFrontWrapper.removeAllViews(); + + // Now set any Y offsets to 0 as there is no need to account for the old front wrapper + // making the container higher than it should be. + for (int i = 0; i < mInfoBarWrappers.size(); i++) { + mInfoBarWrappers.get(i).setTranslationY(0); + } + updateLayoutParams(); + announceForAccessibility(mFrontItem.getAccessibilityText()); + } + + @Override + int getAnimationType() { + return InfoBarAnimationListener.ANIMATION_TYPE_SHOW; + } + } + + /** + * The animation to show a back infobar. The infobar slides up behind the existing infobars, so + * its top edge peeks out just a bit. + */ + private class BackInfoBarAppearingAnimation extends InfoBarAnimation { + private InfoBarWrapper mAppearingWrapper; + + BackInfoBarAppearingAnimation(InfoBarUiItem appearingItem) { + mAppearingWrapper = new InfoBarWrapper(getContext(), appearingItem); + } + + @Override + void prepareAnimation() { + addWrapper(mAppearingWrapper); + } + + @Override + Animator createAnimator() { + mAppearingWrapper.setTranslationY(mAppearingWrapper.getHeight()); + return createTranslationYAnimator(mAppearingWrapper, 0f) + .setDuration(DURATION_SLIDE_UP_MS); + } + + @Override + public void onAnimationEnd() { + mAppearingWrapper.removeView(mAppearingWrapper.getItem().getView()); + } + + @Override + int getAnimationType() { + return InfoBarAnimationListener.ANIMATION_TYPE_SHOW; + } + } + + /** + * The animation to hide the front infobar and reveal the second-to-front infobar. The front + * infobar slides down and off the screen. The back infobar(s) will adjust to the size of the + * new front infobar, and then the new front infobar's contents will fade in. + */ + private class FrontInfoBarDisappearingAndRevealingAnimation extends InfoBarAnimation { + private InfoBarWrapper mOldFrontWrapper; + private InfoBarWrapper mNewFrontWrapper; + private View mNewFrontContents; + + @Override + void prepareAnimation() { + mOldFrontWrapper = mInfoBarWrappers.get(0); + mNewFrontWrapper = mInfoBarWrappers.get(1); + mNewFrontContents = mNewFrontWrapper.getItem().getView(); + mNewFrontWrapper.addView(mNewFrontContents); + } + + @Override + Animator createAnimator() { + // The amount by which mNewFrontWrapper will grow (negative value indicates shrinking). + int deltaHeight = (mNewFrontWrapper.getHeight() - mBackInfobarHeight) + - mOldFrontWrapper.getHeight(); + int startTranslationY = Math.max(deltaHeight, 0); + int endTranslationY = Math.max(-deltaHeight, 0); + + // Slide the front infobar down and away. + AnimatorSet animator = new AnimatorSet(); + mOldFrontWrapper.setTranslationY(startTranslationY); + animator.play(createTranslationYAnimator( + mOldFrontWrapper, startTranslationY + mOldFrontWrapper.getHeight()) + .setDuration(DURATION_SLIDE_UP_MS)); + + // Slide the other infobars to their new positions. + // Note: animator.play() causes these animations to run simultaneously. + for (int i = 1; i < mInfoBarWrappers.size(); i++) { + mInfoBarWrappers.get(i).setTranslationY(startTranslationY); + animator.play(createTranslationYAnimator(mInfoBarWrappers.get(i), endTranslationY) + .setDuration(DURATION_SLIDE_UP_MS)); + } + + mNewFrontContents.setAlpha(0f); + animator.play(ObjectAnimator.ofFloat(mNewFrontContents, View.ALPHA, 1f) + .setDuration(DURATION_FADE_MS)) + .after(DURATION_SLIDE_UP_MS); + + return animator; + } + + @Override + void onAnimationEnd() { + mOldFrontWrapper.removeAllViews(); + removeWrapper(mOldFrontWrapper); + for (int i = 0; i < mInfoBarWrappers.size(); i++) { + mInfoBarWrappers.get(i).setTranslationY(0); + } + announceForAccessibility(mNewFrontWrapper.getItem().getAccessibilityText()); + } + + @Override + int getAnimationType() { + return InfoBarAnimationListener.ANIMATION_TYPE_HIDE; + } + } + + /** + * The animation to hide the backmost infobar, or the front infobar if there's only one infobar. + * The infobar simply slides down out of the container. + */ + private class InfoBarDisappearingAnimation extends InfoBarAnimation { + private InfoBarWrapper mDisappearingWrapper; + + @Override + void prepareAnimation() { + mDisappearingWrapper = mInfoBarWrappers.get(mInfoBarWrappers.size() - 1); + } + + @Override + Animator createAnimator() { + return createTranslationYAnimator( + mDisappearingWrapper, mDisappearingWrapper.getHeight()) + .setDuration(DURATION_SLIDE_DOWN_MS); + } + + @Override + void onAnimationEnd() { + mDisappearingWrapper.removeAllViews(); + removeWrapper(mDisappearingWrapper); + } + + @Override + int getAnimationType() { + return InfoBarAnimationListener.ANIMATION_TYPE_HIDE; + } + } + + /** + * The animation to swap the contents of the front infobar. The current contents fade out, + * then the infobar resizes to fit the new contents, then the new contents fade in. + */ + private class FrontInfoBarSwapContentsAnimation extends InfoBarAnimation { + private InfoBarWrapper mFrontWrapper; + private View mOldContents; + private View mNewContents; + + @Override + void prepareAnimation() { + mFrontWrapper = mInfoBarWrappers.get(0); + mOldContents = mFrontWrapper.getChildAt(0); + mNewContents = mFrontWrapper.getItem().getView(); + mFrontWrapper.addView(mNewContents); + } + + @Override + Animator createAnimator() { + int deltaHeight = mNewContents.getHeight() - mOldContents.getHeight(); + InfoBarContainerLayout.this.setTranslationY(Math.max(0, deltaHeight)); + mNewContents.setAlpha(0f); + + AnimatorSet animator = new AnimatorSet(); + animator.playSequentially(ObjectAnimator.ofFloat(mOldContents, View.ALPHA, 0f) + .setDuration(DURATION_FADE_OUT_MS), + ObjectAnimator + .ofFloat(InfoBarContainerLayout.this, View.TRANSLATION_Y, + Math.max(0, -deltaHeight)) + .setDuration(DURATION_SLIDE_UP_MS), + ObjectAnimator.ofFloat(mNewContents, View.ALPHA, 1f) + .setDuration(DURATION_FADE_OUT_MS)); + return animator; + } + + @Override + void onAnimationEnd() { + mFrontWrapper.removeViewAt(0); + InfoBarContainerLayout.this.setTranslationY(0f); + mFrontWrapper.getItem().setControlsEnabled(true); + announceForAccessibility(mFrontWrapper.getItem().getAccessibilityText()); + } + + @Override + int getAnimationType() { + return InfoBarAnimationListener.ANIMATION_TYPE_SWAP; + } + } + + /** + * Controls whether infobars fill the full available width, or whether they "float" in the + * middle of the available space. The latter case happens if the available space is wider than + * the max width allowed for infobars. + * + * Also handles the shadows on the sides of the infobars in floating mode. The side shadows are + * separate views -- rather than being part of each InfoBarWrapper -- to avoid a double-shadow + * effect, which would happen during animations when two InfoBarWrappers overlap each other. + */ + private static class FloatingBehavior { + /** The InfoBarContainerLayout. */ + private FrameLayout mLayout; + + /** + * The max width of the infobars. If the available space is wider than this, the infobars + * will switch to floating mode. + */ + private final int mMaxWidth; + + /** The width of the left and right shadows. */ + private final int mShadowWidth; + + /** Whether the layout is currently floating. */ + private boolean mIsFloating; + + /** The shadows that appear on the sides of the infobars in floating mode. */ + private View mLeftShadowView; + private View mRightShadowView; + + FloatingBehavior(FrameLayout layout) { + mLayout = layout; + Resources res = mLayout.getContext().getResources(); + mMaxWidth = res.getDimensionPixelSize(R.dimen.infobar_max_width); + mShadowWidth = res.getDimensionPixelSize(R.dimen.infobar_shadow_width); + } + + /** + * This should be called in onMeasure() before super.onMeasure(). The return value is a new + * widthMeasureSpec that should be passed to super.onMeasure(). + */ + int beforeOnMeasure(int widthMeasureSpec) { + int width = MeasureSpec.getSize(widthMeasureSpec); + boolean isFloating = width > mMaxWidth; + if (isFloating != mIsFloating) { + mIsFloating = isFloating; + onIsFloatingChanged(); + } + + if (isFloating) { + int mode = MeasureSpec.getMode(widthMeasureSpec); + width = Math.min(width, mMaxWidth + 2 * mShadowWidth); + widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, mode); + } + return widthMeasureSpec; + } + + /** + * This should be called in onMeasure() after super.onMeasure(). + */ + void afterOnMeasure(int measuredHeight) { + if (!mIsFloating) return; + // Measure side shadows to match the parent view's height. + int widthSpec = MeasureSpec.makeMeasureSpec(mShadowWidth, MeasureSpec.EXACTLY); + int heightSpec = MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY); + mLeftShadowView.measure(widthSpec, heightSpec); + mRightShadowView.measure(widthSpec, heightSpec); + } + + /** + * This should be called whenever the Y-position of an infobar changes. + */ + void updateShadowPosition() { + if (!mIsFloating) return; + float minY = mLayout.getHeight(); + int childCount = mLayout.getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = mLayout.getChildAt(i); + if (child != mLeftShadowView && child != mRightShadowView) { + minY = Math.min(minY, child.getY()); + } + } + mLeftShadowView.setY(minY); + mRightShadowView.setY(minY); + } + + private void onIsFloatingChanged() { + if (mIsFloating) { + initShadowViews(); + mLayout.setPadding(mShadowWidth, 0, mShadowWidth, 0); + mLayout.setClipToPadding(false); + mLayout.addView(mLeftShadowView); + mLayout.addView(mRightShadowView); + } else { + mLayout.setPadding(0, 0, 0, 0); + mLayout.removeView(mLeftShadowView); + mLayout.removeView(mRightShadowView); + } + } + + @SuppressLint("RtlHardcoded") + private void initShadowViews() { + if (mLeftShadowView != null) return; + + mLeftShadowView = new View(mLayout.getContext()); + mLeftShadowView.setBackgroundResource(R.drawable.infobar_shadow_left); + LayoutParams leftLp = new FrameLayout.LayoutParams(0, 0, Gravity.LEFT); + leftLp.leftMargin = -mShadowWidth; + mLeftShadowView.setLayoutParams(leftLp); + + mRightShadowView = new View(mLayout.getContext()); + mRightShadowView.setBackgroundResource(R.drawable.infobar_shadow_left); + LayoutParams rightLp = new FrameLayout.LayoutParams(0, 0, Gravity.RIGHT); + rightLp.rightMargin = -mShadowWidth; + mRightShadowView.setScaleX(-1f); + mRightShadowView.setLayoutParams(rightLp); + } + } + + /** + * The height of back infobars, i.e. the distance between the top of the front infobar and the + * top of the next infobar back. + */ + private final int mBackInfobarHeight; + + /** + * All the Items, in front to back order. + * This list is updated immediately when addInfoBar(), removeInfoBar(), and swapInfoBar() are + * called; so during animations, it does *not* match the currently visible views. + */ + private final ArrayList<InfoBarUiItem> mItems = new ArrayList<>(); + + /** + * The currently visible InfoBarWrappers, in front to back order. + */ + private final ArrayList<InfoBarWrapper> mInfoBarWrappers = new ArrayList<>(); + + /** A observer that is notified when animations finish. */ + private final InfoBarAnimationListener mAnimationListener; + + /** The current animation, or null if no animation is happening currently. */ + private InfoBarAnimation mAnimation; + + private FloatingBehavior mFloatingBehavior; + + /** The runnable to make infobar container fully visible. */ + private Runnable mMakeContainerVisibleRunnable; + + /** + * Determines whether any animations need to run in order to make the visible views match the + * current list of Items in mItems. If so, kicks off the next animation that's needed. + */ + private void processPendingAnimations() { + // If an animation is running, wait until it finishes before beginning the next animation. + if (mAnimation != null) return; + + // The steps below are ordered to minimize movement during animations. In particular, + // removals happen before additions or swaps, and changes are made to back infobars before + // front infobars. + + // First, remove any infobars that are no longer in mItems, if any. Check the back infobars + // before the front. + for (int i = mInfoBarWrappers.size() - 1; i >= 0; i--) { + InfoBarUiItem visibleItem = mInfoBarWrappers.get(i).getItem(); + if (!mItems.contains(visibleItem)) { + if (i == 0 && mInfoBarWrappers.size() >= 2) { + // Remove the front infobar and reveal the second-to-front infobar. + runAnimation(new FrontInfoBarDisappearingAndRevealingAnimation()); + return; + + } else { + // Move the infobar to the very back if it's not already there. + InfoBarWrapper wrapper = mInfoBarWrappers.get(i); + if (i != mInfoBarWrappers.size() - 1) { + removeWrapper(wrapper); + addWrapper(wrapper); + } + + // Remove the backmost infobar (which may be the front infobar). + runAnimation(new InfoBarDisappearingAnimation()); + return; + } + } + } + + // Second, run swap animation on front infobar if needed. + if (!mInfoBarWrappers.isEmpty()) { + InfoBarUiItem frontItem = mInfoBarWrappers.get(0).getItem(); + View frontContents = mInfoBarWrappers.get(0).getChildAt(0); + if (frontContents != frontItem.getView()) { + runAnimation(new FrontInfoBarSwapContentsAnimation()); + return; + } + } + + // Third, check if we should add any infobars in front of visible infobars. This can happen + // if an infobar has been inserted into mItems, in front of the currently visible item. To + // detect this the items at the beginning of mItems are compared against the first item in + // mInfoBarWrappers. + if (!mInfoBarWrappers.isEmpty()) { + // Find the infobar with the highest index that isn't currently being shown. + InfoBarUiItem currentVisibleItem = mInfoBarWrappers.get(0).getItem(); + InfoBarUiItem itemToInsert = null; + for (int checkIndex = 0; checkIndex < mItems.size(); checkIndex++) { + if (mItems.get(checkIndex) == currentVisibleItem) { + // There are no remaining infobars that can possibly override the + // currently displayed one. + break; + } else { + // Found an infobar that isn't being displayed yet. Track it so that + // it can be animated in. + itemToInsert = mItems.get(checkIndex); + } + } + if (itemToInsert != null) { + runAnimation(new FrontInfoBarAppearingAnimation(itemToInsert)); + return; + } + } + + // Fourth, check if we should add any infobars at the back. + int desiredChildCount = Math.min(mItems.size(), MAX_STACK_DEPTH); + if (mInfoBarWrappers.size() < desiredChildCount) { + InfoBarUiItem itemToShow = mItems.get(mInfoBarWrappers.size()); + runAnimation(mInfoBarWrappers.isEmpty() + ? new FirstInfoBarAppearingAnimation(itemToShow) + : new BackInfoBarAppearingAnimation(itemToShow)); + return; + } + + // Fifth, now that we've stabilized, let listeners know that we have no more animations. + InfoBarUiItem frontItem = + mInfoBarWrappers.size() > 0 ? mInfoBarWrappers.get(0).getItem() : null; + mAnimationListener.notifyAllAnimationsFinished(frontItem); + } + + private void runAnimation(InfoBarAnimation animation) { + mAnimation = animation; + mAnimation.prepareAnimation(); + if (isLayoutRequested()) { + // onLayout() will call mAnimation.start(). + } else { + mAnimation.start(); + } + } + + private void addWrapper(InfoBarWrapper wrapper) { + addView(wrapper, 0, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + mInfoBarWrappers.add(wrapper); + updateLayoutParams(); + } + + private void addWrapperToFront(InfoBarWrapper wrapper) { + addView(wrapper, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + mInfoBarWrappers.add(0, wrapper); + updateLayoutParams(); + } + + private void removeWrapper(InfoBarWrapper wrapper) { + removeView(wrapper); + mInfoBarWrappers.remove(wrapper); + updateLayoutParams(); + } + + private void updateLayoutParams() { + // Stagger the top margins so the back infobars peek out a bit. + int childCount = mInfoBarWrappers.size(); + for (int i = 0; i < childCount; i++) { + View child = mInfoBarWrappers.get(i); + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + lp.topMargin = (childCount - 1 - i) * mBackInfobarHeight; + child.setLayoutParams(lp); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + widthMeasureSpec = mFloatingBehavior.beforeOnMeasure(widthMeasureSpec); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + mFloatingBehavior.afterOnMeasure(getMeasuredHeight()); + } + + @Override + public void announceForAccessibility(CharSequence text) { + if (TextUtils.isEmpty(text)) return; + super.announceForAccessibility(text); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + mFloatingBehavior.updateShadowPosition(); + + // Animations start after a layout has completed, at which point all views are guaranteed + // to have valid sizes and positions. + if (mAnimation != null && !mAnimation.isStarted()) { + mAnimation.start(); + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + // Trap any attempts to fiddle with the infobars while we're animating. + return super.onInterceptTouchEvent(ev) || mAnimation != null + || (!mInfoBarWrappers.isEmpty() + && !mInfoBarWrappers.get(0).getItem().areControlsEnabled()); + } + + @Override + @SuppressLint("ClickableViewAccessibility") + public boolean onTouchEvent(MotionEvent event) { + super.onTouchEvent(event); + // Consume all touch events so they do not reach the ContentView. + return true; + } + + @Override + public boolean onHoverEvent(MotionEvent event) { + super.onHoverEvent(event); + // Consume all hover events so they do not reach the ContentView. In touch exploration mode, + // this prevents the user from interacting with the part of the ContentView behind the + // infobars. http://crbug.com/430701 + return true; + } +} diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/InfoBarContainerView.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/InfoBarContainerView.java new file mode 100644 index 00000000000..553608310f2 --- /dev/null +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/InfoBarContainerView.java @@ -0,0 +1,257 @@ +// Copyright 2013 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.weblayer_private; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.content.Context; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.RelativeLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import org.chromium.base.MathUtils; +import org.chromium.ui.display.DisplayAndroid; +import org.chromium.ui.display.DisplayUtil; + +/** + * The {@link View} for the {@link InfoBarContainer}. + */ +public class InfoBarContainerView extends SwipableOverlayView { + /** + * Observes container view changes. + */ + public interface ContainerViewObserver extends InfoBarContainer.InfoBarAnimationListener { + /** + * Called when the height of shown content changed. + * @param shownFraction The ratio of height of shown content to the height of the container + * view. + */ + void onShownRatioChanged(float shownFraction); + } + + /** Top margin, including the toolbar and tabstrip height and 48dp of web contents. */ + private static final int TOP_MARGIN_PHONE_DP = 104; + private static final int TOP_MARGIN_TABLET_DP = 144; + + /** Length of the animation to fade the InfoBarContainer back into View. */ + private static final long REATTACH_FADE_IN_MS = 250; + + /** Whether or not the InfoBarContainer is allowed to hide when the user scrolls. */ + private static boolean sIsAllowedToAutoHide = true; + + private final ContainerViewObserver mContainerViewObserver; + private final InfoBarContainerLayout mLayout; + + /** Parent view that contains the InfoBarContainerLayout. */ + private ViewGroup mParentView; + + private TabImpl mTab; + + /** Animation used to snap the container to the nearest state if scroll direction changes. */ + private Animator mScrollDirectionChangeAnimation; + + /** Whether or not the current scroll is downward. */ + private boolean mIsScrollingDownward; + + /** Tracks the previous event's scroll offset to determine if a scroll is up or down. */ + private int mLastScrollOffsetY; + + /** + * @param context The {@link Context} that this view is attached to. + * @param containerViewObserver The {@link ContainerViewObserver} that gets notified on + * container view changes. + * @param isTablet Whether this view is displayed on tablet or not. + */ + InfoBarContainerView(@NonNull Context context, + @NonNull ContainerViewObserver containerViewObserver, TabImpl tab, boolean isTablet) { + super(context, null); + mTab = tab; + mContainerViewObserver = containerViewObserver; + + // TODO(newt): move this workaround into the infobar views if/when they're scrollable. + // Workaround for http://crbug.com/407149. See explanation in onMeasure() below. + setVerticalScrollBarEnabled(false); + + updateLayoutParams(context, isTablet); + + Runnable makeContainerVisibleRunnable = () -> runUpEventAnimation(true); + mLayout = new InfoBarContainerLayout(context, makeContainerVisibleRunnable, + new InfoBarContainer.InfoBarAnimationListener() { + @Override + public void notifyAnimationFinished(int animationType) { + mContainerViewObserver.notifyAnimationFinished(animationType); + } + + @Override + public void notifyAllAnimationsFinished(InfoBarUiItem frontInfoBar) { + mContainerViewObserver.notifyAllAnimationsFinished(frontInfoBar); + } + }); + + addView(mLayout, + new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, + Gravity.CENTER_HORIZONTAL)); + } + + void destroy() { + removeFromParentView(); + mTab = null; + } + + // SwipableOverlayView implementation. + @Override + @VisibleForTesting + public boolean isAllowedToAutoHide() { + return sIsAllowedToAutoHide; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (getVisibility() != View.GONE) { + setVisibility(VISIBLE); + setAlpha(0f); + animate().alpha(1f).setDuration(REATTACH_FADE_IN_MS); + } + } + + @Override + protected void runUpEventAnimation(boolean visible) { + if (mScrollDirectionChangeAnimation != null) mScrollDirectionChangeAnimation.cancel(); + super.runUpEventAnimation(visible); + } + + @Override + protected boolean isIndependentlyAnimating() { + return mScrollDirectionChangeAnimation != null; + } + + // View implementation. + @Override + public void setTranslationY(float translationY) { + int contentHeightDelta = mTab != null + ? mTab.getBrowser().getViewController().getBottomContentHeightDelta() + : 0; + + // Push the infobar container up by any delta caused by the bottom toolbar while ensuring + // that it does not ascend beyond the top of the bottom toolbar nor descend beyond its own + // height. + float newTranslationY = MathUtils.clamp( + translationY - contentHeightDelta, -contentHeightDelta, getHeight()); + + super.setTranslationY(newTranslationY); + + float shownFraction = 0; + if (getHeight() > 0) { + shownFraction = contentHeightDelta > 0 ? 1f : 1f - (translationY / getHeight()); + } + mContainerViewObserver.onShownRatioChanged(shownFraction); + } + + /** + * Sets whether the InfoBarContainer is allowed to auto-hide when the user scrolls the page. + * Expected to be called when Touch Exploration is enabled. + * @param isAllowed Whether auto-hiding is allowed. + */ + public static void setIsAllowedToAutoHide(boolean isAllowed) { + sIsAllowedToAutoHide = isAllowed; + } + + /** + * Notifies that an infobar's View ({@link InfoBar#getView}) has changed. If the infobar is + * visible, a view swapping animation will be run. + */ + void notifyInfoBarViewChanged() { + mLayout.notifyInfoBarViewChanged(); + } + + /** + * Sets the parent {@link ViewGroup} that contains the {@link InfoBarContainer}. + */ + void setParentView(ViewGroup parent) { + mParentView = parent; + // Don't attach the container to the new parent if it is not previously attached. + if (removeFromParentView()) addToParentView(); + } + + /** + * Adds this class to the parent view {@link #mParentView}. + */ + void addToParentView() { + // If mTab is null, destroy() was called. This should not be added after destroyed. + assert mTab != null; + super.addToParentView(mParentView, + mTab.getBrowser().getViewController().getDesiredInfoBarContainerViewIndex()); + } + + /** + * Adds an {@link InfoBar} to the layout. + * @param infoBar The {@link InfoBar} to be added. + */ + void addInfoBar(InfoBar infoBar) { + infoBar.createView(); + mLayout.addInfoBar(infoBar); + } + + /** + * Removes an {@link InfoBar} from the layout. + * @param infoBar The {@link InfoBar} to be removed. + */ + void removeInfoBar(InfoBar infoBar) { + mLayout.removeInfoBar(infoBar); + } + + /** + * Hides or stops hiding this View. + * @param isHidden Whether this View is should be hidden. + */ + void setHidden(boolean isHidden) { + setVisibility(isHidden ? View.GONE : View.VISIBLE); + } + + /** + * Run an animation when the scrolling direction of a gesture has changed (this does not mean + * the gesture has ended). + * @param visible Whether or not the view should be visible. + */ + private void runDirectionChangeAnimation(boolean visible) { + mScrollDirectionChangeAnimation = createVerticalSnapAnimation(visible); + mScrollDirectionChangeAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mScrollDirectionChangeAnimation = null; + } + }); + mScrollDirectionChangeAnimation.start(); + } + + @Override + // Ensure that this view's custom layout params are passed when adding it to its parent. + public ViewGroup.MarginLayoutParams createLayoutParams() { + return (ViewGroup.MarginLayoutParams) getLayoutParams(); + } + + private void updateLayoutParams(Context context, boolean isTablet) { + RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + lp.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); + int topMarginDp = isTablet ? TOP_MARGIN_TABLET_DP : TOP_MARGIN_PHONE_DP; + lp.topMargin = DisplayUtil.dpToPx(DisplayAndroid.getNonMultiDisplay(context), topMarginDp); + setLayoutParams(lp); + } + + /** + * Returns true if any animations are pending or in progress. + */ + @VisibleForTesting + public boolean isAnimating() { + return mLayout.isAnimating(); + } +} diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/InfoBarUiItem.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/InfoBarUiItem.java new file mode 100644 index 00000000000..5a653d069c3 --- /dev/null +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/InfoBarUiItem.java @@ -0,0 +1,69 @@ +// Copyright 2020 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.weblayer_private; + +import android.view.View; + +import androidx.annotation.IntDef; + +import org.chromium.chrome.browser.infobar.InfoBarIdentifier; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * An interface for items that can be added to an InfoBarContainerLayout. + */ +public interface InfoBarUiItem { + // The infobar priority. + @IntDef({InfoBarPriority.CRITICAL, InfoBarPriority.USER_TRIGGERED, + InfoBarPriority.PAGE_TRIGGERED, InfoBarPriority.BACKGROUND}) + @Retention(RetentionPolicy.SOURCE) + public @interface InfoBarPriority { + int CRITICAL = 0; + int USER_TRIGGERED = 1; + int PAGE_TRIGGERED = 2; + int BACKGROUND = 3; + } + + /** + * Returns the View that represents this infobar. This should have no background or borders; + * a background and shadow will be added by a wrapper view. + */ + View getView(); + + /** + * Returns whether controls for this View should be clickable. If false, all input events on + * this item will be ignored. + */ + boolean areControlsEnabled(); + + /** + * Sets whether or not controls for this View should be clickable. This does not affect the + * visual state of the infobar. + * @param state If false, all input events on this Item will be ignored. + */ + void setControlsEnabled(boolean state); + + /** + * Returns the accessibility text to announce when this infobar is first shown. + */ + CharSequence getAccessibilityText(); + + /** + * Returns the priority of an infobar. High priority infobar is shown in front of low + * priority infobar. If infobars have the same priorities, the most recently added one + * is shown behind previous ones. + * + */ + int getPriority(); + + /** + * Returns the type of infobar, as best as can be determined at this time. See + * components/infobars/core/infobar_delegate.h. + */ + @InfoBarIdentifier + int getInfoBarIdentifier(); +} diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/InfoBarWrapper.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/InfoBarWrapper.java new file mode 100644 index 00000000000..8a574254a96 --- /dev/null +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/InfoBarWrapper.java @@ -0,0 +1,44 @@ +// Copyright 2016 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.weblayer_private; + +import android.content.Context; +import android.content.res.Resources; +import android.view.Gravity; +import android.view.View; +import android.widget.FrameLayout; + +/** + * Layout that holds an infobar's contents and provides a background color and a top shadow. + */ +class InfoBarWrapper extends FrameLayout { + private final InfoBarUiItem mItem; + + /** + * Constructor for inflating from Java. + */ + InfoBarWrapper(Context context, InfoBarUiItem item) { + super(context); + mItem = item; + Resources res = context.getResources(); + int peekingHeight = res.getDimensionPixelSize(R.dimen.infobar_peeking_height); + int shadowHeight = res.getDimensionPixelSize(R.dimen.infobar_shadow_height); + setMinimumHeight(peekingHeight + shadowHeight); + + // setBackgroundResource() changes the padding, so call setPadding() second. + setBackgroundResource(R.drawable.weblayer_infobar_wrapper_bg); + setPadding(0, shadowHeight, 0, 0); + } + + InfoBarUiItem getItem() { + return mItem; + } + + @Override + public void onViewAdded(View child) { + child.setLayoutParams(new LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, Gravity.TOP)); + } +} diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/IntentUtils.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/IntentUtils.java new file mode 100644 index 00000000000..5d2dfec5c69 --- /dev/null +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/IntentUtils.java @@ -0,0 +1,48 @@ +// Copyright 2020 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.weblayer_private; + +import android.content.Intent; +import android.os.RemoteException; +import android.util.AndroidRuntimeException; + +/** A utility class for creating and handling common intents. */ +public class IntentUtils { + private static final String sExtraTabId = "TAB_ID"; + private static final String sActivateTabAction = + "org.chromium.weblayer.intent_utils.ACTIVATE_TAB"; + + /** + * Handles an intent generated by this class. + * @return true if the intent was handled, or false if the intent wasn't generated by this + * class. + */ + public static boolean handleIntent(Intent intent) { + if (!intent.getAction().equals(sActivateTabAction)) return false; + + int tabId = intent.getIntExtra(sExtraTabId, -1); + TabImpl tab = TabImpl.getTabById(tabId); + if (tab == null) return true; + + try { + tab.getClient().bringTabToFront(); + } catch (RemoteException e) { + throw new AndroidRuntimeException(e); + } + return true; + } + + /** + * Creates an intent to bring a tab to the foreground. + * This intent should also bring the app to the foreground. + * @param tabId the identifier for the tab. + */ + public static Intent createBringTabToFrontIntent(int tabId) { + Intent intent = WebLayerImpl.createIntent(); + intent.putExtra(sExtraTabId, tabId); + intent.setAction(sActivateTabAction); + return intent; + } +}; diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/MediaSessionManager.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/MediaSessionManager.java new file mode 100644 index 00000000000..a36125ace70 --- /dev/null +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/MediaSessionManager.java @@ -0,0 +1,140 @@ +// Copyright 2020 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.weblayer_private; + +import android.annotation.SuppressLint; +import android.app.Service; +import android.content.Intent; +import android.support.v4.media.session.MediaSessionCompat; + +import org.chromium.components.browser_ui.media.MediaNotificationController; +import org.chromium.components.browser_ui.media.MediaNotificationInfo; +import org.chromium.components.browser_ui.media.MediaSessionHelper; +import org.chromium.components.browser_ui.notifications.ChromeNotification; +import org.chromium.components.browser_ui.notifications.ChromeNotificationBuilder; +import org.chromium.components.browser_ui.notifications.ForegroundServiceUtils; +import org.chromium.components.browser_ui.notifications.NotificationMetadata; + +/** + * A glue class for MediaSession. + * This class defines delegates that provide WebLayer-specific behavior to shared MediaSession code. + * It also manages the lifetime of {@link MediaNotificationController} and the {@link Service} + * associated with the notification. + */ +class MediaSessionManager { + // This is a singleton because there's only at most one MediaSession active at a time. + @SuppressLint("StaticFieldLeak") + static MediaNotificationController sController; + + private static int sNotificationId = 0; + + static void serviceStarted(Service service, Intent intent) { + if (sController != null && sController.processIntent(service, intent)) return; + + // The service has been started with startForegroundService() but the + // notification hasn't been shown. See similar logic in {@link + // ChromeMediaNotificationControllerDelegate}. + MediaNotificationController.finishStartingForegroundServiceOnO( + service, createChromeNotificationBuilder().buildChromeNotification()); + // Call stopForeground to guarantee Android unset the foreground bit. + ForegroundServiceUtils.getInstance().stopForeground( + service, Service.STOP_FOREGROUND_REMOVE); + service.stopSelf(); + } + + static void serviceDestroyed() { + if (sController != null) sController.onServiceDestroyed(); + sController = null; + } + + static MediaSessionHelper.Delegate createMediaSessionHelperDelegate(int tabId) { + return new MediaSessionHelper.Delegate() { + @Override + public Intent createBringTabToFrontIntent() { + return IntentUtils.createBringTabToFrontIntent(tabId); + } + + @Override + public boolean fetchLargeFaviconImage() { + // TODO(crbug.com/1076463): WebLayer doesn't support favicons. + return false; + } + + @Override + public MediaNotificationInfo.Builder createMediaNotificationInfoBuilder() { + ensureNotificationId(); + return new MediaNotificationInfo.Builder().setInstanceId(tabId).setId( + sNotificationId); + } + + @Override + public void showMediaNotification(MediaNotificationInfo notificationInfo) { + assert notificationInfo.id == sNotificationId; + if (sController == null) { + sController = new MediaNotificationController( + new WebLayerMediaNotificationControllerDelegate()); + } + sController.mThrottler.queueNotification(notificationInfo); + } + + @Override + public void hideMediaNotification() { + if (sController != null) sController.hideNotification(tabId); + } + + @Override + public void activateAndroidMediaSession() { + if (sController != null) sController.activateAndroidMediaSession(tabId); + } + }; + } + + private static class WebLayerMediaNotificationControllerDelegate + implements MediaNotificationController.Delegate { + @Override + public Intent createServiceIntent() { + return WebLayerImpl.createMediaSessionServiceIntent(); + } + + @Override + public String getAppName() { + return WebLayerImpl.getClientApplicationName(); + } + + @Override + public String getNotificationGroupName() { + return "org.chromium.weblayer.MediaSession"; + } + + @Override + public ChromeNotificationBuilder createChromeNotificationBuilder() { + return MediaSessionManager.createChromeNotificationBuilder(); + } + + @Override + public void onMediaSessionUpdated(MediaSessionCompat session) { + // This is only relevant when casting. + } + + @Override + public void logNotificationShown(ChromeNotification notification) {} + } + + private static ChromeNotificationBuilder createChromeNotificationBuilder() { + ensureNotificationId(); + + // Only the null tag will work as expected, because {@link Service#startForeground()} only + // takes an ID and no tag. If we pass a tag here, then the notification that's used to + // display a paused state (no foreground service) will not be identified as the same one + // that's used with the foreground service. + return WebLayerNotificationBuilder.create( + WebLayerNotificationChannels.ChannelId.MEDIA_PLAYBACK, + new NotificationMetadata(0, null /*notificationTag*/, sNotificationId)); + } + + private static void ensureNotificationId() { + if (sNotificationId == 0) sNotificationId = WebLayerImpl.getMediaSessionNotificationId(); + } +} diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/MediaStreamManager.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/MediaStreamManager.java index 446af44ea47..90925f10df5 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/MediaStreamManager.java +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/MediaStreamManager.java @@ -21,7 +21,6 @@ import org.chromium.components.browser_ui.notifications.NotificationManagerProxy import org.chromium.components.browser_ui.notifications.NotificationManagerProxyImpl; import org.chromium.components.browser_ui.notifications.NotificationMetadata; import org.chromium.components.browser_ui.notifications.PendingIntentProvider; -import org.chromium.components.browser_ui.notifications.channels.ChannelsInitializer; import org.chromium.components.webrtc.MediaCaptureNotificationUtil; import org.chromium.components.webrtc.MediaCaptureNotificationUtil.MediaType; import org.chromium.content_public.browser.WebContents; @@ -65,6 +64,7 @@ public class MediaStreamManager { /** * @return a string that prefixes all intents that can be handled by {@link forwardIntent}. + * @Deprecated in M85+, this class does not handle intents. Remove in M88. */ public static String getIntentPrefix() { return WEBRTC_PREFIX; @@ -73,6 +73,7 @@ public class MediaStreamManager { /** * Handles an intent coming from a media streaming notification. * @param intent the intent which was previously posted via {@link update}. + * @Deprecated in M85+, this class does not handle intents. Remove in M88. */ public static void forwardIntent(Intent intent) { assert intent.getAction().equals(ACTIVATE_TAB_INTENT); @@ -208,28 +209,28 @@ public class MediaStreamManager { } Context appContext = ContextUtils.getApplicationContext(); - Intent intent = WebLayerImpl.createIntent(); - intent.putExtra(EXTRA_TAB_ID, mNotificationId); - intent.setAction(ACTIVATE_TAB_INTENT); + Intent intent = null; + if (WebLayerFactoryImpl.getClientMajorVersion() >= 85) { + intent = IntentUtils.createBringTabToFrontIntent(mNotificationId); + } else { + intent = WebLayerImpl.createIntent(); + intent.putExtra(EXTRA_TAB_ID, mNotificationId); + intent.setAction(ACTIVATE_TAB_INTENT); + } PendingIntentProvider contentIntent = PendingIntentProvider.getBroadcast(appContext, mNotificationId, intent, 0); int mediaType = audio && video ? MediaType.AUDIO_AND_VIDEO : audio ? MediaType.AUDIO_ONLY : MediaType.VIDEO_ONLY; - NotificationManagerProxy notificationManagerProxy = getNotificationManager(); - ChannelsInitializer channelsInitializer = new ChannelsInitializer(notificationManagerProxy, - WebLayerNotificationChannels.getInstance(), appContext.getResources()); - // TODO(crbug/1076098): don't pass a URL in incognito. ChromeNotification notification = MediaCaptureNotificationUtil.createNotification( - new WebLayerNotificationBuilder(appContext, + WebLayerNotificationBuilder.create( WebLayerNotificationChannels.ChannelId.WEBRTC_CAM_AND_MIC, - channelsInitializer, new NotificationMetadata(0, AV_STREAM_TAG, mNotificationId)), mediaType, mTab.getWebContents().getVisibleUrl().getSpec(), WebLayerImpl.getClientApplicationName(), contentIntent, null /*stopIntent*/); - notificationManagerProxy.notify(notification); + getNotificationManager().notify(notification); updateActiveNotifications(true); notifyClient(audio, video); diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/MojoInterfaceRegistrar.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/MojoInterfaceRegistrar.java new file mode 100644 index 00000000000..d124792cd0d --- /dev/null +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/MojoInterfaceRegistrar.java @@ -0,0 +1,28 @@ +// Copyright 2020 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.weblayer_private; + +import org.chromium.base.annotations.CalledByNative; +import org.chromium.content_public.browser.InterfaceRegistrar; +import org.chromium.content_public.browser.WebContents; +import org.chromium.services.service_manager.InterfaceRegistry; +import org.chromium.webshare.mojom.ShareService; + +/** + * Registers Java implementations of mojo interfaces. + */ +class MojoInterfaceRegistrar { + @CalledByNative + private static void registerMojoInterfaces() { + InterfaceRegistrar.Registry.addWebContentsRegistrar(new WebContentsInterfaceRegistrar()); + } + + private static class WebContentsInterfaceRegistrar implements InterfaceRegistrar<WebContents> { + @Override + public void registerInterfaces(InterfaceRegistry registry, final WebContents webContents) { + registry.addInterface(ShareService.MANAGER, new WebShareServiceFactory(webContents)); + } + } +} diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/NavigationControllerImpl.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/NavigationControllerImpl.java index c44b3b66031..3957ed46557 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/NavigationControllerImpl.java +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/NavigationControllerImpl.java @@ -113,6 +113,13 @@ public final class NavigationControllerImpl extends INavigationController.Stub { mNativeNavigationController, index); } + @Override + public boolean isNavigationEntrySkippable(int index) { + StrictModeWorkaround.apply(); + return NavigationControllerImplJni.get().isNavigationEntrySkippable( + mNativeNavigationController, index); + } + @CalledByNative private NavigationImpl createNavigation(long nativeNavigationImpl) { return new NavigationImpl(mNavigationControllerClient, nativeNavigationImpl); @@ -159,6 +166,12 @@ public final class NavigationControllerImpl extends INavigationController.Stub { mNavigationControllerClient.onFirstContentfulPaint(); } + @CalledByNative + private void onOldPageNoLongerRendered(String uri) throws RemoteException { + if (WebLayerFactoryImpl.getClientMajorVersion() < 85) return; + mNavigationControllerClient.onOldPageNoLongerRendered(uri); + } + @NativeMethods interface Natives { void setNavigationControllerImpl( @@ -178,5 +191,6 @@ public final class NavigationControllerImpl extends INavigationController.Stub { int getNavigationListCurrentIndex(long nativeNavigationControllerImpl); String getNavigationEntryDisplayUri(long nativeNavigationControllerImpl, int index); String getNavigationEntryTitle(long nativeNavigationControllerImpl, int index); + boolean isNavigationEntrySkippable(long nativeNavigationControllerImpl, int index); } } diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/NewTabCallbackProxy.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/NewTabCallbackProxy.java index c5d665b7c08..25645acdc76 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/NewTabCallbackProxy.java +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/NewTabCallbackProxy.java @@ -48,12 +48,10 @@ public final class NewTabCallbackProxy { } @CalledByNative - public void onNewTab(long nativeTab, @ImplNewTabType int mode) throws RemoteException { + public void onNewTab(TabImpl tab, @ImplNewTabType int mode) throws RemoteException { // This class should only be created while the tab is attached to a fragment. assert mTab.getBrowser() != null; - TabImpl tab = - new TabImpl(mTab.getProfile(), mTab.getBrowser().getWindowAndroid(), nativeTab); - mTab.getBrowser().addTab(tab); + assert mTab.getBrowser().equals(tab.getBrowser()); mTab.getClient().onNewTab(tab.getId(), mode); } diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/PageInfoControllerDelegateImpl.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/PageInfoControllerDelegateImpl.java index 687be42fe1a..314b6b7ff29 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/PageInfoControllerDelegateImpl.java +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/PageInfoControllerDelegateImpl.java @@ -7,10 +7,15 @@ package org.chromium.weblayer_private; import android.content.Context; import android.content.Intent; +import androidx.annotation.NonNull; + import org.chromium.base.StrictModeContext; import org.chromium.base.supplier.Supplier; +import org.chromium.components.content_settings.CookieControlsBridge; +import org.chromium.components.content_settings.CookieControlsObserver; import org.chromium.components.embedder_support.util.UrlConstants; import org.chromium.components.page_info.PageInfoControllerDelegate; +import org.chromium.content_public.browser.WebContents; import org.chromium.ui.modaldialog.ModalDialogManager; import org.chromium.url.GURL; import org.chromium.weblayer_private.interfaces.SiteSettingsIntentHelper; @@ -20,18 +25,27 @@ import org.chromium.weblayer_private.interfaces.SiteSettingsIntentHelper; */ public class PageInfoControllerDelegateImpl extends PageInfoControllerDelegate { private final Context mContext; + private final WebContents mWebContents; private final String mProfileName; - public PageInfoControllerDelegateImpl(Context context, String profileName, GURL url, - Supplier<ModalDialogManager> modalDialogManager) { + static PageInfoControllerDelegateImpl create(WebContents webContents) { + TabImpl tab = TabImpl.fromWebContents(webContents); + assert tab != null; + return new PageInfoControllerDelegateImpl(tab.getBrowser().getContext(), webContents, + tab.getProfile(), tab.getBrowser().getWindowAndroid()::getModalDialogManager); + } + + private PageInfoControllerDelegateImpl(Context context, WebContents webContents, + ProfileImpl profile, Supplier<ModalDialogManager> modalDialogManager) { super(modalDialogManager, new AutocompleteSchemeClassifierImpl(), /** vrHandler= */ null, /** isSiteSettingsAvailable= */ - UrlConstants.HTTP_SCHEME.equals(url.getScheme()) - || UrlConstants.HTTPS_SCHEME.equals(url.getScheme()), - /** cookieControlsShown= */ false); + isHttpOrHttps(webContents.getVisibleUrl()), + /** cookieControlsShown= */ + CookieControlsBridge.isCookieControlsEnabled(profile)); mContext = context; - mProfileName = profileName; + mWebContents = webContents; + mProfileName = profile.getName(); } /** @@ -47,4 +61,18 @@ public class PageInfoControllerDelegateImpl extends PageInfoControllerDelegate { mContext.startActivity(intent); } } + + /** + * {@inheritDoc} + */ + @Override + @NonNull + public CookieControlsBridge createCookieControlsBridge(CookieControlsObserver observer) { + return new CookieControlsBridge(observer, mWebContents, null); + } + + private static boolean isHttpOrHttps(GURL url) { + String scheme = url.getScheme(); + return UrlConstants.HTTP_SCHEME.equals(scheme) || UrlConstants.HTTPS_SCHEME.equals(scheme); + } } diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/ProfileImpl.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/ProfileImpl.java index ba5cc8c56dd..c359dc3513d 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/ProfileImpl.java +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/ProfileImpl.java @@ -5,6 +5,7 @@ package org.chromium.weblayer_private; import android.content.Intent; +import android.text.TextUtils; import android.webkit.ValueCallback; import androidx.annotation.NonNull; @@ -25,7 +26,10 @@ import org.chromium.weblayer_private.interfaces.SettingType; import org.chromium.weblayer_private.interfaces.StrictModeWorkaround; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * Implementation of IProfile. @@ -168,6 +172,40 @@ public final class ProfileImpl extends IProfile.Stub implements BrowserContextHa return mCookieManager; } + @Override + public void getBrowserPersistenceIds(@NonNull IObjectWrapper callback) { + StrictModeWorkaround.apply(); + checkNotDestroyed(); + ValueCallback<Set<String>> valueCallback = + (ValueCallback<Set<String>>) ObjectWrapper.unwrap(callback, ValueCallback.class); + Callback<String[]> baseCallback = (String[] result) -> { + valueCallback.onReceiveValue(new HashSet<String>(Arrays.asList(result))); + }; + ProfileImplJni.get().getBrowserPersistenceIds(mNativeProfile, baseCallback); + } + + @Override + public void removeBrowserPersistenceStorage(String[] ids, @NonNull IObjectWrapper callback) { + StrictModeWorkaround.apply(); + checkNotDestroyed(); + ValueCallback<Boolean> valueCallback = + (ValueCallback<Boolean>) ObjectWrapper.unwrap(callback, ValueCallback.class); + Callback<Boolean> baseCallback = valueCallback::onReceiveValue; + for (String id : ids) { + if (TextUtils.isEmpty(id)) { + throw new IllegalArgumentException("id must be non-null and non-empty"); + } + } + ProfileImplJni.get().removeBrowserPersistenceStorage(mNativeProfile, ids, baseCallback); + } + + @Override + public void prepareForPossibleCrossOriginNavigation() { + StrictModeWorkaround.apply(); + checkNotDestroyed(); + ProfileImplJni.get().prepareForPossibleCrossOriginNavigation(mNativeProfile); + } + void checkNotDestroyed() { if (!mBeingDeleted) return; throw new IllegalArgumentException("Profile being destroyed: " + mName); @@ -232,5 +270,9 @@ public final class ProfileImpl extends IProfile.Stub implements BrowserContextHa void ensureBrowserContextInitialized(long nativeProfileImpl); void setBooleanSetting(long nativeProfileImpl, int type, boolean value); boolean getBooleanSetting(long nativeProfileImpl, int type); + void getBrowserPersistenceIds(long nativeProfileImpl, Callback<String[]> callback); + void removeBrowserPersistenceStorage( + long nativeProfileImpl, String[] ids, Callback<Boolean> callback); + void prepareForPossibleCrossOriginNavigation(long nativeProfileImpl); } } diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/README.md b/chromium/weblayer/browser/java/org/chromium/weblayer_private/README.md new file mode 100644 index 00000000000..35e0aedea89 --- /dev/null +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/README.md @@ -0,0 +1,36 @@ +# Which Context should I use? + +The code in this directory references different types of contexts. Please read about what each +represents before deciding which one you should use. + +## Embedder's Activity Context + +The fragment that WebLayer is loaded in holds a reference to the activity that it is currently +attached to. This is what's referred to by [`mEmbedderActivityContext`][link1] in BrowserImpl and +BrowserFragmentImpl. It should be used to reference anything associated with the activity. For +instance, embedder-specific resources, like Color resources which are resolved according to the +theme of the embedding activity. + +[link1]: https://source.chromium.org/chromium/chromium/src/+/6c336f4d55231595c038756f58a9e61d416a9c8f:weblayer/browser/java/org/chromium/weblayer_private/BrowserFragmentImpl.java;bpv=1;bpt=1 + +## WebLayer's Activity Context + +WebLayer has a lot of resources of its own which need to be accessed by the implementation code. We +thus wrap the embedder's activity context so that resource and assert look-ups against the wrapped +context go to the WebView or WebLayer support APK and not the embedder's APK. This wrapped Context +is what's returned by [`BrowserImpl.getContext()`][link2]. Use this when referencing WebLayer specific +resources. This is expected to be the most common use case. + +[link2]: https://source.chromium.org/chromium/chromium/src/+/master:weblayer/browser/java/org/chromium/weblayer_private/BrowserImpl.java?q=f:browserimpl%20getContext&ss=chromium%2Fchromium%2Fsrc + +## Embedder's Application Context + +Occasionally, we need the embedder's application context, as opposed to its activity context. For +instance, fetching the current locale which applies to the entire application. +Similar to WebLayer's Activity Context, this context is also wrapped in our implementation so we can +reference WebLayer-specific resources. This is what's returned by +[`ContextUtils.getApplicationContext()`][link3]. +It shouldn't be downcast to Application (or any subclass thereof) since it's wrapped in a +ContextWrapper. + +[link3]: https://source.chromium.org/chromium/chromium/src/+/master:base/android/java/src/org/chromium/base/ContextUtils.java?q=f:base%2FContextUtils%20getApplicationContext()&ss=chromium%2Fchromium%2Fsrc diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/SiteSettingsFragmentImpl.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/SiteSettingsFragmentImpl.java index d30bc58aed9..072df069fb9 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/SiteSettingsFragmentImpl.java +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/SiteSettingsFragmentImpl.java @@ -12,15 +12,18 @@ import android.os.Handler; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.View; +import android.view.View.OnAttachStateChangeListener; import android.view.ViewGroup; import android.view.Window; +import androidx.appcompat.app.AppCompatDelegate; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentController; import androidx.fragment.app.FragmentHostCallback; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; +import org.chromium.components.browser_ui.settings.SettingsUtils; import org.chromium.components.browser_ui.site_settings.SingleCategorySettings; import org.chromium.components.browser_ui.site_settings.SingleWebsiteSettings; import org.chromium.components.browser_ui.site_settings.SiteSettings; @@ -59,6 +62,7 @@ public class SiteSettingsFragmentImpl extends RemoteFragmentImpl { // resource IDs. private Context mContext; + private boolean mStarted; private FragmentController mFragmentController; /** @@ -78,6 +82,12 @@ public class SiteSettingsFragmentImpl extends RemoteFragmentImpl { private PassthroughFragmentActivity(SiteSettingsFragmentImpl fragmentImpl) { mFragmentImpl = fragmentImpl; attachBaseContext(mFragmentImpl.getWebLayerContext()); + // This class doesn't extend AppCompatActivity, so some appcompat functionality doesn't + // get initialized, which leads to some appcompat widgets (like switches) rendering + // incorrectly. There are some resource issues with having this class extend + // AppCompatActivity, but until we sort those out, creating an AppCompatDelegate will + // perform the necessary initialization. + AppCompatDelegate.create(this, null); } @Override @@ -182,8 +192,9 @@ public class SiteSettingsFragmentImpl extends RemoteFragmentImpl { @Override public LayoutInflater onGetLayoutInflater() { - return (LayoutInflater) mFragmentImpl.getWebLayerContext().getSystemService( - Context.LAYOUT_INFLATER_SERVICE); + Context context = mFragmentImpl.getWebLayerContext(); + return ((LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)) + .cloneInContext(context); } @Override @@ -271,6 +282,24 @@ public class SiteSettingsFragmentImpl extends RemoteFragmentImpl { throw new RuntimeException("Failed to create Site Settings Fragment", e); } } + + root.addOnAttachStateChangeListener(new OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View view) { + // Add the shadow scroll listener here once the View is attached to the Window. + SiteSettingsPreferenceFragment preferenceFragment = + (SiteSettingsPreferenceFragment) mFragmentController + .getSupportFragmentManager() + .findFragmentByTag(FRAGMENT_TAG); + ViewGroup listView = preferenceFragment.getListView(); + listView.getViewTreeObserver().addOnScrollChangedListener( + SettingsUtils.getShowShadowOnScrollListener( + listView, view.findViewById(R.id.shadow))); + } + + @Override + public void onViewDetachedFromWindow(View v) {} + }); return root; } @@ -298,7 +327,11 @@ public class SiteSettingsFragmentImpl extends RemoteFragmentImpl { @Override public void onStart() { super.onStart(); - mFragmentController.dispatchActivityCreated(); + + if (!mStarted) { + mStarted = true; + mFragmentController.dispatchActivityCreated(); + } mFragmentController.noteStateNotSaved(); mFragmentController.execPendingActions(); mFragmentController.dispatchStart(); diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/SwipableOverlayView.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/SwipableOverlayView.java new file mode 100644 index 00000000000..7f46f8afcd2 --- /dev/null +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/SwipableOverlayView.java @@ -0,0 +1,421 @@ +// Copyright 2014 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.weblayer_private; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.graphics.Region; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; +import android.widget.FrameLayout; + +import androidx.annotation.IntDef; + +import org.chromium.base.MathUtils; +import org.chromium.content_public.browser.GestureListenerManager; +import org.chromium.content_public.browser.GestureStateListenerWithScroll; +import org.chromium.content_public.browser.WebContents; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * View that slides up from the bottom of the page and slides away as the user scrolls the page. + * Meant to be tacked onto the {@link org.chromium.content_public.browser.WebContents}'s view and + * alerted when either the page scroll position or viewport size changes. + * + * GENERAL BEHAVIOR + * This View is brought onto the screen by sliding upwards from the bottom of the screen. Afterward + * the View slides onto and off of the screen vertically as the user scrolls upwards or + * downwards on the page. + * + * As the scroll offset or the viewport height are updated via a scroll or fling, the difference + * from the initial value is used to determine the View's Y-translation. If a gesture is stopped, + * the View will be snapped back into the center of the screen or entirely off of the screen, based + * on how much of the View is visible, or where the user is currently located on the page. + */ +public abstract class SwipableOverlayView extends FrameLayout { + private static final float FULL_THRESHOLD = 0.5f; + private static final float VERTICAL_FLING_SHOW_THRESHOLD = 0.2f; + private static final float VERTICAL_FLING_HIDE_THRESHOLD = 0.9f; + + @IntDef({Gesture.NONE, Gesture.SCROLLING, Gesture.FLINGING}) + @Retention(RetentionPolicy.SOURCE) + private @interface Gesture { + int NONE = 0; + int SCROLLING = 1; + int FLINGING = 2; + } + + private static final long ANIMATION_DURATION_MS = 250; + + /** Detects when the user is dragging the WebContents. */ + private final GestureStateListenerWithScroll mGestureStateListener; + + /** Listens for changes in the layout. */ + private final View.OnLayoutChangeListener mLayoutChangeListener; + + /** Interpolator used for the animation. */ + private final Interpolator mInterpolator; + + /** Tracks whether the user is scrolling or flinging. */ + private @Gesture int mGestureState; + + /** Animation currently being used to translate the View. */ + private Animator mCurrentAnimation; + + /** Used to determine when the layout has changed and the Viewport must be updated. */ + private int mParentHeight; + + /** Offset from the top of the page when the current gesture was first started. */ + private int mInitialOffsetY; + + /** How tall the View is, including its margins. */ + private int mTotalHeight; + + /** Whether or not the View ever been fully displayed. */ + private boolean mIsBeingDisplayedForFirstTime; + + /** The WebContents to which the overlay is added. */ + private WebContents mWebContents; + + /** + * Creates a SwipableOverlayView. + * @param context Context for acquiring resources. + * @param attrs Attributes from the XML layout inflation. + */ + public SwipableOverlayView(Context context, AttributeSet attrs) { + super(context, attrs); + mGestureStateListener = createGestureStateListener(); + mGestureState = Gesture.NONE; + mLayoutChangeListener = createLayoutChangeListener(); + mInterpolator = new DecelerateInterpolator(1.0f); + + // We make this view 'draw' to provide a placeholder for its animations. + setWillNotDraw(false); + } + + /** + * Set the given WebContents for scrolling changes. + */ + public void setWebContents(WebContents webContents) { + if (mWebContents != null) { + GestureListenerManager.fromWebContents(mWebContents) + .removeListener(mGestureStateListener); + } + + mWebContents = webContents; + // See comment in onLayout() as to why the listener is only attached if mTotalHeight is > 0. + if (mWebContents != null && mTotalHeight > 0) { + GestureListenerManager.fromWebContents(mWebContents).addListener(mGestureStateListener); + } + } + + public WebContents getWebContents() { + return mWebContents; + } + + protected void addToParentView(ViewGroup parentView, int index) { + if (parentView == null) return; + if (getParent() == null) { + parentView.addView(this, index, createLayoutParams()); + + // Listen for the layout to know when to animate the View coming onto the screen. + addOnLayoutChangeListener(mLayoutChangeListener); + } + } + + /** + * Removes the SwipableOverlayView from its parent and stops monitoring the WebContents. + * @return Whether the View was removed from its parent. + */ + public boolean removeFromParentView() { + if (getParent() == null) return false; + + ((ViewGroup) getParent()).removeView(this); + removeOnLayoutChangeListener(mLayoutChangeListener); + return true; + } + + /** + * Creates a set of LayoutParams that makes the View hug the bottom of the screen. Override it + * for other types of behavior. + * @return LayoutParams for use when adding the View to its parent. + */ + public ViewGroup.MarginLayoutParams createLayoutParams() { + return new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, + Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (!isAllowedToAutoHide()) setTranslationY(0.0f); + } + + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + super.onWindowFocusChanged(hasWindowFocus); + if (!isAllowedToAutoHide()) setTranslationY(0.0f); + } + + /** + * See {@link #android.view.ViewGroup.onLayout(boolean, int, int, int, int)}. + */ + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + // Update the viewport height when the parent View's height changes (e.g. after rotation). + int currentParentHeight = getParent() == null ? 0 : ((View) getParent()).getHeight(); + if (mParentHeight != currentParentHeight) { + mParentHeight = currentParentHeight; + mGestureState = Gesture.NONE; + if (mCurrentAnimation != null) mCurrentAnimation.end(); + } + + // Update the known effective height of the View. + MarginLayoutParams params = (MarginLayoutParams) getLayoutParams(); + mTotalHeight = getMeasuredHeight() + params.topMargin + params.bottomMargin; + + // Adding a listener to GestureListenerManager results in extra IPCs on every frame, which + // is very costly. Only attach the listener if needed. + if (mWebContents != null) { + if (mTotalHeight > 0) { + GestureListenerManager.fromWebContents(mWebContents) + .addListener(mGestureStateListener); + } else { + GestureListenerManager.fromWebContents(mWebContents) + .removeListener(mGestureStateListener); + } + } + + super.onLayout(changed, l, t, r, b); + } + + /** + * Creates a listener than monitors the WebContents for scrolls and flings. + * The listener updates the location of this View to account for the user's gestures. + * @return GestureStateListenerWithScroll to send to the WebContents. + */ + private GestureStateListenerWithScroll createGestureStateListener() { + return new GestureStateListenerWithScroll() { + /** Tracks the previous event's scroll offset to determine if a scroll is up or down. */ + private int mLastScrollOffsetY; + + /** Location of the View when the current gesture was first started. */ + private float mInitialTranslationY; + + /** The initial extent of the scroll when triggered. */ + private float mInitialExtentY; + + @Override + public void onFlingStartGesture(int scrollOffsetY, int scrollExtentY) { + if (!isAllowedToAutoHide() || !cancelCurrentAnimation()) return; + resetInternalScrollState(scrollOffsetY, scrollExtentY); + mGestureState = Gesture.FLINGING; + } + + @Override + public void onFlingEndGesture(int scrollOffsetY, int scrollExtentY) { + if (mGestureState != Gesture.FLINGING) return; + mGestureState = Gesture.NONE; + + updateTranslation(scrollOffsetY, scrollExtentY); + + boolean isScrollingDownward = scrollOffsetY > mLastScrollOffsetY; + + boolean isVisibleInitially = mInitialTranslationY < mTotalHeight; + float percentageVisible = 1.0f - (getTranslationY() / mTotalHeight); + float visibilityThreshold = isVisibleInitially ? VERTICAL_FLING_HIDE_THRESHOLD + : VERTICAL_FLING_SHOW_THRESHOLD; + boolean isVisibleEnough = percentageVisible > visibilityThreshold; + boolean isNearTopOfPage = scrollOffsetY < (mTotalHeight * FULL_THRESHOLD); + + boolean show = (!isScrollingDownward && isVisibleEnough) || isNearTopOfPage; + + runUpEventAnimation(show); + } + + @Override + public void onScrollStarted(int scrollOffsetY, int scrollExtentY) { + if (!isAllowedToAutoHide() || !cancelCurrentAnimation()) return; + resetInternalScrollState(scrollOffsetY, scrollExtentY); + mLastScrollOffsetY = scrollOffsetY; + mGestureState = Gesture.SCROLLING; + } + + @Override + public void onScrollEnded(int scrollOffsetY, int scrollExtentY) { + if (mGestureState != Gesture.SCROLLING) return; + mGestureState = Gesture.NONE; + + updateTranslation(scrollOffsetY, scrollExtentY); + + runUpEventAnimation(shouldSnapToVisibleState(scrollOffsetY)); + } + + @Override + public void onScrollOffsetOrExtentChanged(int scrollOffsetY, int scrollExtentY) { + mLastScrollOffsetY = scrollOffsetY; + + if (!shouldConsumeScroll(scrollOffsetY, scrollExtentY)) { + resetInternalScrollState(scrollOffsetY, scrollExtentY); + return; + } + + // This function is called for both fling and scrolls. + if (mGestureState == Gesture.NONE || !cancelCurrentAnimation() + || isIndependentlyAnimating()) { + return; + } + + updateTranslation(scrollOffsetY, scrollExtentY); + } + + private void updateTranslation(int scrollOffsetY, int scrollExtentY) { + float scrollDiff = + (scrollOffsetY - mInitialOffsetY) + (scrollExtentY - mInitialExtentY); + float translation = + MathUtils.clamp(mInitialTranslationY + scrollDiff, mTotalHeight, 0); + + // If the container has reached the completely shown position, reset the initial + // scroll so any movement will start hiding it again. + if (translation <= 0f) resetInternalScrollState(scrollOffsetY, scrollExtentY); + + setTranslationY(translation); + } + + /** + * Resets the internal values that a scroll or fling will base its calculations off of. + */ + private void resetInternalScrollState(int scrollOffsetY, int scrollExtentY) { + mInitialOffsetY = scrollOffsetY; + mInitialExtentY = scrollExtentY; + mInitialTranslationY = getTranslationY(); + } + }; + } + + /** + * @param scrollOffsetY The current scroll offset on the Y axis. + * @param scrollExtentY The current scroll extent on the Y axis. + * @return Whether or not the scroll should be consumed by the view. + */ + protected boolean shouldConsumeScroll(int scrollOffsetY, int scrollExtentY) { + return true; + } + + /** + * @param scrollOffsetY The current scroll offset on the Y axis. + * @return Whether the view should snap to a visible state. + */ + protected boolean shouldSnapToVisibleState(int scrollOffsetY) { + boolean isNearTopOfPage = scrollOffsetY < (mTotalHeight * FULL_THRESHOLD); + boolean isVisibleEnough = getTranslationY() < mTotalHeight * FULL_THRESHOLD; + return isNearTopOfPage || isVisibleEnough; + } + + /** + * @return Whether or not the view is animating independent of the user's scroll position. + */ + protected boolean isIndependentlyAnimating() { + return false; + } + + /** + * Creates a listener that is used only to animate the View coming onto the screen. + * @return The SimpleOnGestureListener that will monitor the View. + */ + private View.OnLayoutChangeListener createLayoutChangeListener() { + return new View.OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + removeOnLayoutChangeListener(mLayoutChangeListener); + + // Animate the View coming in from the bottom of the screen. + setTranslationY(mTotalHeight); + mIsBeingDisplayedForFirstTime = true; + runUpEventAnimation(true); + } + }; + } + + /** + * Create an animation that snaps the View into position vertically. + * @param visible If true, snaps the View to the bottom-center of the screen. If false, + * translates the View below the bottom-center of the screen so that it is + * effectively invisible. + * @return An animator with the snap animation. + */ + protected Animator createVerticalSnapAnimation(boolean visible) { + float targetTranslationY = visible ? 0.0f : mTotalHeight; + float yDifference = Math.abs(targetTranslationY - getTranslationY()) / mTotalHeight; + long duration = Math.max(0, (long) (ANIMATION_DURATION_MS * yDifference)); + + Animator animator = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, targetTranslationY); + animator.setDuration(duration); + animator.setInterpolator(mInterpolator); + + return animator; + } + + /** + * Run an animation when a gesture has ended (an 'up' motion event). + * @param visible Whether or not the view should be visible. + */ + protected void runUpEventAnimation(boolean visible) { + if (mCurrentAnimation != null) mCurrentAnimation.cancel(); + mCurrentAnimation = createVerticalSnapAnimation(visible); + mCurrentAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mGestureState = Gesture.NONE; + mCurrentAnimation = null; + mIsBeingDisplayedForFirstTime = false; + } + }); + mCurrentAnimation.start(); + } + + /** + * Cancels the current animation, unless the View is coming onto the screen for the first time. + * @return True if the animation was canceled or wasn't running, false otherwise. + */ + private boolean cancelCurrentAnimation() { + if (mIsBeingDisplayedForFirstTime) return false; + if (mCurrentAnimation != null) mCurrentAnimation.cancel(); + return true; + } + + /** + * @return Whether the SwipableOverlayView is allowed to hide itself on scroll. + */ + protected boolean isAllowedToAutoHide() { + return true; + } + + /** + * Override gatherTransparentRegion to make this view's layout a placeholder for its + * animations. This is only called during layout, so it doesn't really make sense to apply + * post-layout properties like it does by default. Together with setWillNotDraw(false), + * this ensures no child animation within this view's layout will be clipped by a SurfaceView. + */ + @Override + public boolean gatherTransparentRegion(Region region) { + float translationY = getTranslationY(); + setTranslationY(0); + boolean result = super.gatherTransparentRegion(region); + // Restoring TranslationY invalidates this view unnecessarily. However, this function + // is called as part of layout, which implies a full redraw is about to occur anyway. + setTranslationY(translationY); + return result; + } +} diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/TabImpl.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/TabImpl.java index 38d6cf9557c..382bfb7c605 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/TabImpl.java +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/TabImpl.java @@ -18,13 +18,16 @@ import android.view.ViewStructure; import android.view.autofill.AutofillValue; import android.webkit.ValueCallback; +import androidx.annotation.VisibleForTesting; + import org.chromium.base.Callback; import org.chromium.base.annotations.CalledByNative; import org.chromium.base.annotations.JNINamespace; import org.chromium.base.annotations.NativeMethods; import org.chromium.components.autofill.AutofillActionModeCallback; import org.chromium.components.autofill.AutofillProvider; -import org.chromium.components.autofill.AutofillProviderImpl; +import org.chromium.components.browser_ui.http_auth.LoginPrompt; +import org.chromium.components.browser_ui.media.MediaSessionHelper; import org.chromium.components.browser_ui.util.BrowserControlsVisibilityDelegate; import org.chromium.components.browser_ui.util.ComposedBrowserControlsVisibilityDelegate; import org.chromium.components.embedder_support.contextmenu.ContextMenuParams; @@ -55,7 +58,9 @@ import org.chromium.weblayer_private.interfaces.INavigationControllerClient; import org.chromium.weblayer_private.interfaces.IObjectWrapper; import org.chromium.weblayer_private.interfaces.ITab; import org.chromium.weblayer_private.interfaces.ITabClient; +import org.chromium.weblayer_private.interfaces.IWebMessageCallbackClient; import org.chromium.weblayer_private.interfaces.ObjectWrapper; +import org.chromium.weblayer_private.interfaces.ScrollNotificationType; import org.chromium.weblayer_private.interfaces.StrictModeWorkaround; import java.util.ArrayList; @@ -67,7 +72,7 @@ import java.util.Map; * Implementation of ITab. */ @JNINamespace("weblayer") -public final class TabImpl extends ITab.Stub { +public final class TabImpl extends ITab.Stub implements LoginPrompt.Observer { private static int sNextId = 1; // Map from id to TabImpl. private static final Map<Integer, TabImpl> sTabMap = new HashMap<Integer, TabImpl>(); @@ -83,6 +88,7 @@ public final class TabImpl extends ITab.Stub { private TabViewAndroidDelegate mViewAndroidDelegate; // BrowserImpl this TabImpl is in. This is only null during creation. private BrowserImpl mBrowser; + private LoginPrompt mLoginPrompt; /** * The AutofillProvider that integrates with system-level autofill. This is null until * updateFromBrowser() is invoked. @@ -107,10 +113,12 @@ public final class TabImpl extends ITab.Stub { private boolean mWaitingForMatchRects; private InterceptNavigationDelegateClientImpl mInterceptNavigationDelegateClient; private InterceptNavigationDelegateImpl mInterceptNavigationDelegate; + private InfoBarContainer mInfoBarContainer; + private MediaSessionHelper mMediaSessionHelper; private boolean mPostContainerViewInitDone; - private AccessibilityUtil.Observer mAccessibilityObserver; + private WebLayerAccessibilityUtil.Observer mAccessibilityObserver; private static class InternalAccessDelegateImpl implements ViewEventSink.InternalAccessDelegate { @@ -165,6 +173,37 @@ public final class TabImpl extends ITab.Stub { viewController.onBottomControlsChanged(bottomControlsOffsetY); } } + + @Override + public void onBackgroundColorChanged(int color) { + if (WebLayerFactoryImpl.getClientMajorVersion() >= 85) { + try { + mClient.onBackgroundColorChanged(color); + } catch (RemoteException e) { + throw new APICallException(e); + } + } + } + + @Override + protected void onVerticalScrollDirectionChanged( + boolean directionUp, float currentScrollRatio) { + if (WebLayerFactoryImpl.getClientMajorVersion() >= 85) { + try { + mClient.onScrollNotification(directionUp + ? ScrollNotificationType.DIRECTION_CHANGED_UP + : ScrollNotificationType.DIRECTION_CHANGED_DOWN, + currentScrollRatio); + } catch (RemoteException e) { + throw new APICallException(e); + } + } + } + } + + public static TabImpl fromWebContents(WebContents webContents) { + if (webContents == null || webContents.isDestroyed()) return null; + return TabImplJni.get().fromWebContents(webContents); } public static TabImpl getTabById(int tabId) { @@ -223,12 +262,21 @@ public final class TabImpl extends ITab.Stub { mInterceptNavigationDelegateClient.initializeWithDelegate(mInterceptNavigationDelegate); sTabMap.put(mId, this); + mInfoBarContainer = new InfoBarContainer(this); mAccessibilityObserver = (boolean enabled) -> { setBrowserControlsVisibilityConstraint(ImplControlsVisibilityReason.ACCESSIBILITY, enabled ? BrowserControlsState.SHOWN : BrowserControlsState.BOTH); }; // addObserver() calls to observer when added. WebLayerAccessibilityUtil.get().addObserver(mAccessibilityObserver); + + // MediaSession only works if the client is new enough. Sadly, passing + // kDisableMediaSessionAPI does not fully disable the API, so a check is also necessary + // before installing this observer. + if (WebLayerFactoryImpl.getClientMajorVersion() >= 85) { + mMediaSessionHelper = new MediaSessionHelper( + mWebContents, MediaSessionManager.createMediaSessionHelperDelegate(mId)); + } } private void doInitAfterSettingContainerView() { @@ -280,7 +328,7 @@ public final class TabImpl extends ITab.Stub { // Set up |mAutofillProvider| to operate in the new Context. It's safe to assume // the context won't change unless it is first nulled out, since the fragment // must be detached before it can be reattached to a new Context. - mAutofillProvider = new AutofillProviderImpl( + mAutofillProvider = new AutofillProvider( mBrowser.getContext(), mBrowser.getAutofillView(), "WebLayer"); TabImplJni.get().onAutofillProviderChanged(mNativeTab, mAutofillProvider); } @@ -333,17 +381,27 @@ public final class TabImpl extends ITab.Stub { assert mBrowser != null; TabImplJni.get().setBrowserControlsContainerViews( mNativeTab, topControlsContainerViewHandle, bottomControlsContainerViewHandle); + mInfoBarContainer.onTabDidGainActive(); updateWebContentsVisibility(); - mWebContents.onShow(); } /** * Called when this TabImpl is no longer the active TabImpl. */ public void onDidLoseActive() { + if (mAutofillProvider != null) { + mAutofillProvider.hidePopup(); + } + hideFindInPageUiAndNotifyClient(); - mWebContents.onHide(); updateWebContentsVisibility(); + + // This method is called as part of the final phase of TabImpl destruction, at which + // point mInfoBarContainer has already been destroyed. + if (mInfoBarContainer != null) { + mInfoBarContainer.onTabDidLoseActive(); + } + TabImplJni.get().setBrowserControlsContainerViews(mNativeTab, 0, 0); } @@ -351,7 +409,8 @@ public final class TabImpl extends ITab.Stub { * Returns whether this Tab is visible. */ public boolean isVisible() { - return (mBrowser.getActiveTab() == this && mBrowser.isStarted()); + return (mBrowser.getActiveTab() == this + && (mBrowser.isStarted() || mBrowser.isFragmentStoppedForConfigurationChange())); } private void updateWebContentsVisibility() { @@ -379,10 +438,17 @@ public final class TabImpl extends ITab.Stub { return mWebContents; } - long getNativeTab() { + // Public for tests. + @VisibleForTesting + public long getNativeTab() { return mNativeTab; } + @VisibleForTesting + public InfoBarContainer getInfoBarContainerForTesting() { + return mInfoBarContainer; + } + @Override public NavigationControllerImpl createNavigationController(INavigationControllerClient client) { StrictModeWorkaround.apply(); @@ -522,6 +588,7 @@ public final class TabImpl extends ITab.Stub { @Override public boolean dismissTransientUi() { + StrictModeWorkaround.apply(); BrowserViewController viewController = getViewController(); if (viewController != null && viewController.dismissTabModalOverlay()) return true; @@ -541,10 +608,34 @@ public final class TabImpl extends ITab.Stub { @Override public String getGuid() { + StrictModeWorkaround.apply(); return TabImplJni.get().getGuid(mNativeTab); } @Override + public boolean setData(Map data) { + StrictModeWorkaround.apply(); + String[] flattenedMap = new String[data.size() * 2]; + int i = 0; + for (Map.Entry<String, String> entry : ((Map<String, String>) data).entrySet()) { + flattenedMap[i++] = entry.getKey(); + flattenedMap[i++] = entry.getValue(); + } + return TabImplJni.get().setData(mNativeTab, flattenedMap); + } + + @Override + public Map getData() { + StrictModeWorkaround.apply(); + String[] data = TabImplJni.get().getData(mNativeTab); + Map<String, String> map = new HashMap<>(); + for (int i = 0; i < data.length; i += 2) { + map.put(data[i], data[i + 1]); + } + return map; + } + + @Override public void captureScreenShot(float scale, IObjectWrapper valueCallback) { StrictModeWorkaround.apply(); ValueCallback<Pair<Bitmap, Integer>> unwrappedCallback = @@ -553,6 +644,18 @@ public final class TabImpl extends ITab.Stub { TabImplJni.get().captureScreenShot(mNativeTab, scale, unwrappedCallback); } + @Override + public boolean canTranslate() { + StrictModeWorkaround.apply(); + return TabImplJni.get().canTranslate(mNativeTab); + } + + @Override + public void showTranslateUi() { + StrictModeWorkaround.apply(); + TabImplJni.get().showTranslateUi(mNativeTab); + } + @CalledByNative private static void runCaptureScreenShotCallback( ValueCallback<Pair<Bitmap, Integer>> callback, Bitmap bitmap, int errorCode) { @@ -634,6 +737,53 @@ public final class TabImpl extends ITab.Stub { getBrowser().destroyTab(this); } + @CalledByNative + private void showHttpAuthPrompt(String host, String url) { + mLoginPrompt = new LoginPrompt(mBrowser.getContext(), host, url, this); + mLoginPrompt.show(); + } + + @CalledByNative + private void closeHttpAuthPrompt() { + mLoginPrompt = null; + } + + @Override + public void cancel() { + TabImplJni.get().cancelHttpAuth(mNativeTab); + } + + @Override + public void proceed(String username, String password) { + TabImplJni.get().setHttpAuth(mNativeTab, username, password); + } + + @Override + public void registerWebMessageCallback( + String jsObjectName, List<String> allowedOrigins, IWebMessageCallbackClient client) { + if (jsObjectName.isEmpty()) { + throw new IllegalArgumentException("JS object name must not be empty"); + } + if (allowedOrigins.isEmpty()) { + throw new IllegalArgumentException("At least one origin must be specified"); + } + for (String origin : allowedOrigins) { + if (TextUtils.isEmpty(origin)) { + throw new IllegalArgumentException("Origin must not be non-empty"); + } + } + String registerError = TabImplJni.get().registerWebMessageCallback(mNativeTab, jsObjectName, + allowedOrigins.toArray(new String[allowedOrigins.size()]), client); + if (!TextUtils.isEmpty(registerError)) { + throw new IllegalArgumentException(registerError); + } + } + + @Override + public void unregisterWebMessageCallback(String jsObjectName) { + TabImplJni.get().unregisterWebMessageCallback(mNativeTab, jsObjectName); + } + public void destroy() { // Ensure that this method isn't called twice. assert mInterceptNavigationDelegate != null; @@ -668,6 +818,9 @@ public final class TabImpl extends ITab.Stub { mInterceptNavigationDelegateClient.destroy(); mInterceptNavigationDelegate = null; + mInfoBarContainer.destroy(); + mInfoBarContainer = null; + mMediaStreamManager.destroy(); mMediaStreamManager = null; @@ -734,6 +887,11 @@ public final class TabImpl extends ITab.Stub { onBrowserControlsStateUpdated(mBrowserControlsVisibility.get()); } + @VisibleForTesting + public boolean canBrowserControlsScrollForTesting() { + return mBrowserControlsVisibility.get() == BrowserControlsState.BOTH; + } + private void onBrowserControlsStateUpdated(int state) { // If something has overridden the FIP's SHOWN constraint, cancel FIP. This causes FIP to // dismiss when entering fullscreen. @@ -770,8 +928,14 @@ public final class TabImpl extends ITab.Stub { return (mBrowser.getActiveTab() == this) ? mBrowser.getViewController() : null; } + @VisibleForTesting + public boolean canInfoBarContainerScrollForTesting() { + return mInfoBarContainer.getContainerViewForTesting().isAllowedToAutoHide(); + } + @NativeMethods interface Natives { + TabImpl fromWebContents(WebContents webContents); long createTab(long profile, TabImpl caller); void setJavaImpl(long nativeTabImpl, TabImpl impl); void onAutofillProviderChanged(long nativeTabImpl, AutofillProvider autofillProvider); @@ -786,6 +950,15 @@ public final class TabImpl extends ITab.Stub { String getGuid(long nativeTabImpl); void captureScreenShot(long nativeTabImpl, float scale, ValueCallback<Pair<Bitmap, Integer>> valueCallback); + boolean setData(long nativeTabImpl, String[] data); + String[] getData(long nativeTabImpl); boolean isRendererControllingBrowserControlsOffsets(long nativeTabImpl); + void setHttpAuth(long nativeTabImpl, String username, String password); + void cancelHttpAuth(long nativeTabImpl); + String registerWebMessageCallback(long nativeTabImpl, String jsObjectName, + String[] allowedOrigins, IWebMessageCallbackClient client); + void unregisterWebMessageCallback(long nativeTabImpl, String jsObjectName); + boolean canTranslate(long nativeTabImpl); + void showTranslateUi(long nativeTabImpl); } } diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/TranslateCompactInfoBar.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/TranslateCompactInfoBar.java new file mode 100644 index 00000000000..a97315e6fa4 --- /dev/null +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/TranslateCompactInfoBar.java @@ -0,0 +1,578 @@ +// Copyright 2017 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.weblayer_private; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLayoutChangeListener; +import android.widget.ImageButton; +import android.widget.LinearLayout; + +import androidx.core.content.ContextCompat; + +import com.google.android.material.tabs.TabLayout; + +import org.chromium.base.StrictModeContext; +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.annotations.NativeMethods; +import org.chromium.base.metrics.RecordHistogram; +import org.chromium.ui.widget.Toast; + +/** + * Java version of the compact translate infobar. + */ +@JNINamespace("weblayer") +public class TranslateCompactInfoBar extends InfoBar + implements TabLayout.OnTabSelectedListener, TranslateMenuHelper.TranslateMenuListener { + public static final int TRANSLATING_INFOBAR = 1; + public static final int AFTER_TRANSLATING_INFOBAR = 2; + + private static final int SOURCE_TAB_INDEX = 0; + private static final int TARGET_TAB_INDEX = 1; + + // Action ID for Snackbar. + // Actions performed by clicking on on the overflow menu. + public static final int ACTION_OVERFLOW_ALWAYS_TRANSLATE = 0; + public static final int ACTION_OVERFLOW_NEVER_SITE = 1; + public static final int ACTION_OVERFLOW_NEVER_LANGUAGE = 2; + // Actions triggered automatically. (when translation or denied count reaches the threshold.) + public static final int ACTION_AUTO_ALWAYS_TRANSLATE = 3; + public static final int ACTION_AUTO_NEVER_LANGUAGE = 4; + + private final int mInitialStep; + private final int mDefaultTextColor; + private final TranslateOptions mOptions; + + private long mNativeTranslateInfoBarPtr; + private TranslateTabLayout mTabLayout; + + // Metric to track the total number of translations in a page, including reverts to original. + private int mTotalTranslationCount; + + // Histogram names for logging metrics. + private static final String INFOBAR_HISTOGRAM_TRANSLATE_LANGUAGE = + "Translate.CompactInfobar.Language.Translate"; + private static final String INFOBAR_HISTOGRAM_MORE_LANGUAGES_LANGUAGE = + "Translate.CompactInfobar.Language.MoreLanguages"; + private static final String INFOBAR_HISTOGRAM_PAGE_NOT_IN_LANGUAGE = + "Translate.CompactInfobar.Language.PageNotIn"; + private static final String INFOBAR_HISTOGRAM_ALWAYS_TRANSLATE_LANGUAGE = + "Translate.CompactInfobar.Language.AlwaysTranslate"; + private static final String INFOBAR_HISTOGRAM_NEVER_TRANSLATE_LANGUAGE = + "Translate.CompactInfobar.Language.NeverTranslate"; + private static final String INFOBAR_HISTOGRAM = "Translate.CompactInfobar.Event"; + private static final String INFOBAR_HISTOGRAM_TRANSLATION_COUNT = + "Translate.CompactInfobar.TranslationsPerPage"; + + /** + * This is used to back a UMA histogram, so it should be treated as + * append-only. The values should not be changed or reused, and + * INFOBAR_HISTOGRAM_BOUNDARY should be the last. + */ + private static final int INFOBAR_IMPRESSION = 0; + private static final int INFOBAR_TARGET_TAB_TRANSLATE = 1; + private static final int INFOBAR_DECLINE = 2; + private static final int INFOBAR_OPTIONS = 3; + private static final int INFOBAR_MORE_LANGUAGES = 4; + private static final int INFOBAR_MORE_LANGUAGES_TRANSLATE = 5; + private static final int INFOBAR_PAGE_NOT_IN = 6; + private static final int INFOBAR_ALWAYS_TRANSLATE = 7; + private static final int INFOBAR_NEVER_TRANSLATE = 8; + private static final int INFOBAR_NEVER_TRANSLATE_SITE = 9; + private static final int INFOBAR_SCROLL_HIDE = 10; + private static final int INFOBAR_SCROLL_SHOW = 11; + private static final int INFOBAR_REVERT = 12; + private static final int INFOBAR_SNACKBAR_ALWAYS_TRANSLATE_IMPRESSION = 13; + private static final int INFOBAR_SNACKBAR_NEVER_TRANSLATE_IMPRESSION = 14; + private static final int INFOBAR_SNACKBAR_NEVER_TRANSLATE_SITE_IMPRESSION = 15; + private static final int INFOBAR_SNACKBAR_CANCEL_ALWAYS = 16; + private static final int INFOBAR_SNACKBAR_CANCEL_NEVER_SITE = 17; + private static final int INFOBAR_SNACKBAR_CANCEL_NEVER = 18; + private static final int INFOBAR_ALWAYS_TRANSLATE_UNDO = 19; + private static final int INFOBAR_CLOSE_DEPRECATED = 20; + private static final int INFOBAR_SNACKBAR_AUTO_ALWAYS_IMPRESSION = 21; + private static final int INFOBAR_SNACKBAR_AUTO_NEVER_IMPRESSION = 22; + private static final int INFOBAR_SNACKBAR_CANCEL_AUTO_ALWAYS = 23; + private static final int INFOBAR_SNACKBAR_CANCEL_AUTO_NEVER = 24; + private static final int INFOBAR_HISTOGRAM_BOUNDARY = 25; + + // Need 2 instances of TranslateMenuHelper to prevent a race condition bug which happens when + // showing language menu after dismissing overflow menu. + private TranslateMenuHelper mOverflowMenuHelper; + private TranslateMenuHelper mLanguageMenuHelper; + + private ImageButton mMenuButton; + private InfoBarCompactLayout mParent; + + private boolean mMenuExpanded; + private boolean mIsFirstLayout = true; + private boolean mUserInteracted; + + @CalledByNative + private static InfoBar create(TabImpl tab, int initialStep, String sourceLanguageCode, + String targetLanguageCode, boolean alwaysTranslate, boolean triggeredFromMenu, + String[] languages, String[] languageCodes, int[] hashCodes, int tabTextColor) { + recordInfobarAction(INFOBAR_IMPRESSION); + return new TranslateCompactInfoBar(initialStep, sourceLanguageCode, targetLanguageCode, + alwaysTranslate, triggeredFromMenu, languages, languageCodes, hashCodes, + tabTextColor); + } + + TranslateCompactInfoBar(int initialStep, String sourceLanguageCode, String targetLanguageCode, + boolean alwaysTranslate, boolean triggeredFromMenu, String[] languages, + String[] languageCodes, int[] hashCodes, int tabTextColor) { + super(R.drawable.infobar_translate_compact, 0, null, null); + + mInitialStep = initialStep; + mDefaultTextColor = tabTextColor; + mOptions = TranslateOptions.create(sourceLanguageCode, targetLanguageCode, languages, + languageCodes, alwaysTranslate, triggeredFromMenu, hashCodes); + } + + @Override + protected boolean usesCompactLayout() { + return true; + } + + @Override + protected void createCompactLayoutContent(InfoBarCompactLayout parent) { + LinearLayout content; + // LayoutInflater may trigger accessing the disk. + try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) { + content = (LinearLayout) LayoutInflater.from(getContext()) + .inflate(R.layout.weblayer_infobar_translate_compact_content, parent, + false); + } + + // When parent tab is being switched out (view detached), dismiss all menus and snackbars. + content.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View view) {} + + @Override + public void onViewDetachedFromWindow(View view) { + dismissMenusAndSnackbars(); + } + }); + + mTabLayout = + (TranslateTabLayout) content.findViewById(R.id.weblayer_translate_infobar_tabs); + if (mDefaultTextColor > 0) { + mTabLayout.setTabTextColors( + ContextCompat.getColor(getContext(), R.color.default_text_color), + ContextCompat.getColor( + getContext(), R.color.weblayer_tab_layout_selected_tab_color)); + } + mTabLayout.addTabs(mOptions.sourceLanguageName(), mOptions.targetLanguageName()); + + if (mInitialStep == TRANSLATING_INFOBAR) { + // Set translating status in the beginning for pages translated automatically. + mTabLayout.getTabAt(TARGET_TAB_INDEX).select(); + mTabLayout.showProgressBarOnTab(TARGET_TAB_INDEX); + mUserInteracted = true; + } else if (mInitialStep == AFTER_TRANSLATING_INFOBAR) { + // Focus on target tab since we are after translation. + mTabLayout.getTabAt(TARGET_TAB_INDEX).select(); + } + + mTabLayout.addOnTabSelectedListener(this); + + // Dismiss all menus and end scrolling animation when there is layout changed. + mTabLayout.addOnLayoutChangeListener(new OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) { + // Dismiss all menus to prevent menu misplacement. + dismissMenus(); + + if (mIsFirstLayout) { + // Scrolls to the end to make sure the target language tab is visible when + // language tabs is too long. + mTabLayout.startScrollingAnimationIfNeeded(); + mIsFirstLayout = false; + return; + } + + // End scrolling animation when layout changed. + mTabLayout.endScrollingAnimationIfPlaying(); + } + } + }); + + mMenuButton = content.findViewById(R.id.weblayer_translate_infobar_menu_button); + mMenuButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mTabLayout.endScrollingAnimationIfPlaying(); + recordInfobarAction(INFOBAR_OPTIONS); + initMenuHelper(TranslateMenu.MENU_OVERFLOW); + mOverflowMenuHelper.show(TranslateMenu.MENU_OVERFLOW, getParentWidth()); + mMenuExpanded = true; + } + }); + + parent.addContent(content, 1.0f); + mParent = parent; + } + + private void initMenuHelper(int menuType) { + boolean isIncognito = TranslateCompactInfoBarJni.get().isIncognito( + mNativeTranslateInfoBarPtr, TranslateCompactInfoBar.this); + switch (menuType) { + case TranslateMenu.MENU_OVERFLOW: + if (mOverflowMenuHelper == null) { + mOverflowMenuHelper = new TranslateMenuHelper( + getContext(), mMenuButton, mOptions, this, isIncognito); + } + return; + case TranslateMenu.MENU_TARGET_LANGUAGE: + case TranslateMenu.MENU_SOURCE_LANGUAGE: + if (mLanguageMenuHelper == null) { + mLanguageMenuHelper = new TranslateMenuHelper( + getContext(), mMenuButton, mOptions, this, isIncognito); + } + return; + default: + assert false : "Unsupported Menu Item Id"; + } + } + + private void startTranslating(int tabPosition) { + if (TARGET_TAB_INDEX == tabPosition) { + // Already on the target tab. + mTabLayout.showProgressBarOnTab(TARGET_TAB_INDEX); + onButtonClicked(ActionType.TRANSLATE); + mUserInteracted = true; + } else { + mTabLayout.getTabAt(TARGET_TAB_INDEX).select(); + } + } + + @CalledByNative + private void onPageTranslated(int errorType) { + incrementAndRecordTranslationsPerPageCount(); + if (mTabLayout != null) { + mTabLayout.hideProgressBar(); + if (errorType != 0) { + Toast.makeText(getContext(), R.string.translate_infobar_error, Toast.LENGTH_SHORT) + .show(); + // Disable OnTabSelectedListener then revert selection. + mTabLayout.removeOnTabSelectedListener(this); + mTabLayout.getTabAt(SOURCE_TAB_INDEX).select(); + // Add OnTabSelectedListener back. + mTabLayout.addOnTabSelectedListener(this); + } + } + } + + @CalledByNative + private void setNativePtr(long nativePtr) { + mNativeTranslateInfoBarPtr = nativePtr; + } + + @CalledByNative + private void setAutoAlwaysTranslate() { + createAndShowSnackbar(ACTION_AUTO_ALWAYS_TRANSLATE); + } + + @Override + protected void onNativeDestroyed() { + mNativeTranslateInfoBarPtr = 0; + super.onNativeDestroyed(); + } + + private void closeInfobar(boolean explicitly) { + if (isDismissed()) return; + + if (!mUserInteracted) { + recordInfobarAction(INFOBAR_DECLINE); + } + + // NOTE: In Chrome there is a check for whether auto "never translate" should be triggered + // via a snackbar here. However, WebLayer does not have snackbars and thus does not have + // this check as there would be no way to inform the user of the functionality being + // triggered. The user of course has the option of choosing "never translate" from the + // overflow menu. + + // This line will dismiss this infobar. + super.onCloseButtonClicked(); + } + + @Override + public void onCloseButtonClicked() { + mTabLayout.endScrollingAnimationIfPlaying(); + closeInfobar(true); + } + + @Override + public void onTabSelected(TabLayout.Tab tab) { + switch (tab.getPosition()) { + case SOURCE_TAB_INDEX: + incrementAndRecordTranslationsPerPageCount(); + recordInfobarAction(INFOBAR_REVERT); + onButtonClicked(ActionType.TRANSLATE_SHOW_ORIGINAL); + return; + case TARGET_TAB_INDEX: + recordInfobarAction(INFOBAR_TARGET_TAB_TRANSLATE); + recordInfobarLanguageData( + INFOBAR_HISTOGRAM_TRANSLATE_LANGUAGE, mOptions.targetLanguageCode()); + startTranslating(TARGET_TAB_INDEX); + return; + default: + assert false : "Unexpected Tab Index"; + } + } + + @Override + public void onTabUnselected(TabLayout.Tab tab) {} + + @Override + public void onTabReselected(TabLayout.Tab tab) {} + + @Override + public void onOverflowMenuItemClicked(int itemId) { + switch (itemId) { + case TranslateMenu.ID_OVERFLOW_MORE_LANGUAGE: + recordInfobarAction(INFOBAR_MORE_LANGUAGES); + initMenuHelper(TranslateMenu.MENU_TARGET_LANGUAGE); + mLanguageMenuHelper.show(TranslateMenu.MENU_TARGET_LANGUAGE, getParentWidth()); + return; + case TranslateMenu.ID_OVERFLOW_ALWAYS_TRANSLATE: + // Only show snackbar when "Always Translate" is enabled. + if (!mOptions.getTranslateState(TranslateOptions.Type.ALWAYS_LANGUAGE)) { + recordInfobarAction(INFOBAR_ALWAYS_TRANSLATE); + recordInfobarLanguageData(INFOBAR_HISTOGRAM_ALWAYS_TRANSLATE_LANGUAGE, + mOptions.sourceLanguageCode()); + createAndShowSnackbar(ACTION_OVERFLOW_ALWAYS_TRANSLATE); + } else { + recordInfobarAction(INFOBAR_ALWAYS_TRANSLATE_UNDO); + handleTranslateOptionPostSnackbar(ACTION_OVERFLOW_ALWAYS_TRANSLATE); + } + return; + case TranslateMenu.ID_OVERFLOW_NEVER_LANGUAGE: + recordInfobarAction(INFOBAR_NEVER_TRANSLATE); + recordInfobarLanguageData( + INFOBAR_HISTOGRAM_NEVER_TRANSLATE_LANGUAGE, mOptions.sourceLanguageCode()); + createAndShowSnackbar(ACTION_OVERFLOW_NEVER_LANGUAGE); + return; + case TranslateMenu.ID_OVERFLOW_NEVER_SITE: + recordInfobarAction(INFOBAR_NEVER_TRANSLATE_SITE); + createAndShowSnackbar(ACTION_OVERFLOW_NEVER_SITE); + return; + case TranslateMenu.ID_OVERFLOW_NOT_THIS_LANGUAGE: + recordInfobarAction(INFOBAR_PAGE_NOT_IN); + initMenuHelper(TranslateMenu.MENU_SOURCE_LANGUAGE); + mLanguageMenuHelper.show(TranslateMenu.MENU_SOURCE_LANGUAGE, getParentWidth()); + return; + default: + assert false : "Unexpected overflow menu code"; + } + } + + @Override + public void onTargetMenuItemClicked(String code) { + // Reset target code in both UI and native. + if (mNativeTranslateInfoBarPtr != 0 && mOptions.setTargetLanguage(code)) { + recordInfobarAction(INFOBAR_MORE_LANGUAGES_TRANSLATE); + recordInfobarLanguageData( + INFOBAR_HISTOGRAM_MORE_LANGUAGES_LANGUAGE, mOptions.targetLanguageCode()); + TranslateCompactInfoBarJni.get().applyStringTranslateOption(mNativeTranslateInfoBarPtr, + TranslateCompactInfoBar.this, TranslateOption.TARGET_CODE, code); + // Adjust UI. + mTabLayout.replaceTabTitle(TARGET_TAB_INDEX, mOptions.getRepresentationFromCode(code)); + startTranslating(mTabLayout.getSelectedTabPosition()); + } + } + + @Override + public void onSourceMenuItemClicked(String code) { + // Reset source code in both UI and native. + if (mNativeTranslateInfoBarPtr != 0 && mOptions.setSourceLanguage(code)) { + recordInfobarLanguageData( + INFOBAR_HISTOGRAM_PAGE_NOT_IN_LANGUAGE, mOptions.sourceLanguageCode()); + TranslateCompactInfoBarJni.get().applyStringTranslateOption(mNativeTranslateInfoBarPtr, + TranslateCompactInfoBar.this, TranslateOption.SOURCE_CODE, code); + // Adjust UI. + mTabLayout.replaceTabTitle(SOURCE_TAB_INDEX, mOptions.getRepresentationFromCode(code)); + startTranslating(mTabLayout.getSelectedTabPosition()); + } + } + + // Dismiss all overflow menus that remains open. + // This is called when infobar started hiding or layout changed. + private void dismissMenus() { + if (mOverflowMenuHelper != null) mOverflowMenuHelper.dismiss(); + if (mLanguageMenuHelper != null) mLanguageMenuHelper.dismiss(); + } + + // Dismiss all overflow menus and snackbars that belong to this infobar and remain open. + private void dismissMenusAndSnackbars() { + dismissMenus(); + } + + @Override + protected void onStartedHiding() { + dismissMenusAndSnackbars(); + } + + @Override + protected CharSequence getAccessibilityMessage(CharSequence defaultMessage) { + return getContext().getString(R.string.translate_button); + } + + /** + * Returns true if overflow menu is showing. This is only used for automation testing. + */ + public boolean isShowingOverflowMenuForTesting() { + if (mOverflowMenuHelper == null) return false; + return mOverflowMenuHelper.isShowing(); + } + + /** + * Returns true if language menu is showing. This is only used for automation testing. + */ + public boolean isShowingLanguageMenuForTesting() { + if (mLanguageMenuHelper == null) return false; + return mLanguageMenuHelper.isShowing(); + } + + /** + * Returns true if the tab at the given |tabIndex| is selected. This is only used for automation + * testing. + */ + private boolean isTabSelectedForTesting(int tabIndex) { + return mTabLayout.getTabAt(tabIndex).isSelected(); + } + + /** + * Returns true if the target tab is selected. This is only used for automation testing. + */ + public boolean isSourceTabSelectedForTesting() { + return this.isTabSelectedForTesting(SOURCE_TAB_INDEX); + } + + /** + * Returns true if the target tab is selected. This is only used for automation testing. + */ + public boolean isTargetTabSelectedForTesting() { + return this.isTabSelectedForTesting(TARGET_TAB_INDEX); + } + + private void createAndShowSnackbar(int actionId) { + // NOTE: WebLayer doesn't have snackbars, so the relevant action is just taken directly. + // TODO(blundell): If WebLayer ends up staying with this implementation long-term, update + // the nomenclature of this file to avoid any references to snackbars. + handleTranslateOptionPostSnackbar(actionId); + } + + private void handleTranslateOptionPostSnackbar(int actionId) { + // Quit if native is destroyed. + if (mNativeTranslateInfoBarPtr == 0) return; + + switch (actionId) { + case ACTION_OVERFLOW_ALWAYS_TRANSLATE: + toggleAlwaysTranslate(); + // Start translating if always translate is selected and if page is not already + // translated to the target language. + if (mOptions.getTranslateState(TranslateOptions.Type.ALWAYS_LANGUAGE) + && mTabLayout.getSelectedTabPosition() == SOURCE_TAB_INDEX) { + startTranslating(mTabLayout.getSelectedTabPosition()); + } + return; + case ACTION_AUTO_ALWAYS_TRANSLATE: + toggleAlwaysTranslate(); + return; + case ACTION_OVERFLOW_NEVER_LANGUAGE: + case ACTION_AUTO_NEVER_LANGUAGE: + mUserInteracted = true; + mOptions.toggleNeverTranslateLanguageState( + !mOptions.getTranslateState(TranslateOptions.Type.NEVER_LANGUAGE)); + TranslateCompactInfoBarJni.get().applyBoolTranslateOption( + mNativeTranslateInfoBarPtr, TranslateCompactInfoBar.this, + TranslateOption.NEVER_TRANSLATE, + mOptions.getTranslateState(TranslateOptions.Type.NEVER_LANGUAGE)); + return; + case ACTION_OVERFLOW_NEVER_SITE: + mUserInteracted = true; + mOptions.toggleNeverTranslateDomainState( + !mOptions.getTranslateState(TranslateOptions.Type.NEVER_DOMAIN)); + TranslateCompactInfoBarJni.get().applyBoolTranslateOption( + mNativeTranslateInfoBarPtr, TranslateCompactInfoBar.this, + TranslateOption.NEVER_TRANSLATE_SITE, + mOptions.getTranslateState(TranslateOptions.Type.NEVER_DOMAIN)); + return; + default: + assert false : "Unsupported Menu Item Id, in handle post snackbar"; + } + } + + private void toggleAlwaysTranslate() { + mOptions.toggleAlwaysTranslateLanguageState( + !mOptions.getTranslateState(TranslateOptions.Type.ALWAYS_LANGUAGE)); + TranslateCompactInfoBarJni.get().applyBoolTranslateOption(mNativeTranslateInfoBarPtr, + TranslateCompactInfoBar.this, TranslateOption.ALWAYS_TRANSLATE, + mOptions.getTranslateState(TranslateOptions.Type.ALWAYS_LANGUAGE)); + } + + private static void recordInfobarAction(int action) { + RecordHistogram.recordEnumeratedHistogram( + INFOBAR_HISTOGRAM, action, INFOBAR_HISTOGRAM_BOUNDARY); + } + + private void recordInfobarLanguageData(String histogram, String langCode) { + Integer hashCode = mOptions.getUMAHashCodeFromCode(langCode); + if (hashCode != null) { + RecordHistogram.recordSparseHistogram(histogram, hashCode); + } + } + + private void incrementAndRecordTranslationsPerPageCount() { + RecordHistogram.recordCountHistogram( + INFOBAR_HISTOGRAM_TRANSLATION_COUNT, ++mTotalTranslationCount); + } + + // Return the width of parent in pixels. Return 0 if there is no parent. + private int getParentWidth() { + return mParent != null ? mParent.getWidth() : 0; + } + + @CalledByNative + // Selects the tab corresponding to |actionType| to simulate the user pressing on this tab. + private void selectTabForTesting(int actionType) { + if (actionType == ActionType.TRANSLATE) { + mTabLayout.getTabAt(TARGET_TAB_INDEX).select(); + } else if (actionType == ActionType.TRANSLATE_SHOW_ORIGINAL) { + mTabLayout.getTabAt(SOURCE_TAB_INDEX).select(); + } else { + assert false; + } + } + + @CalledByNative + // Simulates a click of the overflow menu item for "never translate this language." + private void clickNeverTranslateLanguageMenuItemForTesting() { + onOverflowMenuItemClicked(TranslateMenu.ID_OVERFLOW_NEVER_LANGUAGE); + } + + @CalledByNative + // Simulates a click of the overflow menu item for "never translate this site." + private void clickNeverTranslateSiteMenuItemForTesting() { + onOverflowMenuItemClicked(TranslateMenu.ID_OVERFLOW_NEVER_SITE); + } + + @NativeMethods + interface Natives { + void applyStringTranslateOption(long nativeTranslateCompactInfoBar, + TranslateCompactInfoBar caller, int option, String value); + void applyBoolTranslateOption(long nativeTranslateCompactInfoBar, + TranslateCompactInfoBar caller, int option, boolean value); + boolean shouldAutoNeverTranslate(long nativeTranslateCompactInfoBar, + TranslateCompactInfoBar caller, boolean menuExpanded); + boolean isIncognito(long nativeTranslateCompactInfoBar, TranslateCompactInfoBar caller); + } +} diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/TranslateMenu.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/TranslateMenu.java new file mode 100644 index 00000000000..cfb1a06c2f5 --- /dev/null +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/TranslateMenu.java @@ -0,0 +1,75 @@ +// Copyright 2017 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.weblayer_private; + +import java.util.ArrayList; +import java.util.List; + +/** + * Translate menu config and its item entity definition. + */ +public final class TranslateMenu { + /** + * The menu item entity. + */ + static final class MenuItem { + public final int mType; + public final int mId; + public final String mCode; + public final boolean mWithDivider; + + MenuItem(int itemType, int itemId, boolean withDivider) { + this(itemType, itemId, EMPTY_STRING, withDivider); + } + + MenuItem(int itemType, int itemId, String code) { + this(itemType, itemId, code, false); + } + + MenuItem(int itemType, int itemId, String code, boolean withDivider) { + mType = itemType; + mId = itemId; + mCode = code; + mWithDivider = withDivider; + } + } + + public static final String EMPTY_STRING = ""; + + // Menu type config. + public static final int MENU_OVERFLOW = 0; + public static final int MENU_TARGET_LANGUAGE = 1; + public static final int MENU_SOURCE_LANGUAGE = 2; + + // Menu item type config. + public static final int ITEM_LANGUAGE = 0; + public static final int ITEM_CHECKBOX_OPTION = 1; + public static final int MENU_ITEM_TYPE_COUNT = 2; + + // Menu Item ID config for MENU_OVERFLOW. + public static final int ID_OVERFLOW_MORE_LANGUAGE = 0; + public static final int ID_OVERFLOW_ALWAYS_TRANSLATE = 1; + public static final int ID_OVERFLOW_NEVER_SITE = 2; + public static final int ID_OVERFLOW_NEVER_LANGUAGE = 3; + public static final int ID_OVERFLOW_NOT_THIS_LANGUAGE = 4; + + /** + * Build overflow menu item list. + */ + static List<MenuItem> getOverflowMenu(boolean isIncognito) { + List<MenuItem> menu = new ArrayList<MenuItem>(); + menu.add(new MenuItem(ITEM_CHECKBOX_OPTION, ID_OVERFLOW_MORE_LANGUAGE, true)); + if (!isIncognito) { + // "Always translate" does nothing in incognito mode, so just hide it. + menu.add(new MenuItem(ITEM_CHECKBOX_OPTION, ID_OVERFLOW_ALWAYS_TRANSLATE, false)); + } + menu.add(new MenuItem(ITEM_CHECKBOX_OPTION, ID_OVERFLOW_NEVER_LANGUAGE, false)); + menu.add(new MenuItem(ITEM_CHECKBOX_OPTION, ID_OVERFLOW_NEVER_SITE, false)); + menu.add(new MenuItem(ITEM_CHECKBOX_OPTION, ID_OVERFLOW_NOT_THIS_LANGUAGE, false)); + return menu; + } + + private TranslateMenu() {} +} diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/TranslateMenuHelper.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/TranslateMenuHelper.java new file mode 100644 index 00000000000..16454817172 --- /dev/null +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/TranslateMenuHelper.java @@ -0,0 +1,321 @@ +// Copyright 2017 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.weblayer_private; + +import android.content.Context; +import android.graphics.Rect; +import android.os.Build; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.ListPopupWindow; +import android.widget.PopupWindow; +import android.widget.TextView; + +import androidx.core.content.ContextCompat; + +import java.util.ArrayList; +import java.util.List; + +/** + * A Helper class for managing the Translate Overflow Menu. + */ +public class TranslateMenuHelper implements AdapterView.OnItemClickListener { + private final TranslateMenuListener mMenuListener; + private final TranslateOptions mOptions; + + private ContextThemeWrapper mContextWrapper; + private TranslateMenuAdapter mAdapter; + private View mAnchorView; + private ListPopupWindow mPopup; + private boolean mIsIncognito; + + /** + * Interface for receiving the click event of menu item. + */ + public interface TranslateMenuListener { + void onOverflowMenuItemClicked(int itemId); + void onTargetMenuItemClicked(String code); + void onSourceMenuItemClicked(String code); + } + + public TranslateMenuHelper(Context context, View anchorView, TranslateOptions options, + TranslateMenuListener itemListener, boolean isIncognito) { + mContextWrapper = new ContextThemeWrapper(context, R.style.OverflowMenuThemeOverlay); + mAnchorView = anchorView; + mOptions = options; + mMenuListener = itemListener; + mIsIncognito = isIncognito; + } + + /** + * Build translate menu by menu type. + */ + private List<TranslateMenu.MenuItem> getMenuList(int menuType) { + List<TranslateMenu.MenuItem> menuList = new ArrayList<TranslateMenu.MenuItem>(); + if (menuType == TranslateMenu.MENU_OVERFLOW) { + // TODO(googleo): Add language short list above static menu after its data is ready. + menuList.addAll(TranslateMenu.getOverflowMenu(mIsIncognito)); + } else { + for (int i = 0; i < mOptions.allLanguages().size(); ++i) { + String code = mOptions.allLanguages().get(i).mLanguageCode; + // Avoid source language in the source language list. + if (menuType == TranslateMenu.MENU_SOURCE_LANGUAGE + && code.equals(mOptions.sourceLanguageCode())) { + continue; + } + // Avoid target language in the target language list. + if (menuType == TranslateMenu.MENU_TARGET_LANGUAGE + && code.equals(mOptions.targetLanguageCode())) { + continue; + } + menuList.add(new TranslateMenu.MenuItem(TranslateMenu.ITEM_LANGUAGE, i, code)); + } + } + return menuList; + } + + /** + * Show the overflow menu. + * @param menuType The type of overflow menu to show. + * @param maxwidth Maximum width of menu. Set to 0 when not specified. + */ + public void show(int menuType, int maxWidth) { + if (mPopup == null) { + mPopup = new ListPopupWindow(mContextWrapper, null, android.R.attr.popupMenuStyle); + mPopup.setModal(true); + mPopup.setAnchorView(mAnchorView); + mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); + + // Need to explicitly set the background here. Relying on it being set in the style + // caused an incorrectly drawn background. + // TODO(martiw): We might need a new menu background here. + mPopup.setBackgroundDrawable( + ContextCompat.getDrawable(mContextWrapper, R.drawable.popup_bg_tinted)); + + mPopup.setOnItemClickListener(this); + + // The menu must be shifted down by the height of the anchor view in order to be + // displayed over and above it. + int anchorHeight = mAnchorView.getHeight(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + // Setting a positive offset here shifts the menu down. + mPopup.setVerticalOffset(anchorHeight); + } else { + // The framework's PopupWindow positioning changed between N and M. Setting + // a negative offset here shifts the menu down rather than up. + mPopup.setVerticalOffset(-anchorHeight); + } + + mAdapter = new TranslateMenuAdapter(menuType); + mPopup.setAdapter(mAdapter); + } else { + mAdapter.refreshMenu(menuType); + } + + if (menuType == TranslateMenu.MENU_OVERFLOW) { + // Use measured width when it is a overflow menu. + Rect bgPadding = new Rect(); + mPopup.getBackground().getPadding(bgPadding); + int measuredWidth = measureMenuWidth(mAdapter) + bgPadding.left + bgPadding.right; + mPopup.setWidth((maxWidth > 0 && measuredWidth > maxWidth) ? maxWidth : measuredWidth); + } else { + // Use fixed width otherwise. + int popupWidth = mContextWrapper.getResources().getDimensionPixelSize( + R.dimen.weblayer_infobar_translate_menu_width); + mPopup.setWidth(popupWidth); + } + + // When layout is RTL, set the horizontal offset to align the menu with the left side of the + // screen. + if (mAnchorView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + int[] tempLocation = new int[2]; + mAnchorView.getLocationOnScreen(tempLocation); + mPopup.setHorizontalOffset(-tempLocation[0]); + } + + if (!mPopup.isShowing()) { + mPopup.show(); + mPopup.getListView().setItemsCanFocus(true); + } + } + + private int measureMenuWidth(TranslateMenuAdapter adapter) { + final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + + final int count = adapter.getCount(); + int width = 0; + int itemType = 0; + View itemView = null; + for (int i = 0; i < count; i++) { + final int positionType = adapter.getItemViewType(i); + if (positionType != itemType) { + itemType = positionType; + itemView = null; + } + itemView = adapter.getView(i, itemView, null); + itemView.measure(widthMeasureSpec, heightMeasureSpec); + width = Math.max(width, itemView.getMeasuredWidth()); + } + return width; + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + dismiss(); + + TranslateMenu.MenuItem item = mAdapter.getItem(position); + switch (mAdapter.mMenuType) { + case TranslateMenu.MENU_OVERFLOW: + mMenuListener.onOverflowMenuItemClicked(item.mId); + return; + case TranslateMenu.MENU_TARGET_LANGUAGE: + mMenuListener.onTargetMenuItemClicked(item.mCode); + return; + case TranslateMenu.MENU_SOURCE_LANGUAGE: + mMenuListener.onSourceMenuItemClicked(item.mCode); + return; + default: + assert false : "Unsupported Menu Item Id"; + } + } + + /** + * Dismisses the translate option menu. + */ + public void dismiss() { + if (isShowing()) { + mPopup.dismiss(); + } + } + + /** + * @return Whether the menu is currently showing. + */ + public boolean isShowing() { + if (mPopup == null) { + return false; + } + return mPopup.isShowing(); + } + + /** + * The provides the views of the menu items and dividers. + */ + private final class TranslateMenuAdapter extends ArrayAdapter<TranslateMenu.MenuItem> { + private final LayoutInflater mInflater; + private int mMenuType; + + public TranslateMenuAdapter(int menuType) { + super(mContextWrapper, R.layout.weblayer_translate_menu_item, getMenuList(menuType)); + mInflater = LayoutInflater.from(mContextWrapper); + mMenuType = menuType; + } + + private void refreshMenu(int menuType) { + // MENU_OVERFLOW is static and it should not reload. + if (menuType == TranslateMenu.MENU_OVERFLOW) return; + + clear(); + + mMenuType = menuType; + addAll(getMenuList(menuType)); + notifyDataSetChanged(); + } + + private String getItemViewText(TranslateMenu.MenuItem item) { + if (mMenuType == TranslateMenu.MENU_OVERFLOW) { + // Overflow menu items are manually defined one by one. + String source = mOptions.sourceLanguageName(); + switch (item.mId) { + case TranslateMenu.ID_OVERFLOW_ALWAYS_TRANSLATE: + return mContextWrapper.getString( + R.string.translate_option_always_translate, source); + case TranslateMenu.ID_OVERFLOW_MORE_LANGUAGE: + return mContextWrapper.getString(R.string.translate_option_more_language); + case TranslateMenu.ID_OVERFLOW_NEVER_SITE: + return mContextWrapper.getString(R.string.translate_never_translate_site); + case TranslateMenu.ID_OVERFLOW_NEVER_LANGUAGE: + return mContextWrapper.getString( + R.string.translate_option_never_translate, source); + case TranslateMenu.ID_OVERFLOW_NOT_THIS_LANGUAGE: + return mContextWrapper.getString( + R.string.translate_option_not_source_language, source); + default: + assert false : "Unexpected Overflow Item Id"; + } + } else { + // Get source and target language menu items text by language code. + return mOptions.getRepresentationFromCode(item.mCode); + } + return ""; + } + + @Override + public int getItemViewType(int position) { + return getItem(position).mType; + } + + @Override + public int getViewTypeCount() { + return TranslateMenu.MENU_ITEM_TYPE_COUNT; + } + + private View getItemView( + View menuItemView, int position, ViewGroup parent, int resourceId) { + if (menuItemView == null) { + menuItemView = mInflater.inflate(resourceId, parent, false); + } + ((TextView) menuItemView.findViewById(R.id.weblayer_menu_item_text)) + .setText(getItemViewText(getItem(position))); + return menuItemView; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View menuItemView = convertView; + switch (getItemViewType(position)) { + case TranslateMenu.ITEM_CHECKBOX_OPTION: + menuItemView = getItemView(menuItemView, position, parent, + R.layout.weblayer_translate_menu_item_checked); + + ImageView checkboxIcon = + menuItemView.findViewById(R.id.weblayer_menu_item_icon); + if (getItem(position).mId == TranslateMenu.ID_OVERFLOW_ALWAYS_TRANSLATE + && mOptions.getTranslateState(TranslateOptions.Type.ALWAYS_LANGUAGE)) { + checkboxIcon.setVisibility(View.VISIBLE); + } else if (getItem(position).mId == TranslateMenu.ID_OVERFLOW_NEVER_LANGUAGE + && mOptions.getTranslateState(TranslateOptions.Type.NEVER_LANGUAGE)) { + checkboxIcon.setVisibility(View.VISIBLE); + } else if (getItem(position).mId == TranslateMenu.ID_OVERFLOW_NEVER_SITE + && mOptions.getTranslateState(TranslateOptions.Type.NEVER_DOMAIN)) { + checkboxIcon.setVisibility(View.VISIBLE); + } else { + checkboxIcon.setVisibility(View.INVISIBLE); + } + + View divider = + (View) menuItemView.findViewById(R.id.weblayer_menu_item_divider); + if (getItem(position).mWithDivider) { + divider.setVisibility(View.VISIBLE); + } + break; + case TranslateMenu.ITEM_LANGUAGE: + menuItemView = getItemView( + menuItemView, position, parent, R.layout.weblayer_translate_menu_item); + break; + default: + assert false : "Unexpected MenuItem type"; + } + return menuItemView; + } + } +} diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/TranslateOptions.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/TranslateOptions.java new file mode 100644 index 00000000000..ba38d4e26f6 --- /dev/null +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/TranslateOptions.java @@ -0,0 +1,278 @@ +// Copyright 2013 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.weblayer_private; + +import android.text.TextUtils; + +import androidx.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A class that keeps the state of the different translation options and + * languages. + */ +public class TranslateOptions { + /** + * A container for Language Code and it's translated representation and it's native UMA + * specific hashcode. + * For example for Spanish when viewed from a French locale, this will contain es, Espagnol, + * 114573335 + **/ + public static class TranslateLanguageData { + public final String mLanguageCode; + public final String mLanguageRepresentation; + public final Integer mLanguageUMAHashCode; + + public TranslateLanguageData( + String languageCode, String languageRepresentation, Integer uMAhashCode) { + assert languageCode != null; + assert languageRepresentation != null; + mLanguageCode = languageCode; + mLanguageRepresentation = languageRepresentation; + mLanguageUMAHashCode = uMAhashCode; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof TranslateLanguageData)) return false; + TranslateLanguageData other = (TranslateLanguageData) obj; + return this.mLanguageCode.equals(other.mLanguageCode) + && this.mLanguageRepresentation.equals(other.mLanguageRepresentation) + && this.mLanguageUMAHashCode.equals(other.mLanguageUMAHashCode); + } + + @Override + public int hashCode() { + return (mLanguageCode + mLanguageRepresentation).hashCode(); + } + + @Override + public String toString() { + return "mLanguageCode:" + mLanguageCode + " - mlanguageRepresentation " + + mLanguageRepresentation + " - mLanguageUMAHashCode " + mLanguageUMAHashCode; + } + } + + // Values must be numerated from 0 and can't have gaps + // (they're used for indexing mOptions). + @IntDef({Type.NEVER_LANGUAGE, Type.NEVER_DOMAIN, Type.ALWAYS_LANGUAGE}) + @Retention(RetentionPolicy.SOURCE) + public @interface Type { + int NEVER_LANGUAGE = 0; + int NEVER_DOMAIN = 1; + int ALWAYS_LANGUAGE = 2; + + int NUM_ENTRIES = 3; + } + + private String mSourceLanguageCode; + private String mTargetLanguageCode; + + private final ArrayList<TranslateLanguageData> mAllLanguages; + + // language code to translated language name map + // Conceptually final + private Map<String, String> mCodeToRepresentation; + + // Language code to its UMA hashcode representation. + private Map<String, Integer> mCodeToUMAHashCode; + + // Will reflect the state before the object was ever modified + private final boolean[] mOriginalOptions; + + private final String mOriginalSourceLanguageCode; + private final String mOriginalTargetLanguageCode; + private final boolean mTriggeredFromMenu; + + private final boolean[] mOptions; + + private TranslateOptions(String sourceLanguageCode, String targetLanguageCode, + ArrayList<TranslateLanguageData> allLanguages, boolean neverLanguage, + boolean neverDomain, boolean alwaysLanguage, boolean triggeredFromMenu, + boolean[] originalOptions) { + assert Type.NUM_ENTRIES == 3; + mOptions = new boolean[Type.NUM_ENTRIES]; + mOptions[Type.NEVER_LANGUAGE] = neverLanguage; + mOptions[Type.NEVER_DOMAIN] = neverDomain; + mOptions[Type.ALWAYS_LANGUAGE] = alwaysLanguage; + + mOriginalOptions = originalOptions == null ? mOptions.clone() : originalOptions.clone(); + + mSourceLanguageCode = sourceLanguageCode; + mTargetLanguageCode = targetLanguageCode; + mOriginalSourceLanguageCode = mSourceLanguageCode; + mOriginalTargetLanguageCode = mTargetLanguageCode; + mTriggeredFromMenu = triggeredFromMenu; + + mAllLanguages = allLanguages; + mCodeToRepresentation = new HashMap<String, String>(); + mCodeToUMAHashCode = new HashMap<String, Integer>(); + for (TranslateLanguageData language : allLanguages) { + mCodeToRepresentation.put(language.mLanguageCode, language.mLanguageRepresentation); + mCodeToUMAHashCode.put(language.mLanguageCode, language.mLanguageUMAHashCode); + } + } + + /** + * Creates a TranslateOptions by the given data. + */ + public static TranslateOptions create(String sourceLanguageCode, String targetLanguageCode, + String[] languages, String[] codes, boolean alwaysTranslate, boolean triggeredFromMenu, + int[] hashCodes) { + assert languages.length == codes.length; + + ArrayList<TranslateLanguageData> languageList = new ArrayList<TranslateLanguageData>(); + for (int i = 0; i < languages.length; ++i) { + Integer hashCode = hashCodes != null ? Integer.valueOf(hashCodes[i]) : null; + languageList.add(new TranslateLanguageData(codes[i], languages[i], hashCode)); + } + return new TranslateOptions(sourceLanguageCode, targetLanguageCode, languageList, false, + false, alwaysTranslate, triggeredFromMenu, null); + } + + /** + * Returns a copy of the current instance. + */ + TranslateOptions copy() { + return new TranslateOptions(mSourceLanguageCode, mTargetLanguageCode, mAllLanguages, + mOptions[Type.NEVER_LANGUAGE], mOptions[Type.NEVER_DOMAIN], + mOptions[Type.ALWAYS_LANGUAGE], mTriggeredFromMenu, mOriginalOptions); + } + + public String sourceLanguageName() { + return getRepresentationFromCode(mSourceLanguageCode); + } + + public String targetLanguageName() { + return getRepresentationFromCode(mTargetLanguageCode); + } + + public String sourceLanguageCode() { + return mSourceLanguageCode; + } + + public String targetLanguageCode() { + return mTargetLanguageCode; + } + + public boolean triggeredFromMenu() { + return mTriggeredFromMenu; + } + + public boolean optionsChanged() { + return (!mSourceLanguageCode.equals(mOriginalSourceLanguageCode)) + || (!mTargetLanguageCode.equals(mOriginalTargetLanguageCode)) + || (mOptions[Type.NEVER_LANGUAGE] != mOriginalOptions[Type.NEVER_LANGUAGE]) + || (mOptions[Type.NEVER_DOMAIN] != mOriginalOptions[Type.NEVER_DOMAIN]) + || (mOptions[Type.ALWAYS_LANGUAGE] != mOriginalOptions[Type.ALWAYS_LANGUAGE]); + } + + public List<TranslateLanguageData> allLanguages() { + return mAllLanguages; + } + + public boolean getTranslateState(@Type int type) { + return mOptions[type]; + } + + public boolean setSourceLanguage(String languageCode) { + boolean canSet = canSetLanguage(languageCode, mTargetLanguageCode); + if (canSet) mSourceLanguageCode = languageCode; + return canSet; + } + + public boolean setTargetLanguage(String languageCode) { + boolean canSet = canSetLanguage(mSourceLanguageCode, languageCode); + if (canSet) mTargetLanguageCode = languageCode; + return canSet; + } + + /** + * Sets the new state of never translate domain. + * + * @return true if the toggling was possible + */ + public void toggleNeverTranslateDomainState(boolean value) { + mOptions[Type.NEVER_DOMAIN] = value; + } + + /** + * Sets the new state of never translate language. + * + * @return true if the toggling was possible + */ + public boolean toggleNeverTranslateLanguageState(boolean value) { + // Do not toggle if we are activating NeverLanguage but AlwaysTranslate + // for a language pair with the same source language is already active. + if (mOptions[Type.ALWAYS_LANGUAGE] && value) return false; + mOptions[Type.NEVER_LANGUAGE] = value; + return true; + } + + /** + * Sets the new state of never translate a language pair. + * + * @return true if the toggling was possible + */ + public boolean toggleAlwaysTranslateLanguageState(boolean value) { + // Do not toggle if we are activating AlwaysLanguage but NeverLanguage is active already. + if (mOptions[Type.NEVER_LANGUAGE] && value) return false; + mOptions[Type.ALWAYS_LANGUAGE] = value; + return true; + } + + /** + * Gets the language's translated representation from a given language code. + * @param languageCode ISO code for the language + * @return The translated representation of the language, or "" if not found. + */ + public String getRepresentationFromCode(String languageCode) { + return isValidLanguageCode(languageCode) ? mCodeToRepresentation.get(languageCode) : ""; + } + + /** + * Gets the language's UMA hashcode representation from a given language code. + * @param languageCode ISO code for the language + * @return The UMA hashcode representation of the language, or null if not found. + */ + public Integer getUMAHashCodeFromCode(String languageCode) { + return isValidLanguageUMAHashCode(languageCode) ? mCodeToUMAHashCode.get(languageCode) + : null; + } + + private boolean isValidLanguageCode(String languageCode) { + return !TextUtils.isEmpty(languageCode) && mCodeToRepresentation.containsKey(languageCode); + } + + private boolean isValidLanguageUMAHashCode(String languageCode) { + return !TextUtils.isEmpty(languageCode) && mCodeToUMAHashCode.containsKey(languageCode); + } + + private boolean canSetLanguage(String sourceCode, String targetCode) { + return isValidLanguageCode(sourceCode) && isValidLanguageCode(targetCode); + } + + @Override + public String toString() { + return new StringBuilder() + .append(sourceLanguageCode()) + .append(" -> ") + .append(targetLanguageCode()) + .append(" - ") + .append("Never Language:") + .append(mOptions[Type.NEVER_LANGUAGE]) + .append(" Always Language:") + .append(mOptions[Type.ALWAYS_LANGUAGE]) + .append(" Never Domain:") + .append(mOptions[Type.NEVER_DOMAIN]) + .toString(); + } +} diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/TranslateTabContent.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/TranslateTabContent.java new file mode 100644 index 00000000000..4cde0b46193 --- /dev/null +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/TranslateTabContent.java @@ -0,0 +1,63 @@ +// Copyright 2017 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.weblayer_private; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +/** + * The content of the tab shown in the TranslateTabLayout. + */ +public class TranslateTabContent extends FrameLayout { + private TextView mTextView; + private ProgressBar mProgressBar; + + /** + * Constructor for inflating from XML. + */ + public TranslateTabContent(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mTextView = (TextView) findViewById(R.id.weblayer_translate_infobar_tab_text); + mProgressBar = (ProgressBar) findViewById(R.id.weblayer_translate_infobar_tab_progressbar); + } + + /** + * Sets the text color for all the states (normal, selected, focused) to be this color. + * @param colors The color state list of the title text. + */ + public void setTextColor(ColorStateList colors) { + mTextView.setTextColor(colors); + } + + /** + * Set the title text for this tab. + * @param tabTitle The new title string. + */ + public void setText(CharSequence tabTitle) { + mTextView.setText(tabTitle); + } + + /** Hide progress bar and show text. */ + public void hideProgressBar() { + mProgressBar.setVisibility(View.INVISIBLE); + mTextView.setVisibility(View.VISIBLE); + } + + /** Show progress bar and hide text. */ + public void showProgressBar() { + mTextView.setVisibility(View.INVISIBLE); + mProgressBar.setVisibility(View.VISIBLE); + } +} diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/TranslateTabLayout.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/TranslateTabLayout.java new file mode 100644 index 00000000000..37c27f37cc6 --- /dev/null +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/TranslateTabLayout.java @@ -0,0 +1,240 @@ +// Copyright 2017 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.weblayer_private; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; + +import androidx.annotation.NonNull; + +import com.google.android.material.tabs.TabLayout; + +import org.chromium.base.StrictModeContext; +import org.chromium.components.browser_ui.widget.animation.Interpolators; + +/** + * TabLayout shown in the TranslateCompactInfoBar. + */ +public class TranslateTabLayout extends TabLayout { + /** The tab in which a spinning progress bar is showing. */ + private Tab mTabShowingProgressBar; + + /** The amount of waiting time before starting the scrolling animation. */ + private static final long START_POSITION_WAIT_DURATION_MS = 1000; + + /** The amount of time it takes to scroll to the end during the scrolling animation. */ + private static final long SCROLL_DURATION_MS = 300; + + /** We define the keyframes of the scrolling animation in this object. */ + ObjectAnimator mScrollToEndAnimator; + + /** Start padding of a Tab. Used for width calculation only. Will not be applied to views. */ + private int mTabPaddingStart; + + /** End padding of a Tab. Used for width calculation only. Will not be applied to views. */ + private int mTabPaddingEnd; + + /** + * Constructor for inflating from XML. + */ + @SuppressLint("CustomViewStyleable") // TODO(crbug.com/807725): Remove and fix. + public TranslateTabLayout(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.TabLayout, 0, R.style.Widget_Design_TabLayout); + mTabPaddingStart = mTabPaddingEnd = + a.getDimensionPixelSize(R.styleable.TabLayout_tabPadding, 0); + mTabPaddingStart = + a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingStart, mTabPaddingStart); + mTabPaddingEnd = + a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingEnd, mTabPaddingEnd); + } + + /** + * Add new Tabs with title strings. + * @param titles Titles of the tabs to be added. + */ + public void addTabs(CharSequence... titles) { + for (CharSequence title : titles) { + addTabWithTitle(title); + } + } + + /** + * Add a new Tab with the title string. + * @param tabTitle Title string of the new tab. + */ + public void addTabWithTitle(CharSequence tabTitle) { + TranslateTabContent tabContent; + // LayoutInflater may trigger accessing the disk. + try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) { + tabContent = + (TranslateTabContent) LayoutInflater.from(getContext()) + .inflate(R.layout.weblayer_infobar_translate_tab_content, this, false); + } + // Set text color using tabLayout's ColorStateList. So that the title text will change + // color when selected and unselected. + tabContent.setTextColor(getTabTextColors()); + tabContent.setText(tabTitle); + + Tab tab = newTab(); + tab.setCustomView(tabContent); + tab.setContentDescription(tabTitle); + super.addTab(tab); + } + + /** + * Replace the title string of a tab. + * @param tabPos The position of the tab to modify. + * @param tabTitle The new title string. + */ + public void replaceTabTitle(int tabPos, CharSequence tabTitle) { + if (tabPos < 0 || tabPos >= getTabCount()) { + return; + } + Tab tab = getTabAt(tabPos); + ((TranslateTabContent) tab.getCustomView()).setText(tabTitle); + tab.setContentDescription(tabTitle); + } + + /** + * Show the spinning progress bar on a specified tab. + * @param tabPos The position of the tab to show the progress bar. + */ + public void showProgressBarOnTab(int tabPos) { + if (tabPos < 0 || tabPos >= getTabCount() || mTabShowingProgressBar != null) { + return; + } + mTabShowingProgressBar = getTabAt(tabPos); + + // TODO(martiw) See if we need to setContentDescription as "Translating" here. + + if (tabIsSupported(mTabShowingProgressBar)) { + ((TranslateTabContent) mTabShowingProgressBar.getCustomView()).showProgressBar(); + } + } + + /** + * Hide the spinning progress bar in the tabs. + */ + public void hideProgressBar() { + if (mTabShowingProgressBar == null) return; + + if (tabIsSupported(mTabShowingProgressBar)) { + ((TranslateTabContent) mTabShowingProgressBar.getCustomView()).hideProgressBar(); + } + + mTabShowingProgressBar = null; + } + + // Overridden to block children's touch event when showing progress bar. + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + // Allow touches to propagate to children only if the layout can be interacted with. + if (mTabShowingProgressBar != null) { + return true; + } + endScrollingAnimationIfPlaying(); + return super.onInterceptTouchEvent(ev); + } + + /** Check if the tab is supported in TranslateTabLayout. */ + private boolean tabIsSupported(Tab tab) { + return (tab.getCustomView() instanceof TranslateTabContent); + } + + // Overridden to make sure only supported Tabs can be added. + @Override + public void addTab(@NonNull Tab tab, int position, boolean setSelected) { + if (!tabIsSupported(tab)) { + throw new IllegalArgumentException(); + } + super.addTab(tab, position, setSelected); + } + + // Overrided to make sure only supported Tabs can be added. + @Override + public void addTab(@NonNull Tab tab, boolean setSelected) { + if (!tabIsSupported(tab)) { + throw new IllegalArgumentException(); + } + super.addTab(tab, setSelected); + } + + /** + * Calculate and return the width of a specified tab. Tab doesn't provide a means of getting + * the width so we need to calculate the width by summing up the tab paddings and content width. + * @param position Tab position. + * @return Tab's width in pixels. + */ + private int getTabWidth(int position) { + if (getTabAt(position) == null) return 0; + return getTabAt(position).getCustomView().getWidth() + mTabPaddingStart + mTabPaddingEnd; + } + + /** + * Calculate the total width of all tabs and return it. + * @return Total width of all tabs in pixels. + */ + private int getTabsTotalWidth() { + int totalWidth = 0; + for (int i = 0; i < getTabCount(); i++) { + totalWidth += getTabWidth(i); + } + return totalWidth; + } + + /** + * Calculate the maximum scroll distance (by subtracting layout width from total width of tabs) + * and return it. + * @return Maximum scroll distance in pixels. + */ + private int maxScrollDistance() { + int scrollDistance = getTabsTotalWidth() - getWidth(); + return scrollDistance > 0 ? scrollDistance : 0; + } + + /** + * Perform the scrolling animation if this tablayout has any scrollable distance. + */ + // TODO(crbug.com/900912): Figure out whether setScrollX is actually available. + @SuppressLint("ObjectAnimatorBinding") + public void startScrollingAnimationIfNeeded() { + int maxScrollDistance = maxScrollDistance(); + if (maxScrollDistance == 0) { + return; + } + // The steps of the scrolling animation: + // 1. wait for START_POSITION_WAIT_DURATION_MS. + // 2. scroll to the end in SCROLL_DURATION_MS. + mScrollToEndAnimator = ObjectAnimator.ofInt(this, "scrollX", + getLayoutDirection() == LAYOUT_DIRECTION_RTL ? 0 : maxScrollDistance); + mScrollToEndAnimator.setStartDelay(START_POSITION_WAIT_DURATION_MS); + mScrollToEndAnimator.setDuration(SCROLL_DURATION_MS); + mScrollToEndAnimator.setInterpolator(Interpolators.DECELERATE_INTERPOLATOR); + mScrollToEndAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mScrollToEndAnimator = null; + } + }); + mScrollToEndAnimator.start(); + } + + /** + * End the scrolling animation if it is playing. + */ + public void endScrollingAnimationIfPlaying() { + if (mScrollToEndAnimator != null) mScrollToEndAnimator.end(); + } +} diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/UrlBarControllerImpl.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/UrlBarControllerImpl.java index 31a6a5484b2..4f3d7b439f1 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/UrlBarControllerImpl.java +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/UrlBarControllerImpl.java @@ -28,6 +28,7 @@ import org.chromium.components.omnibox.SecurityButtonAnimationDelegate; import org.chromium.components.omnibox.SecurityStatusIcon; import org.chromium.components.page_info.PageInfoController; import org.chromium.components.page_info.PermissionParamsListBuilderDelegate; +import org.chromium.content_public.browser.WebContents; import org.chromium.weblayer_private.interfaces.IObjectWrapper; import org.chromium.weblayer_private.interfaces.IUrlBarController; import org.chromium.weblayer_private.interfaces.ObjectWrapper; @@ -159,20 +160,19 @@ public class UrlBarControllerImpl extends IUrlBarController.Stub { ContextCompat.getColor(embedderContext, mUrlIconColor))); } - mSecurityButton.setOnClickListener(v -> { showPageInfoUi(v); }); if (mShowPageInfoWhenUrlTextClicked) { - mUrlTextView.setOnClickListener(v -> { showPageInfoUi(v); }); + setOnClickListener(v -> { showPageInfoUi(v); }); + } else { + mSecurityButton.setOnClickListener(v -> { showPageInfoUi(v); }); } } private void showPageInfoUi(View v) { + WebContents webContents = mBrowserImpl.getActiveTab().getWebContents(); PageInfoController.show(mBrowserImpl.getWindowAndroid().getActivity().get(), - mBrowserImpl.getActiveTab().getWebContents(), + webContents, /* contentPublisher= */ null, PageInfoController.OpenedFromSource.TOOLBAR, - new PageInfoControllerDelegateImpl(mBrowserImpl.getContext(), - mBrowserImpl.getProfile().getName(), - mBrowserImpl.getActiveTab().getWebContents().getVisibleUrl(), - mBrowserImpl.getWindowAndroid()::getModalDialogManager), + PageInfoControllerDelegateImpl.create(webContents), new PermissionParamsListBuilderDelegate(mBrowserImpl.getProfile())); } diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebLayerAccessibilityUtil.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebLayerAccessibilityUtil.java index 9054076d7b4..6cce5a8b27d 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebLayerAccessibilityUtil.java +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebLayerAccessibilityUtil.java @@ -4,6 +4,8 @@ package org.chromium.weblayer_private; +import org.chromium.ui.util.AccessibilityUtil; + /** * Exposes information about the current accessibility state. */ diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebLayerFactoryImpl.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebLayerFactoryImpl.java index ad83409a452..9e79ff718ec 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebLayerFactoryImpl.java +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebLayerFactoryImpl.java @@ -12,6 +12,7 @@ import org.chromium.components.version_info.VersionConstants; import org.chromium.weblayer_private.interfaces.IWebLayer; import org.chromium.weblayer_private.interfaces.IWebLayerFactory; import org.chromium.weblayer_private.interfaces.StrictModeWorkaround; +import org.chromium.weblayer_private.interfaces.WebLayerVersionConstants; /** * Factory used to create WebLayer as well as verify compatibility. @@ -49,7 +50,13 @@ public final class WebLayerFactoryImpl extends IWebLayerFactory.Stub { @Override public boolean isClientSupported() { StrictModeWorkaround.apply(); - return Math.abs(sClientMajorVersion - getImplementationMajorVersion()) <= 4; + int implMajorVersion = getImplementationMajorVersion(); + // While the client always calls this method, the most recently shipped product gets to + // decide compatibility. If we instead let the implementation always decide, then we would + // not be able to change the allowed skew of older implementations, even if the client could + // support it. + if (sClientMajorVersion > implMajorVersion) return true; + return implMajorVersion - sClientMajorVersion <= WebLayerVersionConstants.MAX_SKEW; } /** diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebLayerImpl.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebLayerImpl.java index 7150b1d5ca7..c9223c62146 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebLayerImpl.java +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebLayerImpl.java @@ -4,6 +4,7 @@ package org.chromium.weblayer_private; +import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -321,6 +322,9 @@ public final class WebLayerImpl extends IWebLayer.Stub { public void onReceivedBroadcast(IObjectWrapper appContextWrapper, Intent intent) { StrictModeWorkaround.apply(); Context context = ObjectWrapper.unwrap(appContextWrapper, Context.class); + + if (IntentUtils.handleIntent(intent)) return; + if (intent.getAction().startsWith(DownloadImpl.getIntentPrefix())) { DownloadImpl.forwardIntent(context, intent, mProfileManager); } else if (intent.getAction().startsWith(MediaStreamManager.getIntentPrefix())) { @@ -329,6 +333,19 @@ public final class WebLayerImpl extends IWebLayer.Stub { } @Override + public void onMediaSessionServiceStarted(IObjectWrapper sessionService, Intent intent) { + StrictModeWorkaround.apply(); + MediaSessionManager.serviceStarted( + ObjectWrapper.unwrap(sessionService, Service.class), intent); + } + + @Override + public void onMediaSessionServiceDestroyed() { + StrictModeWorkaround.apply(); + MediaSessionManager.serviceDestroyed(); + } + + @Override public void enumerateAllProfileNames(IObjectWrapper valueCallback) { StrictModeWorkaround.apply(); final ValueCallback<String[]> callback = @@ -380,6 +397,30 @@ public final class WebLayerImpl extends IWebLayer.Stub { } } + public static Intent createMediaSessionServiceIntent() { + if (sClient == null) { + throw new IllegalStateException("WebLayer should have been initialized already."); + } + + try { + return sClient.createMediaSessionServiceIntent(); + } catch (RemoteException e) { + throw new APICallException(e); + } + } + + public static int getMediaSessionNotificationId() { + if (sClient == null) { + throw new IllegalStateException("WebLayer should have been initialized already."); + } + + try { + return sClient.getMediaSessionNotificationId(); + } catch (RemoteException e) { + throw new APICallException(e); + } + } + public static String getClientApplicationName() { Context context = ContextUtils.getApplicationContext(); return new StringBuilder() diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebLayerNotificationBuilder.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebLayerNotificationBuilder.java index f591a671aa2..50984bb6403 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebLayerNotificationBuilder.java +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebLayerNotificationBuilder.java @@ -4,19 +4,37 @@ package org.chromium.weblayer_private; +import android.annotation.TargetApi; +import android.app.Notification; +import android.app.PendingIntent; import android.content.Context; import android.graphics.drawable.Icon; import android.os.Build; import android.webkit.WebViewFactory; +import androidx.annotation.NonNull; + +import org.chromium.base.ContextUtils; import org.chromium.components.browser_ui.notifications.ChromeNotificationBuilder; import org.chromium.components.browser_ui.notifications.NotificationBuilder; +import org.chromium.components.browser_ui.notifications.NotificationManagerProxyImpl; import org.chromium.components.browser_ui.notifications.NotificationMetadata; import org.chromium.components.browser_ui.notifications.channels.ChannelsInitializer; /** A notification builder for WebLayer which has extra logic to make icons work correctly. */ final class WebLayerNotificationBuilder extends NotificationBuilder { - public WebLayerNotificationBuilder(Context context, String channelId, + /** Creates a notification builder. */ + public static WebLayerNotificationBuilder create( + @WebLayerNotificationChannels.ChannelId String channelId, + @NonNull NotificationMetadata metadata) { + Context appContext = ContextUtils.getApplicationContext(); + ChannelsInitializer initializer = + new ChannelsInitializer(new NotificationManagerProxyImpl(appContext), + WebLayerNotificationChannels.getInstance(), appContext.getResources()); + return new WebLayerNotificationBuilder(appContext, channelId, initializer, metadata); + } + + private WebLayerNotificationBuilder(Context context, String channelId, ChannelsInitializer channelsInitializer, NotificationMetadata metadata) { super(context, channelId, channelsInitializer, metadata); } @@ -25,17 +43,67 @@ final class WebLayerNotificationBuilder extends NotificationBuilder { public ChromeNotificationBuilder setSmallIcon(int icon) { if (WebLayerImpl.isAndroidResource(icon)) { super.setSmallIcon(icon); - return this; + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + super.setSmallIcon(createIcon(icon)); + } else { + // Some fallback is required, or the notification won't appear. + super.setSmallIcon(getFallbackAndroidResource(icon)); } + return this; + } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - super.setSmallIcon( - Icon.createWithResource(WebViewFactory.getLoadedPackageInfo().packageName, - WebLayerImpl.getResourceIdForSystemUi(icon))); + @Override + @SuppressWarnings("deprecation") + public ChromeNotificationBuilder addAction(int icon, CharSequence title, PendingIntent intent) { + if (WebLayerImpl.isAndroidResource(icon)) { + super.addAction(icon, title, intent); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + super.addAction( + new Notification.Action.Builder(createIcon(icon), title, intent).build()); } else { - // Some fallback is required, or the notification won't appear. - super.setSmallIcon(android.R.drawable.radiobutton_on_background); + super.addAction(getFallbackAndroidResource(icon), title, intent); } return this; } + + @TargetApi(Build.VERSION_CODES.M) + private Icon createIcon(int resId) { + return Icon.createWithResource(WebViewFactory.getLoadedPackageInfo().packageName, + WebLayerImpl.getResourceIdForSystemUi(resId)); + } + + /** + * Finds a reasonable replacement for the given app-defined resource from among stock android + * resources. This is useful when {@link Icon} is not available. + */ + private int getFallbackAndroidResource(int appResourceId) { + if (appResourceId == R.drawable.ic_play_arrow_white_36dp) { + return android.R.drawable.ic_media_play; + } + if (appResourceId == R.drawable.ic_pause_white_36dp) { + return android.R.drawable.ic_media_pause; + } + if (appResourceId == R.drawable.ic_stop_white_36dp) { + // There's no ic_media_stop. This standin is at least a square. In practice this + // shouldn't ever come up as stop is only used in (Chrome) cast notifications. + return android.R.drawable.checkbox_off_background; + } + if (appResourceId == R.drawable.ic_skip_previous_white_36dp) { + return android.R.drawable.ic_media_previous; + } + if (appResourceId == R.drawable.ic_skip_next_white_36dp) { + return android.R.drawable.ic_media_next; + } + if (appResourceId == R.drawable.ic_fast_forward_white_36dp) { + return android.R.drawable.ic_media_ff; + } + if (appResourceId == R.drawable.ic_fast_rewind_white_36dp) { + return android.R.drawable.ic_media_rew; + } + if (appResourceId == R.drawable.audio_playing) { + return android.R.drawable.ic_lock_silent_mode_off; + } + + return android.R.drawable.radiobutton_on_background; + } } diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebLayerNotificationChannels.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebLayerNotificationChannels.java index 264421f20eb..5967d90d6ec 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebLayerNotificationChannels.java +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebLayerNotificationChannels.java @@ -51,12 +51,13 @@ class WebLayerNotificationChannels extends ChannelDefinitions { * channel, remove the ID from this StringDef, remove its entry from Predefined Channels.MAP, * and add it to the return value of {@link #getLegacyChannelIds()}. */ - @StringDef({ChannelId.ACTIVE_DOWNLOADS, ChannelId.COMPLETED_DOWNLOADS, + @StringDef({ChannelId.ACTIVE_DOWNLOADS, ChannelId.COMPLETED_DOWNLOADS, ChannelId.MEDIA_PLAYBACK, ChannelId.WEBRTC_CAM_AND_MIC}) @Retention(RetentionPolicy.SOURCE) public @interface ChannelId { String ACTIVE_DOWNLOADS = "org.chromium.weblayer.active_downloads"; String COMPLETED_DOWNLOADS = "org.chromium.weblayer.completed_downloads"; + String MEDIA_PLAYBACK = "org.chromium.weblayer.media_playback"; String WEBRTC_CAM_AND_MIC = "org.chromium.weblayer.webrtc_cam_and_mic"; } @@ -81,6 +82,10 @@ class WebLayerNotificationChannels extends ChannelDefinitions { PredefinedChannel.create(ChannelId.COMPLETED_DOWNLOADS, R.string.notification_category_completed_downloads, NotificationManager.IMPORTANCE_LOW, ChannelGroupId.WEBLAYER)); + map.put(ChannelId.MEDIA_PLAYBACK, + PredefinedChannel.create(ChannelId.MEDIA_PLAYBACK, + R.string.notification_category_media_playback, + NotificationManager.IMPORTANCE_LOW, ChannelGroupId.WEBLAYER)); map.put(ChannelId.WEBRTC_CAM_AND_MIC, PredefinedChannel.create(ChannelId.WEBRTC_CAM_AND_MIC, R.string.notification_category_webrtc_cam_and_mic, diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebLayerSiteSettingsClient.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebLayerSiteSettingsClient.java index d047261bcb4..cabbef5f87a 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebLayerSiteSettingsClient.java +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebLayerSiteSettingsClient.java @@ -11,8 +11,6 @@ import androidx.annotation.Nullable; import androidx.preference.Preference; import org.chromium.base.Callback; -import org.chromium.base.annotations.JNINamespace; -import org.chromium.base.annotations.NativeMethods; import org.chromium.components.browser_ui.settings.ManagedPreferenceDelegate; import org.chromium.components.browser_ui.site_settings.SiteSettingsCategory.Type; import org.chromium.components.browser_ui.site_settings.SiteSettingsClient; @@ -28,7 +26,6 @@ import java.util.Set; /** * A SiteSettingsClient instance that contains WebLayer-specific Site Settings logic. */ -@JNINamespace("weblayer") public class WebLayerSiteSettingsClient implements SiteSettingsClient, ManagedPreferenceDelegate, SiteSettingsHelpClient, SiteSettingsPrefClient, WebappSettingsClient { @@ -75,8 +72,8 @@ public class WebLayerSiteSettingsClient public boolean isCategoryVisible(@Type int type) { return type == Type.ALL_SITES || type == Type.AUTOMATIC_DOWNLOADS || type == Type.CAMERA || type == Type.COOKIES || type == Type.DEVICE_LOCATION || type == Type.JAVASCRIPT - || type == Type.MICROPHONE || type == Type.PROTECTED_MEDIA || type == Type.SOUND - || type == Type.USE_STORAGE; + || type == Type.MICROPHONE || type == Type.POPUPS || type == Type.PROTECTED_MEDIA + || type == Type.SOUND || type == Type.USE_STORAGE; } @Override @@ -122,32 +119,6 @@ public class WebLayerSiteSettingsClient public void launchProtectedContentHelpAndFeedbackActivity(Activity currentActivity) {} // SiteSettingsPrefClient implementation: - // TODO(crbug.com/1071603): Once PrefServiceBridge is componentized we can get rid of the JNI - // methods here and call PrefServiceBridge directly. - - @Override - public boolean getBlockThirdPartyCookies() { - return WebLayerSiteSettingsClientJni.get().getBlockThirdPartyCookies(mBrowserContextHandle); - } - @Override - public void setBlockThirdPartyCookies(boolean newValue) { - WebLayerSiteSettingsClientJni.get().setBlockThirdPartyCookies( - mBrowserContextHandle, newValue); - } - @Override - public boolean isBlockThirdPartyCookiesManaged() { - // WebLayer doesn't support managed prefs. - return false; - } - - @Override - public int getCookieControlsMode() { - return WebLayerSiteSettingsClientJni.get().getCookieControlsMode(mBrowserContextHandle); - } - @Override - public void setCookieControlsMode(int newValue) { - WebLayerSiteSettingsClientJni.get().setCookieControlsMode(mBrowserContextHandle, newValue); - } // The quiet notification UI is a Chrome-specific feature for now. @Override @@ -187,13 +158,4 @@ public class WebLayerSiteSettingsClient public String getNotificationDelegatePackageNameForOrigin(Origin origin) { return null; } - - @NativeMethods - interface Natives { - boolean getBlockThirdPartyCookies(BrowserContextHandle browserContextHandle); - void setBlockThirdPartyCookies(BrowserContextHandle browserContextHandle, boolean newValue); - - int getCookieControlsMode(BrowserContextHandle browserContextHandle); - void setCookieControlsMode(BrowserContextHandle browserContextHandle, int newValue); - } } diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebMessageReplyProxyImpl.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebMessageReplyProxyImpl.java new file mode 100644 index 00000000000..d639a42cf3a --- /dev/null +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebMessageReplyProxyImpl.java @@ -0,0 +1,76 @@ +// Copyright 2020 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.weblayer_private; + +import android.os.RemoteException; + +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.annotations.NativeMethods; +import org.chromium.weblayer_private.interfaces.APICallException; +import org.chromium.weblayer_private.interfaces.IWebMessageCallbackClient; +import org.chromium.weblayer_private.interfaces.IWebMessageReplyProxy; + +/** + * WebMessageReplyProxyImpl is responsible for both sending and receiving WebMessages. + */ +@JNINamespace("weblayer") +public final class WebMessageReplyProxyImpl extends IWebMessageReplyProxy.Stub { + private long mNativeWebMessageReplyProxyImpl; + private final IWebMessageCallbackClient mClient; + // Unique id (scoped to the call to Tab.registerWebMessageCallback()) for this proxy. This is + // sent over AIDL. + private final int mId; + + private WebMessageReplyProxyImpl(long nativeWebMessageReplyProxyImpl, int id, + IWebMessageCallbackClient client, boolean isMainFrame, String sourceOrigin) { + mNativeWebMessageReplyProxyImpl = nativeWebMessageReplyProxyImpl; + mClient = client; + mId = id; + try { + client.onNewReplyProxy(this, mId, isMainFrame, sourceOrigin); + } catch (RemoteException e) { + throw new APICallException(e); + } + } + + @CalledByNative + private static WebMessageReplyProxyImpl create(long nativeWebMessageReplyProxyImpl, int id, + IWebMessageCallbackClient client, boolean isMainFrame, String sourceOrigin) { + return new WebMessageReplyProxyImpl( + nativeWebMessageReplyProxyImpl, id, client, isMainFrame, sourceOrigin); + } + + @CalledByNative + private void onNativeDestroyed() { + mNativeWebMessageReplyProxyImpl = 0; + try { + mClient.onReplyProxyDestroyed(mId); + } catch (RemoteException e) { + throw new APICallException(e); + } + } + + @CalledByNative + private void onPostMessage(String message) { + try { + mClient.onPostMessage(mId, message); + } catch (RemoteException e) { + throw new APICallException(e); + } + } + + @Override + public void postMessage(String message) { + if (mNativeWebMessageReplyProxyImpl != 0) { + WebMessageReplyProxyImplJni.get().postMessage(mNativeWebMessageReplyProxyImpl, message); + } + } + + @NativeMethods + interface Natives { + void postMessage(long nativeWebMessageReplyProxyImpl, String message); + } +} diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebShareServiceFactory.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebShareServiceFactory.java new file mode 100644 index 00000000000..2005e53d2fd --- /dev/null +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/WebShareServiceFactory.java @@ -0,0 +1,40 @@ +// Copyright 2020 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.weblayer_private; + +import org.chromium.components.browser_ui.share.ShareHelper; +import org.chromium.components.browser_ui.share.ShareParams; +import org.chromium.components.browser_ui.webshare.ShareServiceImpl; +import org.chromium.content_public.browser.WebContents; +import org.chromium.services.service_manager.InterfaceFactory; +import org.chromium.webshare.mojom.ShareService; + +/** + * Factory that creates instances of ShareService. + */ +public class WebShareServiceFactory implements InterfaceFactory<ShareService> { + private final WebContents mWebContents; + + public WebShareServiceFactory(WebContents webContents) { + mWebContents = webContents; + } + + @Override + public ShareService createImpl() { + ShareServiceImpl.WebShareDelegate delegate = new ShareServiceImpl.WebShareDelegate() { + @Override + public boolean canShare() { + return mWebContents.getTopLevelNativeWindow().getActivity() != null; + } + + @Override + public void share(ShareParams params) { + ShareHelper.shareWithUi(params); + } + }; + + return new ShareServiceImpl(mWebContents, delegate); + } +} diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/IBrowser.aidl b/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/IBrowser.aidl index 8093e226baa..ed9a123fe9c 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/IBrowser.aidl +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/IBrowser.aidl @@ -34,4 +34,6 @@ interface IBrowser { IUrlBarController getUrlBarController() = 9; void setBottomView(in IObjectWrapper view) = 10; + + ITab createTab() = 11; } diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/INavigationController.aidl b/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/INavigationController.aidl index 3a21e9b518b..653b9a6af12 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/INavigationController.aidl +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/INavigationController.aidl @@ -33,4 +33,7 @@ interface INavigationController { // Added in 82, removed in 83. // void replace(in String uri) = 12; + + // Added in 85. + boolean isNavigationEntrySkippable(int index) = 13; } diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/INavigationControllerClient.aidl b/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/INavigationControllerClient.aidl index 60132f87c71..73432f8bd19 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/INavigationControllerClient.aidl +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/INavigationControllerClient.aidl @@ -6,6 +6,7 @@ package org.chromium.weblayer_private.interfaces; import org.chromium.weblayer_private.interfaces.IClientNavigation; import org.chromium.weblayer_private.interfaces.INavigation; +import org.chromium.weblayer_private.interfaces.IObjectWrapper; /** * Interface used by NavigationController to inform the client of changes. This largely duplicates @@ -29,4 +30,7 @@ interface INavigationControllerClient { void loadProgressChanged(double progress) = 7; void onFirstContentfulPaint() = 8; + + // Added in M85. + void onOldPageNoLongerRendered(in String uri) = 9; } diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/IProfile.aidl b/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/IProfile.aidl index 75966ad04a4..6ec60700f85 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/IProfile.aidl +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/IProfile.aidl @@ -30,4 +30,10 @@ interface IProfile { // Added in Version 84. void setBooleanSetting(int type, boolean value) = 7; boolean getBooleanSetting(int type) = 8; + + // Added in Version 85. + void getBrowserPersistenceIds(in IObjectWrapper resultCallback) = 9; + void removeBrowserPersistenceStorage(in String[] ids, + in IObjectWrapper resultCallback) = 10; + void prepareForPossibleCrossOriginNavigation() = 11; } diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/ITab.aidl b/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/ITab.aidl index b396292d4da..c029b9a6c18 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/ITab.aidl +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/ITab.aidl @@ -4,6 +4,8 @@ package org.chromium.weblayer_private.interfaces; +import java.util.List; + import org.chromium.weblayer_private.interfaces.IDownloadCallbackClient; import org.chromium.weblayer_private.interfaces.IErrorPageCallbackClient; import org.chromium.weblayer_private.interfaces.IFindInPageCallbackClient; @@ -13,6 +15,7 @@ import org.chromium.weblayer_private.interfaces.INavigationController; import org.chromium.weblayer_private.interfaces.INavigationControllerClient; import org.chromium.weblayer_private.interfaces.IObjectWrapper; import org.chromium.weblayer_private.interfaces.ITabClient; +import org.chromium.weblayer_private.interfaces.IWebMessageCallbackClient; interface ITab { void setClient(in ITabClient client) = 0; @@ -51,4 +54,16 @@ interface ITab { // Added in 84 void captureScreenShot(in float scale, in IObjectWrapper resultCallback) = 16; + + // Added in 85 + boolean setData(in Map data) = 17; + + // Added in 85 + Map getData() = 18; + void registerWebMessageCallback(in String jsObjectName, + in List<String> allowedOrigins, + in IWebMessageCallbackClient client) = 19; + void unregisterWebMessageCallback(in String jsObjectName) = 20; + boolean canTranslate() = 21; + void showTranslateUi() = 22; } diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/ITabClient.aidl b/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/ITabClient.aidl index 7313c2c8cec..12b6c4cfeda 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/ITabClient.aidl +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/ITabClient.aidl @@ -36,4 +36,11 @@ interface ITabClient { // Added in M84. void onTabDestroyed() = 8; + + // Added in M85. + void onBackgroundColorChanged(in int color) = 9; + + // Added in M85 + void onScrollNotification( + in int notificationType, in float currentScrollRatio) = 10; } diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/IWebLayer.aidl b/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/IWebLayer.aidl index cdaa4ebb3e3..7c8af14c1f6 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/IWebLayer.aidl +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/IWebLayer.aidl @@ -96,4 +96,8 @@ interface IWebLayer { ISiteSettingsFragment createSiteSettingsFragmentImpl( in IRemoteFragmentClient remoteFragmentClient, in IObjectWrapper fragmentArgs) = 16; + + // Added in Version 85. + void onMediaSessionServiceStarted(in IObjectWrapper sessionService, in Intent intent) = 17; + void onMediaSessionServiceDestroyed() = 18; } diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/IWebLayerClient.aidl b/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/IWebLayerClient.aidl index 9857b597c48..e152bef181b 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/IWebLayerClient.aidl +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/IWebLayerClient.aidl @@ -8,4 +8,6 @@ import android.content.Intent; interface IWebLayerClient { Intent createIntent() = 0; + Intent createMediaSessionServiceIntent() = 1; + int getMediaSessionNotificationId() = 2; } diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/IWebMessageCallbackClient.aidl b/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/IWebMessageCallbackClient.aidl new file mode 100644 index 00000000000..91ce8b87153 --- /dev/null +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/IWebMessageCallbackClient.aidl @@ -0,0 +1,16 @@ +// Copyright 2020 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.weblayer_private.interfaces; + +import org.chromium.weblayer_private.interfaces.IWebMessageReplyProxy; + +interface IWebMessageCallbackClient { + void onNewReplyProxy(in IWebMessageReplyProxy proxy, + in int proxyId, + in boolean isMainFrame, + in String sourceOrigin) = 0; + void onPostMessage(in int proxyId, in String message) = 1; + void onReplyProxyDestroyed(in int proxyId) = 2; +} diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/IWebMessageReplyProxy.aidl b/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/IWebMessageReplyProxy.aidl new file mode 100644 index 00000000000..208c78d53ef --- /dev/null +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/IWebMessageReplyProxy.aidl @@ -0,0 +1,9 @@ +// Copyright 2020 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.weblayer_private.interfaces; + +interface IWebMessageReplyProxy { + void postMessage(in String message) = 0; +} diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/ScrollNotificationType.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/ScrollNotificationType.java new file mode 100644 index 00000000000..424442ffed3 --- /dev/null +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/ScrollNotificationType.java @@ -0,0 +1,18 @@ +// Copyright 2020 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.weblayer_private.interfaces; + +import androidx.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@IntDef({ScrollNotificationType.DIRECTION_CHANGED_UP, + ScrollNotificationType.DIRECTION_CHANGED_DOWN}) +@Retention(RetentionPolicy.SOURCE) +public @interface ScrollNotificationType { + int DIRECTION_CHANGED_UP = 0; + int DIRECTION_CHANGED_DOWN = 1; +} diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/SettingType.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/SettingType.java index b6fd35e49e5..1b37b08d9b3 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/SettingType.java +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/SettingType.java @@ -9,8 +9,13 @@ import androidx.annotation.IntDef; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -@IntDef({SettingType.BASIC_SAFE_BROWSING_ENABLED}) +@IntDef({SettingType.BASIC_SAFE_BROWSING_ENABLED, SettingType.UKM_ENABLED, + SettingType.EXTENDED_REPORTING_SAFE_BROWSING_ENABLED, + SettingType.REAL_TIME_SAFE_BROWSING_ENABLED}) @Retention(RetentionPolicy.SOURCE) public @interface SettingType { int BASIC_SAFE_BROWSING_ENABLED = 0; + int UKM_ENABLED = 1; + int EXTENDED_REPORTING_SAFE_BROWSING_ENABLED = 2; + int REAL_TIME_SAFE_BROWSING_ENABLED = 3; } diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/WebLayerVersionConstants.java b/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/WebLayerVersionConstants.java new file mode 100644 index 00000000000..a76b13c3ba8 --- /dev/null +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/interfaces/WebLayerVersionConstants.java @@ -0,0 +1,19 @@ +// Copyright 2020 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.weblayer_private.interfaces; + +/** + * Versioning related constants. + */ +public interface WebLayerVersionConstants { + /** + * Maximum allowed version skew. If the skew is greater than this, the implementation and client + * are not considered compatible, and WebLayer is unusable. The skew is the absolute value of + * the difference between the client major version and the implementation major version. + * + * @see WebLayer#isAvailable() + */ + int MAX_SKEW = 4; +} diff --git a/chromium/weblayer/browser/java/org/chromium/weblayer_private/test_interfaces/ITestWebLayer.aidl b/chromium/weblayer/browser/java/org/chromium/weblayer_private/test_interfaces/ITestWebLayer.aidl index 3664df20f44..ac1cd24f40f 100644 --- a/chromium/weblayer/browser/java/org/chromium/weblayer_private/test_interfaces/ITestWebLayer.aidl +++ b/chromium/weblayer/browser/java/org/chromium/weblayer_private/test_interfaces/ITestWebLayer.aidl @@ -4,6 +4,9 @@ package org.chromium.weblayer_private.test_interfaces; +import org.chromium.weblayer_private.interfaces.IObjectWrapper; +import org.chromium.weblayer_private.interfaces.ITab; + interface ITestWebLayer { // Force network connectivity state. boolean isNetworkChangeAutoDetectOn() = 1; @@ -19,4 +22,26 @@ interface ITestWebLayer { // Forces the system location setting to enabled. void setSystemLocationSettingEnabled(boolean enabled) = 6; + + // See comments in TestWebLayer for details. + void waitForBrowserControlsMetadataState(in ITab tab, + in int top, + in int bottom, + in IObjectWrapper runnable) = 7; + + void setAccessibilityEnabled(in boolean enabled) = 8; + + boolean canBrowserControlsScroll(in ITab tab) = 9; + + // Creates and shows a test infobar in |tab|, calling |runnable| when the addition (including + // animations) is complete. + void addInfoBar(in ITab tab, in IObjectWrapper runnable) = 10; + + // Gets the infobar container view associated with |tab|. + IObjectWrapper getInfoBarContainerView(in ITab tab) = 11; + + void setIgnoreMissingKeyForTranslateManager(in boolean ignore) = 12; + void forceNetworkConnectivityState(in boolean networkAvailable) = 13; + + boolean canInfoBarContainerScroll(in ITab tab) = 14; } |