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 | |
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')
155 files changed, 6526 insertions, 906 deletions
diff --git a/chromium/weblayer/browser/java/BUILD.gn b/chromium/weblayer/browser/java/BUILD.gn index e5a00fb6c61..38cdda128ce 100644 --- a/chromium/weblayer/browser/java/BUILD.gn +++ b/chromium/weblayer/browser/java/BUILD.gn @@ -9,20 +9,33 @@ import("//weblayer/variables.gni") android_resources("weblayer_resources") { sources = [ + "res/drawable/weblayer_infobar_wrapper_bg.xml", + "res/drawable/weblayer_tab_indicator.xml", "res/layout/site_settings_layout.xml", + "res/layout/weblayer_infobar_translate_compact_content.xml", + "res/layout/weblayer_infobar_translate_tab_content.xml", + "res/layout/weblayer_translate_menu_item.xml", + "res/layout/weblayer_translate_menu_item_checked.xml", "res/layout/weblayer_url_bar.xml", + "res/values/colors.xml", "res/values/dimens.xml", "res/values/styles.xml", ] custom_package = "org.chromium.weblayer_private" deps = [ ":weblayer_strings_grd", + "//components/blocked_content/android:java_resources", + "//components/browser_ui/http_auth/android:java_resources", + "//components/browser_ui/media/android:java_resources", "//components/browser_ui/settings/android:java_resources", "//components/browser_ui/site_settings/android:java_resources", "//components/browser_ui/strings/android:browser_ui_strings_grd", "//components/browser_ui/styles/android:java_resources", + "//components/infobars/android:java_resources", "//components/page_info/android:java_resources", "//components/permissions/android:java_resources", + "//components/translate/content/android:java_resources", + "//third_party/android_deps:com_google_android_material_material_java", "//weblayer:components_java_strings", ] } @@ -35,6 +48,7 @@ java_cpp_template("resource_id_javagen") { sources = [ "ResourceId.template" ] package_path = "org/chromium/weblayer_private/resources" inputs = [ + "//components/resources/android/blocked_content_resource_id.h", "//components/resources/android/page_info_resource_id.h", "//components/resources/android/permissions_resource_id.h", ] @@ -51,6 +65,8 @@ java_strings_grd("weblayer_strings_grd") { java_cpp_enum("generated_enums") { sources = [ "//weblayer/browser/controls_visibility_reason.h", + "//weblayer/browser/infobar_android.h", + "//weblayer/browser/translate_utils.h", "//weblayer/public/download.h", "//weblayer/public/navigation.h", "//weblayer/public/new_tab_delegate.h", @@ -60,7 +76,6 @@ java_cpp_enum("generated_enums") { android_library("java") { sources = [ - "org/chromium/weblayer_private/AccessibilityUtil.java", "org/chromium/weblayer_private/ActionModeCallback.java", "org/chromium/weblayer_private/AutocompleteSchemeClassifierImpl.java", "org/chromium/weblayer_private/AutofillView.java", @@ -69,7 +84,7 @@ android_library("java") { "org/chromium/weblayer_private/BrowserImpl.java", "org/chromium/weblayer_private/BrowserViewController.java", "org/chromium/weblayer_private/ChildProcessServiceImpl.java", - "org/chromium/weblayer_private/ContentView.java", + "org/chromium/weblayer_private/ConfirmInfoBar.java", "org/chromium/weblayer_private/ContentViewRenderView.java", "org/chromium/weblayer_private/CookieManagerImpl.java", "org/chromium/weblayer_private/CrashReporterControllerImpl.java", @@ -80,9 +95,19 @@ android_library("java") { "org/chromium/weblayer_private/FragmentAndroidPermissionDelegate.java", "org/chromium/weblayer_private/FragmentWindowAndroid.java", "org/chromium/weblayer_private/FullscreenCallbackProxy.java", + "org/chromium/weblayer_private/InfoBar.java", + "org/chromium/weblayer_private/InfoBarCompactLayout.java", + "org/chromium/weblayer_private/InfoBarContainer.java", + "org/chromium/weblayer_private/InfoBarContainerLayout.java", + "org/chromium/weblayer_private/InfoBarContainerView.java", + "org/chromium/weblayer_private/InfoBarUiItem.java", + "org/chromium/weblayer_private/InfoBarWrapper.java", + "org/chromium/weblayer_private/IntentUtils.java", "org/chromium/weblayer_private/InterceptNavigationDelegateClientImpl.java", "org/chromium/weblayer_private/LocaleChangedBroadcastReceiver.java", + "org/chromium/weblayer_private/MediaSessionManager.java", "org/chromium/weblayer_private/MediaStreamManager.java", + "org/chromium/weblayer_private/MojoInterfaceRegistrar.java", "org/chromium/weblayer_private/NavigationControllerImpl.java", "org/chromium/weblayer_private/NavigationImpl.java", "org/chromium/weblayer_private/NewTabCallbackProxy.java", @@ -91,8 +116,15 @@ android_library("java") { "org/chromium/weblayer_private/ProfileManager.java", "org/chromium/weblayer_private/RemoteFragmentImpl.java", "org/chromium/weblayer_private/SiteSettingsFragmentImpl.java", + "org/chromium/weblayer_private/SwipableOverlayView.java", "org/chromium/weblayer_private/TabCallbackProxy.java", "org/chromium/weblayer_private/TabImpl.java", + "org/chromium/weblayer_private/TranslateCompactInfoBar.java", + "org/chromium/weblayer_private/TranslateMenu.java", + "org/chromium/weblayer_private/TranslateMenuHelper.java", + "org/chromium/weblayer_private/TranslateOptions.java", + "org/chromium/weblayer_private/TranslateTabContent.java", + "org/chromium/weblayer_private/TranslateTabLayout.java", "org/chromium/weblayer_private/UrlBarControllerImpl.java", "org/chromium/weblayer_private/WebContentsGestureStateTracker.java", "org/chromium/weblayer_private/WebLayerAccessibilityUtil.java", @@ -103,6 +135,8 @@ android_library("java") { "org/chromium/weblayer_private/WebLayerNotificationChannels.java", "org/chromium/weblayer_private/WebLayerSiteSettingsClient.java", "org/chromium/weblayer_private/WebLayerTabModalPresenter.java", + "org/chromium/weblayer_private/WebMessageReplyProxyImpl.java", + "org/chromium/weblayer_private/WebShareServiceFactory.java", "org/chromium/weblayer_private/WebViewCompatibilityHelperImpl.java", "org/chromium/weblayer_private/metrics/MetricsServiceClient.java", "org/chromium/weblayer_private/metrics/UmaUtils.java", @@ -116,26 +150,34 @@ android_library("java") { ":weblayer_resources", "//base:base_java", "//base:jni_java", - "//components/autofill/android:provider_java", + "//components/autofill/android/provider:java", + "//components/browser_ui/client_certificate/android:java", + "//components/browser_ui/http_auth/android:java", + "//components/browser_ui/media/android:java", "//components/browser_ui/modaldialog/android:java", "//components/browser_ui/notifications/android:java", "//components/browser_ui/settings/android:java", + "//components/browser_ui/share/android:java", "//components/browser_ui/site_settings/android:java", "//components/browser_ui/styles/android:java", - "//components/browser_ui/styles/android:java_resources", "//components/browser_ui/util/android:java", + "//components/browser_ui/webshare/android:java", + "//components/browser_ui/widget/android:java", "//components/content_settings/android:java", "//components/crash/android:handler_java", "//components/crash/android:java", "//components/download/internal/common:internal_java", "//components/embedder_support/android:application_java", "//components/embedder_support/android:browser_context_java", + "//components/embedder_support/android:content_view_java", "//components/embedder_support/android:context_menu_java", "//components/embedder_support/android:util_java", "//components/embedder_support/android:web_contents_delegate_java", "//components/embedder_support/android/metrics:java", "//components/external_intents/android:java", "//components/find_in_page/android:java", + "//components/infobars/android:java", + "//components/infobars/core:infobar_enums_java", "//components/javascript_dialogs/android:java", "//components/location/android:settings_java", "//components/metrics:metrics_java", @@ -150,11 +192,22 @@ android_library("java") { "//components/url_formatter/android:url_formatter_java", "//components/variations/android:variations_java", "//components/version_info/android:version_constants_java", + "//components/webapk/android/libs/client:java", + "//components/webapk/android/libs/common:java", "//components/webrtc/android:java", "//content/public/android:content_java", + "//mojo/public/java:bindings_java", "//net/android:net_java", + "//services/network/public/mojom:cookies_mojom_java", "//services/network/public/mojom:mojom_java", + "//services/service_manager/public/java:service_manager_java", + "//third_party/android_deps:androidx_appcompat_appcompat_java", + "//third_party/android_deps:androidx_appcompat_appcompat_resources_java", "//third_party/android_deps:androidx_core_core_java", + "//third_party/android_deps:androidx_fragment_fragment_java", + "//third_party/android_deps:androidx_preference_preference_java", + "//third_party/android_deps:com_google_android_material_material_java", + "//third_party/blink/public/mojom:android_mojo_bindings_java", "//ui/android:ui_full_java", "//ui/android:ui_java", "//url:gurl_java", @@ -188,9 +241,16 @@ android_resources("weblayer_test_resources") { android_library("test_java") { testonly = true - sources = [ "org/chromium/weblayer_private/test/TestWebLayerImpl.java" ] + sources = [ + "org/chromium/weblayer_private/test/TestInfoBar.java", + "org/chromium/weblayer_private/test/TestWebLayerImpl.java", + ] deps = [ + ":interfaces_java", + ":java", ":weblayer_test_resources", + "//base:jni_java", + "//components/location/android:location_java", "//components/permissions/android:java", "//content/public/test/android:content_java_test_support", "//net/android:net_java", @@ -199,6 +259,15 @@ android_library("test_java") { "//ui/android:ui_full_java", ] srcjar_deps = [ ":test_aidl" ] + annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ] +} + +generate_jni("test_jni") { + testonly = true + sources = [ + "org/chromium/weblayer_private/test/TestInfoBar.java", + "org/chromium/weblayer_private/test/TestWebLayerImpl.java", + ] } generate_jni("jni") { @@ -206,25 +275,30 @@ generate_jni("jni") { "org/chromium/weblayer_private/AutocompleteSchemeClassifierImpl.java", "org/chromium/weblayer_private/BrowserControlsContainerView.java", "org/chromium/weblayer_private/BrowserImpl.java", + "org/chromium/weblayer_private/ConfirmInfoBar.java", "org/chromium/weblayer_private/ContentViewRenderView.java", "org/chromium/weblayer_private/CookieManagerImpl.java", "org/chromium/weblayer_private/DownloadCallbackProxy.java", "org/chromium/weblayer_private/DownloadImpl.java", "org/chromium/weblayer_private/ErrorPageCallbackProxy.java", "org/chromium/weblayer_private/FullscreenCallbackProxy.java", + "org/chromium/weblayer_private/InfoBar.java", + "org/chromium/weblayer_private/InfoBarContainer.java", "org/chromium/weblayer_private/LocaleChangedBroadcastReceiver.java", "org/chromium/weblayer_private/MediaStreamManager.java", + "org/chromium/weblayer_private/MojoInterfaceRegistrar.java", "org/chromium/weblayer_private/NavigationControllerImpl.java", "org/chromium/weblayer_private/NavigationImpl.java", "org/chromium/weblayer_private/NewTabCallbackProxy.java", "org/chromium/weblayer_private/ProfileImpl.java", "org/chromium/weblayer_private/TabCallbackProxy.java", "org/chromium/weblayer_private/TabImpl.java", + "org/chromium/weblayer_private/TranslateCompactInfoBar.java", "org/chromium/weblayer_private/UrlBarControllerImpl.java", "org/chromium/weblayer_private/WebLayerExceptionFilter.java", "org/chromium/weblayer_private/WebLayerFactoryImpl.java", "org/chromium/weblayer_private/WebLayerImpl.java", - "org/chromium/weblayer_private/WebLayerSiteSettingsClient.java", + "org/chromium/weblayer_private/WebMessageReplyProxyImpl.java", "org/chromium/weblayer_private/WebViewCompatibilityHelperImpl.java", "org/chromium/weblayer_private/metrics/MetricsServiceClient.java", "org/chromium/weblayer_private/metrics/UmaUtils.java", @@ -246,11 +320,13 @@ android_library("interfaces_java") { "org/chromium/weblayer_private/interfaces/NavigationState.java", "org/chromium/weblayer_private/interfaces/NewTabType.java", "org/chromium/weblayer_private/interfaces/ObjectWrapper.java", + "org/chromium/weblayer_private/interfaces/ScrollNotificationType.java", "org/chromium/weblayer_private/interfaces/SettingType.java", "org/chromium/weblayer_private/interfaces/SiteSettingsFragmentArgs.java", "org/chromium/weblayer_private/interfaces/SiteSettingsIntentHelper.java", "org/chromium/weblayer_private/interfaces/StrictModeWorkaround.java", "org/chromium/weblayer_private/interfaces/UrlBarOptionsKeys.java", + "org/chromium/weblayer_private/interfaces/WebLayerVersionConstants.java", ] deps = [ "//third_party/android_deps:androidx_annotation_annotation_java" ] @@ -316,6 +392,8 @@ android_aidl("aidl") { "org/chromium/weblayer_private/interfaces/IWebLayer.aidl", "org/chromium/weblayer_private/interfaces/IWebLayerClient.aidl", "org/chromium/weblayer_private/interfaces/IWebLayerFactory.aidl", + "org/chromium/weblayer_private/interfaces/IWebMessageCallbackClient.aidl", + "org/chromium/weblayer_private/interfaces/IWebMessageReplyProxy.aidl", ] } diff --git a/chromium/weblayer/browser/java/DEPS b/chromium/weblayer/browser/java/DEPS index 5218c75b075..819b5a6c915 100644 --- a/chromium/weblayer/browser/java/DEPS +++ b/chromium/weblayer/browser/java/DEPS @@ -1,11 +1,14 @@ include_rules = [ + "+components/browser_ui/http_auth", "+components/browser_ui/util/android", "+components/content_settings/android/java", "+components/crash/android/java", "+components/external_intents", + "+components/infobars/android", "+components/location/android/java/src/org/chromium/components/location", "+components/minidump_uploader", "+components/page_info/android/java", + "+components/webapk/android/libs", "+services/device/public/java/src/org/chromium/device/geolocation", # WebLayerNotificationBuilder should be used for all notifications. diff --git a/chromium/weblayer/browser/java/ResourceId.template b/chromium/weblayer/browser/java/ResourceId.template index 0f5352d7e40..1e16c782f49 100644 --- a/chromium/weblayer/browser/java/ResourceId.template +++ b/chromium/weblayer/browser/java/ResourceId.template @@ -11,6 +11,7 @@ class ResourceId { int[] resourceList = { #define LINK_RESOURCE_ID(c_id,java_id) java_id, #define DECLARE_RESOURCE_ID(c_id,java_id) java_id, +#include "components/resources/android/blocked_content_resource_id.h" #include "components/resources/android/page_info_resource_id.h" #include "components/resources/android/permissions_resource_id.h" }; 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; } diff --git a/chromium/weblayer/browser/java/res/drawable/weblayer_infobar_wrapper_bg.xml b/chromium/weblayer/browser/java/res/drawable/weblayer_infobar_wrapper_bg.xml new file mode 100644 index 00000000000..e119c1148fa --- /dev/null +++ b/chromium/weblayer/browser/java/res/drawable/weblayer_infobar_wrapper_bg.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. --> + +<layer-list xmlns:android="http://schemas.android.com/apk/res/android" > + <item> + <bitmap + android:src="@drawable/infobar_shadow_top" + android:gravity="top|fill_horizontal" + android:tileMode="disabled" /> + </item> + <item + android:top="@dimen/infobar_shadow_height" + android:drawable="@color/infobar_background_color" /> +</layer-list> diff --git a/chromium/weblayer/browser/java/res/drawable/weblayer_tab_indicator.xml b/chromium/weblayer/browser/java/res/drawable/weblayer_tab_indicator.xml new file mode 100644 index 00000000000..c8ab7e06481 --- /dev/null +++ b/chromium/weblayer/browser/java/res/drawable/weblayer_tab_indicator.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2019 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. --> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:left="@dimen/weblayer_tab_indicator_padding" + android:right="@dimen/weblayer_tab_indicator_padding" > + <shape android:shape="rectangle" > + <corners + android:topRightRadius="@dimen/weblayer_tab_indicator_radius" + android:topLeftRadius="@dimen/weblayer_tab_indicator_radius" /> + </shape> + </item> +</layer-list> diff --git a/chromium/weblayer/browser/java/res/layout/site_settings_layout.xml b/chromium/weblayer/browser/java/res/layout/site_settings_layout.xml index 7bb3c0b82f1..c9cf58e79f1 100644 --- a/chromium/weblayer/browser/java/res/layout/site_settings_layout.xml +++ b/chromium/weblayer/browser/java/res/layout/site_settings_layout.xml @@ -3,9 +3,14 @@ Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. --> -<FrameLayout +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" - android:layout_height="match_parent" - android:id="@+id/site_settings_container"> -</FrameLayout> + android:layout_height="match_parent"> + <FrameLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:id="@+id/site_settings_container"> + </FrameLayout> + <include layout="@layout/settings_action_bar_shadow" /> +</RelativeLayout> diff --git a/chromium/weblayer/browser/java/res/layout/weblayer_infobar_translate_compact_content.xml b/chromium/weblayer/browser/java/res/layout/weblayer_infobar_translate_compact_content.xml new file mode 100644 index 00000000000..82149b5b142 --- /dev/null +++ b/chromium/weblayer/browser/java/res/layout/weblayer_infobar_translate_compact_content.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/weblayer_translate_infobar_content" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_vertical" + android:orientation="horizontal"> + <!-- TODO(huayinz): Change app:tabIndicatorColor to some common color reference --> + <org.chromium.weblayer_private.TranslateTabLayout + android:id="@+id/weblayer_translate_infobar_tabs" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:requiresFadingEdge="horizontal" + android:fadingEdgeLength="@dimen/weblayer_infobar_translate_fade_edge_length" + app:tabIndicator="@drawable/weblayer_tab_indicator" + app:tabIndicatorFullWidth="false" + app:tabIndicatorHeight="3dp" + app:tabSelectedTextColor="@color/weblayer_tab_layout_selected_tab_color" + app:tabGravity="fill" + app:tabMode="scrollable" /> + + <org.chromium.ui.widget.ChromeImageButton + android:id="@+id/weblayer_translate_infobar_menu_button" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:minHeight="@dimen/min_touch_target_size" + android:minWidth="@dimen/min_touch_target_size" + android:scaleType="center" + android:background="?attr/selectableItemBackground" + android:contentDescription="@string/accessibility_toolbar_btn_menu" + android:src="@drawable/ic_more_vert_24dp" + app:tint="@color/default_icon_color_tint_list" /> +</LinearLayout> diff --git a/chromium/weblayer/browser/java/res/layout/weblayer_infobar_translate_tab_content.xml b/chromium/weblayer/browser/java/res/layout/weblayer_infobar_translate_tab_content.xml new file mode 100644 index 00000000000..3be8f467c43 --- /dev/null +++ b/chromium/weblayer/browser/java/res/layout/weblayer_infobar_translate_tab_content.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. --> + +<org.chromium.weblayer_private.TranslateTabContent + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/weblayer_translate_tabcontent" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + <!-- Add both the textView and progressBar to the tab, and only keep one of them visible. + This way the width of the Tab will always be fixed no matter which one is visible. --> + <TextView + android:id="@+id/weblayer_translate_infobar_tab_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:textAppearance="@style/TextAppearance.Design.Tab" + android:visibility="visible" + android:singleLine="true" /> + <ProgressBar + android:id="@+id/weblayer_translate_infobar_tab_progressbar" + android:layout_width="@dimen/infobar_small_icon_size" + android:layout_height="@dimen/infobar_small_icon_size" + android:layout_gravity="center" + android:indeterminate="true" + android:visibility="invisible" /> +</org.chromium.weblayer_private.TranslateTabContent> diff --git a/chromium/weblayer/browser/java/res/layout/weblayer_translate_menu_item.xml b/chromium/weblayer/browser/java/res/layout/weblayer_translate_menu_item.xml new file mode 100644 index 00000000000..a7948450da3 --- /dev/null +++ b/chromium/weblayer/browser/java/res/layout/weblayer_translate_menu_item.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. --> + +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + style="@style/WebLayerAppMenuItem" + android:layout_width="match_parent" + android:layout_height="wrap_content" > + + <TextView + android:id="@+id/weblayer_menu_item_text" + android:textAppearance="?android:attr/textAppearanceLargePopupMenu" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="start" + android:gravity="center_vertical" + android:paddingTop="13dp" + android:paddingBottom="13dp" /> + +</FrameLayout> diff --git a/chromium/weblayer/browser/java/res/layout/weblayer_translate_menu_item_checked.xml b/chromium/weblayer/browser/java/res/layout/weblayer_translate_menu_item_checked.xml new file mode 100644 index 00000000000..a4bd2cf4692 --- /dev/null +++ b/chromium/weblayer/browser/java/res/layout/weblayer_translate_menu_item_checked.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. --> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <LinearLayout + style="@style/WebLayerAppMenuItem" + android:layout_width="match_parent" + android:layout_height="wrap_content" > + + <TextView + android:id="@+id/weblayer_menu_item_text" + android:textAppearance="?android:attr/textAppearanceLargePopupMenu" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_gravity="start" + android:gravity="center_vertical" + android:paddingTop="13dp" + android:paddingBottom="13dp" + android:paddingEnd="16dp" /> + <org.chromium.ui.widget.ChromeImageView + android:id="@+id/weblayer_menu_item_icon" + android:src="@drawable/ic_check_googblue_24dp" + android:layout_width="24dp" + android:layout_height="match_parent" + android:layout_gravity="end" + android:gravity="center_vertical" + app:tint="@color/default_icon_color_tint_list" /> + + </LinearLayout> + + <View + android:id="@+id/weblayer_menu_item_divider" + android:layout_width="match_parent" + android:layout_height="1dp" + android:background="@color/divider_line_bg_color" + android:visibility="gone" /> + +</LinearLayout> + diff --git a/chromium/weblayer/browser/java/res/layout/weblayer_url_bar.xml b/chromium/weblayer/browser/java/res/layout/weblayer_url_bar.xml index 1e8828d7f25..e5f2b57b80e 100644 --- a/chromium/weblayer/browser/java/res/layout/weblayer_url_bar.xml +++ b/chromium/weblayer/browser/java/res/layout/weblayer_url_bar.xml @@ -24,7 +24,7 @@ android:layout_height="wrap_content" android:layout_width="wrap_content" android:layout_gravity="center_vertical" - android:maxLines="1" + android:singleLine="true" android:paddingEnd="@dimen/url_text_edge_padding" android:paddingStart="@dimen/url_text_edge_padding" android:ellipsize="start" diff --git a/chromium/weblayer/browser/java/res/values/colors.xml b/chromium/weblayer/browser/java/res/values/colors.xml new file mode 100644 index 00000000000..bb00b313960 --- /dev/null +++ b/chromium/weblayer/browser/java/res/values/colors.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. --> + +<resources xmlns:tools="http://schemas.android.com/tools"> + <!-- Please see src/ui/android/java/res/values/colors.xml for the shared common colors. --> + + <color name="weblayer_tab_layout_selected_tab_color">@color/default_text_color_blue</color> + +</resources> diff --git a/chromium/weblayer/browser/java/res/values/dimens.xml b/chromium/weblayer/browser/java/res/values/dimens.xml index dfce8d8e51e..0eee3fc9758 100644 --- a/chromium/weblayer/browser/java/res/values/dimens.xml +++ b/chromium/weblayer/browser/java/res/values/dimens.xml @@ -6,4 +6,13 @@ <resources> <dimen name="security_status_icon_size">18dp</dimen> <dimen name="url_text_edge_padding">5dp</dimen> -</resources>
\ No newline at end of file + + <!-- Dimensions for compact translate infobar. --> + <dimen name="weblayer_infobar_translate_fade_edge_length">18dp</dimen> + <dimen name="weblayer_infobar_translate_menu_width">260dp</dimen> + + <!-- Dimens of tab indicator --> + <dimen name="weblayer_tab_indicator_radius">3dp</dimen> + <dimen name="weblayer_tab_indicator_padding">2dp</dimen> + +</resources> diff --git a/chromium/weblayer/browser/java/res/values/styles.xml b/chromium/weblayer/browser/java/res/values/styles.xml index b90beb2e45c..f362fd9ab69 100644 --- a/chromium/weblayer/browser/java/res/values/styles.xml +++ b/chromium/weblayer/browser/java/res/values/styles.xml @@ -10,40 +10,13 @@ <item name="alertDialogTheme">@style/Theme.Chromium.AlertDialog</item> </style> - <style name="PreferenceTheme"> - <item name="preferenceStyle">@style/PreferenceItem</item> - <item name="preferenceFragmentCompatStyle">@style/SettingsFragment</item> - <item name="preferenceFragmentListStyle">@style/SettingsFragmentList</item> - <item name="dialogPreferenceStyle">@style/DialogPreference</item> - <item name="checkBoxPreferenceStyle">@style/CheckBoxPreference</item> - <item name="switchPreferenceCompatStyle">@style/SwitchPreference</item> - </style> - - <style name="PreferenceItem"> - <item name="android:layout">@layout/preference_compat</item> - </style> - - <style name="SettingsFragment"> - <item name="android:divider">?android:attr/listDivider</item> - </style> - - <style name="SettingsFragmentList"> - <item name="android:paddingStart">0dp</item> - <item name="android:paddingEnd">0dp</item> - </style> - - <style name="DialogPreference"> - <item name="android:layout">@layout/preference_compat</item> - <item name="android:negativeButtonText">@android:string/cancel</item> - </style> - - <style name="CheckBoxPreference"> - <item name="android:layout">@layout/preference_compat</item> - <item name="android:widgetLayout">@layout/preference_widget_checkbox</item> - </style> - - <style name="SwitchPreference"> - <item name="android:layout">@layout/preference_compat</item> - <item name="android:widgetLayout">@layout/preference_widget_switch_compat</item> + <!-- The following styles may be used to style views provided by a CustomViewBinder or attached + to the app menu as headers or footers. --> + + <!-- Styling for an app menu item row. --> + <style name="WebLayerAppMenuItem"> + <item name="android:paddingStart">16dp</item> + <item name="android:paddingEnd">16dp</item> + <item name="android:background">?attr/listChoiceBackgroundIndicator</item> </style> </resources> diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_af.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_af.xtb index 403610204ab..62d289ff62c 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_af.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_af.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="af"> +<translation id="1068672505746868501">Moet nooit bladsye in <ph name="SOURCE_LANGUAGE" /> vertaal nie</translation> +<translation id="124116460088058876">Meer tale</translation> +<translation id="1285320974508926690">Moet nooit hierdie werf vertaal nie</translation> +<translation id="290376772003165898">Bladsy is nie in <ph name="LANGUAGE" /> nie?</translation> +<translation id="5684874026226664614">Oeps. Hierdie bladsy kon nie vertaal word nie.</translation> +<translation id="6040143037577758943">Maak toe</translation> +<translation id="6831043979455480757">Vertaal</translation> +<translation id="7243308994586599757">Opsies is naby die onderkant van die skerm beskikbaar</translation> +<translation id="773466115871691567">Vertaal altyd bladsye in <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Webblaaieraktiwiteit</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_am.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_am.xtb index 2fd6e46a16e..ec9bad225cb 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_am.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_am.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="am"> +<translation id="1068672505746868501">በ<ph name="SOURCE_LANGUAGE" /> ውስጥ ገጾችን በጭራሽ አትተርጉም</translation> +<translation id="124116460088058876">ተጨማሪ ቋንቋዎች</translation> +<translation id="1285320974508926690">ይህን ጣቢያ በጭራሽ አትተርጉም</translation> +<translation id="290376772003165898">ገጽ በ<ph name="LANGUAGE" /> አይደለም?</translation> +<translation id="5684874026226664614">ውይ። ይህ ገጽ ሊተረጎም አይችልም።</translation> +<translation id="6040143037577758943">ዝጋ</translation> +<translation id="6831043979455480757">መተርጎም</translation> +<translation id="7243308994586599757">አማራጮች ከማያ ገጹ ግርጌ አጠገብ ይገኛሉ</translation> +<translation id="773466115871691567">ገጾችን ሁልጊዜ በ<ph name="SOURCE_LANGUAGE" /> ተርጉም</translation> <translation id="8298278839890148234">የድር አሳሽ እንቅስቃሴ</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_ar.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_ar.xtb index 55fcefece9b..b631d5aa80b 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_ar.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_ar.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="ar"> +<translation id="1068672505746868501">عدم ترجمة الصفحات باللغة <ph name="SOURCE_LANGUAGE" /> مُطلقًا</translation> +<translation id="124116460088058876">مزيد من اللغات</translation> +<translation id="1285320974508926690">عدم ترجمة هذا الموقع مطلقًا</translation> +<translation id="290376772003165898">أليست الصفحة باللغة <ph name="LANGUAGE" />؟</translation> +<translation id="5684874026226664614">عفوًا. تعذرت ترجمة هذه الصفحة.</translation> +<translation id="6040143037577758943">إغلاق</translation> +<translation id="6831043979455480757">ترجمة</translation> +<translation id="7243308994586599757">الخيارات المتاحة بالقرب من الجزء السفلي من الشاشة</translation> +<translation id="773466115871691567">ترجمة الصفحات باللغة <ph name="SOURCE_LANGUAGE" /> دائمًا</translation> <translation id="8298278839890148234">نشاط التصفُّح على الويب</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_as.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_as.xtb index db63c417200..409550b1d05 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_as.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_as.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="as"> +<translation id="1068672505746868501">পৃষ্ঠা <ph name="SOURCE_LANGUAGE" />লৈ কেতিয়াও অনুবাদ নকৰিব</translation> +<translation id="124116460088058876">অধিক ভাষা</translation> +<translation id="1285320974508926690">এই ছাইটটো কেতিয়াও অনুবাদ নকৰিব</translation> +<translation id="290376772003165898">পৃষ্ঠাটো <ph name="LANGUAGE" /> ভাষাত নাই নেকি?</translation> +<translation id="5684874026226664614">ওঁহ এই পৃষ্ঠাটো অনুবাদ কৰিব পৰা নগ’ল।</translation> +<translation id="6040143037577758943">বন্ধ কৰক</translation> +<translation id="6831043979455480757">অনুবাদ কৰক</translation> +<translation id="7243308994586599757">স্ক্ৰীণৰ কাষৰ বুটামত উপলব্ধ বিকল্প</translation> +<translation id="773466115871691567">সদায় পৃষ্ঠাসমূহ <ph name="SOURCE_LANGUAGE" />লৈ অনুবাদ কৰক</translation> <translation id="8298278839890148234">ৱেব ব্ৰাউজাৰৰ কার্যকলাপ</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_az.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_az.xtb index 684871cc3c3..cd8a4a44f50 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_az.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_az.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="az"> +<translation id="1068672505746868501">Heç vaxt <ph name="SOURCE_LANGUAGE" /> dilində olan səhifələri tərcümə etməyin</translation> +<translation id="124116460088058876">Digər dillər</translation> +<translation id="1285320974508926690">Bu saytı heç vaxt tərcümə etməyin</translation> +<translation id="290376772003165898">Səhifə <ph name="LANGUAGE" /> dilində deyil?</translation> +<translation id="5684874026226664614">Bu səhifə tərcümə edilə bilmir. Niyəsini bilmirik.</translation> +<translation id="6040143037577758943">Qapat</translation> +<translation id="6831043979455480757">Tərcümə et</translation> +<translation id="7243308994586599757">Seçənəklər ekranın aşağısına yaxın yerdə əlçatandır</translation> +<translation id="773466115871691567">Səhifələri həmişə <ph name="SOURCE_LANGUAGE" /> dilinə tərcümə edin</translation> <translation id="8298278839890148234">Veb axtarış fəaliyyəti</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_be.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_be.xtb index 4c18cf28a2f..eff070e867a 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_be.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_be.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="be"> +<translation id="1068672505746868501">Ніколі не перакладаць старонкі на наступнай мове: <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Іншыя мовы</translation> +<translation id="1285320974508926690">Ніколі не перакладаць гэты сайт</translation> +<translation id="290376772003165898">Мова старонкі – не <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Памылка. Не ўдалося перакласці гэту старонку.</translation> +<translation id="6040143037577758943">Закрыць</translation> +<translation id="6831043979455480757">Перакласці</translation> +<translation id="7243308994586599757">Параметры знаходзяцца ў ніжняй частцы экрана</translation> +<translation id="773466115871691567">Заўсёды перакладаць старонкі на наступнай мове: <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Дзеянні ў вэб-браўзеры</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_bg.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_bg.xtb index 1e7789939c0..a55fe167dff 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_bg.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_bg.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="bg"> +<translation id="1068672505746868501">Страниците на <ph name="SOURCE_LANGUAGE" /> да не се превеждат</translation> +<translation id="124116460088058876">Още езици</translation> +<translation id="1285320974508926690">Този сайт да не се превежда никога</translation> +<translation id="290376772003165898">Страницата не е на <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Ами сега! Тази страница не можа да се преведе.</translation> +<translation id="6040143037577758943">Затваряне</translation> +<translation id="6831043979455480757">Превод</translation> +<translation id="7243308994586599757">Опциите са в долната част на екрана</translation> +<translation id="773466115871691567">Страниците на <ph name="SOURCE_LANGUAGE" /> да се превеждат винаги</translation> <translation id="8298278839890148234">Активност в уеб браузъра</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_bn.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_bn.xtb index 374b81798d7..81f76d848f2 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_bn.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_bn.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="bn"> +<translation id="1068672505746868501"><ph name="SOURCE_LANGUAGE" /> ভাষার পৃষ্ঠার অনুবাদ কখনও দেখতে চাই না</translation> +<translation id="124116460088058876">আরও ভাষা</translation> +<translation id="1285320974508926690">কখনই এই সাইটটিকে অনুবাদ করবেন না</translation> +<translation id="290376772003165898">পৃষ্ঠাটি <ph name="LANGUAGE" /> ভাষায় নয়?</translation> +<translation id="5684874026226664614">ওহো৷ এই পৃষ্ঠাটির অনুবাদ করা যাবে না৷</translation> +<translation id="6040143037577758943">বন্ধ</translation> +<translation id="6831043979455480757">অনুবাদ</translation> +<translation id="7243308994586599757">স্ক্রীনের প্রায় নীচের দিকে বিকল্পগুলি উপলব্ধ</translation> +<translation id="773466115871691567">সব সময় <ph name="SOURCE_LANGUAGE" /> ভাষার পৃষ্ঠার অনুবাদ দেখতে চাই</translation> <translation id="8298278839890148234">ওয়েব ব্রাউজার অ্যাক্টিভিটি</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_bs.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_bs.xtb index 4fb2dbb390e..5d1aefcaea1 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_bs.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_bs.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="bs"> +<translation id="1068672505746868501">Nemoj nikada prevoditi stranice na <ph name="SOURCE_LANGUAGE" /> jezik</translation> +<translation id="124116460088058876">Više jezika</translation> +<translation id="1285320974508926690">Nikada ne prevodi ovu web lokaciju</translation> +<translation id="290376772003165898">Ovo nije <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Ups. Prijevod ove stranice nije uspio.</translation> +<translation id="6040143037577758943">Zatvori</translation> +<translation id="6831043979455480757">Prevedi</translation> +<translation id="7243308994586599757">Opcije su dostupne pri dnu ekrana</translation> +<translation id="773466115871691567">Uvijek prevedi stranice na <ph name="SOURCE_LANGUAGE" /> jezik</translation> <translation id="8298278839890148234">Aktivnost web preglednika</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_ca.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_ca.xtb index ba2d4e4da24..3c181c2e235 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_ca.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_ca.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="ca"> +<translation id="1068672505746868501">No tradueixis mai les pàgines en <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Més idiomes</translation> +<translation id="1285320974508926690">No tradueixis mai aquest lloc</translation> +<translation id="290376772003165898">La pàgina no està en <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Aquesta pàgina no s'ha pogut traduir.</translation> +<translation id="6040143037577758943">Tanca</translation> +<translation id="6831043979455480757">Tradueix</translation> +<translation id="7243308994586599757">Opcions disponibles a la part inferior de la pantalla</translation> +<translation id="773466115871691567">Tradueix sempre les pàgines en <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Activitat del navegador web</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_cs.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_cs.xtb index 5710c42229a..7e7b91a9744 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_cs.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_cs.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="cs"> +<translation id="1068672505746868501">Stránky v jazyce <ph name="SOURCE_LANGUAGE" /> nikdy nepřekládat</translation> +<translation id="124116460088058876">Další jazyky</translation> +<translation id="1285320974508926690">Tento web nikdy nepřekládat</translation> +<translation id="290376772003165898">Stránka není v jazyce <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Jejda. Tuto stránku se nepodařilo přeložit.</translation> +<translation id="6040143037577758943">Zavřít</translation> +<translation id="6831043979455480757">Přeložit</translation> +<translation id="7243308994586599757">Možnosti jsou k dispozici ve spodní části obrazovky</translation> +<translation id="773466115871691567">Stránky v jazyce <ph name="SOURCE_LANGUAGE" /> vždy překládat</translation> <translation id="8298278839890148234">Aktivita ve webovém prohlížeči</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_da.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_da.xtb index 63df7f53aa1..571f36b83ac 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_da.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_da.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="da"> +<translation id="1068672505746868501">Oversæt aldrig sider på <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Flere sprog</translation> +<translation id="1285320974508926690">Oversæt aldrig dette website</translation> +<translation id="290376772003165898">Er siden ikke på <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Ups! Denne side kunne ikke oversættes.</translation> +<translation id="6040143037577758943">Luk</translation> +<translation id="6831043979455480757">Oversæt</translation> +<translation id="7243308994586599757">Du finder indstillingerne nederst på skærmen</translation> +<translation id="773466115871691567">Oversæt altid sider på <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Webbrowseraktivitet</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_de.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_de.xtb index a6c724e09b6..65cd87c466a 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_de.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_de.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="de"> +<translation id="1068672505746868501">Seiten auf <ph name="SOURCE_LANGUAGE" /> nie übersetzen</translation> +<translation id="124116460088058876">Weitere Sprachen</translation> +<translation id="1285320974508926690">Diese Website nie übersetzen</translation> +<translation id="290376772003165898">Diese Seite ist nicht auf <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Hoppla! Diese Seite konnte nicht übersetzt werden.</translation> +<translation id="6040143037577758943">Schließen</translation> +<translation id="6831043979455480757">Übersetzen</translation> +<translation id="7243308994586599757">Optionen unten auf dem Bildschirm verfügbar</translation> +<translation id="773466115871691567">Seiten auf <ph name="SOURCE_LANGUAGE" /> immer übersetzen</translation> <translation id="8298278839890148234">Webbrowseraktivitäten</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_el.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_el.xtb index 91f7efb7d08..1e22b4a980c 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_el.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_el.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="el"> +<translation id="1068672505746868501">Να μην γίνεται μετάφραση σελίδων στα <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Περισσότερες γλώσσες</translation> +<translation id="1285320974508926690">Να μην γίνεται ποτέ μετάφραση αυτού του ιστότοπου</translation> +<translation id="290376772003165898">Η σελίδα δεν είναι στα <ph name="LANGUAGE" />;</translation> +<translation id="5684874026226664614">Ωχ. Δεν ήταν δυνατή η μετάφραση αυτής της σελίδας.</translation> +<translation id="6040143037577758943">Κλείσιμο</translation> +<translation id="6831043979455480757">Μετάφραση</translation> +<translation id="7243308994586599757">Διαθέσιμες επιλογές κοντά κάτω μέρος της οθόνης</translation> +<translation id="773466115871691567">Να μεταφράζονται πάντα οι σελίδες προς τα <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Δραστηριότητα προγράμματος περιήγησης στον ιστό</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_en-GB.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_en-GB.xtb index 078cecbe647..8f63e62c0db 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_en-GB.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_en-GB.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="en-GB"> +<translation id="1068672505746868501">Never translate pages in <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">More languages</translation> +<translation id="1285320974508926690">Never translate this site</translation> +<translation id="290376772003165898">Page is not in <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Oops. This page could not be translated.</translation> +<translation id="6040143037577758943">Close</translation> +<translation id="6831043979455480757">Translate</translation> +<translation id="7243308994586599757">Options available near bottom of the screen</translation> +<translation id="773466115871691567">Always translate pages in <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Web browser activity</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_es-419.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_es-419.xtb index e529fa34f49..276fe173c58 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_es-419.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_es-419.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="es-419"> +<translation id="1068672505746868501">No traducir nunca páginas en <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Más idiomas</translation> +<translation id="1285320974508926690">Nunca traducir este sitio</translation> +<translation id="290376772003165898">¿La página no está en <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">No se puede traducir esta página.</translation> +<translation id="6040143037577758943">Cerrar</translation> +<translation id="6831043979455480757">Traducir</translation> +<translation id="7243308994586599757">Opciones disponibles junto a la parte inferior de la pantalla</translation> +<translation id="773466115871691567">Traducir siempre las páginas en <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Actividad del navegador web</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_es.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_es.xtb index fcf6da55978..8b61d2a834e 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_es.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_es.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="es"> +<translation id="1068672505746868501">No traducir nunca las páginas en <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Más idiomas</translation> +<translation id="1285320974508926690">No traducir nunca este sitio</translation> +<translation id="290376772003165898">¿La página no está en <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">¡Vaya! No se ha podido traducir esta página.</translation> +<translation id="6040143037577758943">Cerrar</translation> +<translation id="6831043979455480757">Traducir</translation> +<translation id="7243308994586599757">Opciones disponibles cerca de la parte inferior de la pantalla</translation> +<translation id="773466115871691567">Traducir siempre las páginas en <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Actividad del navegador web</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_et.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_et.xtb index 37ada0e9df4..ff14c3ea03e 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_et.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_et.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="et"> +<translation id="1068672505746868501">Ära tõlgi kunagi <ph name="SOURCE_LANGUAGE" /> keeles olevaid lehti</translation> +<translation id="124116460088058876">Rohkem keeli</translation> +<translation id="1285320974508926690">Ära kunagi seda saiti tõlgi</translation> +<translation id="290376772003165898">Kas leht ei ole <ph name="LANGUAGE" /> keeles?</translation> +<translation id="5684874026226664614">Vabandust. Lehte ei õnnestunud tõlkida.</translation> +<translation id="6040143037577758943">Sulge</translation> +<translation id="6831043979455480757">Tõlgi</translation> +<translation id="7243308994586599757">Valikud on saadaval ekraani allosas</translation> +<translation id="773466115871691567">Tõlgi alati <ph name="SOURCE_LANGUAGE" /> keeles olevad lehed</translation> <translation id="8298278839890148234">Veebibrauseri tegevused</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_eu.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_eu.xtb index 2698f9fb351..0fe579c1cb0 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_eu.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_eu.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="eu"> +<translation id="1068672505746868501">Ez itzuli inoiz <ph name="SOURCE_LANGUAGE" /> darabilten orriak</translation> +<translation id="124116460088058876">Hizkuntza gehiago</translation> +<translation id="1285320974508926690">Ez itzuli inoiz webgune hau</translation> +<translation id="290376772003165898">Ez al da <ph name="LANGUAGE" /> orriko hizkuntza?</translation> +<translation id="5684874026226664614">Ezin izan da orria itzuli.</translation> +<translation id="6040143037577758943">Itxi</translation> +<translation id="6831043979455480757">Itzuli</translation> +<translation id="7243308994586599757">Pantailaren behealdean agertzen dira dauden aukerak</translation> +<translation id="773466115871691567">Itzuli beti <ph name="SOURCE_LANGUAGE" /> darabilten orriak</translation> <translation id="8298278839890148234">Sareko arakatze-jarduerak</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_fa.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_fa.xtb index 77daea83d86..b3cf59018eb 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_fa.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_fa.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="fa"> +<translation id="1068672505746868501">هرگز صفحههای <ph name="SOURCE_LANGUAGE" /> ترجمه نشوند</translation> +<translation id="124116460088058876">زبانهای بیشتر</translation> +<translation id="1285320974508926690">این سایت هرگز ترجمه نشود</translation> +<translation id="290376772003165898">صفحه به زبان <ph name="LANGUAGE" /> وجود ندارد؟</translation> +<translation id="5684874026226664614">متأسفیم. این صفحه ترجمه نشد.</translation> +<translation id="6040143037577758943">بستن</translation> +<translation id="6831043979455480757">ترجمه</translation> +<translation id="7243308994586599757">گزینهها در نزدیک پایین صفحه نمایش در دسترس هستند</translation> +<translation id="773466115871691567">صفحههای <ph name="SOURCE_LANGUAGE" /> همیشه ترجمه شوند</translation> <translation id="8298278839890148234">فعالیت مرورگر وب</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_fi.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_fi.xtb index 78a8d793d2e..e8e035371e0 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_fi.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_fi.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="fi"> +<translation id="1068672505746868501">Älä koskaan käännä kielellä <ph name="SOURCE_LANGUAGE" /> kirjoitettuja sivuja.</translation> +<translation id="124116460088058876">Lisää kieliä</translation> +<translation id="1285320974508926690">Älä käännä tätä sivustoa</translation> +<translation id="290376772003165898">Eikö sivu ole kirjoitettu kielellä <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Hups, tätä sivua ei voi kääntää.</translation> +<translation id="6040143037577758943">Sulje</translation> +<translation id="6831043979455480757">Käännä</translation> +<translation id="7243308994586599757">Asetukset löytyvät näytön alalaidasta.</translation> +<translation id="773466115871691567">Käännä aina kielellä <ph name="SOURCE_LANGUAGE" /> kirjoitut sivut</translation> <translation id="8298278839890148234">Verkon selaustoiminta</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_fil.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_fil.xtb index 08f97ffffd4..01ddf5eb7ec 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_fil.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_fil.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="fil"> +<translation id="1068672505746868501">Huwag kailanman isalin ang mga page sa <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Higit pang wika</translation> +<translation id="1285320974508926690">Huwag isalin kailanman ang site na ito</translation> +<translation id="290376772003165898">Hindi nakasalin ang page sa <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Oops. Hindi maisalin ang pahinang ito.</translation> +<translation id="6040143037577758943">Isara</translation> +<translation id="6831043979455480757">Isalin</translation> +<translation id="7243308994586599757">May mga opsyon malapit sa ibaba ng screen</translation> +<translation id="773466115871691567">Palaging isalin ang mga page sa <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Aktibidad ng web browser</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_fr-CA.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_fr-CA.xtb index 537978cc551..2415250b42a 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_fr-CA.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_fr-CA.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="fr-CA"> +<translation id="1068672505746868501">Ne jamais traduire les pages en <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Plus de langues</translation> +<translation id="1285320974508926690">Ne jamais traduire ce site</translation> +<translation id="290376772003165898">Cette page n'est pas en <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Oups… Impossible de traduire cette page.</translation> +<translation id="6040143037577758943">Fermer</translation> +<translation id="6831043979455480757">Traduire</translation> +<translation id="7243308994586599757">Options disponibles vers le bas de l’écran</translation> +<translation id="773466115871691567">Toujours traduire les pages en <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Activité de navigation Web</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_fr.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_fr.xtb index 9810a3e323a..334bf0344fd 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_fr.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_fr.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="fr"> +<translation id="1068672505746868501">Ne jamais traduire les pages en <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Plus de langues</translation> +<translation id="1285320974508926690">Ne jamais traduire ce site</translation> +<translation id="290376772003165898">La page n'est pas en <ph name="LANGUAGE" /> ?</translation> +<translation id="5684874026226664614">Petit problème… Impossible de traduire cette page.</translation> +<translation id="6040143037577758943">Fermer</translation> +<translation id="6831043979455480757">Traduire</translation> +<translation id="7243308994586599757">Options disponibles au bas de l'écran</translation> +<translation id="773466115871691567">Toujours traduire les pages en <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Activité de navigation sur le Web</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_gl.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_gl.xtb index c315bcbcd56..3f598e52c3b 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_gl.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_gl.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="gl"> +<translation id="1068672505746868501">Non traducir nunca páxinas en <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Máis idiomas</translation> +<translation id="1285320974508926690">Non traducir nunca este sitio</translation> +<translation id="290376772003165898">A páxina non está en <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Non se puido traducir esta páxina.</translation> +<translation id="6040143037577758943">Pechar</translation> +<translation id="6831043979455480757">Traducir</translation> +<translation id="7243308994586599757">Opcións dispoñibles na parte inferior da pantalla</translation> +<translation id="773466115871691567">Traducir sempre páxinas en <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Actividade de navegación pola Web</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_gu.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_gu.xtb index 9e2ee22696d..957cc2993a6 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_gu.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_gu.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="gu"> +<translation id="1068672505746868501">પેજનો ક્યારેય પણ <ph name="SOURCE_LANGUAGE" />માં અનુવાદ કરશો નહીં</translation> +<translation id="124116460088058876">વધુ ભાષાઓ</translation> +<translation id="1285320974508926690">આ સાઇટનું ક્યારેય ભાષાંતર કરશો નહીં</translation> +<translation id="290376772003165898">પેજ <ph name="LANGUAGE" />માં નથી?</translation> +<translation id="5684874026226664614">અરેરે. આ પૃષ્ઠનો અનુવાદ કરી શકાયો નથી.</translation> +<translation id="6040143037577758943">બંધ કરો</translation> +<translation id="6831043979455480757">અનુવાદ કરો</translation> +<translation id="7243308994586599757">સ્ક્રીનના તળિયા નજીક વિકલ્પો ઉપલબ્ધ છે</translation> +<translation id="773466115871691567">હંમેશાં પેજનો <ph name="SOURCE_LANGUAGE" />માં અનુવાદ કરો</translation> <translation id="8298278839890148234">વેબ બ્રાઉઝરની પ્રવૃત્તિ</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_hi.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_hi.xtb index a9ae0e70db1..6fe2057217c 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_hi.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_hi.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="hi"> +<translation id="1068672505746868501"><ph name="SOURCE_LANGUAGE" /> भाषा के पेज का कभी भी अनुवाद न करें</translation> +<translation id="124116460088058876">ज़्यादा भाषाएं</translation> +<translation id="1285320974508926690">कभी भी इस साइट का अनुवाद न करें</translation> +<translation id="290376772003165898">क्या पेज <ph name="LANGUAGE" /> भाषा में नहीं है?</translation> +<translation id="5684874026226664614">ओह. इस पेज का अनुवाद नहीं किया जा सका.</translation> +<translation id="6040143037577758943">बंद करें</translation> +<translation id="6831043979455480757">अनुवाद करें</translation> +<translation id="7243308994586599757">विकल्प स्क्रीन के नीचे उपलब्ध हैं</translation> +<translation id="773466115871691567"><ph name="SOURCE_LANGUAGE" /> भाषा के पेज का हमेशा अनुवाद करें</translation> <translation id="8298278839890148234">वेब ब्राउज़र की गतिविधि</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_hr.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_hr.xtb index bc0817b9c82..d9fc4ee6cec 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_hr.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_hr.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="hr"> +<translation id="1068672505746868501">Nikad ne prevodi <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Više jezika</translation> +<translation id="1285320974508926690">Nikad nemoj prevoditi ovu web-lokaciju</translation> +<translation id="290376772003165898">Ovo nije <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Ups. Stranicu nije bilo moguće prevesti.</translation> +<translation id="6040143037577758943">Zatvori</translation> +<translation id="6831043979455480757">Prevedi</translation> +<translation id="7243308994586599757">Opcije dostupne pri dnu zaslona</translation> +<translation id="773466115871691567">Uvijek prevodi <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Aktivnost u web-pregledniku</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_hu.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_hu.xtb index 587b17fb1f5..498d74cc4c9 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_hu.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_hu.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="hu"> +<translation id="1068672505746868501">Soha ne fordítsa le a(z) <ph name="SOURCE_LANGUAGE" /> nyelvű oldalakat</translation> +<translation id="124116460088058876">További nyelvek…</translation> +<translation id="1285320974508926690">Ezt a webhelyet soha ne fordítsa le</translation> +<translation id="290376772003165898">Az oldal nem <ph name="LANGUAGE" /> nyelvű?</translation> +<translation id="5684874026226664614">Hoppá! Az oldalt nem sikerült lefordítani.</translation> +<translation id="6040143037577758943">Bezárás</translation> +<translation id="6831043979455480757">Fordítás</translation> +<translation id="7243308994586599757">A beállítások a képernyő alsó részén találhatók</translation> +<translation id="773466115871691567">Mindig fordítsa le a(z) <ph name="SOURCE_LANGUAGE" /> nyelvű oldalakat</translation> <translation id="8298278839890148234">Böngészős tevékenységek</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_hy.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_hy.xtb index c5676da7cb3..e3c1efcec5e 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_hy.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_hy.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="hy"> +<translation id="1068672505746868501">Երբեք չթարգմանել <ph name="SOURCE_LANGUAGE" /> էջերը</translation> +<translation id="124116460088058876">Այլ լեզուներ</translation> +<translation id="1285320974508926690">Երբեք չթարգմանել այս կայքը</translation> +<translation id="290376772003165898">Էջը <ph name="LANGUAGE" /> չէ՞</translation> +<translation id="5684874026226664614">Չհաջողվեց թարգմանել այս էջը:</translation> +<translation id="6040143037577758943">Փակել</translation> +<translation id="6831043979455480757">Թարգմանել</translation> +<translation id="7243308994586599757">Ընտրանքները հասանելի են էկրանի ստորին հատվածում</translation> +<translation id="773466115871691567">Միշտ թարգմանել <ph name="SOURCE_LANGUAGE" /> էջերը</translation> <translation id="8298278839890148234">Գործողություններ դիտարկիչում</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_id.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_id.xtb index 31b5fe5d797..673f32be14c 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_id.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_id.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="id"> +<translation id="1068672505746868501">Jangan pernah menerjemahkan halaman dalam <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Bahasa lainnya</translation> +<translation id="1285320974508926690">Jangan pernah terjemahkan situs ini</translation> +<translation id="290376772003165898">Halaman tidak dalam bahasa <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Ups. Halaman ini tidak dapat diterjemahkan.</translation> +<translation id="6040143037577758943">Tutup</translation> +<translation id="6831043979455480757">Terjemahkan</translation> +<translation id="7243308994586599757">Opsi terdapat di dekat bagian bawah layar</translation> +<translation id="773466115871691567">Selalu terjemahkan halaman dalam bahasa <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Aktivitas penjelajahan web</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_is.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_is.xtb index 923602bb298..2f77ab69860 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_is.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_is.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="is"> +<translation id="1068672505746868501">Þýða aldrei síður á þessu tungumáli: <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Fleiri tungumál</translation> +<translation id="1285320974508926690">Aldrei þýða þetta vefsvæði</translation> +<translation id="290376772003165898">Er tungumál síðunnar ekki <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Úbbs. Þessa síðu er ekki hægt að þýða.</translation> +<translation id="6040143037577758943">Loka</translation> +<translation id="6831043979455480757">Þýða</translation> +<translation id="7243308994586599757">Valkostir eru neðst á skjánum</translation> +<translation id="773466115871691567">Þýða alltaf síður á þessu tungumáli: <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Vafranotkun</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_it.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_it.xtb index c4dc6e6ca5d..1f19226503d 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_it.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_it.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="it"> +<translation id="1068672505746868501">Non tradurre mai le pagine in <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Altre lingue</translation> +<translation id="1285320974508926690">Non tradurre mai questo sito</translation> +<translation id="290376772003165898">La pagina non è in <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Spiacenti. Impossibile tradurre questa pagina.</translation> +<translation id="6040143037577758943">Chiudi</translation> +<translation id="6831043979455480757">Traduci</translation> +<translation id="7243308994586599757">Opzioni disponibili nella parte inferiore dello schermo</translation> +<translation id="773466115871691567">Traduci sempre le pagine in <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Attività del browser web</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_iw.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_iw.xtb index bea498feca7..61e44631e2e 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_iw.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_iw.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="iw"> +<translation id="1068672505746868501">אף פעם אל תתרגם דפים ב<ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">שפות נוספות</translation> +<translation id="1285320974508926690">איני רוצה לקבל תרגום של אתר זה</translation> +<translation id="290376772003165898">הדף לא ב<ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">אופס. לא ניתן היה לתרגם את הדף הזה.</translation> +<translation id="6040143037577758943">סגור</translation> +<translation id="6831043979455480757">תרגום</translation> +<translation id="7243308994586599757">אפשרויות הזמינות באזור החלק התחתון של המסך</translation> +<translation id="773466115871691567">ברצוני לקבל תרגום תמיד דפים ב<ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">פעילות דפדפן אינטרנט</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_ja.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_ja.xtb index 091fdcc87ff..cf3a9d91955 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_ja.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_ja.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="ja"> +<translation id="1068672505746868501"><ph name="SOURCE_LANGUAGE" />のページを翻訳しない</translation> +<translation id="124116460088058876">その他の言語</translation> +<translation id="1285320974508926690">このサイトは翻訳しない</translation> +<translation id="290376772003165898"><ph name="LANGUAGE" />のページではない場合</translation> +<translation id="5684874026226664614">このページを翻訳できませんでした。</translation> +<translation id="6040143037577758943">閉じる</translation> +<translation id="6831043979455480757">翻訳</translation> +<translation id="7243308994586599757">画面の下の方にオプションがあります</translation> +<translation id="773466115871691567"><ph name="SOURCE_LANGUAGE" />のページを常に翻訳する</translation> <translation id="8298278839890148234">ウェブブラウザのアクティビティ</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_ka.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_ka.xtb index 57bc785af7f..2aa6a13f2ff 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_ka.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_ka.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="ka"> +<translation id="1068672505746868501">არასოდეს ითარგმნოს <ph name="SOURCE_LANGUAGE" /> გვერდები</translation> +<translation id="124116460088058876">სხვა ენები</translation> +<translation id="1285320974508926690">არასდროს გადათარგმნო ეს საიტი</translation> +<translation id="290376772003165898">გვერდის ტექსტი არ არის <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">ამ გვერდის გადათარგმნა ვერ მოხერხდა.</translation> +<translation id="6040143037577758943">დახურვა</translation> +<translation id="6831043979455480757">თარგმნა</translation> +<translation id="7243308994586599757">ვარიანტები ხელმისაწვდომია ეკრანის ქვედა ნაწილთან</translation> +<translation id="773466115871691567">ყოველთვის ითარგმნოს <ph name="SOURCE_LANGUAGE" /> გვერდები</translation> <translation id="8298278839890148234">ვებ-ბრაუზერის აქტივობა</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_kk.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_kk.xtb index d18117e9c7f..2e23049928a 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_kk.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_kk.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="kk"> +<translation id="1068672505746868501"><ph name="SOURCE_LANGUAGE" /> тіліндегі беттер ешқашан аударылмасын</translation> +<translation id="124116460088058876">Қосымша тілдер</translation> +<translation id="1285320974508926690">Бұл сайтты ешқашан аудармау</translation> +<translation id="290376772003165898">Бет <ph name="LANGUAGE" /> тілінде емес пе?</translation> +<translation id="5684874026226664614">Бұл бетті аудару мүмкін емес.</translation> +<translation id="6040143037577758943">Жабу</translation> +<translation id="6831043979455480757">Аудару</translation> +<translation id="7243308994586599757">Опциялар экранның төменгі жағында тұрады</translation> +<translation id="773466115871691567"><ph name="SOURCE_LANGUAGE" /> тіліндегі беттер әрқашан аударылсын</translation> <translation id="8298278839890148234">Браузерді қолдану мәліметі</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_km.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_km.xtb index 312955fb520..79e7f51cb82 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_km.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_km.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="km"> +<translation id="1068672505746868501">កុំបកប្រែទំព័រជាភាសា <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">ភាសាច្រើនទៀត</translation> +<translation id="1285320974508926690">មិនបកប្រែគេហទំព័រនេះទៀតឡើយ</translation> +<translation id="290376772003165898">ទំព័រមិនមានជាភាសា <ph name="LANGUAGE" /> ទេ?</translation> +<translation id="5684874026226664614">អូ។ ទំព័រនេះមិនអាចបកប្រែទេ។</translation> +<translation id="6040143037577758943">បិទ</translation> +<translation id="6831043979455480757">បកប្រែ</translation> +<translation id="7243308994586599757">មានជម្រើសនៅក្បែរផ្នែកខាងក្រោមអេក្រង់</translation> +<translation id="773466115871691567">បកប្រែទំព័រជាភាសា <ph name="SOURCE_LANGUAGE" /> ជានិច្ច</translation> <translation id="8298278839890148234">សកម្មភាពកម្មវិធីរុករកតាមអ៊ីនធឺណិត</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_kn.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_kn.xtb index 91524cd5c0f..cabf85f2d68 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_kn.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_kn.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="kn"> +<translation id="1068672505746868501"><ph name="SOURCE_LANGUAGE" /> ನಲ್ಲಿನ ಪುಟಗಳನ್ನು ಎಂದಿಗೂ ಅನುವಾದ ಮಾಡಬೇಡಿ</translation> +<translation id="124116460088058876">ಹೆಚ್ಚಿನ ಭಾಷೆಗಳು</translation> +<translation id="1285320974508926690">ಈ ಸೈಟ್ ಅನ್ನು ಎಂದಿಗೂ ಭಾಷಾಂತರಿಸದಿರಿ</translation> +<translation id="290376772003165898"><ph name="LANGUAGE" /> ನಲ್ಲಿನ ಪುಟ ಇಲ್ಲವೇ?</translation> +<translation id="5684874026226664614">ಓಹ್. ಈ ಪುಟವನ್ನು ಅನುವಾದಿಸಲಾಗುವುದಿಲ್ಲ.</translation> +<translation id="6040143037577758943">ಮುಚ್ಚಿರಿ</translation> +<translation id="6831043979455480757">ಅನುವಾದಿಸು</translation> +<translation id="7243308994586599757">ಪರದೆಯ ಕೆಳಗೆ ಲಭ್ಯವಿರುವ ಆಯ್ಕೆಗಳು</translation> +<translation id="773466115871691567">ಯಾವಾಗಲು <ph name="SOURCE_LANGUAGE" /> ನಲ್ಲಿನ ಪುಟಗಳನ್ನು ಅನುವಾದ ಮಾಡಿ</translation> <translation id="8298278839890148234">ವೆಬ್ ಬ್ರೌಸಿಂಗ್ ಚಟುವಟಿಕೆ</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_ko.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_ko.xtb index 742a5338444..de20f5dc367 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_ko.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_ko.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="ko"> +<translation id="1068672505746868501"><ph name="SOURCE_LANGUAGE" />로 된 페이지는 번역하지 않음</translation> +<translation id="124116460088058876">다른 언어</translation> +<translation id="1285320974508926690">이 사이트 번역 안함</translation> +<translation id="290376772003165898">페이지 언어가 <ph name="LANGUAGE" />가 아닌가요?</translation> +<translation id="5684874026226664614">죄송합니다. 이 페이지를 번역할 수 없습니다.</translation> +<translation id="6040143037577758943">닫기</translation> +<translation id="6831043979455480757">번역</translation> +<translation id="7243308994586599757">화면 하단에서 옵션 선택 가능</translation> +<translation id="773466115871691567"><ph name="SOURCE_LANGUAGE" />로 된 페이지를 항상 번역</translation> <translation id="8298278839890148234">웹브라우저 활동</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_ky.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_ky.xtb index 2d0f4b479b2..717031affb9 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_ky.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_ky.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="ky"> +<translation id="1068672505746868501"><ph name="SOURCE_LANGUAGE" /> тилиндеги барактар эч качан которулбасын</translation> +<translation id="124116460088058876">Дагы тилдер</translation> +<translation id="1285320974508926690">Бул сайт эч качан которулбасын</translation> +<translation id="290376772003165898">Барак <ph name="LANGUAGE" /> тилинде эмес бекен?</translation> +<translation id="5684874026226664614">Ой, бул бет которулган жок.</translation> +<translation id="6040143037577758943">Жабуу</translation> +<translation id="6831043979455480757">Которуу</translation> +<translation id="7243308994586599757">Параметрлер экрандын түбүндө берилген</translation> +<translation id="773466115871691567"><ph name="SOURCE_LANGUAGE" /> тилиндеги барактар дайыма которулсун</translation> <translation id="8298278839890148234">Көрүлгөн вебсайттар</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_lo.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_lo.xtb index fba8f18695e..7ab378b6803 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_lo.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_lo.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="lo"> +<translation id="1068672505746868501">ຢ່າແປໜ້າຕ່າງໆໃນ <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">ພາສາເພີ່ມເຕີມ</translation> +<translation id="1285320974508926690">ຢ່າແປເວັບໄຊທ໌ນີ້</translation> +<translation id="290376772003165898">ໜ້າບໍ່ຢູ່ໃນ <ph name="LANGUAGE" /> ບໍ?</translation> +<translation id="5684874026226664614">ອຸ້ຍ. ບໍ່ສາມາດແປໜ້ານີ້ໄດ້.</translation> +<translation id="6040143037577758943">ປິດ</translation> +<translation id="6831043979455480757">ແປພາສາ</translation> +<translation id="7243308994586599757">ທາງເລືອກມີໃຫ້ໃກ້ປຸ່ມຂອງໜ້າຈໍ</translation> +<translation id="773466115871691567">ແປໜ້າຕ່າງໆສະເໝີໃນ <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">ການເຄື່ອນໄຫວໃນໂປຣແກຣມທ່ອງເວັບ</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_lt.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_lt.xtb index a1a90e6ff7c..05d3d0706ff 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_lt.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_lt.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="lt"> +<translation id="1068672505746868501">Niekada neversti puslapių, parašytų <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Daugiau kalbų</translation> +<translation id="1285320974508926690">Niekada neversti šios svetainės</translation> +<translation id="290376772003165898">Puslapis parašytas ne <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Oi, nepavyko išversti šio puslapio.</translation> +<translation id="6040143037577758943">Uždaryti</translation> +<translation id="6831043979455480757">Vertėjas</translation> +<translation id="7243308994586599757">Parinktys pasiekiamos netoli ekrano apačios</translation> +<translation id="773466115871691567">Visada versti puslapius, parašytus <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Žiniatinklio naršyklės veikla</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_lv.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_lv.xtb index 5d965478866..d53de48768c 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_lv.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_lv.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="lv"> +<translation id="1068672505746868501">Nekad netulkot lapas šādā valodā: <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Citas valodas…</translation> +<translation id="1285320974508926690">Nekad netulkot šo vietni</translation> +<translation id="290376772003165898">Vai lapa nav šajā valodā: <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Hmm... Šo lapu nevarēja iztulkot.</translation> +<translation id="6040143037577758943">Aizvērt</translation> +<translation id="6831043979455480757">Tulkot</translation> +<translation id="7243308994586599757">Opcijas, kas pieejamas ekrāna apakšējā daļā</translation> +<translation id="773466115871691567">Vienmēr tulkot lapas šādā valodā: <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Pārlūkošanas darbības</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_mk.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_mk.xtb index 2f2d3c2b91b..82359fc6539 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_mk.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_mk.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="mk"> +<translation id="1068672505746868501">Никогаш не преведувај страници на <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Повеќе јазици</translation> +<translation id="1285320974508926690">Никогаш не преведувај ја оваа локација</translation> +<translation id="290376772003165898">Страницата не е на <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Оваа страница не може да се преведе.</translation> +<translation id="6040143037577758943">Затвори</translation> +<translation id="6831043979455480757">Преведи</translation> +<translation id="7243308994586599757">Достапни се опции на дното на екранот</translation> +<translation id="773466115871691567">Секогаш преведувај ги страниците на <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Активност на прелистувачот</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_ml.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_ml.xtb index d4a01802579..8d51f41f713 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_ml.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_ml.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="ml"> +<translation id="1068672505746868501"><ph name="SOURCE_LANGUAGE" /> ഭാഷയിലുള്ള പേജുകൾ ഒരിക്കലും വിവർത്തനം ചെയ്യരുത്</translation> +<translation id="124116460088058876">കൂടുതൽ ഭാഷകൾ</translation> +<translation id="1285320974508926690">ഈ സൈറ്റ് ഒരിക്കലും വിവര്ത്തനം ചെയ്യരുത്</translation> +<translation id="290376772003165898">പേജ് <ph name="LANGUAGE" /> ഭാഷയിലല്ലേ?</translation> +<translation id="5684874026226664614">ക്ഷമിക്കണം. ഈ പേജ് വിവർത്തനം ചെയ്യാനായില്ല.</translation> +<translation id="6040143037577758943">അടയ്ക്കുക</translation> +<translation id="6831043979455480757">വിവർത്തനം ചെയ്യുക</translation> +<translation id="7243308994586599757">സ്ക്രീനിന്റെ ചുവടെ ഓപ്ഷനുകൾ ലഭ്യമാണ്</translation> +<translation id="773466115871691567"><ph name="SOURCE_LANGUAGE" /> ഭാഷയിലുള്ള പേജുകൾ എപ്പോഴും വിവർത്തനം ചെയ്യുക</translation> <translation id="8298278839890148234">വെബ് ബ്രൗസർ ആക്റ്റിവിറ്റി</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_mn.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_mn.xtb index 79d5dac7607..2f721cf0b56 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_mn.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_mn.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="mn"> +<translation id="1068672505746868501"><ph name="SOURCE_LANGUAGE" /> хэл дээрх хуудсыг хэзээ ч орчуулахгүй</translation> +<translation id="124116460088058876">Бусад хэл</translation> +<translation id="1285320974508926690">Энэ сайтыг хэзээ ч бүү хөрвүүл</translation> +<translation id="290376772003165898">Хуудас <ph name="LANGUAGE" /> хэл дээр биш байна уу?</translation> +<translation id="5684874026226664614">Өө. Энэ хуудсыг хөрвүүлж чадсангүй.</translation> +<translation id="6040143037577758943">Хаах</translation> +<translation id="6831043979455480757">Хөрвүүлэх</translation> +<translation id="7243308994586599757">Дэлгэцийн доод хэсэгт сонголт боломжтой</translation> +<translation id="773466115871691567"><ph name="SOURCE_LANGUAGE" /> хэл дээрх хуудсыг байнга орчуулна</translation> <translation id="8298278839890148234">Хөтчийн үйл ажиллагаа</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_mr.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_mr.xtb index 5a3e6820752..66b36082e3d 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_mr.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_mr.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="mr"> +<translation id="1068672505746868501">पेज <ph name="SOURCE_LANGUAGE" /> मध्ये कधीही भाषांतरित करू नका</translation> +<translation id="124116460088058876">आणखी भाषा...</translation> +<translation id="1285320974508926690">या साइटचा कधीही भाषांतर करु नका</translation> +<translation id="290376772003165898">पेज <ph name="LANGUAGE" />मध्ये नाही?</translation> +<translation id="5684874026226664614">अरेरे. हे पृष्ठ भाषांतरित केले जाऊ शकले नाही.</translation> +<translation id="6040143037577758943">बंद करा</translation> +<translation id="6831043979455480757">भाषांतर करा</translation> +<translation id="7243308994586599757">स्क्रीनच्या तळाशी पर्याय उपलब्ध आहेत</translation> +<translation id="773466115871691567">नेहमी पेज <ph name="SOURCE_LANGUAGE" />मध्ये भाषांतरित करा</translation> <translation id="8298278839890148234">वेब ब्राउझर अॅक्टिव्हिटी</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_ms.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_ms.xtb index e105952bce6..9bca4a8918a 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_ms.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_ms.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="ms"> +<translation id="1068672505746868501">Jangan sekali-kali terjemahkan halaman dalam <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Lagi bahasa</translation> +<translation id="1285320974508926690">Jangan sekali-kali menterjemahkan tapak ini</translation> +<translation id="290376772003165898">Halaman bukan dalam <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Op. Halaman ini tidak dapat diterjemahkan.</translation> +<translation id="6040143037577758943">Tutup</translation> +<translation id="6831043979455480757">Terjemah</translation> +<translation id="7243308994586599757">Pilihan tersedia berhampiran bahagian bawah skrin</translation> +<translation id="773466115871691567">Sentiasa terjemahkan halaman dalam <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Aktiviti penyemakan imbas</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_my.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_my.xtb index e8bf710e16c..ed292bfc27e 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_my.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_my.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="my"> +<translation id="1068672505746868501"><ph name="SOURCE_LANGUAGE" /> ဘာသာ စာမျက်နှာများကို ဘယ်သောအခါမျှ ဘာသာမပြန်ရန်</translation> +<translation id="124116460088058876">နောက်ထပ် ဘာသာစကားများ</translation> +<translation id="1285320974508926690">ဒီဆိုက်ကို ဘယ်တော့မှ ဘာသာမပြန်ပါနှင့်</translation> +<translation id="290376772003165898">စာမျက်နှာသည် <ph name="LANGUAGE" /> ဖြင့် ဟုတ်ပါသလား။</translation> +<translation id="5684874026226664614">အူးပ်စ်။ ဒီစာမျက်နှာကို ဘာသာပြန် မရနိုင်ခဲ့ပါ။</translation> +<translation id="6040143037577758943">ပိတ်ရန်</translation> +<translation id="6831043979455480757">ဘာသာပြန်ရန်</translation> +<translation id="7243308994586599757">ရွေးစရာများမှာ မျက်နှာပြင်၏ အောက်ခြေပိုင်းနားမှာ ရှိကြသည်</translation> +<translation id="773466115871691567"><ph name="SOURCE_LANGUAGE" /> ဘာသာဖြင့် စာမျက်နှာများအားလုံးကို အမြဲဘာသာပြန်ရန်</translation> <translation id="8298278839890148234">ဝဘ်ဘရောင်ဇာ လုပ်ဆောင်ချက်</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_ne.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_ne.xtb index 06ec47e44f4..11e137141d1 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_ne.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_ne.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="ne"> +<translation id="1068672505746868501"><ph name="SOURCE_LANGUAGE" /> मा भएका पृष्ठहरूलाई कहिल्यै अनुवाद नगर्नुहोस्</translation> +<translation id="124116460088058876">थप भाषाहरू</translation> +<translation id="1285320974508926690">यो साइट कहिले पनि अनुवाद नगर्नुहोस्</translation> +<translation id="290376772003165898"><ph name="LANGUAGE" /> भाषामा पृष्ठ छैन?</translation> +<translation id="5684874026226664614">ओहो। यो पृष्ठ अनुवादन गर्न सकिएन।</translation> +<translation id="6040143037577758943">बन्द गर्नुहोस्</translation> +<translation id="6831043979455480757">अनुवाद गर्नुहोस्</translation> +<translation id="7243308994586599757">विकल्पहरू स्क्रिनको तल नजिकै उपलब्ध छ</translation> +<translation id="773466115871691567"><ph name="SOURCE_LANGUAGE" /> मा रहेका पृष्ठहरूलाई सधैँ अनुवाद गर्नुहोस्</translation> <translation id="8298278839890148234">ब्राउजर प्रयोग गरी गरिएको क्रियाकलाप</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_nl.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_nl.xtb index 759b04e50cc..cd20f14e5ca 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_nl.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_nl.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="nl"> +<translation id="1068672505746868501">Pagina's in het <ph name="SOURCE_LANGUAGE" /> nooit vertalen</translation> +<translation id="124116460088058876">Meer talen</translation> +<translation id="1285320974508926690">Deze site nooit vertalen</translation> +<translation id="290376772003165898">Is deze pagina niet in het <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Deze pagina kan niet worden vertaald.</translation> +<translation id="6040143037577758943">Sluiten</translation> +<translation id="6831043979455480757">Vertalen</translation> +<translation id="7243308994586599757">Opties beschikbaar onder aan het scherm</translation> +<translation id="773466115871691567">Pagina's in het <ph name="SOURCE_LANGUAGE" /> altijd vertalen</translation> <translation id="8298278839890148234">Webbrowseractivititeit</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_no.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_no.xtb index 409a800ee70..0a02095d40b 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_no.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_no.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="no"> +<translation id="1068672505746868501">Oversett aldri sider på <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Flere språk</translation> +<translation id="1285320974508926690">Oversett aldri dette nettstedet</translation> +<translation id="290376772003165898">Er ikke siden på <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Beklager. Denne siden kunne ikke oversettes.</translation> +<translation id="6040143037577758943">Lukk</translation> +<translation id="6831043979455480757">Oversett</translation> +<translation id="7243308994586599757">Du finner alternativer ved bunnen av skjermen</translation> +<translation id="773466115871691567">Oversett alltid sider på <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Nettleseraktivitet</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_or.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_or.xtb index f88f14b33c3..8d046c770c1 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_or.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_or.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="or"> +<translation id="1068672505746868501"><ph name="SOURCE_LANGUAGE" />ରେ ପୃଷ୍ଠାଗୁଡ଼ିକୁ ଅନୁବାଦ କରନ୍ତୁ ନାହିଁ</translation> +<translation id="124116460088058876">ଅନେକ ଭାଷା</translation> +<translation id="1285320974508926690">ଏହି ସାଇଟ୍କୁ କଦାପି ଅନୁବାଦ କରନ୍ତୁ ନାହିଁ</translation> +<translation id="290376772003165898">ପୃଷ୍ଠାଟି <ph name="LANGUAGE" />ରେ ନାହିଁ?</translation> +<translation id="5684874026226664614">ଓହୋଃ! ଏହି ପୃଷ୍ଠା ଅନୁବାଦ କରାଯାଇପାରିଲା ନାହିଁ।</translation> +<translation id="6040143037577758943">ବନ୍ଦ</translation> +<translation id="6831043979455480757">ଅନୁବାଦ କରନ୍ତୁ</translation> +<translation id="7243308994586599757">ସ୍କ୍ରିନ୍ର ନିମ୍ନରେ ବିକଳ୍ପଗୁଡ଼ିକ ଉପଲବ୍ଧ ଅଛି</translation> +<translation id="773466115871691567">ସର୍ବଦା <ph name="SOURCE_LANGUAGE" />ରେ ପୃଷ୍ଠାଗୁଡ଼ିକୁ ଅନୁବାଦ କରନ୍ତୁ</translation> <translation id="8298278839890148234">ୱେବ୍ ବ୍ରାଉଜର୍ କାର୍ଯ୍ୟକଳାପ</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_pa.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_pa.xtb index 0d4c8c78fba..6394a67f909 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_pa.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_pa.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="pa"> +<translation id="1068672505746868501">ਕਦੇ ਵੀ ਪੰਨਿਆਂ ਦਾ ਅਨੁਵਾਦ <ph name="SOURCE_LANGUAGE" /> ਵਿੱਚ ਨਾ ਕਰੋ</translation> +<translation id="124116460088058876">ਹੋਰ ਭਾਸ਼ਾਵਾਂ</translation> +<translation id="1285320974508926690">ਕਦੇ ਵੀ ਇਸ ਸਾਈਟ ਦਾ ਅਨੁਵਾਦ ਨਾ ਕਰੋ</translation> +<translation id="290376772003165898">ਕੀ ਪੰਨਾ <ph name="LANGUAGE" /> ਵਿੱਚ ਨਹੀਂ ਹੈ?</translation> +<translation id="5684874026226664614">ਓਹੋ। ਇਸ ਪੰਨੇ ਦਾ ਅਨੁਵਾਦ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ।</translation> +<translation id="6040143037577758943">ਬੰਦ ਕਰੋ</translation> +<translation id="6831043979455480757">ਅਨੁਵਾਦ ਕਰੋ</translation> +<translation id="7243308994586599757">ਵਿਕਲਪ ਸਕ੍ਰੀਨ ਦੇ ਹੇਠਲੇ ਪਾਸੇ ਦੇ ਕੋਲ ਉਪਲਬਧ ਹਨ</translation> +<translation id="773466115871691567">ਪੰਨਿਆਂ ਦਾ ਅਨੁਵਾਦ ਹਮੇਸ਼ਾਂ <ph name="SOURCE_LANGUAGE" /> ਭਾਸ਼ਾ ਵਿੱਚ ਕਰੋ</translation> <translation id="8298278839890148234">ਵੈੱਬ ਬ੍ਰਾਊਜ਼ਰ ਸਰਗਰਮੀ</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_pl.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_pl.xtb index 52a394f3e02..2185aeb5d3d 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_pl.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_pl.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="pl"> +<translation id="1068672505746868501">Nigdy nie tłumacz stron, których językiem jest <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Więcej języków</translation> +<translation id="1285320974508926690">Nigdy nie tłumacz tej witryny</translation> +<translation id="290376772003165898">Język tej strony to nie <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Nie można przetłumaczyć tej strony.</translation> +<translation id="6040143037577758943">Zamknij</translation> +<translation id="6831043979455480757">Tłumacz</translation> +<translation id="7243308994586599757">Opcje dostępne u dołu ekranu</translation> +<translation id="773466115871691567">Zawsze tłumacz strony, których językiem jest <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Aktywność w przeglądarce</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_pt-BR.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_pt-BR.xtb index dd6972f79e3..45880dbbefa 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_pt-BR.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_pt-BR.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="pt-BR"> +<translation id="1068672505746868501">Nunca traduzir páginas em <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Mais idiomas</translation> +<translation id="1285320974508926690">Nunca traduzir este site</translation> +<translation id="290376772003165898">A página não está em <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Não foi possível traduzir esta página.</translation> +<translation id="6040143037577758943">Fechar</translation> +<translation id="6831043979455480757">Traduzir</translation> +<translation id="7243308994586599757">Opções disponíveis perto da parte inferior da tela</translation> +<translation id="773466115871691567">Sempre traduzir páginas em <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Atividade do navegador da Web</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_pt-PT.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_pt-PT.xtb index 76966566f3d..0b06168100d 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_pt-PT.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_pt-PT.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="pt-PT"> +<translation id="1068672505746868501">Nunca traduzir páginas em <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Mais idiomas</translation> +<translation id="1285320974508926690">Nunca traduzir este site</translation> +<translation id="290376772003165898">A página não está em <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Ups! Não foi possível traduzir esta página.</translation> +<translation id="6040143037577758943">Fechar</translation> +<translation id="6831043979455480757">Traduzir</translation> +<translation id="7243308994586599757">Opções disponíveis junto à parte inferior do ecrã</translation> +<translation id="773466115871691567">Traduza sempre páginas em <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Atividade do navegador de Internet</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_ro.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_ro.xtb index 12f45d28945..87a6bce7c07 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_ro.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_ro.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="ro"> +<translation id="1068672505746868501">Nu traduce niciodată paginile în <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Mai multe limbi</translation> +<translation id="1285320974508926690">Nu traduce niciodată acest site</translation> +<translation id="290376772003165898">Pagina nu este în <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Hopa. Această pagină nu a putut fi tradusă.</translation> +<translation id="6040143037577758943">Închide</translation> +<translation id="6831043979455480757">Tradu</translation> +<translation id="7243308994586599757">Opțiuni disponibile în partea de jos a ecranului</translation> +<translation id="773466115871691567">Tradu întotdeauna paginile în <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Activitatea în browserul web</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_ru.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_ru.xtb index 401647df65a..733f199d730 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_ru.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_ru.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="ru"> +<translation id="1068672505746868501">Не переводить страницы на этом языке: <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Другие языки</translation> +<translation id="1285320974508926690">Никогда не переводить этот сайт</translation> +<translation id="290376772003165898">Язык страницы не <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Не удалось перевести страницу</translation> +<translation id="6040143037577758943">Закрыть</translation> +<translation id="6831043979455480757">Перевести</translation> +<translation id="7243308994586599757">Доступные параметры указаны в нижней части экрана</translation> +<translation id="773466115871691567">Переводить страницы на этом языке: <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Действия в веб-браузере</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_si.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_si.xtb index 1e95ef55536..90b6f032c71 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_si.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_si.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="si"> +<translation id="1068672505746868501"><ph name="SOURCE_LANGUAGE" /> හි පිටු කිසිදාක පරිවර්තනය කරන්න එපා</translation> +<translation id="124116460088058876">තවත් භාෂා</translation> +<translation id="1285320974508926690">මෙම අඩවිය කිසිවිට පරිවර්තනය නොකරන්න</translation> +<translation id="290376772003165898">පිටුව <ph name="LANGUAGE" /> බසින් නොවේ ද?</translation> +<translation id="5684874026226664614">අපොයි. මෙම පිටුව පැටවිය නොහැකි විය.</translation> +<translation id="6040143037577758943">වසන්න</translation> +<translation id="6831043979455480757">පරිවර්තනය කරන්න</translation> +<translation id="7243308994586599757">තිරයේ පහළට ආසන්නව විකල්ප ලබා ගත හැකිය</translation> +<translation id="773466115871691567"><ph name="SOURCE_LANGUAGE" /> හි පිටු සැමවිටම පරිවර්තනය කරන්න</translation> <translation id="8298278839890148234">වෙබ් බ්රවුසර ක්රියාකාරකම</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_sk.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_sk.xtb index 53b2d4ec263..fe4bb0c1612 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_sk.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_sk.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="sk"> +<translation id="1068672505746868501">Nikdy neprekladať stránky v jazyku <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Ďalšie jazyky</translation> +<translation id="1285320974508926690">Nikdy neprekladať tieto webové stránky</translation> +<translation id="290376772003165898">Stránka nie je v jazyku <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Hops. Túto stránku nebolo možné preložiť.</translation> +<translation id="6040143037577758943">Zavrieť</translation> +<translation id="6831043979455480757">Preložiť</translation> +<translation id="7243308994586599757">Možnosti sú k dispozícii v dolnej časti obrazovky</translation> +<translation id="773466115871691567">Vždy prekladať stránky v jazyku <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Aktivita webového prehliadača</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_sl.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_sl.xtb index 152968ea560..bc91c54ae43 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_sl.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_sl.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="sl"> +<translation id="1068672505746868501">Nikoli ne prevedi strani v jeziku <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Več jezikov</translation> +<translation id="1285320974508926690">Nikoli ne prevedi tega spletnega mesta</translation> +<translation id="290376772003165898">Stran ni v jeziku <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Ojoj, te strani bi bilo mogoče prevesti.</translation> +<translation id="6040143037577758943">Zapri</translation> +<translation id="6831043979455480757">Prevedi</translation> +<translation id="7243308994586599757">Možnosti so na voljo pri dnu zaslona</translation> +<translation id="773466115871691567">Vedno prevedi strani v jeziku <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Dejavnost brskalnika</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_sq.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_sq.xtb index 2e0a6950797..c88ab6c0cfc 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_sq.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_sq.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="sq"> +<translation id="1068672505746868501">Mos përkthe asnjëherë faqet që janë në <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Gjuhë të tjera</translation> +<translation id="1285320974508926690">Asnjëherë mos e përkthe këtë sajt</translation> +<translation id="290376772003165898">Faqja nuk është në <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Mos! Kjo faqe nuk mund të përkthehej.</translation> +<translation id="6040143037577758943">Mbyll</translation> +<translation id="6831043979455480757">Përkthe</translation> +<translation id="7243308994586599757">Opsionet janë të disponueshme pranë fundit të ekranit</translation> +<translation id="773466115871691567">Përkthe gjithmonë faqet që janë në <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Aktiviteti i shfletuesit të uebit</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_sr-Latn.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_sr-Latn.xtb index 24f260082a7..d66789dacf8 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_sr-Latn.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_sr-Latn.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="sr-Latn"> +<translation id="1068672505746868501">Nikad ne prevodi stranice na jeziku <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Još jezika</translation> +<translation id="1285320974508926690">Nikad ne prevodi ovaj sajt</translation> +<translation id="290376772003165898">Ova stranica nije na jeziku <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Ups, prevođenje ove stranice nije uspelo.</translation> +<translation id="6040143037577758943">Zatvori</translation> +<translation id="6831043979455480757">Prevedi</translation> +<translation id="7243308994586599757">Opcije su dostupne u dnu ekrana</translation> +<translation id="773466115871691567">Uvek predvodi stranice na jeziku <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Aktivnosti u veb-pregledaču</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_sr.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_sr.xtb index 9008032cbef..d5eb9aa0fef 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_sr.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_sr.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="sr"> +<translation id="1068672505746868501">Никад не преводи странице на језику <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Још језика</translation> +<translation id="1285320974508926690">Никад не преводи овај сајт</translation> +<translation id="290376772003165898">Ова страница није на језику <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Упс, превођење ове странице није успело.</translation> +<translation id="6040143037577758943">Затвори</translation> +<translation id="6831043979455480757">Преведи</translation> +<translation id="7243308994586599757">Опције су доступне у дну екрана</translation> +<translation id="773466115871691567">Увек предводи странице на језику <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Активности у веб-прегледачу</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_sv.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_sv.xtb index 610cb47e53f..532dc6eb359 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_sv.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_sv.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="sv"> +<translation id="1068672505746868501">Översätt aldrig sidor på <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Fler språk</translation> +<translation id="1285320974508926690">Översätt aldrig den här webbplatsen</translation> +<translation id="290376772003165898">Är sidan inte på <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Det gick inte att översätta sidan.</translation> +<translation id="6040143037577758943">Stäng</translation> +<translation id="6831043979455480757">Översätt</translation> +<translation id="7243308994586599757">Alternativ visas nära skärmens nedre kant</translation> +<translation id="773466115871691567">Översätt alltid sidor på <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Webbläsaraktivitet</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_sw.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_sw.xtb index cf0a4b711da..2854041492f 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_sw.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_sw.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="sw"> +<translation id="1068672505746868501">Usiwahi kutafsiri kurasa katika lugha ya <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Lugha zaidi</translation> +<translation id="1285320974508926690">Kamwe usitafsiri tovuti hii</translation> +<translation id="290376772003165898">Je, ukurasa huu haupo katika <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Lo! Ukurasa huu haukuweza kutafsiriwa.</translation> +<translation id="6040143037577758943">Funga</translation> +<translation id="6831043979455480757">Tafsiri</translation> +<translation id="7243308994586599757">Chaguo zinapatikana karibu na sehemu ya chini ya skrini</translation> +<translation id="773466115871691567">Zitafsiri kurasa katika <ph name="SOURCE_LANGUAGE" /> wakati wote</translation> <translation id="8298278839890148234">Shughuli za kivinjari</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_ta.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_ta.xtb index 8a47545a26a..c7f7130e4c5 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_ta.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_ta.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="ta"> +<translation id="1068672505746868501"><ph name="SOURCE_LANGUAGE" /> மொழியில் உள்ள பக்கங்களை ஒருபோதும் மொழிபெயர்க்காதே</translation> +<translation id="124116460088058876">மேலும் மொழிகள்</translation> +<translation id="1285320974508926690">இந்த தளத்தை எப்போதும் மொழிபெயர்க்க வேண்டாம்</translation> +<translation id="290376772003165898"><ph name="LANGUAGE" /> மொழியில் பக்கம் இல்லையா?</translation> +<translation id="5684874026226664614">அச்சச்சோ. இந்தப் பக்கத்தை மொழிபெயர்க்க முடியாது.</translation> +<translation id="6040143037577758943">மூடு</translation> +<translation id="6831043979455480757">மொழிபெயர்</translation> +<translation id="7243308994586599757">திரையின் கீழ்ப்பகுதிக்கு அருகில் கிடைக்கும் விருப்பங்கள்</translation> +<translation id="773466115871691567"><ph name="SOURCE_LANGUAGE" /> மொழியில் உள்ள பக்கங்களை எப்போதும் மொழிபெயர்</translation> <translation id="8298278839890148234">இணைய உலாவியின் செயல்பாடு</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_te.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_te.xtb index 3894c532d87..53279940602 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_te.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_te.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="te"> +<translation id="1068672505746868501"><ph name="SOURCE_LANGUAGE" />లో ఉన్న పేజీలను ఎప్పుడూ అనువదించవద్దు</translation> +<translation id="124116460088058876">మరిన్ని భాషలు</translation> +<translation id="1285320974508926690">ఈ సైట్ను ఎప్పటికీ అనువదించవద్దు</translation> +<translation id="290376772003165898">పేజీ <ph name="LANGUAGE" />లో లేదా?</translation> +<translation id="5684874026226664614">అయ్యో. ఈ పేజీని అనువదించడం సాధ్యపడలేదు.</translation> +<translation id="6040143037577758943">మూసివేయి</translation> +<translation id="6831043979455480757">అనువదించు</translation> +<translation id="7243308994586599757">స్క్రీన్ దిగువభాగం సమీపంలో ఎంపికలు అందుబాటులో ఉంటాయి</translation> +<translation id="773466115871691567"><ph name="SOURCE_LANGUAGE" />లో ఉన్న పేజీలను ఎల్లప్పుడూ అనువదించు</translation> <translation id="8298278839890148234">బ్రౌజింగ్ యాక్టివిటీ</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_th.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_th.xtb index 1c8e14c9ca0..024e4b547a9 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_th.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_th.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="th"> +<translation id="1068672505746868501">ไม่ต้องแปลหน้าเว็บภาษา<ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">ภาษาเพิ่มเติม</translation> +<translation id="1285320974508926690">ไม่ต้องแปลเว็บไซต์นี้</translation> +<translation id="290376772003165898">หน้านี้ไม่ใช่ภาษา<ph name="LANGUAGE" />ใช่ไหม</translation> +<translation id="5684874026226664614">อ๊ะ หน้านี้ไม่สามารถแปลได้</translation> +<translation id="6040143037577758943">ปิด</translation> +<translation id="6831043979455480757">แปลภาษา</translation> +<translation id="7243308994586599757">มีตัวเลือกอยู่ทางด้านล่างของหน้าจอ</translation> +<translation id="773466115871691567">แปลหน้าเว็บภาษา<ph name="SOURCE_LANGUAGE" />ทุกครั้ง</translation> <translation id="8298278839890148234">กิจกรรมของเว็บเบราว์เซอร์</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_tr.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_tr.xtb index 11d4ab4aeef..2f9a035ea1b 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_tr.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_tr.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="tr"> +<translation id="1068672505746868501"><ph name="SOURCE_LANGUAGE" /> dilindeki sayfaları asla çevirme</translation> +<translation id="124116460088058876">Diğer diller</translation> +<translation id="1285320974508926690">Bu siteyi hiçbir zaman çevirme</translation> +<translation id="290376772003165898">Sayfa <ph name="LANGUAGE" /> dilinde değil mi?</translation> +<translation id="5684874026226664614">Hata! Bu sayfa çevrilemedi.</translation> +<translation id="6040143037577758943">Kapat</translation> +<translation id="6831043979455480757">Çevir</translation> +<translation id="7243308994586599757">Sayfanın altına yakın bir yerde kullanılabilen seçenekler</translation> +<translation id="773466115871691567"><ph name="SOURCE_LANGUAGE" /> dilindeki sayfaları her zaman çevir</translation> <translation id="8298278839890148234">Web tarayıcısı etkinliği</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_uk.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_uk.xtb index f8630458317..4a29de8edb5 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_uk.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_uk.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="uk"> +<translation id="1068672505746868501">Ніколи не перекладати сторінки такою мовою: <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Інші мови</translation> +<translation id="1285320974508926690">Ніколи не перекладати цей сайт</translation> +<translation id="290376772003165898">Ця сторінка відображається не такою мовою: <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">На жаль, цю сторінку неможливо перекласти.</translation> +<translation id="6040143037577758943">Закрити</translation> +<translation id="6831043979455480757">Перекласти</translation> +<translation id="7243308994586599757">Опції можна знайти внизу екрана</translation> +<translation id="773466115871691567">Завжди перекладати сторінки такою мовою: <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Дії у веб-переглядачі</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_ur.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_ur.xtb index 57e46884972..4113ad7cdaa 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_ur.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_ur.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="ur"> +<translation id="1068672505746868501"><ph name="SOURCE_LANGUAGE" /> کے صفحات کا کبھی ترجمہ نہ کریں</translation> +<translation id="124116460088058876">مزید زبانیں</translation> +<translation id="1285320974508926690">اس سائٹ کا ترجمہ کبھی نہ کریں</translation> +<translation id="290376772003165898">صفحہ <ph name="LANGUAGE" /> میں نہیں ہے؟</translation> +<translation id="5684874026226664614">افوہ۔ اس صفحہ کا ترجمہ نہیں کیا جا سکا۔</translation> +<translation id="6040143037577758943">بند کریں</translation> +<translation id="6831043979455480757">ترجمہ کریں</translation> +<translation id="7243308994586599757">اسکرین کے نچلے حصہ کے قریب اختیارات دستیاب ہیں</translation> +<translation id="773466115871691567">ہمیشہ <ph name="SOURCE_LANGUAGE" /> کے صفحات کا ترجمہ کریں</translation> <translation id="8298278839890148234">ویب براؤزر کی سرگرمی</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_uz.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_uz.xtb index 92655c71edf..f0af58d90cd 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_uz.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_uz.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="uz"> +<translation id="1068672505746868501"><ph name="SOURCE_LANGUAGE" /> tilidagi sahifalar hech qachon tarjima qilinmasin</translation> +<translation id="124116460088058876">Boshqa tillar</translation> +<translation id="1285320974508926690">Bu sayt hech qachon tarjima qilinmasin</translation> +<translation id="290376772003165898">Sahifa <ph name="LANGUAGE" /> tilida emasmi?</translation> +<translation id="5684874026226664614">Bu sahifani tarjima qilib bo‘lmadi.</translation> +<translation id="6040143037577758943">Yopish</translation> +<translation id="6831043979455480757">Tarjima</translation> +<translation id="7243308994586599757">Parametrlar ekranning quyi qismiga yaqinroq joyda</translation> +<translation id="773466115871691567"><ph name="SOURCE_LANGUAGE" /> tilidagi sahifalar doim tarjima qilinsin</translation> <translation id="8298278839890148234">Veb-brauzerdagi faoliyat</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_vi.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_vi.xtb index 2f57bd60947..2744201fd71 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_vi.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_vi.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="vi"> +<translation id="1068672505746868501">Không bao giờ dịch các trang viết bằng <ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Ngôn ngữ khác</translation> +<translation id="1285320974508926690">Không bao giờ dịch trang web này</translation> +<translation id="290376772003165898">Trang này không được viết bằng <ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Rất tiếc. Không thể dịch trang này.</translation> +<translation id="6040143037577758943">Đóng</translation> +<translation id="6831043979455480757">Dịch</translation> +<translation id="7243308994586599757">Có các tùy chọn ở gần cuối màn hình</translation> +<translation id="773466115871691567">Luôn dịch các trang viết bằng <ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Hoạt động duyệt web</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_zh-CN.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_zh-CN.xtb index 84075ec9807..0c3183bb774 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_zh-CN.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_zh-CN.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="zh-CN"> +<translation id="1068672505746868501">一律不翻译<ph name="SOURCE_LANGUAGE" />网页</translation> +<translation id="124116460088058876">更多语言</translation> +<translation id="1285320974508926690">一律不翻译此网站</translation> +<translation id="290376772003165898">网页的源语言不是<ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">糟糕,此网页内容无法翻译。</translation> +<translation id="6040143037577758943">关闭</translation> +<translation id="6831043979455480757">翻译</translation> +<translation id="7243308994586599757">选项在靠近屏幕底部的位置</translation> +<translation id="773466115871691567">一律翻译<ph name="SOURCE_LANGUAGE" />网页</translation> <translation id="8298278839890148234">网络浏览器活动</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_zh-HK.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_zh-HK.xtb index abd42fba040..3b6aab868dd 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_zh-HK.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_zh-HK.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="zh-HK"> +<translation id="1068672505746868501">永不翻譯來源語言為<ph name="SOURCE_LANGUAGE" />的網頁</translation> +<translation id="124116460088058876">更多語言</translation> +<translation id="1285320974508926690">永不翻譯此網站</translation> +<translation id="290376772003165898">網頁的來源語言不是<ph name="LANGUAGE" />嗎?</translation> +<translation id="5684874026226664614">糟糕!系統無法翻譯這個網頁的內容。</translation> +<translation id="6040143037577758943">關閉</translation> +<translation id="6831043979455480757">翻譯</translation> +<translation id="7243308994586599757">您可在畫面底部附近找到選項</translation> +<translation id="773466115871691567">一律翻譯來源語言為<ph name="SOURCE_LANGUAGE" />的網頁</translation> <translation id="8298278839890148234">網絡瀏覽器活動</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_zh-TW.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_zh-TW.xtb index ecea64ea7f4..d338d98229a 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_zh-TW.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_zh-TW.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="zh-TW"> +<translation id="1068672505746868501">一律不翻譯<ph name="SOURCE_LANGUAGE" />網頁</translation> +<translation id="124116460088058876">更多語言</translation> +<translation id="1285320974508926690">一律不翻譯此網站</translation> +<translation id="290376772003165898">不是<ph name="LANGUAGE" />網頁嗎?</translation> +<translation id="5684874026226664614">糟糕!系統無法翻譯這個網頁的內容。</translation> +<translation id="6040143037577758943">關閉</translation> +<translation id="6831043979455480757">翻譯</translation> +<translation id="7243308994586599757">選項在接近畫面底部的位置</translation> +<translation id="773466115871691567">一律翻譯<ph name="SOURCE_LANGUAGE" />網頁</translation> <translation id="8298278839890148234">網路瀏覽器活動</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/translations/weblayer_strings_zu.xtb b/chromium/weblayer/browser/java/translations/weblayer_strings_zu.xtb index 1da5d91a768..9e01afbe908 100644 --- a/chromium/weblayer/browser/java/translations/weblayer_strings_zu.xtb +++ b/chromium/weblayer/browser/java/translations/weblayer_strings_zu.xtb @@ -1,5 +1,14 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="zu"> +<translation id="1068672505746868501">Ungahumushi amakhasi ngesi-<ph name="SOURCE_LANGUAGE" /></translation> +<translation id="124116460088058876">Izilimi eziningi</translation> +<translation id="1285320974508926690">Ungalokothi uhumushe leli sayithi</translation> +<translation id="290376772003165898">Ikhasi alikho ngesi-<ph name="LANGUAGE" />?</translation> +<translation id="5684874026226664614">Eshu. Leli khasi alikwazi ukuhunyushwa.</translation> +<translation id="6040143037577758943">Vala</translation> +<translation id="6831043979455480757">Humusha</translation> +<translation id="7243308994586599757">Izinketho ziyatholakala eduze kwangaphansi kwesikrini</translation> +<translation id="773466115871691567">Njalo humusha amakhasi ngesi-<ph name="SOURCE_LANGUAGE" /></translation> <translation id="8298278839890148234">Umsebenzi wesiphequluli sewebhu</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/weblayer/browser/java/weblayer_strings.grd b/chromium/weblayer/browser/java/weblayer_strings.grd index cd1e72cc645..79a4dcddd27 100644 --- a/chromium/weblayer/browser/java/weblayer_strings.grd +++ b/chromium/weblayer/browser/java/weblayer_strings.grd @@ -172,6 +172,34 @@ <message name="IDS_WEBLAYER_NOTIFICATION_CHANNEL_GROUP_NAME" desc="The user-facing label for the notification channel group used for notifications arising from web browsing activity."> Web browser activity </message> + <message name="IDS_WEBLAYER_BOTTOM_BAR_SCREEN_POSITION" desc="Accessibility label to inform users about the InfoBar location"> + Options available near bottom of the screen + </message> + <message name="IDS_WEBLAYER_INFOBAR_CLOSE" desc="Accessibility label for the dismiss infobar Button"> + Close + </message> + <!-- TranslateInfoBar --> + <message name="IDS_TRANSLATE_INFOBAR_ERROR"> + Oops. This page could not be translated. + </message> + <message name="IDS_TRANSLATE_BUTTON" desc="Possible texts to display on the translate infobar buttons. [CHAR-LIMIT=24]"> + Translate + </message> + <message name="IDS_TRANSLATE_NEVER_TRANSLATE_SITE" desc="Text to display on the never translate site (like www.google.com) button. [CHAR-LIMIT=64]"> + Never translate this site + </message> + <message name="IDS_TRANSLATE_OPTION_ALWAYS_TRANSLATE" desc="Option in the Chrome menu. User can click the 'Always Translate' option to indicate that they want Chrome to translate pages in this language automatically. Imperative."> + Always translate pages in <ph name="SOURCE_LANGUAGE">%1$s<ex>French</ex></ph> + </message> + <message name="IDS_TRANSLATE_OPTION_NEVER_TRANSLATE" desc="Option in the Chrome menu. User can click the 'Never Translate' option to indicate that they never want Chrome to translate pages in this language. The variable SOURCE_LANGUAGE could be any of 50+ languages supported by Google Translate, like French, Spanish, German, Italian, Japanese, Korean, etc. Imperative."> + Never translate pages in <ph name="SOURCE_LANGUAGE">%1$s<ex>French</ex></ph> + </message> + <message name="IDS_TRANSLATE_OPTION_MORE_LANGUAGE" desc="Option in the Chrome menu. Lets the user open a dialog to choose other target languages for translation, from a list of available languages. [CHAR-LIMIT=64]"> + More languages + </message> + <message name="IDS_TRANSLATE_OPTION_NOT_SOURCE_LANGUAGE" desc="Option in the Chrome menu. Sometimes a web page's source language is not correctly identified by Google Translate, and this menu option lets the user open a submenu to select another language as the source language to translate. Phrased as a question as if to query the user, 'Is this page not in [source language identified]? If so, click here.' [CHAR-LIMIT=64]"> + Page is not in <ph name="LANGUAGE">%1$s<ex>French</ex></ph>? + </message> </messages> </release> </grit> |