diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:34:42 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:34:42 +0000 |
commit | da4c7e7ed675c3bf405668739c3012d140856109 (patch) | |
tree | cdd868dba063fecba609a1d819de271f0d51b23e /mobile/android/android-components/components/browser/menu | |
parent | Adding upstream version 125.0.3. (diff) | |
download | firefox-da4c7e7ed675c3bf405668739c3012d140856109.tar.xz firefox-da4c7e7ed675c3bf405668739c3012d140856109.zip |
Adding upstream version 126.0.upstream/126.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/android-components/components/browser/menu')
222 files changed, 14711 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/browser/menu/README.md b/mobile/android/android-components/components/browser/menu/README.md new file mode 100644 index 0000000000..4d12c5c13d --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/README.md @@ -0,0 +1,152 @@ +# [Android Components](../../../README.md) > Browser > Menu + +A generic menu with customizable items primarily for browser toolbars. + +## Usage + +### Setting up the dependency + +Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)): + +```Groovy +implementation "org.mozilla.components:browser-menu:{latest-version}" +``` + +### BrowserMenu +Sample code can be found in [Sample Toolbar app](https://github.com/mozilla-mobile/android-components/tree/main/samples/toolbar). + +There are multiple properties that you customize of the menu browser by just adding them into your dimens.xml file. + +```xml +<resources xmlns:tools="http://schemas.android.com/tools"> + + <!--Change how rounded the corners of the menu should be--> + <dimen name="mozac_browser_menu_corner_radius" tools:ignore="UnusedResources">4dp</dimen> + + <!--Change how much shadow the menu should have--> + <dimen name="mozac_browser_menu_elevation" tools:ignore="UnusedResources">4dp</dimen> + + <!--Change the width of the menu--> + <dimen name="mozac_browser_menu_width" tools:ignore="UnusedResources">250dp</dimen> + + <!--Change the dynamic width of the menu--> + <dimen name="mozac_browser_menu_width_min" tools:ignore="UnusedResources">200dp</dimen> + <dimen name="mozac_browser_menu_width_max" tools:ignore="UnusedResources">300dp</dimen> + + <!--Change the top and bottom padding of the menu--> + <dimen name="mozac_browser_menu_padding_vertical" tools:ignore="UnusedResources">8dp</dimen> + +</resources> +``` +BrowserMenu can have a dynamic width: +- Using the same value for `mozac_browser_menu_width_min` and `mozac_browser_menu_width_max` means BrowserMenu will have a fixed width - `mozac_browser_menu_width`. +_This is the default behavior_. +- Different values for `mozac_browser_menu_width_min` and `mozac_browser_menu_width_max` means BrowserMenu will have a dynamic width depending on the widest BrowserMenuItem and between the aforementioned dimensions also taking into account display width. + + +### BrowserMenuDivider +```kotlin + + BrowserMenuDivider() + +``` + +To customize the divider you could use a 1. Quick customization or a 2. Full customization: + +1) If you just want to change the height of the divider, add this item your ``dimes.xml`` file, and your +prefer height size. + +```xml + <dimen name="mozac_browser_menu_item_divider_height" tools:ignore="UnusedResources">YOUR_HEIGHT</dimen> +``` +2) For full customization, override the default style of the divider by adding this style item in your `style.xml` file, and customize to your liking. +```xml + <style name="Mozac.Browser.Menu.Item.Divider.Horizontal" tools:ignore="UnusedResources"> + <item name="android:background">YOUR_BACKGROUND</item> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">YOUR_HEIGHT</item> + </style> +``` + +### BrowserMenuImageText +```kotlin + BrowserMenuImageText( + label = "Share", + imageResource = R.drawable.mozac_ic_share_android_24, + iconTintColorResource = R.color.photonBlue90 + ) { + Toast.makeText(applicationContext, "Share", Toast.LENGTH_SHORT).show() + } +``` + +To customize the menu you could use separate properties 1 or full access to the style of the menu 2: + +1) If you just want to change a specify property, just add one these dimen items to your ``dimes.xml`` file. + +```xml + <!--Menu Item --> + <!--Change the text_size for ALL menu items NOT only for the BrowserMenuImageText --> + <dimen name="mozac_browser_menu_item_text_size" tools:ignore="UnusedResources">16sp</dimen> + <!--Menu Item --> + + <!--Icon--> + <!--Change the icon's width--> + <dimen name="mozac_browser_menu_item_image_text_icon_width" tools:ignore="UnusedResources">24dp</dimen> <!--Default value--> + + <!--Change the icon's height--> + <dimen name="mozac_browser_menu_item_image_text_icon_height" tools:ignore="UnusedResources">24dp</dimen> <!--Default value--> + + <!--Icon--> + + <!--Label--> + <!--Change the separation between the label and the icon--> + <dimen name="mozac_browser_menu_item_image_text_label_padding_start" tools:ignore="UnusedResources">20dp</dimen> <!--Default value--> + + <!--Label--> +``` + +2) For full customization, override the default style of menu by adding this style item in your `style.xml` file, and customize to your liking. + +```xml + <!--Change the appearance of all text menu items--> + <style name="Mozac.Browser.Menu.Item.Text" parent="@android:style/TextAppearance.Material.Menu" tools:ignore="UnusedResources"> + <item name="android:background">?android:attr/selectableItemBackground</item> + <item name="android:textSize">@dimen/mozac_browser_menu_item_text_size</item> + <item name="android:ellipsize">end</item> + <item name="android:lines">1</item> + <item name="android:focusable">true</item> + <item name="android:clickable">true</item> + </style> + + <style name="Mozac.Browser.Menu.Item.ImageText.Icon" parent="" tools:ignore="UnusedResources"> + <item name="android:layout_width">@dimen/mozac_browser_menu_item_image_text_icon_width</item> + <item name="android:layout_height">@dimen/mozac_browser_menu_item_image_text_icon_height</item> + </style> + + <style name="Mozac.Browser.Menu.Item.ImageText.Label" parent="Mozac.Browser.Menu.Item.Text" tools:ignore="UnusedResources"> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:paddingStart">@dimen/mozac_browser_menu_item_image_text_label_padding_start</item> + </style> +``` + +## Facts + +This component emits the following [Facts](../../support/base/README.md#Facts): + +| Action | Item | Extras | Description | +|--------|-------------------------|-------------------|--------------------------------------| +| Click | web_extension_menu_item | `menuItemExtras` | Web extension menu item was clicked. | + + +#### `menuItemExtras` + +| Key | Type | Value | +|------|--------|----------------------------------------------------------| +| "id" | String | Web extension id of the clicked web extension menu item. | + +## License + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/ diff --git a/mobile/android/android-components/components/browser/menu/build.gradle b/mobile/android/android-components/components/browser/menu/build.gradle new file mode 100644 index 0000000000..7e305cda83 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/build.gradle @@ -0,0 +1,57 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-parcelize' + +android { + defaultConfig { + minSdkVersion config.minSdkVersion + compileSdk config.compileSdkVersion + targetSdkVersion config.targetSdkVersion + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + buildFeatures { + viewBinding true + } + + namespace 'mozilla.components.browser.menu' +} + +dependencies { + implementation project(':concept-engine') + implementation project(':concept-menu') + implementation project(':browser-state') + implementation project(':support-base') + implementation project(':support-ktx') + implementation project(':ui-colors') + implementation project(':ui-icons') + + implementation ComponentsDependencies.androidx_appcompat + implementation ComponentsDependencies.androidx_core_ktx + implementation ComponentsDependencies.androidx_recyclerview + implementation ComponentsDependencies.androidx_cardview + implementation ComponentsDependencies.androidx_constraintlayout + implementation ComponentsDependencies.androidx_coordinatorlayout + + testImplementation project(':support-test') + + testImplementation ComponentsDependencies.androidx_test_core + testImplementation ComponentsDependencies.androidx_test_junit + testImplementation ComponentsDependencies.testing_robolectric + testImplementation ComponentsDependencies.testing_coroutines + +} + +apply from: '../../../android-lint.gradle' +apply from: '../../../publish.gradle' +ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description) diff --git a/mobile/android/android-components/components/browser/menu/lint.xml b/mobile/android/android-components/components/browser/menu/lint.xml new file mode 100644 index 0000000000..81bcc3bfb8 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/lint.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<lint> + <issue id="Overdraw" severity="ignore" /> +</lint>
\ No newline at end of file diff --git a/mobile/android/android-components/components/browser/menu/proguard-rules.pro b/mobile/android/android-components/components/browser/menu/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/mobile/android/android-components/components/browser/menu/src/main/AndroidManifest.xml b/mobile/android/android-components/components/browser/menu/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..feab9bdd95 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> + <application android:supportsRtl="true" /> +</manifest>
\ No newline at end of file diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenu.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenu.kt new file mode 100644 index 0000000000..bdafd294ca --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenu.kt @@ -0,0 +1,320 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Build +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.view.accessibility.AccessibilityNodeInfo +import android.widget.PopupWindow +import androidx.annotation.VisibleForTesting +import androidx.cardview.widget.CardView +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.widget.PopupWindowCompat +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.browser.menu.BrowserMenu.Orientation.DOWN +import mozilla.components.browser.menu.BrowserMenu.Orientation.UP +import mozilla.components.browser.menu.view.DynamicWidthRecyclerView +import mozilla.components.browser.menu.view.ExpandableLayout +import mozilla.components.browser.menu.view.StickyItemPlacement +import mozilla.components.browser.menu.view.StickyItemsLinearLayoutManager +import mozilla.components.concept.menu.MenuStyle +import mozilla.components.support.ktx.android.view.isRTL +import mozilla.components.support.ktx.android.view.onNextGlobalLayout + +/** + * A popup menu composed of BrowserMenuItem objects. + */ +open class BrowserMenu internal constructor( + internal val adapter: BrowserMenuAdapter, +) : View.OnAttachStateChangeListener { + protected var currentPopup: PopupWindow? = null + + @VisibleForTesting + internal var menuList: RecyclerView? = null + internal var currAnchor: View? = null + internal var isShown = false + + @VisibleForTesting + internal lateinit var menuPositioningData: MenuPositioningData + internal var backgroundColor: Int = Color.RED + + /** + * @param anchor the view on which to pin the popup window. + * @param orientation the preferred orientation to show the popup window. + * @param style Custom styling for this menu. + * @param endOfMenuAlwaysVisible when is set to true makes sure the bottom of the menu is always visible otherwise, + * the top of the menu is always visible. + */ + @Suppress("InflateParams", "ComplexMethod") + open fun show( + anchor: View, + orientation: Orientation = DOWN, + style: MenuStyle? = null, + endOfMenuAlwaysVisible: Boolean = false, + onDismiss: () -> Unit = {}, + ): PopupWindow { + var view = LayoutInflater.from(anchor.context).inflate(R.layout.mozac_browser_menu, null) + + adapter.menu = this + + menuList = view.findViewById<DynamicWidthRecyclerView>(R.id.mozac_browser_menu_recyclerView).apply { + layoutManager = StickyItemsLinearLayoutManager.get<BrowserMenuAdapter>( + anchor.context, + StickyItemPlacement.BOTTOM, + false, + ) + + adapter = this@BrowserMenu.adapter + minWidth = style?.minWidth ?: resources.getDimensionPixelSize(R.dimen.mozac_browser_menu_width_min) + maxWidth = style?.maxWidth ?: resources.getDimensionPixelSize(R.dimen.mozac_browser_menu_width_max) + } + + setColors(view, style) + + menuList?.accessibilityDelegate = object : View.AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfo, + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + info.collectionInfo = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + AccessibilityNodeInfo.CollectionInfo( + adapter.interactiveCount, + 0, + false, + ) + } else { + @Suppress("DEPRECATION") + AccessibilityNodeInfo.CollectionInfo.obtain( + adapter.interactiveCount, + 0, + false, + ) + } + } + } + + // Data needed to infer whether to show a collapsed menu + // And then to properly place it. + menuPositioningData = inferMenuPositioningData( + view as ViewGroup, + anchor, + MenuPositioningData(askedOrientation = orientation), + ) + + view = configureExpandableMenu(view, endOfMenuAlwaysVisible) + return getNewPopupWindow(view).apply { + setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + isFocusable = true + elevation = view.resources.getDimension(R.dimen.mozac_browser_menu_elevation) + + setOnDismissListener { + adapter.menu = null + currentPopup = null + isShown = false + onDismiss() + } + + displayPopup(menuPositioningData).also { + anchor.addOnAttachStateChangeListener(this@BrowserMenu) + currAnchor = anchor + } + }.also { + currentPopup = it + isShown = true + } + } + + @VisibleForTesting + internal fun configureExpandableMenu( + view: ViewGroup, + endOfMenuAlwaysVisible: Boolean, + ): ViewGroup { + // If the menu is placed at the bottom it should start as collapsed. + if (menuPositioningData.inferredMenuPlacement is BrowserMenuPlacement.AnchoredToBottom.Dropdown || + menuPositioningData.inferredMenuPlacement is BrowserMenuPlacement.AnchoredToBottom.ManualAnchoring + ) { + val collapsingMenuIndexLimit = adapter.visibleItems.indexOfFirst { it.isCollapsingMenuLimit } + val stickyFooterPosition = adapter.visibleItems.indexOfLast { it.isSticky } + if (collapsingMenuIndexLimit > 0) { + return ExpandableLayout.wrapContentInExpandableView( + view, + collapsingMenuIndexLimit, + stickyFooterPosition, + ) { dismiss() } + } + } else { + // The menu is by default set as a bottom one. Reconfigure it as a top one. + menuList?.layoutManager = StickyItemsLinearLayoutManager.get<BrowserMenuAdapter>( + view.context, + StickyItemPlacement.TOP, + ) + + // By default the menu is laid out from and scrolled to top - showing the top most items. + // For the top menu it may be desired to initially show the bottom most items. + menuList?.let { list -> + list.setEndOfMenuAlwaysVisibleCompact( + endOfMenuAlwaysVisible, + list.layoutManager as LinearLayoutManager, + ) + } + } + + return view + } + + @VisibleForTesting + internal fun getNewPopupWindow(view: ViewGroup): PopupWindow { + // If the menu is expandable we need to give it all the possible space to expand. + // Also, by setting MATCH_PARENT, expanding the menu will not expand the Window + // of the PopupWindow which for a bottom anchored menu means glitchy animations. + val popupHeight = if (view is ExpandableLayout) { + WindowManager.LayoutParams.MATCH_PARENT + } else { + // Otherwise wrap the menu. Allowing it to be as big as the parent would result in + // layout issues if the menu is smaller than the available screen estate. + WindowManager.LayoutParams.WRAP_CONTENT + } + + return PopupWindow( + view, + WindowManager.LayoutParams.WRAP_CONTENT, + popupHeight, + ) + } + + private fun RecyclerView.setEndOfMenuAlwaysVisibleCompact( + endOfMenuAlwaysVisible: Boolean, + layoutManager: LinearLayoutManager, + ) { + // In devices with Android 6 and below stackFromEnd is not working properly, + // as a result, we have to provided a backwards support. + // See: https://github.com/mozilla-mobile/android-components/issues/3211 + if (endOfMenuAlwaysVisible && Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { + scrollOnceToTheBottom(this) + } else { + layoutManager.stackFromEnd = endOfMenuAlwaysVisible + } + } + + @VisibleForTesting + internal fun scrollOnceToTheBottom(recyclerView: RecyclerView) { + recyclerView.onNextGlobalLayout { + recyclerView.adapter?.let { recyclerView.scrollToPosition(it.itemCount - 1) } + } + } + + fun dismiss() { + currentPopup?.dismiss() + } + + fun invalidate() { + menuList?.let { adapter.invalidate(it) } + } + + @VisibleForTesting + internal fun setColors(menuLayout: View, colorState: MenuStyle?) { + val listParent: CardView = menuLayout.findViewById(R.id.mozac_browser_menu_menuView) + backgroundColor = colorState?.backgroundColor?.let { + listParent.setCardBackgroundColor(it) + it.defaultColor + } ?: listParent.cardBackgroundColor.defaultColor + } + + companion object { + /** + * Determines the orientation to be used for a menu based on the positioning of the [parent] in the layout. + */ + fun determineMenuOrientation(parent: View?): Orientation { + if (parent == null) { + return DOWN + } + + val params = parent.layoutParams + return if (params is CoordinatorLayout.LayoutParams) { + if ((params.gravity and Gravity.BOTTOM) == Gravity.BOTTOM) { + UP + } else { + DOWN + } + } else { + DOWN + } + } + } + + enum class Orientation(val concept: mozilla.components.concept.menu.Orientation) { + UP(mozilla.components.concept.menu.Orientation.UP), + DOWN(mozilla.components.concept.menu.Orientation.DOWN), + } + + override fun onViewDetachedFromWindow(v: View) { + currentPopup?.dismiss() + currAnchor?.removeOnAttachStateChangeListener(this) + } + + override fun onViewAttachedToWindow(v: View) { + // no-op + } +} + +@VisibleForTesting +internal fun PopupWindow.displayPopup(currentData: MenuPositioningData) { + // Try to use the preferred orientation, if doesn't fit fallback to the best fit. + when (currentData.inferredMenuPlacement) { + is BrowserMenuPlacement.AnchoredToTop.Dropdown -> showPopupWithDownOrientation(currentData) + is BrowserMenuPlacement.AnchoredToBottom.Dropdown -> showPopupWithUpOrientation(currentData) + + is BrowserMenuPlacement.AnchoredToTop.ManualAnchoring, + is BrowserMenuPlacement.AnchoredToBottom.ManualAnchoring, + -> showAtAnchorLocation(currentData) + else -> { + // no-op + } + } +} + +@VisibleForTesting +internal fun PopupWindow.showPopupWithUpOrientation(menuPositioningData: MenuPositioningData) { + val anchor = menuPositioningData.inferredMenuPlacement!!.anchor + val xOffset = if (anchor.isRTL) -anchor.width else 0 + animationStyle = menuPositioningData.inferredMenuPlacement.animation + + // Positioning the menu above and overlapping the anchor. + val yOffset = if (menuPositioningData.availableHeightToBottom < 0) { + // The anchor is partially below of the bottom of the screen, let's make the menu completely visible. + menuPositioningData.availableHeightToBottom - menuPositioningData.containerViewHeight + } else { + -menuPositioningData.containerViewHeight + } + showAsDropDown(anchor, xOffset, yOffset) +} + +private fun PopupWindow.showPopupWithDownOrientation(menuPositioningData: MenuPositioningData) { + val anchor = menuPositioningData.inferredMenuPlacement!!.anchor + val xOffset = if (anchor.isRTL) -anchor.width else 0 + animationStyle = menuPositioningData.inferredMenuPlacement.animation + // Menu should overlay the anchor. + showAsDropDown(anchor, xOffset, -anchor.height) +} + +private fun PopupWindow.showAtAnchorLocation(menuPositioningData: MenuPositioningData) { + val anchor = menuPositioningData.inferredMenuPlacement!!.anchor + val anchorPosition = IntArray(2) + animationStyle = menuPositioningData.inferredMenuPlacement.animation + + anchor.getLocationOnScreen(anchorPosition) + val (x, y) = anchorPosition + PopupWindowCompat.setOverlapAnchor(this, true) + showAtLocation(anchor, Gravity.START or Gravity.TOP, x, y) +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuAdapter.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuAdapter.kt new file mode 100644 index 0000000000..f2a8ea954e --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuAdapter.kt @@ -0,0 +1,68 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu + +import android.content.Context +import android.graphics.Color +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.browser.menu.view.StickyItemsAdapter + +/** + * Adapter implementation used by the browser menu to display menu items in a RecyclerView. + */ +internal class BrowserMenuAdapter( + context: Context, + items: List<BrowserMenuItem>, +) : RecyclerView.Adapter<BrowserMenuItemViewHolder>(), StickyItemsAdapter { + var menu: BrowserMenu? = null + + internal val visibleItems = items.filter { it.visible() } + internal val interactiveCount = visibleItems.sumOf { it.interactiveCount() } + private val inflater = LayoutInflater.from(context) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + BrowserMenuItemViewHolder(inflater.inflate(viewType, parent, false)) + + override fun getItemCount() = visibleItems.size + + override fun getItemViewType(position: Int): Int = visibleItems[position].getLayoutResource() + + override fun onBindViewHolder(holder: BrowserMenuItemViewHolder, position: Int) { + visibleItems[position].bind(menu!!, holder.itemView) + } + + fun invalidate(recyclerView: RecyclerView) { + visibleItems.withIndex().forEach { + val (index, item) = it + recyclerView.findViewHolderForAdapterPosition(index)?.apply { + item.invalidate(itemView) + } + } + } + + @Suppress("TooGenericExceptionCaught") + override fun isStickyItem(position: Int): Boolean { + return try { + visibleItems[position].isSticky + } catch (e: IndexOutOfBoundsException) { + false + } + } + + override fun setupStickyItem(stickyItem: View) { + menu?.let { + stickyItem.setBackgroundColor(it.backgroundColor) + } + } + + override fun tearDownStickyItem(stickyItem: View) { + stickyItem.setBackgroundColor(Color.TRANSPARENT) + } +} + +class BrowserMenuItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuBuilder.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuBuilder.kt new file mode 100644 index 0000000000..fb5e917fa5 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuBuilder.kt @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu + +import android.content.Context + +/** + * Helper class for building browser menus. + * + * @param items List of BrowserMenuItem objects to compose the menu from. + * @param extras Map of extra values that are added to emitted facts + * @param endOfMenuAlwaysVisible when is set to true makes sure the bottom of the menu is always visible otherwise, + * the top of the menu is always visible. + */ +open class BrowserMenuBuilder( + val items: List<BrowserMenuItem>, + val extras: Map<String, Any> = emptyMap(), + val endOfMenuAlwaysVisible: Boolean = false, +) { + /** + * Builds and returns a browser menu with [items] + */ + open fun build(context: Context): BrowserMenu { + val adapter = BrowserMenuAdapter(context, items) + return BrowserMenu(adapter) + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuHighlight.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuHighlight.kt new file mode 100644 index 0000000000..881eff83b5 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuHighlight.kt @@ -0,0 +1,109 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu + +import android.content.Context +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.core.content.ContextCompat +import mozilla.components.browser.menu.item.NO_ID +import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect +import mozilla.components.concept.menu.candidate.LowPriorityHighlightEffect +import mozilla.components.concept.menu.candidate.MenuEffect + +/** + * Describes how to display a [mozilla.components.browser.menu.item.BrowserMenuHighlightableItem] + * when it is highlighted. + */ +sealed class BrowserMenuHighlight { + abstract val label: String? + abstract val canPropagate: Boolean + + /** + * Converts the highlight into a corresponding [MenuEffect] from concept-menu. + */ + abstract fun asEffect(context: Context): MenuEffect + + /** + * Displays a notification dot. + * Used for highlighting new features to the user, such as what's new or a recommended feature. + * + * @property notificationTint Tint for the notification dot displayed on the icon and menu button. + * @property label Label to override the normal label of the menu item. + * @property canPropagate Indicate whether other components should consider this highlight when + * displaying their own highlight. + */ + data class LowPriority( + @ColorInt val notificationTint: Int, + override val label: String? = null, + override val canPropagate: Boolean = true, + ) : BrowserMenuHighlight() { + override fun asEffect(context: Context) = LowPriorityHighlightEffect( + notificationTint = notificationTint, + ) + } + + /** + * Changes the background of the menu item. + * Used for errors that require user attention, like sync errors. + * + * @property backgroundTint Tint for the menu item background color. + * Also used to highlight the menu button. + * @property label Label to override the normal label of the menu item. + * @property endImageResource Icon to display at the end of the menu item when highlighted. + * @property canPropagate Indicate whether other components should consider this highlight when + * displaying their own highlight. + */ + data class HighPriority( + @ColorInt val backgroundTint: Int, + override val label: String? = null, + val endImageResource: Int = NO_ID, + override val canPropagate: Boolean = true, + ) : BrowserMenuHighlight() { + override fun asEffect(context: Context) = HighPriorityHighlightEffect( + backgroundTint = backgroundTint, + ) + } + + /** + * Described how to display a highlightable menu item when it is highlighted. + * Replaced by [LowPriority] and [HighPriority] which lets a priority be specified. + * This class only exists so that [mozilla.components.browser.menu.item.BrowserMenuHighlightableItem.Highlight] + * can subclass it. + * + * @property canPropagate Indicate whether other components should consider this highlight when + * displaying their own highlight. + */ + @Deprecated("Replace with LowPriority or HighPriority highlight") + open class ClassicHighlight( + @DrawableRes val startImageResource: Int, + @DrawableRes val endImageResource: Int, + @DrawableRes val backgroundResource: Int, + @ColorRes val colorResource: Int, + override val canPropagate: Boolean = true, + ) : BrowserMenuHighlight() { + override val label: String? = null + + override fun asEffect(context: Context) = HighPriorityHighlightEffect( + backgroundTint = ContextCompat.getColor(context, colorResource), + ) + } +} + +/** + * Indicates that a menu item shows a highlight. + */ +interface HighlightableMenuItem { + /** + * Highlight object representing how the menu item will be displayed when highlighted. + */ + val highlight: BrowserMenuHighlight + + /** + * Whether or not to display the highlight + */ + val isHighlighted: () -> Boolean +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuItem.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuItem.kt new file mode 100644 index 0000000000..b9dd7114d7 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuItem.kt @@ -0,0 +1,64 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu + +import android.content.Context +import android.view.View +import mozilla.components.browser.menu.view.ExpandableLayout +import mozilla.components.browser.menu.view.StickyItemsLinearLayoutManager +import mozilla.components.concept.menu.candidate.MenuCandidate + +/** + * Interface to be implemented by menu items to be shown in the browser menu. + */ +interface BrowserMenuItem { + /** + * Lambda expression that returns true if this item should be shown in the menu. Returns false + * if this item should be hidden. + */ + val visible: () -> Boolean + + /** + * Lambda expression that returns the number of interactive elements in this menu item. + * For example, a simple item will have 1, divider will have 0, and a composite + * item, like a tool bar, will have several. + */ + val interactiveCount: () -> Int get() = { 1 } + + /** + * Whether this menu item can serve as the limit of a collapsing menu. + * + * @see [ExpandableLayout] + */ + val isCollapsingMenuLimit: Boolean get() = false + + /** + * Whether this menu item should not be scrollable off-screen. + * + * @see [StickyItemsLinearLayoutManager] + */ + val isSticky: Boolean get() = false + + /** + * Returns the layout resource ID of the layout to be inflated for showing a menu item of this + * type. + */ + fun getLayoutResource(): Int + + /** + * Called by the browser menu to display the data of this item using the passed view. + */ + fun bind(menu: BrowserMenu, view: View) + + /** + * Called by the browser menu to update the displayed data of this item using the passed view. + */ + fun invalidate(view: View) = Unit + + /** + * Converts the menu item into a menu candidate. + */ + fun asCandidate(context: Context): MenuCandidate? = null +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuPlacement.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuPlacement.kt new file mode 100644 index 0000000000..4a0c4cc06f --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuPlacement.kt @@ -0,0 +1,64 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu + +import android.view.View + +/** + * Configuration of where and how a PopupWindow for a menu should be displayed. + */ +internal sealed class BrowserMenuPlacement { + /** + * Android View that the PopupWindow should be anchored to. + */ + abstract val anchor: View + + /** + * Menu position specific animation to be used when showing the PopupWindow. + */ + abstract val animation: Int + + /** + * Menu placed below the anchor. Anchored to the top. + */ + class AnchoredToTop { + /** + * The PopupWindow should be anchored to the top and shown as a dropdown. + */ + data class Dropdown( + override val anchor: View, + override val animation: Int = R.style.Mozac_Browser_Menu_Animation_OverflowMenuTop, + ) : BrowserMenuPlacement() + + /** + * The PopupWindow should be anchored to the top and placed at a specific location. + */ + data class ManualAnchoring( + override val anchor: View, + override val animation: Int = R.style.Mozac_Browser_Menu_Animation_OverflowMenuTop, + ) : BrowserMenuPlacement() + } + + /** + * Menu placed above the anchor. Anchored to the bottom. + */ + class AnchoredToBottom { + /** + * The PopupWindow should be anchored to the bottom and shown as a dropdown. + */ + data class Dropdown( + override val anchor: View, + override val animation: Int = R.style.Mozac_Browser_Menu_Animation_OverflowMenuBottom, + ) : BrowserMenuPlacement() + + /** + * The PopupWindow should be anchored to the bottom and placed at a specific location. + */ + data class ManualAnchoring( + override val anchor: View, + override val animation: Int = R.style.Mozac_Browser_Menu_Animation_OverflowMenuBottom, + ) : BrowserMenuPlacement() + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuPositioning.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuPositioning.kt new file mode 100644 index 0000000000..b6bce41f6b --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuPositioning.kt @@ -0,0 +1,143 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +@file:Suppress("MatchingDeclarationName") + +package mozilla.components.browser.menu + +import android.graphics.Rect +import android.view.View +import android.view.ViewGroup +import androidx.annotation.Px + +/** + * All data needed for menu positioning. + */ +internal data class MenuPositioningData( + /** + * Where and how should the menu be placed in relation to the [BrowserMenuPlacement.anchor]. + */ + val inferredMenuPlacement: BrowserMenuPlacement? = null, + + /** + * The orientation asked by users of this class when initializing it. + */ + val askedOrientation: BrowserMenu.Orientation = BrowserMenu.Orientation.DOWN, + + /** + * Whether the menu fits in the space between [display top, anchor] in a top - down layout. + */ + val fitsUp: Boolean = false, + + /** + * Whether the menu fits in the space between [anchor, display top] in a top - down layout. + */ + val fitsDown: Boolean = false, + + /** + * Distance between [display top, anchor top margin]. Used for better positioning the menu. + */ + @Px val availableHeightToTop: Int = 0, + + /** + * Distance between [display bottom, anchor bottom margin]. Used for better positioning the menu. + */ + @Px val availableHeightToBottom: Int = 0, + + /** + * [View#measuredHeight] of the menu. May be bigger than the available screen height. + */ + @Px val containerViewHeight: Int = 0, +) + +/** + * Measure, calculate, obtain all data needed to know how the menu shown in a PopupWindow should be positioned. + * + * This method assumes [currentData] already contains the [MenuPositioningData.askedOrientation]. + * + * @param containerView the menu layout that will be wrapped in the PopupWindow. + * @param anchor view the PopupWindow will be aligned to. + * @param currentData current known data for how the menu should be positioned. + * + * @return new [MenuPositioningData] containing the current constraints of the PopupWindow. + */ +internal fun inferMenuPositioningData( + containerView: ViewGroup, + anchor: View, + currentData: MenuPositioningData, +): MenuPositioningData { + // Measure the menu allowing it to expand entirely. + val spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) + containerView.measure(spec, spec) + + val (availableHeightToTop, availableHeightToBottom) = getMaxAvailableHeightToTopAndBottom(anchor) + val containerHeight = containerView.measuredHeight + + val fitsUp = availableHeightToTop >= containerHeight || availableHeightToTop > availableHeightToBottom + val fitsDown = availableHeightToBottom >= containerHeight || availableHeightToBottom > availableHeightToTop + + return inferMenuPosition( + anchor, + currentData.copy( + fitsUp = fitsUp, + fitsDown = fitsDown, + availableHeightToTop = availableHeightToTop, + availableHeightToBottom = availableHeightToBottom, + containerViewHeight = containerHeight, + ), + ) +} + +/** + * Infer where and how the PopupWindow should be shown based on the data available in [currentData]. + * Should be called only once per menu to be shown. + * + * @param anchor view the PopupWindow will be aligned to. + * @param currentData current known data for how the menu should be positioned. + * + * @return new MenuPositioningData updated to contain the inferred [BrowserMenuPlacement] + */ +internal fun inferMenuPosition(anchor: View, currentData: MenuPositioningData): MenuPositioningData { + // Try to use the preferred orientation, if doesn't fit fallback to the best fit. + + val menuPlacement: BrowserMenuPlacement = + if (currentData.askedOrientation == BrowserMenu.Orientation.DOWN && currentData.fitsDown) { + BrowserMenuPlacement.AnchoredToTop.Dropdown(anchor) + } else if (currentData.askedOrientation == BrowserMenu.Orientation.UP && currentData.fitsUp) { + BrowserMenuPlacement.AnchoredToBottom.Dropdown(anchor) + } else { + if (!currentData.fitsUp && !currentData.fitsDown) { + if (currentData.availableHeightToTop < currentData.availableHeightToBottom) { + BrowserMenuPlacement.AnchoredToTop.ManualAnchoring(anchor) + } else { + BrowserMenuPlacement.AnchoredToBottom.ManualAnchoring(anchor) + } + } else { + if (currentData.fitsDown) { + BrowserMenuPlacement.AnchoredToTop.Dropdown(anchor) + } else { + BrowserMenuPlacement.AnchoredToBottom.Dropdown(anchor) + } + } + } + + return currentData.copy(inferredMenuPlacement = menuPlacement) +} + +private fun getMaxAvailableHeightToTopAndBottom(anchor: View): Pair<Int, Int> { + val anchorPosition = IntArray(2) + val displayFrame = Rect() + + val appView = anchor.rootView + appView.getWindowVisibleDisplayFrame(displayFrame) + + anchor.getLocationOnScreen(anchorPosition) + + val bottomEdge = displayFrame.bottom + + val distanceToBottom = bottomEdge - (anchorPosition[1] + anchor.height) + val distanceToTop = anchorPosition[1] - displayFrame.top + + return distanceToTop to distanceToBottom +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/WebExtensionBrowserMenu.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/WebExtensionBrowserMenu.kt new file mode 100644 index 0000000000..afebfb14dd --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/WebExtensionBrowserMenu.kt @@ -0,0 +1,141 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu + +import android.view.View +import android.widget.PopupWindow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.distinctUntilChangedBy +import mozilla.components.browser.menu.facts.emitOpenMenuItemFact +import mozilla.components.browser.menu.item.WebExtensionBrowserMenuItem +import mozilla.components.browser.state.selector.selectedTab +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.SessionState +import mozilla.components.browser.state.state.WebExtensionState +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.webextension.Action +import mozilla.components.concept.menu.MenuStyle +import mozilla.components.lib.state.ext.flowScoped + +/** + * A [BrowserMenu] capable of displaying browser and page actions from web extensions. + */ +class WebExtensionBrowserMenu internal constructor( + adapter: BrowserMenuAdapter, + private val store: BrowserStore, +) : BrowserMenu(adapter) { + private var scope: CoroutineScope? = null + + override fun show( + anchor: View, + orientation: Orientation, + style: MenuStyle?, + endOfMenuAlwaysVisible: Boolean, + onDismiss: () -> Unit, + ): PopupWindow { + scope = store.flowScoped { flow -> + flow.distinctUntilChangedBy { it.selectedTab } + .collect { state -> + getOrUpdateWebExtensionMenuItems(state, state.selectedTab) + invalidate() + } + } + + return super.show( + anchor, + orientation, + style, + endOfMenuAlwaysVisible, + onDismiss, + ).apply { + setOnDismissListener { + adapter.menu = null + currentPopup = null + scope?.cancel() + webExtensionBrowserActions.clear() + webExtensionPageActions.clear() + onDismiss() + } + } + } + + companion object { + internal val webExtensionBrowserActions = HashMap<String, WebExtensionBrowserMenuItem>() + internal val webExtensionPageActions = HashMap<String, WebExtensionBrowserMenuItem>() + + internal fun getOrUpdateWebExtensionMenuItems( + state: BrowserState, + tab: SessionState? = null, + ): List<WebExtensionBrowserMenuItem> { + val menuItems = ArrayList<WebExtensionBrowserMenuItem>() + val extensions = state.extensions.values.toList() + extensions.filter { it.enabled }.sortedBy { it.name } + .forEach { extension -> + if (!extension.allowedInPrivateBrowsing && tab?.content?.private == true) { + return@forEach + } + + extension.browserAction?.let { browserAction -> + addOrUpdateAction( + extension = extension, + globalAction = browserAction, + tabAction = tab?.extensionState?.get(extension.id)?.browserAction, + menuItems = menuItems, + ) + } + + extension.pageAction?.let { pageAction -> + val tabPageAction = tab?.extensionState?.get(extension.id)?.pageAction + + // Unlike browser actions, page actions are not displayed by default (only if enabled): + // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/page_action + if (pageAction.copyWithOverride(tabPageAction).enabled == true) { + addOrUpdateAction( + extension = extension, + globalAction = pageAction, + tabAction = tabPageAction, + menuItems = menuItems, + isPageAction = true, + ) + } + } + } + + return menuItems + } + + private fun addOrUpdateAction( + extension: WebExtensionState, + globalAction: Action, + tabAction: Action?, + menuItems: ArrayList<WebExtensionBrowserMenuItem>, + isPageAction: Boolean = false, + ): Boolean { + val actionMap = if (isPageAction) webExtensionPageActions else webExtensionBrowserActions + + // Add the global browser/page action if it doesn't exist + val browserMenuItem = actionMap.getOrPut(extension.id) { + val listener = { + emitOpenMenuItemFact(extension.id) + globalAction.onClick() + } + val browserMenuItem = WebExtensionBrowserMenuItem( + action = globalAction, + listener = listener, + id = extension.id, + ) + browserMenuItem + } + + // Apply tab-specific override of browser/page action + tabAction?.let { + browserMenuItem.action = globalAction.copyWithOverride(it) + } + + return menuItems.add(browserMenuItem) + } + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/WebExtensionBrowserMenuBuilder.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/WebExtensionBrowserMenuBuilder.kt new file mode 100644 index 0000000000..494014bf66 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/WebExtensionBrowserMenuBuilder.kt @@ -0,0 +1,166 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu + +import android.content.Context +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import mozilla.components.browser.menu.item.BackPressMenuItem +import mozilla.components.browser.menu.item.BrowserMenuDivider +import mozilla.components.browser.menu.item.BrowserMenuImageText +import mozilla.components.browser.menu.item.NO_ID +import mozilla.components.browser.menu.item.ParentBrowserMenuItem +import mozilla.components.browser.menu.item.WebExtensionBrowserMenuItem +import mozilla.components.browser.menu.item.WebExtensionPlaceholderMenuItem +import mozilla.components.browser.state.selector.selectedTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.ui.icons.R as iconsR + +/** + * Browser menu builder with web extension support. It allows [WebExtensionBrowserMenu] to add + * web extension browser actions in a nested menu item. If there are no web extensions installed + * and @param showAddonsInMenu is true the web extension menu item would return an add-on manager menu item instead. + * + * @param store [BrowserStore] required to render web extension browser actions + * @param style Indicates how items should look like. + * @param onAddonsManagerTapped Callback to be invoked when add-ons manager menu item is selected. + * @param appendExtensionSubMenuAtStart Used when the menu does not have a [WebExtensionPlaceholderMenuItem] + * to specify the place the extensions sub-menu should be inserted. True if web extension sub menu + * appear at the top (start) of the menu, false if web extensions appear at the bottom of the menu. + * Default to false (bottom). This is also used to decide the back press menu item placement at top or bottom. + * @param showAddonsInMenu Whether to show the 'Add-ons' item in menu + */ +class WebExtensionBrowserMenuBuilder( + items: List<BrowserMenuItem>, + extras: Map<String, Any> = emptyMap(), + endOfMenuAlwaysVisible: Boolean = false, + private val store: BrowserStore, + private val style: Style = Style(), + private val onAddonsManagerTapped: () -> Unit = {}, + private val appendExtensionSubMenuAtStart: Boolean = false, + private val showAddonsInMenu: Boolean = true, +) : BrowserMenuBuilder(items, extras, endOfMenuAlwaysVisible) { + + /** + * Builds and returns a browser menu with combination of [items] and web extension browser actions. + */ + override fun build(context: Context): BrowserMenu { + val extensionMenuItems = + WebExtensionBrowserMenu.getOrUpdateWebExtensionMenuItems(store.state, store.state.selectedTab) + + val finalList = items.toMutableList() + + val filteredExtensionMenuItems = extensionMenuItems.filter { webExtensionBrowserMenuItem -> + replaceMenuPlaceholderWithExtensions(finalList, webExtensionBrowserMenuItem) + } + + val menuItems = if (showAddonsInMenu) { + createAddonsMenuItems(context, finalList, filteredExtensionMenuItems) + } else { + finalList + } + + val adapter = BrowserMenuAdapter(context, menuItems) + return BrowserMenu(adapter) + } + + private fun replaceMenuPlaceholderWithExtensions( + items: MutableList<BrowserMenuItem>, + menuItem: WebExtensionBrowserMenuItem, + ): Boolean { + // Check if we have a placeholder + val index = items.indexOfFirst { browserMenuItem -> + (browserMenuItem as? WebExtensionPlaceholderMenuItem)?.id == menuItem.id + } + // Replace placeholder with corresponding web extension, and remove it from extensions menu list + if (index != -1) { + menuItem.setIconTint( + (items[index] as? WebExtensionPlaceholderMenuItem)?.iconTintColorResource, + ) + items[index] = menuItem + } + return index == -1 + } + + private fun createAddonsMenuItems( + context: Context, + items: MutableList<BrowserMenuItem>, + filteredExtensionMenuItems: List<WebExtensionBrowserMenuItem>, + ): List<BrowserMenuItem> { + val addonsMenuItem = if (filteredExtensionMenuItems.isNotEmpty()) { + val backPressMenuItem = BackPressMenuItem( + contentDescription = context.getString(R.string.mozac_browser_menu_extensions_content_description), + label = context.getString(R.string.mozac_browser_menu_extensions), + imageResource = style.backPressMenuItemDrawableRes, + iconTintColorResource = style.webExtIconTintColorResource, + ) + + val addonsManagerMenuItem = BrowserMenuImageText( + label = context.getString(R.string.mozac_browser_menu_extensions_manager), + imageResource = style.addonsManagerMenuItemDrawableRes, + iconTintColorResource = style.webExtIconTintColorResource, + ) { + onAddonsManagerTapped.invoke() + } + + val webExtSubMenuItems = if (appendExtensionSubMenuAtStart) { + listOf(backPressMenuItem) + BrowserMenuDivider() + + filteredExtensionMenuItems + + BrowserMenuDivider() + addonsManagerMenuItem + } else { + listOf(addonsManagerMenuItem) + BrowserMenuDivider() + + filteredExtensionMenuItems + + BrowserMenuDivider() + backPressMenuItem + } + + val webExtBrowserMenuAdapter = BrowserMenuAdapter(context, webExtSubMenuItems) + val webExtMenu = WebExtensionBrowserMenu(webExtBrowserMenuAdapter, store) + + ParentBrowserMenuItem( + label = context.getString(R.string.mozac_browser_menu_extensions), + imageResource = style.addonsManagerMenuItemDrawableRes, + iconTintColorResource = style.webExtIconTintColorResource, + subMenu = webExtMenu, + endOfMenuAlwaysVisible = endOfMenuAlwaysVisible, + ) + } else { + BrowserMenuImageText( + label = context.getString(R.string.mozac_browser_menu_extensions), + imageResource = style.addonsManagerMenuItemDrawableRes, + iconTintColorResource = style.webExtIconTintColorResource, + ) { + onAddonsManagerTapped.invoke() + } + } + val mainMenuIndex = items.indexOfFirst { browserMenuItem -> + (browserMenuItem as? WebExtensionPlaceholderMenuItem)?.id == + WebExtensionPlaceholderMenuItem.MAIN_EXTENSIONS_MENU_ID + } + + return if (mainMenuIndex != -1) { + items[mainMenuIndex] = addonsMenuItem + items + // if we do not have a placeholder we should add the extension submenu at top or bottom + } else { + if (appendExtensionSubMenuAtStart) { + listOf(addonsMenuItem) + items + } else { + items + addonsMenuItem + } + } + } + + /** + * Allows to customize how items should look like. + */ + data class Style( + @ColorRes + val webExtIconTintColorResource: Int = NO_ID, + @DrawableRes + val backPressMenuItemDrawableRes: Int = iconsR.drawable.mozac_ic_back_24, + @DrawableRes + val addonsManagerMenuItemDrawableRes: Int = iconsR.drawable.mozac_ic_extension_24, + ) +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/ext/BrowserMenuItem.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/ext/BrowserMenuItem.kt new file mode 100644 index 0000000000..2411c19cc8 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/ext/BrowserMenuItem.kt @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.ext + +import android.content.Context +import mozilla.components.browser.menu.BrowserMenuHighlight +import mozilla.components.browser.menu.BrowserMenuItem +import mozilla.components.browser.menu.HighlightableMenuItem + +/** + * Get the highlight effect present in the list of menu items, if any. + */ +@Suppress("Deprecation") +fun List<BrowserMenuItem>.getHighlight() = asSequence() + .filter { it.visible() } + .mapNotNull { it as? HighlightableMenuItem } + .filter { it.isHighlighted() } + .map { it.highlight } + .filter { it.canPropagate } + .maxByOrNull { + // Select the highlight with the highest priority + when (it) { + is BrowserMenuHighlight.HighPriority -> 2 + is BrowserMenuHighlight.LowPriority -> 1 + is BrowserMenuHighlight.ClassicHighlight -> 0 + } + } + +/** + * Converts the menu items into a menu candidate list. + */ +fun List<BrowserMenuItem>.asCandidateList(context: Context) = + mapNotNull { it.asCandidate(context) } diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/ext/View.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/ext/View.kt new file mode 100644 index 0000000000..39e0e647c7 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/ext/View.kt @@ -0,0 +1,16 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.ext + +import android.util.TypedValue +import android.view.View + +/** + * Adds ripple effect to the view + */ +fun View.addRippleEffect() = with(TypedValue()) { + context.theme.resolveAttribute(android.R.attr.selectableItemBackground, this, true) + setBackgroundResource(resourceId) +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/facts/BrowserMenuFacts.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/facts/BrowserMenuFacts.kt new file mode 100644 index 0000000000..e35e3840f0 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/facts/BrowserMenuFacts.kt @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.facts + +import mozilla.components.support.base.Component +import mozilla.components.support.base.facts.Action +import mozilla.components.support.base.facts.Fact +import mozilla.components.support.base.facts.collect + +/** + * Facts emitted for telemetry related to [BrowserMenu]. + */ +class BrowserMenuFacts { + /** + * Items that specify which portion of the [BrowserMenu] was interacted with. + */ + object Items { + const val WEB_EXTENSION_MENU_ITEM = "web_extension_menu_item" + } +} + +private fun emitMenuFact( + action: Action, + item: String, + value: String? = null, + metadata: Map<String, Any>? = null, +) { + Fact( + Component.BROWSER_MENU, + action, + item, + value, + metadata, + ).collect() +} + +internal fun emitOpenMenuItemFact(extensionId: String) { + emitMenuFact( + Action.CLICK, + BrowserMenuFacts.Items.WEB_EXTENSION_MENU_ITEM, + metadata = mapOf("id" to extensionId), + ) +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/AbstractParentBrowserMenuItem.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/AbstractParentBrowserMenuItem.kt new file mode 100644 index 0000000000..729cdfdec5 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/AbstractParentBrowserMenuItem.kt @@ -0,0 +1,66 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.view.View +import androidx.annotation.VisibleForTesting +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.BrowserMenuItem + +/** + * An abstract menu item for handling nested sub menu items on view click. + * + * @param subMenu Target sub menu to be shown when this menu item is clicked. + * @param endOfMenuAlwaysVisible when is set to true makes sure the bottom of the menu is always visible + * otherwise, the top of the menu is always visible. + * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu. + * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards + * depending on the menu position). + */ +abstract class AbstractParentBrowserMenuItem( + private val subMenu: BrowserMenu, + private val endOfMenuAlwaysVisible: Boolean, + override val isCollapsingMenuLimit: Boolean = false, + override val isSticky: Boolean = false, +) : BrowserMenuItem { + /** + * Listener called when the sub menu is shown. + */ + var onSubMenuShow: () -> Unit = {} + + /** + * Listener called when the sub menu is dismissed. + */ + var onSubMenuDismiss: () -> Unit = {} + abstract override var visible: () -> Boolean + abstract override fun getLayoutResource(): Int + + override fun bind(menu: BrowserMenu, view: View) { + view.setOnClickListener { + menu.dismiss() + subMenu.show( + anchor = menu.currAnchor ?: view, + orientation = BrowserMenu.determineMenuOrientation(view.parent as? View?), + endOfMenuAlwaysVisible = endOfMenuAlwaysVisible, + ) { + onSubMenuDismiss() + } + onSubMenuShow() + } + } + + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + internal fun onBackPressed(menu: BrowserMenu, view: View) { + if (subMenu.isShown) { + subMenu.dismiss() + onSubMenuDismiss() + menu.show( + anchor = menu.currAnchor ?: view, + orientation = BrowserMenu.determineMenuOrientation(view.parent as? View?), + endOfMenuAlwaysVisible = endOfMenuAlwaysVisible, + ) + } + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BackPressMenuItem.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BackPressMenuItem.kt new file mode 100644 index 0000000000..c04a91ecf2 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BackPressMenuItem.kt @@ -0,0 +1,72 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.content.Context +import android.view.View +import android.view.View.AccessibilityDelegate +import android.view.accessibility.AccessibilityNodeInfo +import android.widget.Button +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.concept.menu.candidate.NestedMenuCandidate +import mozilla.components.concept.menu.candidate.TextMenuCandidate + +/** + * A back press menu item for a nested sub menu entry. + * + * @param backPressListener Callback to be invoked when the back press menu item is clicked. + */ +class BackPressMenuItem( + val contentDescription: String, + label: String, + @DrawableRes + imageResource: Int, + @ColorRes + iconTintColorResource: Int = NO_ID, + @ColorRes + textColorResource: Int = NO_ID, + private var backPressListener: () -> Unit = {}, +) : BrowserMenuImageText(label, imageResource, iconTintColorResource, textColorResource) { + + /** + * Binds the view according to its super, but use [backPressListener] for on view clicks. + */ + override fun bind(menu: BrowserMenu, view: View) { + super.bind(menu, view) + + view.setOnClickListener { + backPressListener.invoke() + menu.dismiss() + } + view.accessibilityDelegate = object : AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) { + super.onInitializeAccessibilityNodeInfo(host, info) + info.className = Button::class.java.name + } + } + view.contentDescription = contentDescription + } + + /** + * Sets and replaces the existing [backPressListener] for the back press item. + */ + fun setListener(onClickListener: () -> Unit) { + backPressListener = onClickListener + } + + override fun asCandidate(context: Context): NestedMenuCandidate { + val parentCandidate = super.asCandidate(context) as TextMenuCandidate + return NestedMenuCandidate( + id = hashCode(), + text = parentCandidate.text, + start = parentCandidate.start, + subMenuItems = null, + textStyle = parentCandidate.textStyle, + containerStyle = parentCandidate.containerStyle, + ) + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuCategory.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuCategory.kt new file mode 100644 index 0000000000..9c0b29bbee --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuCategory.kt @@ -0,0 +1,81 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.content.Context +import android.graphics.Typeface +import android.view.View +import android.widget.TextView +import androidx.annotation.ColorRes +import androidx.core.content.ContextCompat.getColor +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.BrowserMenuItem +import mozilla.components.browser.menu.R +import mozilla.components.concept.menu.candidate.ContainerStyle +import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate +import mozilla.components.concept.menu.candidate.TextAlignment +import mozilla.components.concept.menu.candidate.TextStyle +import mozilla.components.concept.menu.candidate.TypefaceStyle + +/** + * A browser menu item displaying styleable text, usable for menu categories + * + * @param label The visible label of this menu item. + * @param textSize: The size of the label. + * @param textColorResource: The color resource to apply to the text. + * @param backgroundColorResource: The color resource to apply to the item background. + * @param textStyle: The style to apply to the text. + * @param textAlignment The alignment of text + * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu. + * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards + * depending on the menu position). + */ +class BrowserMenuCategory( + internal val label: String, + private val textSize: Float = NO_ID.toFloat(), + @ColorRes + private val textColorResource: Int = NO_ID, + @ColorRes + private val backgroundColorResource: Int = NO_ID, + @TypefaceStyle private val textStyle: Int = Typeface.BOLD, + @TextAlignment private val textAlignment: Int = View.TEXT_ALIGNMENT_VIEW_START, + override val isCollapsingMenuLimit: Boolean = false, + override val isSticky: Boolean = false, +) : BrowserMenuItem { + override var visible: () -> Boolean = { true } + + override fun getLayoutResource() = R.layout.mozac_browser_menu_category + + override fun bind(menu: BrowserMenu, view: View) { + val textView = view as TextView + textView.text = label + + if (textSize != NO_ID.toFloat()) { + textView.textSize = textSize + } + + if (textColorResource != NO_ID) { + textView.setColorResource(textColorResource) + } + + textView.setTypeface(textView.typeface, textStyle) + textView.textAlignment = textAlignment + + if (backgroundColorResource != NO_ID) { + view.setBackgroundResource(backgroundColorResource) + } + } + + override fun asCandidate(context: Context) = DecorativeTextMenuCandidate( + label, + textStyle = TextStyle( + size = if (textSize == NO_ID.toFloat()) null else textSize, + color = if (textColorResource == NO_ID) null else getColor(context, textColorResource), + textStyle = textStyle, + textAlignment = textAlignment, + ), + containerStyle = ContainerStyle(isVisible = visible()), + ) +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuCheckbox.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuCheckbox.kt new file mode 100644 index 0000000000..e271ecd0bd --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuCheckbox.kt @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.content.Context +import mozilla.components.browser.menu.R +import mozilla.components.concept.menu.candidate.CompoundMenuCandidate + +/** + * A simple browser menu checkbox. + * + * @param label The visible label of this menu item. + * @param initialState The initial value the checkbox should have. + * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu. + * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards + * depending on the menu position). + * @param listener Callback to be invoked when this menu item is checked. + */ +class BrowserMenuCheckbox( + label: String, + initialState: () -> Boolean = { false }, + override val isCollapsingMenuLimit: Boolean = false, + override val isSticky: Boolean = false, + listener: (Boolean) -> Unit, +) : BrowserMenuCompoundButton(label, isCollapsingMenuLimit, isSticky, initialState, listener) { + override fun getLayoutResource() = R.layout.mozac_browser_menu_item_checkbox + + override fun asCandidate(context: Context) = super.asCandidate(context).copy( + end = CompoundMenuCandidate.ButtonType.CHECKBOX, + ) +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuCompoundButton.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuCompoundButton.kt new file mode 100644 index 0000000000..09071b4c39 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuCompoundButton.kt @@ -0,0 +1,72 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.content.Context +import android.view.View +import android.view.ViewTreeObserver +import android.widget.CompoundButton +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.BrowserMenuItem +import mozilla.components.concept.menu.candidate.CompoundMenuCandidate +import mozilla.components.concept.menu.candidate.ContainerStyle + +/** + * A browser menu compound button. A basic sub-class would only have to provide a layout resource to + * satisfy [BrowserMenuItem.getLayoutResource] which contains a [View] that inherits from [CompoundButton]. + * + * @param label The visible label of this menu item. + * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu. + * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards + * depending on the menu position). + * @param initialState The initial value the checkbox should have. + * @param listener Callback to be invoked when this menu item is checked. + */ +abstract class BrowserMenuCompoundButton( + internal val label: String, + override val isCollapsingMenuLimit: Boolean = false, + override val isSticky: Boolean = false, + private val initialState: () -> Boolean = { false }, + private val listener: (Boolean) -> Unit, +) : BrowserMenuItem { + override var visible: () -> Boolean = { true } + + override fun bind(menu: BrowserMenu, view: View) { + // A CompoundButton containing CompoundDrawables needs to know where to place them (LTR / RTL). + // If the View is not yet attached to Window the direction inference will fail and the menu item + // will return from it's onMeasure a width smaller with the size + padding of the compound drawables. + // Work around this by setting a valid layout direction and reset it to inherit from parent later. + if (!view.isAttachedToWindow) { + view.layoutDirection = View.LAYOUT_DIRECTION_LOCALE + + view.viewTreeObserver.addOnPreDrawListener( + object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + view.viewTreeObserver.removeOnPreDrawListener(this) + view.layoutDirection = View.LAYOUT_DIRECTION_INHERIT + return true + } + }, + ) + } + + (view as CompoundButton).apply { + text = label + isChecked = initialState() + setOnCheckedChangeListener { _, checked -> + listener(checked) + menu.dismiss() + } + } + } + + override fun asCandidate(context: Context) = CompoundMenuCandidate( + label, + isChecked = initialState(), + end = CompoundMenuCandidate.ButtonType.CHECKBOX, + containerStyle = ContainerStyle(isVisible = visible()), + onCheckedChange = listener, + ) +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuDivider.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuDivider.kt new file mode 100644 index 0000000000..6e16b12f53 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuDivider.kt @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.content.Context +import android.view.View +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.BrowserMenuItem +import mozilla.components.browser.menu.R +import mozilla.components.concept.menu.candidate.ContainerStyle +import mozilla.components.concept.menu.candidate.DividerMenuCandidate + +/** + * A browser menu item to display a horizontal divider. + */ +class BrowserMenuDivider : BrowserMenuItem { + override var visible: () -> Boolean = { true } + + override val interactiveCount: () -> Int = { 0 } + + override fun getLayoutResource() = R.layout.mozac_browser_menu_item_divider + + override fun bind(menu: BrowserMenu, view: View) = Unit + + override fun asCandidate(context: Context) = DividerMenuCandidate( + containerStyle = ContainerStyle(isVisible = visible()), + ) +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableItem.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableItem.kt new file mode 100644 index 0000000000..e21dccf827 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableItem.kt @@ -0,0 +1,212 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.content.Context +import android.content.res.ColorStateList +import android.view.View +import android.widget.TextView +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.appcompat.widget.AppCompatImageView +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.BrowserMenuHighlight +import mozilla.components.browser.menu.HighlightableMenuItem +import mozilla.components.browser.menu.R +import mozilla.components.concept.menu.candidate.DrawableMenuIcon +import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect +import mozilla.components.concept.menu.candidate.LowPriorityHighlightEffect +import mozilla.components.concept.menu.candidate.TextMenuCandidate + +@Suppress("Deprecation") +private val defaultHighlight = BrowserMenuHighlightableItem.Highlight(0, 0, 0, 0) + +/** + * A menu item for displaying text with an image icon and a highlight state which sets the + * background of the menu item and a second image icon to the right of the text. + * + * @param label The default visible label of this menu item. + * @param startImageResource ID of a drawable resource to be shown as a leftmost icon. + * @param iconTintColorResource Optional ID of color resource to tint the icon. + * @param textColorResource Optional ID of color resource to tint the text. + * @param enabled Sets the enabled status for the view. By default, it is true. + * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu. + * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards + * depending on the menu position). + * @param highlight Highlight object representing how the menu item will be displayed when highlighted. + * @param isHighlighted Whether or not to display the highlight + * @param listener Callback to be invoked when this menu item is clicked. + */ +class BrowserMenuHighlightableItem( + private val label: String, + @DrawableRes private val startImageResource: Int, + @ColorRes iconTintColorResource: Int = NO_ID, + @ColorRes private val textColorResource: Int = NO_ID, + enabled: Boolean = true, + override val isCollapsingMenuLimit: Boolean = false, + override val isSticky: Boolean = false, + override val highlight: BrowserMenuHighlight, + override val isHighlighted: () -> Boolean = { true }, + private val listener: () -> Unit = {}, +) : BrowserMenuImageText( + label, + startImageResource, + iconTintColorResource, + textColorResource, + enabled, + isCollapsingMenuLimit, + isSticky, + listener, +), + HighlightableMenuItem { + + @Deprecated("Use the new constructor") + @Suppress("Deprecation") // Constructor uses old highlight type + constructor( + label: String, + @DrawableRes + imageResource: Int, + @ColorRes + iconTintColorResource: Int = NO_ID, + @ColorRes + textColorResource: Int = NO_ID, + enabled: Boolean = true, + isCollapsingMenuLimit: Boolean = false, + isSticky: Boolean = false, + highlight: Highlight? = null, + listener: () -> Unit = {}, + ) : this( + label, + imageResource, + iconTintColorResource, + textColorResource, + enabled, + isCollapsingMenuLimit, + isSticky, + highlight ?: defaultHighlight, + { highlight != null }, + listener, + ) + + private var wasHighlighted = false + + override fun getLayoutResource() = R.layout.mozac_browser_menu_highlightable_item + + override fun bind(menu: BrowserMenu, view: View) { + super.bind(menu, view) + + val endImageView = view.findViewById<AppCompatImageView>(R.id.end_image) + endImageView.setTintResource(iconTintColorResource) + + val highlightedTextView = view.findViewById<TextView>(R.id.highlight_text) + highlightedTextView.text = highlight.label ?: label + + wasHighlighted = isHighlighted() + updateHighlight(view, wasHighlighted) + } + + override fun invalidate(view: View) { + val isNowHighlighted = isHighlighted() + if (isNowHighlighted != wasHighlighted) { + wasHighlighted = isNowHighlighted + updateHighlight(view, isNowHighlighted) + } + } + + private fun updateHighlight(view: View, isHighlighted: Boolean) { + val startImageView = view.findViewById<AppCompatImageView>(R.id.image) + val endImageView = view.findViewById<AppCompatImageView>(R.id.end_image) + val notificationDotView = view.findViewById<AppCompatImageView>(R.id.notification_dot) + val textView = view.findViewById<TextView>(R.id.text) + val highlightedTextView = view.findViewById<TextView>(R.id.highlight_text) + + if (isHighlighted) { + @Suppress("Deprecation") + when (highlight) { + is BrowserMenuHighlight.HighPriority -> { + textView.visibility = View.INVISIBLE + highlightedTextView.visibility = View.VISIBLE + view.setBackgroundColor(highlight.backgroundTint) + if (highlight.endImageResource != NO_ID) { + endImageView.setImageResource(highlight.endImageResource) + } + endImageView.visibility = View.VISIBLE + } + is BrowserMenuHighlight.LowPriority -> { + textView.visibility = View.INVISIBLE + highlightedTextView.visibility = View.VISIBLE + notificationDotView.imageTintList = ColorStateList.valueOf(highlight.notificationTint) + notificationDotView.visibility = View.VISIBLE + view.contentDescription = "${notificationDotView.contentDescription}, ${textView.text}" + } + is BrowserMenuHighlight.ClassicHighlight -> { + view.setBackgroundResource(highlight.backgroundResource) + if (highlight.startImageResource != NO_ID) { + startImageView.setImageResource(highlight.startImageResource) + } + if (highlight.endImageResource != NO_ID) { + endImageView.setImageResource(highlight.endImageResource) + } + endImageView.visibility = View.VISIBLE + } + } + } else { + textView.visibility = View.VISIBLE + highlightedTextView.visibility = View.INVISIBLE + view.background = null + endImageView.setImageDrawable(null) + endImageView.visibility = View.GONE + notificationDotView.visibility = View.GONE + } + } + + override fun asCandidate(context: Context): TextMenuCandidate { + val base = super.asCandidate(context) as TextMenuCandidate + if (!isHighlighted()) return base + + @Suppress("Deprecation") + return when (highlight) { + is BrowserMenuHighlight.HighPriority -> base.copy( + text = highlight.label ?: label, + end = if (highlight.endImageResource == NO_ID) { + null + } else { + DrawableMenuIcon( + context, + highlight.endImageResource, + ) + }, + effect = HighPriorityHighlightEffect( + backgroundTint = highlight.backgroundTint, + ), + ) + is BrowserMenuHighlight.LowPriority -> base.copy( + text = highlight.label ?: label, + start = (base.start as? DrawableMenuIcon)?.copy( + effect = LowPriorityHighlightEffect(notificationTint = highlight.notificationTint), + ), + ) + is BrowserMenuHighlight.ClassicHighlight -> base + } + } + + /** + * Described how to display a [BrowserMenuHighlightableItem] when it is highlighted. + * Replaced by [BrowserMenuHighlight] which lets a priority be specified. + */ + @Deprecated("Replace with BrowserMenuHighlight.LowPriority or BrowserMenuHighlight.HighPriority") + @Suppress("Deprecation") + class Highlight( + @DrawableRes startImageResource: Int = NO_ID, + @DrawableRes endImageResource: Int = NO_ID, + @DrawableRes backgroundResource: Int, + @ColorRes colorResource: Int, + ) : BrowserMenuHighlight.ClassicHighlight( + startImageResource, + endImageResource, + backgroundResource, + colorResource, + ) +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableSwitch.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableSwitch.kt new file mode 100644 index 0000000000..4eb2c1cb8e --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableSwitch.kt @@ -0,0 +1,99 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.content.Context +import android.content.res.ColorStateList +import android.view.View +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.appcompat.widget.AppCompatImageView +import androidx.appcompat.widget.SwitchCompat +import androidx.core.view.isVisible +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.BrowserMenuHighlight +import mozilla.components.browser.menu.HighlightableMenuItem +import mozilla.components.browser.menu.R +import mozilla.components.concept.menu.candidate.CompoundMenuCandidate +import mozilla.components.concept.menu.candidate.DrawableMenuIcon +import mozilla.components.concept.menu.candidate.LowPriorityHighlightEffect + +/** + * A browser menu switch that can show a highlighted icon. + * + * @param label The visible label of this menu item. + * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu. + * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards + * depending on the menu position). + * @param initialState The initial value the checkbox should have. + * @param listener Callback to be invoked when this menu item is checked. + */ +class BrowserMenuHighlightableSwitch( + label: String, + @DrawableRes private val startImageResource: Int, + @ColorRes private val iconTintColorResource: Int = NO_ID, + @ColorRes private val textColorResource: Int = NO_ID, + override val isCollapsingMenuLimit: Boolean = false, + override val isSticky: Boolean = false, + override val highlight: BrowserMenuHighlight.LowPriority, + override val isHighlighted: () -> Boolean = { true }, + initialState: () -> Boolean = { false }, + listener: (Boolean) -> Unit, +) : BrowserMenuCompoundButton(label, isCollapsingMenuLimit, isSticky, initialState, listener), HighlightableMenuItem { + + private var wasHighlighted = false + + override fun getLayoutResource(): Int = R.layout.mozac_browser_menu_highlightable_switch + + override fun bind(menu: BrowserMenu, view: View) { + super.bind(menu, view.findViewById<SwitchCompat>(R.id.switch_widget)) + setTints(view) + + val notificationDotView = view.findViewById<AppCompatImageView>(R.id.notification_dot) + notificationDotView.imageTintList = ColorStateList.valueOf(highlight.notificationTint) + + wasHighlighted = isHighlighted() + updateHighlight(view, wasHighlighted) + } + + override fun invalidate(view: View) { + val isNowHighlighted = isHighlighted() + if (isNowHighlighted != wasHighlighted) { + wasHighlighted = isNowHighlighted + updateHighlight(view, isNowHighlighted) + } + } + + private fun setTints(view: View) { + val switch = view.findViewById<SwitchCompat>(R.id.switch_widget) + switch.setColorResource(textColorResource) + + val imageView = view.findViewById<AppCompatImageView>(R.id.image) + imageView.setImageResource(startImageResource) + imageView.setTintResource(iconTintColorResource) + } + + private fun updateHighlight(view: View, isHighlighted: Boolean) { + val notificationDotView = view.findViewById<AppCompatImageView>(R.id.notification_dot) + val switch = view.findViewById<SwitchCompat>(R.id.switch_widget) + + notificationDotView.isVisible = isHighlighted + switch.text = if (isHighlighted) highlight.label ?: label else label + } + + override fun asCandidate(context: Context): CompoundMenuCandidate { + val base = super.asCandidate(context) + return if (isHighlighted()) { + base.copy( + text = highlight.label ?: label, + start = (base.start as? DrawableMenuIcon)?.copy( + effect = LowPriorityHighlightEffect(notificationTint = highlight.notificationTint), + ), + ) + } else { + base + } + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuImageSwitch.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuImageSwitch.kt new file mode 100644 index 0000000000..d98afe2bb8 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuImageSwitch.kt @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.content.Context +import android.view.View +import androidx.annotation.DrawableRes +import androidx.annotation.VisibleForTesting +import androidx.appcompat.widget.SwitchCompat +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.R +import mozilla.components.concept.menu.candidate.CompoundMenuCandidate +import mozilla.components.concept.menu.candidate.DrawableMenuIcon +import java.lang.reflect.Modifier + +/** + * A simple browser menu switch. + * + * @param imageResource ID of a drawable resource to be shown as icon. + * @param label The visible label of this menu item. + * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu. + * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards + * depending on the menu position). + * @param initialState The initial value the checkbox should have. + * @param listener Callback to be invoked when this menu item is checked. + */ +class BrowserMenuImageSwitch( + @get:VisibleForTesting(otherwise = Modifier.PRIVATE) + @DrawableRes + val imageResource: Int, + label: String, + override val isCollapsingMenuLimit: Boolean = false, + override val isSticky: Boolean = false, + initialState: () -> Boolean = { false }, + listener: (Boolean) -> Unit, +) : BrowserMenuCompoundButton(label, isCollapsingMenuLimit, isSticky, initialState, listener) { + override fun getLayoutResource(): Int = R.layout.mozac_browser_menu_item_image_switch + + override fun bind(menu: BrowserMenu, view: View) { + super.bind(menu, view) + bindImage(view as SwitchCompat) + } + + private fun bindImage(switch: SwitchCompat) { + switch.setCompoundDrawablesRelativeWithIntrinsicBounds( + imageResource, + 0, + 0, + 0, + ) + } + + override fun asCandidate(context: Context) = super.asCandidate(context).copy( + start = DrawableMenuIcon(context, imageResource), + end = CompoundMenuCandidate.ButtonType.SWITCH, + ) +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuImageText.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuImageText.kt new file mode 100644 index 0000000000..25542f6a4e --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuImageText.kt @@ -0,0 +1,111 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.content.Context +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.appcompat.widget.AppCompatImageView +import androidx.core.content.ContextCompat +import androidx.core.content.ContextCompat.getColor +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.BrowserMenuItem +import mozilla.components.browser.menu.R +import mozilla.components.concept.menu.candidate.ContainerStyle +import mozilla.components.concept.menu.candidate.DrawableMenuIcon +import mozilla.components.concept.menu.candidate.MenuCandidate +import mozilla.components.concept.menu.candidate.TextMenuCandidate +import mozilla.components.concept.menu.candidate.TextStyle + +internal const val NO_ID = -1 + +internal fun ImageView.setTintResource(@ColorRes tintColorResource: Int) { + if (tintColorResource != NO_ID) { + imageTintList = ContextCompat.getColorStateList(context, tintColorResource) + } +} + +internal fun TextView.setColorResource(@ColorRes textColorResource: Int) { + if (textColorResource != NO_ID) { + setTextColor(ContextCompat.getColor(context, textColorResource)) + } +} + +/** + * A menu item for displaying text with an image icon. + * + * @param label The visible label of this menu item. + * @param imageResource ID of a drawable resource to be shown as icon. + * @param iconTintColorResource Optional ID of color resource to tint the icon. + * @param textColorResource Optional ID of color resource to tint the text. + * @param enabled Sets the enabled status for the view. By default, it is true. + * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu. + * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards + * depending on the menu position). + * @param listener Callback to be invoked when this menu item is clicked. + */ +open class BrowserMenuImageText( + private val label: String, + @DrawableRes + internal val imageResource: Int, + @ColorRes + open var iconTintColorResource: Int = NO_ID, + @ColorRes + private val textColorResource: Int = NO_ID, + open var enabled: Boolean = true, + override val isCollapsingMenuLimit: Boolean = false, + override val isSticky: Boolean = false, + private val listener: () -> Unit = {}, +) : BrowserMenuItem { + + override var visible: () -> Boolean = { true } + + override fun getLayoutResource() = R.layout.mozac_browser_menu_item_image_text + + override fun bind(menu: BrowserMenu, view: View) { + bindText(view) + + bindImage(view) + + view.setOnClickListener { + listener.invoke() + menu.dismiss() + } + view.isEnabled = enabled + view.contentDescription = label + } + + private fun bindText(view: View) { + val textView = view.findViewById<TextView>(R.id.text) + textView.text = label + textView.setColorResource(textColorResource) + textView.isEnabled = enabled + } + + private fun bindImage(view: View) { + val imageView = view.findViewById<AppCompatImageView>(R.id.image) + with(imageView) { + setImageResource(imageResource) + setTintResource(iconTintColorResource) + } + } + + override fun asCandidate(context: Context): MenuCandidate = TextMenuCandidate( + label, + start = DrawableMenuIcon( + context, + resource = imageResource, + tint = if (iconTintColorResource == NO_ID) null else getColor(context, iconTintColorResource), + ), + textStyle = TextStyle( + color = if (textColorResource == NO_ID) null else getColor(context, textColorResource), + ), + containerStyle = ContainerStyle(isVisible = visible()), + onClick = listener, + ) +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuImageTextCheckboxButton.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuImageTextCheckboxButton.kt new file mode 100644 index 0000000000..8d75b2a90a --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuImageTextCheckboxButton.kt @@ -0,0 +1,111 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.view.View +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.annotation.VisibleForTesting +import androidx.appcompat.widget.AppCompatCheckBox +import androidx.core.content.ContextCompat +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.R +import mozilla.components.support.ktx.android.util.dpToPx + +/** + * A browser menu item with image and label and a custom checkbox. + * + * @param imageResource ID of a drawable resource to be shown as icon. + * @param iconTintColorResource Optional ID of color resource to tint the icon. + * @param label The visible label of this menu item. + * @param textColorResource Optional ID of color resource to tint the text. + * @param enabled Sets the enabled status for the view. By default, it is true. + * @param labelListener Callback to be invoked when this menu item is clicked. + * @param primaryStateIconResource ID of a drawable resource for checkbox drawable in primary state. + * @param secondaryStateIconResource ID of a drawable resource for checkbox drawable in secondary state. + * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu. + * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards + * depending on the menu position). + * @param iconTintColorResource Optional ID of color resource to tint the checkbox drawable. + * @param primaryLabel The visible label of the checkbox in primary state. + * @param secondaryLabel The visible label of this menu item in secondary state. + * @param isInPrimaryState Lambda to return true/false to indicate checkbox primary or secondary state. + * @param onCheckedChangedListener Callback to be invoked when checkbox is clicked. + */ +@Suppress("LongParameterList") +class BrowserMenuImageTextCheckboxButton( + @DrawableRes imageResource: Int, + private val label: String, + @ColorRes iconTintColorResource: Int = NO_ID, + @ColorRes internal val textColorResource: Int = NO_ID, + enabled: Boolean = true, + @get:VisibleForTesting internal val labelListener: () -> Unit, + @DrawableRes val primaryStateIconResource: Int, + @DrawableRes val secondaryStateIconResource: Int, + @ColorRes internal val tintColorResource: Int = NO_ID, + private val primaryLabel: String, + private val secondaryLabel: String, + override val isCollapsingMenuLimit: Boolean = false, + override val isSticky: Boolean = false, + val isInPrimaryState: () -> Boolean = { true }, + private val onCheckedChangedListener: (Boolean) -> Unit, +) : BrowserMenuImageText( + label, + imageResource, + iconTintColorResource, + textColorResource, + enabled, + isCollapsingMenuLimit, + isSticky, + labelListener, +) { + override var visible: () -> Boolean = { true } + override fun getLayoutResource(): Int = R.layout.mozac_browser_menu_item_image_text_checkbox_button + + override fun bind(menu: BrowserMenu, view: View) { + super.bind(menu, view) + + view.findViewById<View>(R.id.accessibilityRegion).apply { + setOnClickListener { labelListener.invoke() } + contentDescription = label + } + + bindCheckbox(menu, view.findViewById(R.id.checkbox) as AppCompatCheckBox) + } + + private fun bindCheckbox(menu: BrowserMenu, button: AppCompatCheckBox) { + val buttonText = if (isInPrimaryState()) primaryLabel else secondaryLabel + val tintColor = ContextCompat.getColor(button.context, tintColorResource) + val buttonDrawableIcon = if (isInPrimaryState()) { + ContextCompat.getDrawable(button.context, primaryStateIconResource) + } else { + ContextCompat.getDrawable(button.context, secondaryStateIconResource) + } + buttonDrawableIcon?.setTint(tintColor) + val displayMetrics = button.context.resources.displayMetrics + + buttonDrawableIcon?.setBounds( + 0, + 0, + CHECKBOX_ICON_SIZE_DP.dpToPx(displayMetrics), + CHECKBOX_ICON_SIZE_DP.dpToPx(displayMetrics), + ) + + button.apply { + text = buttonText + setTextColor(tintColor) + setCompoundDrawables(buttonDrawableIcon, null, null, null) + + setOnCheckedChangeListener { _, isChecked -> + onCheckedChangedListener(isChecked) + menu.dismiss() + } + } + } + + companion object { + private const val CHECKBOX_ICON_SIZE_DP = 19 + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuItemToolbar.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuItemToolbar.kt new file mode 100644 index 0000000000..bd15f5c93b --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuItemToolbar.kt @@ -0,0 +1,212 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.content.Context +import android.os.Build +import android.view.View +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.appcompat.widget.AppCompatImageButton +import androidx.appcompat.widget.TooltipCompat +import androidx.core.content.ContextCompat.getColor +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.BrowserMenuItem +import mozilla.components.browser.menu.R +import mozilla.components.concept.menu.candidate.ContainerStyle +import mozilla.components.concept.menu.candidate.DrawableMenuIcon +import mozilla.components.concept.menu.candidate.RowMenuCandidate +import mozilla.components.concept.menu.candidate.SmallMenuCandidate +import mozilla.components.support.ktx.android.content.res.resolveAttribute + +/** + * A toolbar of buttons to show inside the browser menu. + * + * @param items buttons that will be shown in a horizontal layout + * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu. + * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards + * depending on the menu position). + */ +class BrowserMenuItemToolbar( + private val items: List<Button>, + override val isCollapsingMenuLimit: Boolean = false, + override val isSticky: Boolean = false, +) : BrowserMenuItem { + override var visible: () -> Boolean = { true } + + override val interactiveCount: () -> Int = { items.size } + + override fun getLayoutResource() = R.layout.mozac_browser_menu_item_toolbar + + override fun bind(menu: BrowserMenu, view: View) { + val layout = view as LinearLayout + layout.removeAllViews() + + val selectableBackground = + layout.context.theme.resolveAttribute(android.R.attr.selectableItemBackgroundBorderless) + + for (item in items) { + val button = AppCompatImageButton(layout.context) + item.bind(button) + + button.setFocusable(true) + button.setBackgroundResource(selectableBackground) + button.setOnClickListener { + item.listener() + menu.dismiss() + } + button.setOnLongClickListener { + item.longClickListener?.invoke() + menu.dismiss() + true + } + button.isLongClickable = item.longClickListener != null + + layout.addView(button, LinearLayout.LayoutParams(0, MATCH_PARENT, 1f)) + } + } + + override fun invalidate(view: View) { + val layout = view as LinearLayout + items.withIndex().forEach { (index, item) -> + item.invalidate(layout.getChildAt(index) as AppCompatImageButton) + } + } + + override fun asCandidate(context: Context) = RowMenuCandidate( + items = items.map { it.asCandidate(context) }, + containerStyle = ContainerStyle(isVisible = visible()), + ) + + /** + * A button to be shown in a toolbar inside the browser menu. + * + * @param imageResource ID of a drawable resource to be shown as icon. + * @param contentDescription The button's content description, used for accessibility support. + * @param iconTintColorResource Optional ID of color resource to tint the icon. + * @param isEnabled Lambda to return true/false to indicate if this button should be enabled or disabled. + * @param longClickListener Callback to be invoked when the button is long clicked. + * @param listener Callback to be invoked when the button is pressed. + */ + open class Button( + @DrawableRes val imageResource: Int, + val contentDescription: String, + @ColorRes val iconTintColorResource: Int = NO_ID, + val isEnabled: () -> Boolean = { true }, + val longClickListener: (() -> Unit)? = null, + val listener: () -> Unit, + ) { + + internal open fun bind(view: ImageView) { + view.setImageResource(imageResource) + view.contentDescription = contentDescription + setTooltipTextCompatible(view, contentDescription) + view.setTintResource(iconTintColorResource) + view.isEnabled = isEnabled() + } + + internal open fun invalidate(view: ImageView) { + view.isEnabled = isEnabled() + } + + internal open fun asCandidate(context: Context) = SmallMenuCandidate( + contentDescription, + icon = DrawableMenuIcon( + context, + resource = imageResource, + tint = if (iconTintColorResource == NO_ID) null else getColor(context, iconTintColorResource), + ), + containerStyle = ContainerStyle(isEnabled = isEnabled()), + onClick = listener, + ) + + internal fun setTooltipTextCompatible(view: ImageView, contentDescription: String) { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O) { + CustomTooltip.setTooltipText(view, contentDescription) + } else { + TooltipCompat.setTooltipText(view, contentDescription) + } + } + } + + /** + * A button that either shows an primary state or an secondary state based on the provided + * <code>isInPrimaryState</code> lambda. + * + * @param primaryImageResource ID of a drawable resource to be shown as primary icon. + * @param primaryContentDescription The button's primary content description, used for accessibility support. + * @param primaryImageTintResource Optional ID of color resource to tint the primary icon. + * @param secondaryImageResource Optional ID of a different drawable resource to be shown as secondary icon. + * @param secondaryContentDescription Optional secondary content description for button, for accessibility support. + * @param secondaryImageTintResource Optional ID of secondary color resource to tint the icon. + * @param isInPrimaryState Lambda to return true/false to indicate if this button should be primary or secondary. + * @param disableInSecondaryState Optional boolean to disable the button when in secondary state. + * @param longClickListener Callback to be invoked when the button is long clicked. + * @param listener Callback to be invoked when the button is pressed. + */ + open class TwoStateButton( + @DrawableRes val primaryImageResource: Int, + val primaryContentDescription: String, + @ColorRes val primaryImageTintResource: Int = NO_ID, + @DrawableRes val secondaryImageResource: Int = primaryImageResource, + val secondaryContentDescription: String = primaryContentDescription, + @ColorRes val secondaryImageTintResource: Int = primaryImageTintResource, + val isInPrimaryState: () -> Boolean = { true }, + val disableInSecondaryState: Boolean = false, + longClickListener: (() -> Unit)? = null, + listener: () -> Unit, + ) : Button( + primaryImageResource, + primaryContentDescription, + primaryImageTintResource, + isInPrimaryState, + longClickListener = longClickListener, + listener = listener, + ) { + + private var wasInPrimaryState = false + + override fun bind(view: ImageView) { + if (isInPrimaryState()) { + super.bind(view) + } else { + view.setImageResource(secondaryImageResource) + view.contentDescription = secondaryContentDescription + setTooltipTextCompatible(view, secondaryContentDescription) + view.setTintResource(secondaryImageTintResource) + view.isEnabled = !disableInSecondaryState + } + wasInPrimaryState = isInPrimaryState() + } + + override fun invalidate(view: ImageView) { + if (isInPrimaryState() != wasInPrimaryState) { + bind(view) + } + } + + override fun asCandidate(context: Context): SmallMenuCandidate = if (isInPrimaryState()) { + super.asCandidate(context) + } else { + SmallMenuCandidate( + secondaryContentDescription, + icon = DrawableMenuIcon( + context, + resource = secondaryImageResource, + tint = if (secondaryImageTintResource == NO_ID) { + null + } else { + getColor(context, secondaryImageTintResource) + }, + ), + containerStyle = ContainerStyle(isEnabled = !disableInSecondaryState), + onClick = listener, + ) + } + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuSwitch.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuSwitch.kt new file mode 100644 index 0000000000..abea7b218f --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuSwitch.kt @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.content.Context +import mozilla.components.browser.menu.R +import mozilla.components.concept.menu.candidate.CompoundMenuCandidate + +/** + * A simple browser menu switch. + * + * @param label The visible label of this menu item. + * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu. + * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards + * depending on the menu position). + * @param initialState The initial value the checkbox should have. + * @param listener Callback to be invoked when this menu item is checked. + */ +class BrowserMenuSwitch( + label: String, + override val isCollapsingMenuLimit: Boolean = false, + override val isSticky: Boolean = false, + initialState: () -> Boolean = { false }, + listener: (Boolean) -> Unit, +) : BrowserMenuCompoundButton(label, isCollapsingMenuLimit, isSticky, initialState, listener) { + override fun getLayoutResource(): Int = R.layout.mozac_browser_menu_item_switch + + override fun asCandidate(context: Context) = super.asCandidate(context).copy( + end = CompoundMenuCandidate.ButtonType.SWITCH, + ) +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/CustomTooltip.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/CustomTooltip.kt new file mode 100644 index 0000000000..9e7ce8b674 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/CustomTooltip.kt @@ -0,0 +1,154 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.annotation.SuppressLint +import android.text.TextUtils +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.View.OnLongClickListener +import android.view.WindowManager +import android.widget.LinearLayout +import android.widget.PopupWindow +import android.widget.TextView +import androidx.core.view.ViewCompat +import androidx.core.widget.PopupWindowCompat +import mozilla.components.browser.menu.R + +/** + * A tooltip shown on long click on an anchor view. + * There can be only one tooltip shown at a given moment. + */ +internal class CustomTooltip private constructor( + private val anchor: View, + private val tooltipText: CharSequence, +) : OnLongClickListener, View.OnAttachStateChangeListener { + private val hideTooltipRunnable = Runnable { hide() } + private var popupWindow: PopupWindow? = null + + init { + anchor.setOnLongClickListener(this) + } + + override fun onLongClick(view: View): Boolean { + if (ViewCompat.isAttachedToWindow(anchor)) { + show() + anchor.addOnAttachStateChangeListener(this) + } + return true + } + + private fun computeOffsets(): Offset { + // Measure pop-up + val spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) + popupWindow?.contentView?.measure(spec, spec) + + val rootView = anchor.rootView + val rootPosition = IntArray(2) + val anchorPosition = IntArray(2) + rootView.getLocationOnScreen(rootPosition) + anchor.getLocationOnScreen(anchorPosition) + + val rootY = rootPosition[1] + val anchorY = anchorPosition[1] + + val rootHeight = rootView.height + val tooltipHeight = popupWindow?.contentView?.measuredHeight ?: 0 + val tooltipWidth = popupWindow?.contentView?.measuredWidth ?: 0 + + val checkY = rootY + rootHeight - (anchorY + anchor.height + tooltipHeight + TOOLTIP_EXTRA_VERTICAL_OFFSET_DP) + val belowY = TOOLTIP_EXTRA_VERTICAL_OFFSET_DP + val aboveY = -(anchor.height + tooltipHeight + TOOLTIP_EXTRA_VERTICAL_OFFSET_DP) + + // align anchor center with tooltip center + val offsetX = anchor.width / 2 - tooltipWidth / 2 + // make sure tooltip is visible and it's not displayed below, outside the view + val offsetY = if (checkY > 0) belowY else aboveY + return Offset(offsetX, offsetY) + } + + @SuppressLint("InflateParams") + fun show() { + activeTooltip?.hide() + activeTooltip = this + + val layout = LayoutInflater.from(anchor.context) + .inflate(R.layout.mozac_browser_tooltip_layout, null) + + layout.findViewById<TextView>(R.id.mozac_browser_tooltip_text).text = tooltipText + popupWindow = PopupWindow( + layout, + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT, + false, + ) + + val offsets = computeOffsets() + + popupWindow?.let { + PopupWindowCompat.setWindowLayoutType(it, WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL) + it.isTouchable = false + it.showAsDropDown(anchor, offsets.x, offsets.y, Gravity.CENTER) + } + + anchor.removeCallbacks(hideTooltipRunnable) + anchor.postDelayed(hideTooltipRunnable, LONG_CLICK_HIDE_TIMEOUT_MS) + } + + fun hide() { + if (activeTooltip === this) { + activeTooltip = null + popupWindow?.let { + it.dismiss() + popupWindow = null + anchor.removeOnAttachStateChangeListener(this) + } + } + } + + override fun onViewAttachedToWindow(v: View) { + // no-op + } + + override fun onViewDetachedFromWindow(v: View) { + hide() + anchor.removeCallbacks(hideTooltipRunnable) + } + + companion object { + private const val LONG_CLICK_HIDE_TIMEOUT_MS: Long = 2500 + private const val TOOLTIP_EXTRA_VERTICAL_OFFSET_DP = 8 + + // The tooltip currently being shown properly disposed in hide() / onViewDetachedFromWindow() + @SuppressLint("StaticFieldLeak") + private var activeTooltip: CustomTooltip? = null + + /** + * Set the tooltip text for the view. + * @param view view to set the tooltip for + * @param tooltipText the tooltip text + */ + fun setTooltipText(view: View, tooltipText: CharSequence) { + // check for dynamic content description + if (TextUtils.isEmpty(tooltipText)) { + activeTooltip?.let { + if (it.anchor === view) { + it.hide() + } + } + view.setOnLongClickListener(null) + view.isLongClickable = false + } else { + CustomTooltip(view, tooltipText) + } + } + } + + /** + * A data class for storing x and y offsets + */ + data class Offset(val x: Int, val y: Int) +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/ParentBrowserMenuItem.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/ParentBrowserMenuItem.kt new file mode 100644 index 0000000000..5d910967ac --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/ParentBrowserMenuItem.kt @@ -0,0 +1,105 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.content.Context +import android.view.View +import android.widget.TextView +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.appcompat.widget.AppCompatImageView +import androidx.core.content.ContextCompat +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.R +import mozilla.components.browser.menu.ext.asCandidateList +import mozilla.components.concept.menu.candidate.ContainerStyle +import mozilla.components.concept.menu.candidate.DrawableMenuIcon +import mozilla.components.concept.menu.candidate.NestedMenuCandidate +import mozilla.components.concept.menu.candidate.TextStyle + +/** + * A parent menu item for displaying text and an image icon with a nested sub menu. + * It handles back pressing if the sub menu contains a [BackPressMenuItem]. + * + * @param label The visible label of this menu item. + * @param imageResource ID of a drawable resource to be shown as icon. + * @param iconTintColorResource Optional ID of color resource to tint the icon. + * @param textColorResource Optional ID of color resource to tint the text. + * @property subMenu Target sub menu to be shown when this menu item is clicked. + * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu. + * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards + * depending on the menu position). + * @param endOfMenuAlwaysVisible when is set to true makes sure the bottom of the menu is always visible + * otherwise, the top of the menu is always visible. + */ +class ParentBrowserMenuItem( + internal val label: String, + @DrawableRes + private val imageResource: Int, + @ColorRes + private val iconTintColorResource: Int = NO_ID, + @ColorRes + private val textColorResource: Int = NO_ID, + internal val subMenu: BrowserMenu, + override val isCollapsingMenuLimit: Boolean = false, + override val isSticky: Boolean = false, + endOfMenuAlwaysVisible: Boolean = false, +) : AbstractParentBrowserMenuItem(subMenu, isCollapsingMenuLimit, endOfMenuAlwaysVisible) { + + override var visible: () -> Boolean = { true } + override fun getLayoutResource() = R.layout.mozac_browser_menu_item_parent_menu + + override fun bind(menu: BrowserMenu, view: View) { + bindText(view) + bindImage(view) + bindBackPress(menu, view) + + super.bind(menu, view) + } + + private fun bindText(view: View) { + val textView = view.findViewById<TextView>(R.id.text) + textView.text = label + textView.setColorResource(textColorResource) + } + + private fun bindImage(view: View) { + val imageView = view.findViewById<AppCompatImageView>(R.id.image) + with(imageView) { + setImageResource(imageResource) + setTintResource(iconTintColorResource) + } + val overflowView = view.findViewById<AppCompatImageView>(R.id.overflowImage) + with(overflowView) { + visibility = View.VISIBLE + setTintResource(iconTintColorResource) + } + } + + private fun bindBackPress(menu: BrowserMenu, view: View) { + val backPressMenuItem = + subMenu.adapter.visibleItems.find { it is BackPressMenuItem } as? BackPressMenuItem + backPressMenuItem?.let { + backPressMenuItem.setListener { + onBackPressed(menu, view) + } + } + } + + override fun asCandidate(context: Context) = NestedMenuCandidate( + id = hashCode(), + text = label, + start = DrawableMenuIcon( + context, + resource = imageResource, + tint = if (iconTintColorResource == NO_ID) null else ContextCompat.getColor(context, iconTintColorResource), + ), + subMenuItems = subMenu.adapter.visibleItems.asCandidateList(context), + textStyle = TextStyle( + color = if (textColorResource == NO_ID) null else ContextCompat.getColor(context, textColorResource), + ), + containerStyle = ContainerStyle(isVisible = visible()), + ) +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/SimpleBrowserMenuHighlightableItem.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/SimpleBrowserMenuHighlightableItem.kt new file mode 100644 index 0000000000..3f7c2803f1 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/SimpleBrowserMenuHighlightableItem.kt @@ -0,0 +1,109 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.content.Context +import android.view.View +import android.widget.TextView +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.core.content.ContextCompat +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.BrowserMenuItem +import mozilla.components.browser.menu.R +import mozilla.components.browser.menu.ext.addRippleEffect +import mozilla.components.concept.menu.candidate.ContainerStyle +import mozilla.components.concept.menu.candidate.MenuCandidate +import mozilla.components.concept.menu.candidate.TextMenuCandidate +import mozilla.components.concept.menu.candidate.TextStyle + +/** + * A menu item for displaying text with a highlight state which sets the + * background of the menu item. + * + * @param label The default visible label of this menu item. + * @param textColorResource Optional ID of color resource to tint the text. + * @param textSize The size of the label. + * @param backgroundTint Tint for the menu item background color + * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu. + * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards + * depending on the menu position). + * @param isHighlighted Whether or not to display the highlight + * @param listener Callback to be invoked when this menu item is clicked. + */ +class SimpleBrowserMenuHighlightableItem( + private val label: String, + @ColorRes private val textColorResource: Int = NO_ID, + private val textSize: Float = NO_ID.toFloat(), + @ColorInt val backgroundTint: Int, + override val isCollapsingMenuLimit: Boolean = false, + override val isSticky: Boolean = false, + var isHighlighted: () -> Boolean = { true }, + private val listener: () -> Unit = {}, +) : BrowserMenuItem { + + override var visible: () -> Boolean = { true } + private var wasHighlighted = false + + override fun getLayoutResource() = R.layout.mozac_browser_menu_item_simple + + override fun bind(menu: BrowserMenu, view: View) { + bindText(view) + + view.setOnClickListener { + listener.invoke() + menu.dismiss() + } + + wasHighlighted = isHighlighted() + updateHighlight(view, wasHighlighted) + } + + private fun bindText(view: View) { + val textView = view as TextView + textView.text = label + textView.addRippleEffect() + + if (textColorResource != NO_ID) { + textView.setColorResource(textColorResource) + } + + if (textSize != NO_ID.toFloat()) { + textView.textSize = textSize + } + } + + override fun invalidate(view: View) { + val isNowHighlighted = isHighlighted() + if (isNowHighlighted != wasHighlighted) { + wasHighlighted = isNowHighlighted + updateHighlight(view, isNowHighlighted) + } + } + + private fun updateHighlight(view: View, isHighlighted: Boolean) { + val textView = view as TextView + + if (isHighlighted) { + textView.setBackgroundColor(backgroundTint) + } else { + textView.addRippleEffect() + } + } + + override fun asCandidate(context: Context): MenuCandidate { + val textStyle = TextStyle( + size = if (textSize == NO_ID.toFloat()) null else textSize, + color = if (textColorResource == NO_ID) null else ContextCompat.getColor(context, textColorResource), + ) + val containerStyle = ContainerStyle(isVisible = visible()) + return TextMenuCandidate( + label, + textStyle = textStyle, + containerStyle = containerStyle, + onClick = listener, + ) + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/SimpleBrowserMenuItem.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/SimpleBrowserMenuItem.kt new file mode 100644 index 0000000000..9a350e3317 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/SimpleBrowserMenuItem.kt @@ -0,0 +1,84 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.content.Context +import android.view.View +import android.widget.TextView +import androidx.annotation.ColorRes +import androidx.core.content.ContextCompat.getColor +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.BrowserMenuItem +import mozilla.components.browser.menu.R +import mozilla.components.concept.menu.candidate.ContainerStyle +import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate +import mozilla.components.concept.menu.candidate.MenuCandidate +import mozilla.components.concept.menu.candidate.TextMenuCandidate +import mozilla.components.concept.menu.candidate.TextStyle + +/** + * A simple browser menu item displaying text. + * + * @param label The visible label of this menu item. + * @param textSize: The size of the label. + * @param textColorResource: The color resource to apply to the text. + * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu. + * @param listener Callback to be invoked when this menu item is clicked. + */ +class SimpleBrowserMenuItem( + private val label: String, + private val textSize: Float = NO_ID.toFloat(), + @ColorRes + private val textColorResource: Int = NO_ID, + override val isCollapsingMenuLimit: Boolean = false, + private val listener: (() -> Unit)? = null, +) : BrowserMenuItem { + override var visible: () -> Boolean = { true } + + override fun getLayoutResource() = R.layout.mozac_browser_menu_item_simple + + override fun bind(menu: BrowserMenu, view: View) { + val textView = view as TextView + textView.text = label + + if (textSize != NO_ID.toFloat()) { + textView.textSize = textSize + } + + textView.setColorResource(textColorResource) + + if (listener != null) { + textView.setOnClickListener { + listener.invoke() + menu.dismiss() + } + } else { + // Remove the ripple effect + textView.background = null + } + } + + override fun asCandidate(context: Context): MenuCandidate { + val textStyle = TextStyle( + size = if (textSize == NO_ID.toFloat()) null else textSize, + color = if (textColorResource == NO_ID) null else getColor(context, textColorResource), + ) + val containerStyle = ContainerStyle(isVisible = visible()) + return if (listener != null) { + TextMenuCandidate( + label, + textStyle = textStyle, + containerStyle = containerStyle, + onClick = listener, + ) + } else { + DecorativeTextMenuCandidate( + label, + textStyle = textStyle, + containerStyle = containerStyle, + ) + } + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/TwoStateBrowserMenuImageText.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/TwoStateBrowserMenuImageText.kt new file mode 100644 index 0000000000..6cb2b70eb2 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/TwoStateBrowserMenuImageText.kt @@ -0,0 +1,133 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.content.Context +import android.view.View +import android.widget.TextView +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.appcompat.widget.AppCompatImageView +import androidx.core.content.ContextCompat +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.R +import mozilla.components.concept.menu.candidate.ContainerStyle +import mozilla.components.concept.menu.candidate.DrawableMenuIcon +import mozilla.components.concept.menu.candidate.MenuCandidate +import mozilla.components.concept.menu.candidate.TextMenuCandidate +import mozilla.components.concept.menu.candidate.TextStyle + +/** + * A browser menu item with two states, used for displaying text with an image icon + * + * @param primaryLabel The visible label of the checkbox in primary state. + * @param secondaryLabel The visible label of this menu item in secondary state. + * @param textColorResource Optional ID of color resource to tint the text. + * @param enabled Sets the enabled status for the view. By default, it is true. + * @param primaryStateIconResource ID of a drawable resource to be shown as icon in primary state. + * @param secondaryStateIconResource ID of a drawable resource to be shown as icon in secondary state. + * @param iconTintColorResource Optional ID of color resource to tint the checkbox drawable. + * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu. + * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards + * depending on the menu position). + * @param isInPrimaryState Lambda to return true/false to indicate item is in primary state. + * @param isInSecondaryState Lambda to return true/false to indicate item is in secondary state + * @param primaryStateAction Callback to be invoked when this menu item is clicked in primary state. + * @param secondaryStateAction Callback to be invoked when this menu item is clicked in secondary state. + */ +class TwoStateBrowserMenuImageText( + private val primaryLabel: String, + private val secondaryLabel: String, + @ColorRes internal val textColorResource: Int = NO_ID, + enabled: Boolean = true, + @DrawableRes val primaryStateIconResource: Int, + @DrawableRes val secondaryStateIconResource: Int, + @ColorRes iconTintColorResource: Int = NO_ID, + override val isCollapsingMenuLimit: Boolean = false, + override val isSticky: Boolean = false, + val isInPrimaryState: () -> Boolean = { true }, + val isInSecondaryState: () -> Boolean = { false }, + private val primaryStateAction: () -> Unit = { }, + private val secondaryStateAction: () -> Unit = { }, +) : BrowserMenuImageText( + primaryLabel, + primaryStateIconResource, + iconTintColorResource, + textColorResource, + enabled, + isCollapsingMenuLimit, + isSticky, + primaryStateAction, +) { + override var visible: () -> Boolean = { isInPrimaryState() || isInSecondaryState() } + + override fun getLayoutResource(): Int = + R.layout.mozac_browser_menu_item_image_text + + override fun bind(menu: BrowserMenu, view: View) { + val isInPrimaryState = isInPrimaryState() + bindText(view, isInPrimaryState) + bindImage(view, isInPrimaryState) + + val listener = if (isInPrimaryState) primaryStateAction else secondaryStateAction + view.setOnClickListener { + listener.invoke() + menu.dismiss() + } + } + + private fun bindText(view: View, isInPrimaryState: Boolean) { + val textView = view.findViewById<TextView>(R.id.text) + textView.text = if (isInPrimaryState) primaryLabel else secondaryLabel + textView.setColorResource(textColorResource) + } + + private fun bindImage(view: View, isInPrimaryState: Boolean) { + val imageView = view.findViewById<AppCompatImageView>(R.id.image) + val imageResource = + if (isInPrimaryState) primaryStateIconResource else secondaryStateIconResource + + with(imageView) { + setImageResource(imageResource) + setTintResource(iconTintColorResource) + } + } + + override fun asCandidate(context: Context): MenuCandidate = TextMenuCandidate( + if (isInPrimaryState()) { + primaryLabel + } else { + secondaryLabel + }, + start = DrawableMenuIcon( + context, + resource = if (isInPrimaryState()) { + primaryStateIconResource + } else { + secondaryStateIconResource + }, + tint = if (iconTintColorResource == NO_ID) { + null + } else { + ContextCompat.getColor( + context, + iconTintColorResource, + ) + }, + ), + textStyle = TextStyle( + color = if (textColorResource == NO_ID) { + null + } else { + ContextCompat.getColor( + context, + textColorResource, + ) + }, + ), + containerStyle = ContainerStyle(isVisible = visible()), + onClick = if (isInPrimaryState()) primaryStateAction else secondaryStateAction, + ) +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/WebExtensionBrowserMenuItem.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/WebExtensionBrowserMenuItem.kt new file mode 100644 index 0000000000..7eed3ac516 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/WebExtensionBrowserMenuItem.kt @@ -0,0 +1,168 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.content.Context +import android.graphics.drawable.Drawable +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.VisibleForTesting +import androidx.appcompat.content.res.AppCompatResources.getDrawable +import androidx.core.graphics.drawable.toDrawable +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.BrowserMenuItem +import mozilla.components.browser.menu.R +import mozilla.components.concept.engine.webextension.Action +import mozilla.components.concept.menu.candidate.AsyncDrawableMenuIcon +import mozilla.components.concept.menu.candidate.ContainerStyle +import mozilla.components.concept.menu.candidate.TextMenuCandidate +import mozilla.components.concept.menu.candidate.TextMenuIcon +import mozilla.components.concept.menu.candidate.TextStyle +import mozilla.components.support.base.log.Log +import mozilla.components.ui.icons.R as iconsR + +/** + * A browser menu item displaying a web extension action. + * + * @param action the [Action] to display. + * @param listener a callback to be invoked when this menu item is clicked. + * @param isCollapsingMenuLimit Whether this menu item can serve as the limit of a collapsing menu. + * @param isSticky whether this item menu should not be scrolled offscreen (downwards or upwards + * depending on the menu position). + */ +class WebExtensionBrowserMenuItem( + internal var action: Action, + internal val listener: () -> Unit, + internal val id: String = "", + override val isCollapsingMenuLimit: Boolean = false, + override val isSticky: Boolean = false, +) : BrowserMenuItem { + override var visible: () -> Boolean = { true } + + override fun getLayoutResource() = R.layout.mozac_browser_menu_web_extension + + @VisibleForTesting + internal var iconTintColorResource: Int? = null + + @Suppress("TooGenericExceptionCaught") + override fun bind(menu: BrowserMenu, view: View) { + val container = view.findViewById<View>(R.id.container) + updateItem(view) + container.setOnClickListener { + listener.invoke() + menu.dismiss() + } + } + + override fun invalidate(view: View) { + val labelView = view.findViewById<TextView>(R.id.action_label) + val badgeView = view.findViewById<TextView>(R.id.badge_text) + val imageView = view.findViewById<ImageView>(R.id.action_image) + + updateItem(view) + + labelView.invalidate() + badgeView.invalidate() + imageView.invalidate() + } + + @VisibleForTesting + internal fun updateItem(view: View) { + val imageView = view.findViewById<ImageView>(R.id.action_image) + val labelView = view.findViewById<TextView>(R.id.action_label) + val badgeView = view.findViewById<TextView>(R.id.badge_text) + val container = view.findViewById<View>(R.id.container) + + container.isEnabled.updateIfChange(new = action.enabled ?: true) { + container.isEnabled = it + } + + imageView.contentDescription.updateIfChange(action.title) { + imageView.contentDescription = it + } + labelView.text.updateIfChange(action.title) { + labelView.text = it + } + badgeView.setBadgeText(action.badgeText) + action.badgeTextColor?.let { badgeView.setTextColor(it) } + action.badgeBackgroundColor?.let { badgeView.background?.setTint(it) } + setupIcon(view, imageView, iconTintColorResource) + } + + private inline fun <T> T.updateIfChange(new: T, setter: (T) -> Unit) { + if (this != new) { + setter(new) + } + } + + override fun asCandidate(context: Context) = TextMenuCandidate( + action.title.orEmpty(), + start = AsyncDrawableMenuIcon( + loadDrawable = { _, height -> loadIcon(context, height) }, + ), + end = action.badgeText?.let { badgeText -> + TextMenuIcon( + badgeText, + backgroundTint = action.badgeBackgroundColor, + textStyle = TextStyle( + color = action.badgeTextColor, + ), + ) + }, + containerStyle = ContainerStyle( + isVisible = visible(), + isEnabled = action.enabled ?: false, + ), + onClick = listener, + ) + + @VisibleForTesting + internal fun setupIcon(view: View, imageView: ImageView, iconTintColorResource: Int?) { + MainScope().launch { + loadIcon(view.context, imageView.measuredHeight)?.let { + iconTintColorResource?.let { tint -> imageView.setTintResource(tint) } + imageView.setImageDrawable(it) + } + } + } + + @Suppress("TooGenericExceptionCaught") + private suspend fun loadIcon(context: Context, height: Int): Drawable? { + return try { + action.loadIcon?.invoke(height)?.toDrawable(context.resources) + } catch (throwable: Throwable) { + Log.log( + Log.Priority.ERROR, + "mozac-webextensions", + throwable, + "Failed to load browser action icon, falling back to default.", + ) + + getDrawable(context, iconsR.drawable.mozac_ic_web_extension_default_icon) + } + } + + /** + * Sets the tint to be applied to the extension icon + */ + fun setIconTint(iconTintColorResource: Int?) { + iconTintColorResource?.let { this.iconTintColorResource = it } + } +} + +/** + * Sets the badgeText and the visibility of the TextView based on empty/nullability of the badgeText. + */ +fun TextView.setBadgeText(badgeText: String?) { + if (badgeText.isNullOrEmpty()) { + visibility = View.INVISIBLE + } else { + visibility = View.VISIBLE + text = badgeText + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/WebExtensionPlaceholderMenuItem.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/WebExtensionPlaceholderMenuItem.kt new file mode 100644 index 0000000000..b76c55e793 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/WebExtensionPlaceholderMenuItem.kt @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.view.View +import androidx.annotation.ColorRes +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.BrowserMenuItem +import mozilla.components.browser.menu.R + +/** + * A browser menu item that is to be used only as a placeholder for inserting web extensions in main menu. + * The id of the web extension to be inserted has to correspond to the id of the browser menu item. + * + * @param id The id of this menu item. + * @param iconTintColorResource Optional ID of color resource to tint the icon. + */ +class WebExtensionPlaceholderMenuItem( + val id: String, + @ColorRes + val iconTintColorResource: Int = NO_ID, +) : BrowserMenuItem { + override var visible: () -> Boolean = { false } + + override fun getLayoutResource() = R.layout.mozac_browser_menu_item_simple + + override fun bind(menu: BrowserMenu, view: View) { + // no binding, item not visible. + } + + companion object { + const val MAIN_EXTENSIONS_MENU_ID = "mainExtensionsMenu" + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/DynamicWidthRecyclerView.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/DynamicWidthRecyclerView.kt new file mode 100644 index 0000000000..d4d46a9aca --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/DynamicWidthRecyclerView.kt @@ -0,0 +1,86 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.view + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import androidx.annotation.Px +import androidx.annotation.VisibleForTesting +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.browser.menu.R + +/** + * [RecyclerView] with automatically set width between widthMin / widthMax xml attributes. + */ +class DynamicWidthRecyclerView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, +) : RecyclerView(context, attrs) { + @VisibleForTesting + @Px + internal var maxWidthOfAllChildren: Int = 0 + set(value) { + if (field == 0) field = value + } + + @Px var minWidth: Int = -1 + + @Px var maxWidth: Int = -1 + + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + override fun onMeasure(widthSpec: Int, heightSpec: Int) { + if (minWidth in 1 until maxWidth) { + // Ignore any bounds set in xml. Allow for children to expand entirely. + callParentOnMeasure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), heightSpec) + + // First measure will report the width/height for the entire list + // The first layout pass will actually remove child views that do not fit the screen + // so future onMeasure calls will report skewed values. + maxWidthOfAllChildren = measuredWidth + + // Children now have "unspecified" width. Let's set some bounds. + setReconciledDimensions(maxWidthOfAllChildren, measuredHeight) + } else { + // Default behavior. layout_width / layout_height properties will be used for measuring. + callParentOnMeasure(widthSpec, heightSpec) + } + } + + @VisibleForTesting + internal fun setReconciledDimensions( + desiredWidth: Int, + desiredHeight: Int, + ) { + val minimumTapArea = resources.getDimensionPixelSize(R.dimen.mozac_browser_menu_material_min_tap_area) + val minimumItemWidth = resources.getDimensionPixelSize(R.dimen.mozac_browser_menu_material_min_item_width) + + val reconciledWidth = desiredWidth + .coerceAtLeast(minWidth) + // Follow material guidelines where the minimum width is 112dp. + .coerceAtLeast(minimumItemWidth) + .coerceAtMost(maxWidth) + // Leave at least 48dp as a tappable “exit area” available whenever the menu is open. + .coerceAtMost(getScreenWidth() - minimumTapArea) + + callSetMeasuredDimension(reconciledWidth, desiredHeight) + } + + @VisibleForTesting + internal fun getScreenWidth(): Int = resources.displayMetrics.widthPixels + + @SuppressLint("WrongCall") + @VisibleForTesting + // Used for testing protected super.onMeasure(..) calls will be executed. + internal fun callParentOnMeasure(widthSpec: Int, heightSpec: Int) { + super.onMeasure(widthSpec, heightSpec) + } + + @VisibleForTesting + // Used for testing final protected setMeasuredDimension(..) calls were executed + internal fun callSetMeasuredDimension(width: Int, height: Int) { + setMeasuredDimension(width, height) + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/ExpandableLayout.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/ExpandableLayout.kt new file mode 100644 index 0000000000..1eb2e56509 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/ExpandableLayout.kt @@ -0,0 +1,436 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.view + +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Rect +import android.view.MotionEvent +import android.view.ViewConfiguration +import android.view.ViewGroup +import android.view.animation.AccelerateDecelerateInterpolator +import android.widget.FrameLayout +import androidx.annotation.VisibleForTesting +import androidx.core.animation.doOnEnd +import androidx.core.view.children +import androidx.core.view.marginBottom +import androidx.core.view.marginLeft +import androidx.core.view.marginRight +import androidx.core.view.marginTop +import androidx.core.view.updateLayoutParams +import androidx.recyclerview.widget.RecyclerView + +/** + * ViewGroup intended to wrap another to then allow for the following automatic behavior: + * - when first laid out on the screen the wrapped view is collapsed. + * - informs about touches in the empty space left by the collapsed view through [blankTouchListener]. + * - when users swipe up it will expand. Once expanded it remains so. + */ +@Suppress("TooManyFunctions", "LargeClass") +internal class ExpandableLayout private constructor(context: Context) : FrameLayout(context) { + /** + * The wrapped view that needs to be collapsed / expanded. + */ + @VisibleForTesting + internal lateinit var wrappedView: ViewGroup + + /** + * Listener of touches in the empty space left by the collapsed view. + */ + @VisibleForTesting + internal var blankTouchListener: (() -> Unit)? = null + + /** + * Index of the last menu item that should be visible when the wrapped view is collapsed. + */ + @VisibleForTesting + internal var lastVisibleItemIndexWhenCollapsed: Int = Int.MAX_VALUE + + /** + * Index of the sticky footer, if such an item is set. + */ + @VisibleForTesting + internal var stickyItemIndex: Int = RecyclerView.NO_POSITION + + /** + * Height of wrapped view when collapsed. + * Calculated once based on the position of the "isCollapsingMenuLimit" BrowserMenuItem. + * Capped by [parentHeight] + */ + @VisibleForTesting + internal var collapsedHeight: Int = NOT_CALCULATED_DEFAULT_HEIGHT + + /** + * Height of wrapped view when expanded. + * Calculated once based on measuredHeighWithMargins(). + * Capped by [parentHeight] + */ + @VisibleForTesting + internal var expandedHeight: Int = NOT_CALCULATED_DEFAULT_HEIGHT + + /** + * Available space given by the parent. + */ + @VisibleForTesting + internal var parentHeight: Int = NOT_CALCULATED_DEFAULT_HEIGHT + + /** + * Whether to intercept touches while the view is collapsed. + * If true: + * - a swipe up will be intercepted and used to expand the wrapped view. + * - a swipe in the empty space left by the collapsed view will be intercepted + * and [blankTouchListener] will be called. + * - other touches / gestures will be left to pass through to the children. + */ + @VisibleForTesting + internal var isCollapsed = true + + /** + * Whether to intercept touches while the view is expanding. + * If true: + * - all touches / gestures will be intercepted. + */ + @VisibleForTesting + internal var isExpandInProgress = false + + /** + * Distance in pixels a touch can wander before we think the user is scrolling. + * (If this would be bigger than that of a child the child will react to the scroll first) + */ + @VisibleForTesting + internal var touchSlop = ViewConfiguration.get(context).scaledTouchSlop.toFloat() + + /** + * Y axis coordinate of the [MotionEvent.ACTION_DOWN] event. + * Used to calculate the distance scrolled, to know when the view should be expanded. + */ + @VisibleForTesting + internal var initialYCoord = NOT_CALCULATED_Y_TOUCH_COORD + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + callParentOnMeasure(widthMeasureSpec, heightMeasureSpec) + + // Avoid new separate measure calls specifically for our usecase. Piggyback on the already requested ones. + // Calculate our needed dimensions and collapse the menu when based on them. + if (isCollapsed && getOrCalculateCollapsedHeight() > 0 && getOrCalculateExpandedHeight(heightMeasureSpec) > 0) { + collapse() + } + } + + // While this view is collapsed (not fully expanded) we want to intercept all vertical scrolls + // that will be used as an indicator to expand the view, + // while letting all simple touch events get handled by children's click listeners. + // + // Also if this view is collapsed (full height but translated) we want to treat any touch in the + // invisible space as a dismiss event. + @Suppress("ComplexMethod", "ReturnCount") + override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { + if (shouldInterceptTouches()) { + return when (ev?.actionMasked) { + MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> { + false // Allow click listeners firing for children. + } + MotionEvent.ACTION_DOWN -> { + if (isExpandInProgress) { + return true + } + + // Check if user clicked in the empty space left by this collapsed View. + if (!isTouchingTheWrappedView(ev)) { + blankTouchListener?.invoke() + } + + initialYCoord = ev.y + + false // Allow click listeners firing for children. + } + MotionEvent.ACTION_MOVE -> { + if (isScrollingUp(ev)) { + expand() + true + } else { + false + } + } + else -> { + // In general, we don't want to intercept touch events. + // They should be handled by the child view. + return callParentOnInterceptTouchEvent(ev) + } + } + } else { + return if (ev != null && !isTouchingTheWrappedView(ev)) { + // If the menu is expanded but smaller than the parent height + // and the user touches above the menu, in the empty space. + blankTouchListener?.invoke() + true + } else if (isExpandInProgress) { + // Swallow all menu touches while the menu is expanding. + true + } else { + callParentOnInterceptTouchEvent(ev) + } + } + } + + @VisibleForTesting + internal fun shouldInterceptTouches() = isCollapsed && !isExpandInProgress + + @VisibleForTesting + internal fun isTouchingTheWrappedView(ev: MotionEvent): Boolean { + val childrenBounds = Rect() + wrappedView.getHitRect(childrenBounds) + return childrenBounds.contains(ev.x.toInt(), ev.y.toInt()) + } + + @VisibleForTesting + internal fun collapse() { + wrappedView.translationY = parentHeight.toFloat() - collapsedHeight + wrappedView.updateLayoutParams { + height = collapsedHeight + } + } + + @VisibleForTesting + internal fun expand() { + isCollapsed = false + isExpandInProgress = true + + val initialTranslation = wrappedView.translationY + val distanceToExpandedHeight = expandedHeight - collapsedHeight + getExpandViewAnimator(distanceToExpandedHeight).apply { + doOnEnd { + isExpandInProgress = false + } + + addUpdateListener { + wrappedView.translationY = initialTranslation - it.animatedValue as Int + wrappedView.updateLayoutParams { + height = collapsedHeight + it.animatedValue as Int + } + } + start() + } + } + + @VisibleForTesting + internal fun getExpandViewAnimator(expandDelta: Int): ValueAnimator { + return ValueAnimator.ofInt(0, expandDelta).apply { + this.interpolator = AccelerateDecelerateInterpolator() + this.duration = DEFAULT_DURATION_EXPAND_ANIMATOR + } + } + + @VisibleForTesting + internal fun getOrCalculateCollapsedHeight(): Int { + // Memoize the value. + // Method will be called multiple times. Result will always be the same. + if (collapsedHeight < 0) { + collapsedHeight = calculateCollapsedHeight() + } + + return collapsedHeight + } + + @VisibleForTesting + internal fun getOrCalculateExpandedHeight(heightSpec: Int): Int { + if (expandedHeight < 0) { + // Value from a measurement done with MeasureSpec.UNSPECIFIED. + // May need to be capped by the parent height. + expandedHeight = wrappedView.measuredHeight + } + + val heightSpecSize = MeasureSpec.getSize(heightSpec) + // heightSpecSize can be 0 for a MeasureSpec.UNSPECIFIED. + // Ignore that, wait for a heightSpec that will contain parent height. + if (parentHeight < 0 && heightSpecSize > 0) { + parentHeight = heightSpecSize + + // Ensure a menu with a bigger height than the parent will be correctly laid out. + expandedHeight = minOf(expandedHeight, parentHeight) + + // Ensure the collapsedHeight we calculated is not bigger than the expanded height + // now capped by parent height. + // This might happen if the menu is shown in landscape and there is no space to show + // the lastVisibleItemIndexWhenCollapsed. + if (collapsedHeight >= expandedHeight) { + // If there's no space to show the lastVisibleItemIndexWhenCollapsed even if the + // wrappedView is collapsed there's no need to collapse the view. + collapsedHeight = expandedHeight + isExpandInProgress = false + isCollapsed = false + } + } + + return expandedHeight + } + + @Suppress("WrongCall") + @VisibleForTesting + // Used for testing protected super.onMeasure(..) calls will be executed. + internal fun callParentOnMeasure(widthSpec: Int, heightSpec: Int) { + super.onMeasure(widthSpec, heightSpec) + } + + @Suppress("WrongCall") + @VisibleForTesting + // Used for testing protected super.onInterceptTouchEvent(..) calls will be executed. + internal fun callParentOnInterceptTouchEvent(ev: MotionEvent?): Boolean { + return super.onInterceptTouchEvent(ev) + } + + /** + * Whether based on the previous movements, when considering this [event] + * it can be inferred that the user is currently scrolling up. + */ + @VisibleForTesting + internal fun isScrollingUp(event: MotionEvent): Boolean { + val yDistance = initialYCoord - event.y + + return yDistance >= touchSlop + } + + // We need a dynamic way to get the intended collapsed height of this view before it will be laid out on the screen. + // This method assumes the following layout: + // ____________________________________________________ + // this -> | ----------------------------------- | + // | ViewGroup -> | ---------------- | | + // | | RecyclerView-> | View | | | + // | | | View | | | + // | | | View | | | + // | | | SpecialView | | | + // | | | View | | | + // | | ---------------- | | + // | ----------------------------------- | + // ---------------------------------------------------- + // for which we want to measure the distance (height) between [this#top, half of SpecialView]. + // That distance will be the collapsed height of the ViewGroup used when this will be first shown on the screen. + // Users will be able to afterwards expand the ViewGroup to the full height. + @VisibleForTesting + @Suppress("ReturnCount") + internal fun calculateCollapsedHeight(): Int { + val listView = (wrappedView.getChildAt(0) as RecyclerView) + // Reconcile adapter positions with listView children positions. + // Avoid IndexOutOfBounds / NullPointer exceptions. + val validLastVisibleItemIndexWhenCollapsed = getChildPositionForAdapterIndex( + listView, + lastVisibleItemIndexWhenCollapsed, + ) + val validStickyItemIndex = getChildPositionForAdapterIndex( + listView, + stickyItemIndex, + ) + + // Simple sanity check + if (validLastVisibleItemIndexWhenCollapsed >= listView.childCount || + validLastVisibleItemIndexWhenCollapsed <= 0 + ) { + return measuredHeight + } + + var result = 0 + result += wrappedView.marginTop + result += wrappedView.marginBottom + result += wrappedView.paddingTop + result += wrappedView.paddingBottom + result += listView.marginTop + result += listView.marginBottom + result += listView.paddingTop + result += listView.paddingBottom + + run loop@{ + listView.children.forEachIndexed { index, view -> + if (index < validLastVisibleItemIndexWhenCollapsed) { + result += view.marginTop + result += view.marginBottom + result += view.measuredHeight + } else if (index == validLastVisibleItemIndexWhenCollapsed) { + result += view.marginTop + + // Edgecase: if the same item is the sticky footer and the lastVisibleItemIndexWhenCollapsed + // the menu will be collapsed to this item but shown with full height. + if (index == validStickyItemIndex) { + result += view.measuredHeight + return@loop + } else { + result += view.measuredHeight / 2 + } + } else { + // If there is a sticky item below we need to add it's height as an offset. + // Otherwise the sticky item will cover the the view of lastVisibleItemIndexWhenCollapsed. + if (index <= validStickyItemIndex) { + result += listView.getChildAt(validStickyItemIndex).measuredHeight + } + return@loop + } + } + } + + return result + } + + /** + * In a dynamic menu - one in which items or their positions may change the adapter position and + * the RecyclerView position for the same item may differ. + * This method helps reconcile that. + * + * @return the RecyclerView position for the item at the [adapterIndex] in the adapter or + * [RecyclerView.NO_POSITION] if there is no child for the indicated adapter position. + */ + @VisibleForTesting + internal fun getChildPositionForAdapterIndex(listView: RecyclerView, adapterIndex: Int): Int { + listView.children.forEachIndexed { index, view -> + if (listView.getChildAdapterPosition(view) == adapterIndex) { + return index + } + } + + return RecyclerView.NO_POSITION + } + + internal companion object { + @VisibleForTesting + const val NOT_CALCULATED_DEFAULT_HEIGHT = -1 + + @VisibleForTesting + const val NOT_CALCULATED_Y_TOUCH_COORD = 0f + + /** + * Duration of the expand animation. Same value as the one from [R.android.integer.config_shortAnimTime] + */ + @VisibleForTesting + const val DEFAULT_DURATION_EXPAND_ANIMATOR = 200L + + /** + * Wraps a content view in an [ExpandableLayout]. + * + * @param contentView the content view to wrap. + * @return a [ExpandableLayout] that wraps the content view. + */ + internal fun wrapContentInExpandableView( + contentView: ViewGroup, + lastVisibleItemIndexWhenCollapsed: Int = Int.MAX_VALUE, + stickyFooterItemIndex: Int = RecyclerView.NO_POSITION, + blankTouchListener: (() -> Unit)? = null, + ): ExpandableLayout { + val expandableView = ExpandableLayout(contentView.context) + val params = MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) + .apply { + leftMargin = contentView.marginLeft + topMargin = contentView.marginTop + rightMargin = contentView.marginRight + bottomMargin = contentView.marginBottom + } + expandableView.addView(contentView, params) + + expandableView.wrappedView = contentView + expandableView.stickyItemIndex = stickyFooterItemIndex + expandableView.blankTouchListener = blankTouchListener + expandableView.lastVisibleItemIndexWhenCollapsed = lastVisibleItemIndexWhenCollapsed + + return expandableView + } + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/MenuButton.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/MenuButton.kt new file mode 100644 index 0000000000..f9b3db51cd --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/MenuButton.kt @@ -0,0 +1,211 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.view + +import android.content.Context +import android.content.res.ColorStateList +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import android.widget.ImageView +import androidx.annotation.ColorInt +import androidx.annotation.VisibleForTesting +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.BrowserMenu.Orientation +import mozilla.components.browser.menu.BrowserMenuBuilder +import mozilla.components.browser.menu.BrowserMenuHighlight +import mozilla.components.browser.menu.R +import mozilla.components.concept.menu.MenuButton +import mozilla.components.concept.menu.MenuController +import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect +import mozilla.components.concept.menu.candidate.LowPriorityHighlightEffect +import mozilla.components.concept.menu.candidate.MenuCandidate +import mozilla.components.concept.menu.candidate.MenuEffect +import mozilla.components.concept.menu.ext.effects +import mozilla.components.concept.menu.ext.max +import mozilla.components.support.base.observer.Observable +import mozilla.components.support.base.observer.ObserverRegistry +import mozilla.components.support.ktx.android.view.hideKeyboard + +/** + * A `three-dot` button used for expanding menus. + * + * If you are using a browser toolbar, do not use this class directly. + */ +class MenuButton @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : FrameLayout(context, attrs, defStyleAttr), + MenuButton, + View.OnClickListener, + Observable<MenuButton.Observer> by ObserverRegistry() { + + private val menuControllerObserver = object : MenuController.Observer { + /** + * Change the menu button appearance when the menu list changes. + */ + override fun onMenuListSubmit(list: List<MenuCandidate>) { + val effect = list.effects().max() + + // If a highlighted item is found, show the indicator + setEffect(effect) + } + + override fun onDismiss() = notifyObservers { onDismiss() } + } + + /** + * Listener called when the menu is shown. + */ + @Deprecated("Use the Observable interface to listen for onShow") + var onShow: () -> Unit = {} + + /** + * Listener called when the menu is dismissed. + */ + @Deprecated("Use the Observable interface to listen for onDismiss") + var onDismiss: () -> Unit = {} + + /** + * Callback to get the orientation for the menu. + * This is called every time the menu should be displayed. + * This has no effect when a [MenuController] is set. + */ + var getOrientation: () -> Orientation = { + BrowserMenu.determineMenuOrientation(parent as? View?) + } + + /** + * Sets a [MenuController] that will be used to create a menu when this button is clicked. + * If present, [menuBuilder] will be ignored. + */ + override var menuController: MenuController? = null + set(value) { + // Clean up old controller + field?.dismiss() + field?.unregister(menuControllerObserver) + + // Attach new controller + field = value + value?.register(menuControllerObserver, this) + } + + /** + * Sets a [BrowserMenuBuilder] that will be used to create a menu when this button is clicked. + */ + var menuBuilder: BrowserMenuBuilder? = null + set(value) { + field = value + menu?.dismiss() + if (value == null) menu = null + } + + var recordClickEvent: () -> Unit = {} + + @VisibleForTesting internal var menu: BrowserMenu? = null + + private val menuIcon: ImageView + private val highlightView: ImageView + private val notificationIconView: ImageView + + init { + View.inflate(context, R.layout.mozac_browser_menu_button, this) + setOnClickListener(this) + menuIcon = findViewById(R.id.icon) + highlightView = findViewById(R.id.highlight) + notificationIconView = findViewById(R.id.notification_dot) + + // Hook up deprecated callbacks using new observer system + @Suppress("Deprecation") + val internalObserver = object : MenuButton.Observer { + override fun onShow() = this@MenuButton.onShow() + override fun onDismiss() = this@MenuButton.onDismiss() + } + register(internalObserver) + } + + /** + * Shows the menu, or dismisses it if already open. + */ + override fun onClick(v: View) { + this.hideKeyboard() + recordClickEvent() + + // If a legacy menu is open, dismiss it. + if (menu != null) { + menu?.dismiss() + return + } + + val menuController = menuController + if (menuController != null) { + // Use the newer menu controller if set + menuController.show(anchor = this) + } else { + menu = menuBuilder?.build(context) + val endAlwaysVisible = menuBuilder?.endOfMenuAlwaysVisible ?: false + menu?.show( + anchor = this, + orientation = getOrientation(), + endOfMenuAlwaysVisible = endAlwaysVisible, + ) { + menu = null + notifyObservers { onDismiss() } + } + } + notifyObservers { onShow() } + } + + /** + * Show the indicator for a browser menu highlight. + */ + fun setHighlight(highlight: BrowserMenuHighlight?) = + setEffect(highlight?.asEffect(context)) + + /** + * Show the indicator for a browser menu effect. + */ + override fun setEffect(effect: MenuEffect?) { + when (effect) { + is HighPriorityHighlightEffect -> { + highlightView.imageTintList = ColorStateList.valueOf(effect.backgroundTint) + highlightView.visibility = View.VISIBLE + notificationIconView.visibility = View.GONE + } + is LowPriorityHighlightEffect -> { + notificationIconView.setColorFilter(effect.notificationTint) + highlightView.visibility = View.GONE + notificationIconView.visibility = View.VISIBLE + } + null -> { + highlightView.visibility = View.GONE + notificationIconView.visibility = View.GONE + } + } + } + + /** + * Sets the tint of the 3-dot menu icon. + */ + override fun setColorFilter(@ColorInt color: Int) { + menuIcon.setColorFilter(color) + } + + /** + * Dismiss the menu, if open. + */ + fun dismissMenu() { + menuController?.dismiss() + menu?.dismiss() + } + + /** + * Invalidates the [BrowserMenu], if open. + */ + fun invalidateBrowserMenu() { + menu?.invalidate() + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/StickyFooterLinearLayoutManager.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/StickyFooterLinearLayoutManager.kt new file mode 100644 index 0000000000..b2c2d6a18a --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/StickyFooterLinearLayoutManager.kt @@ -0,0 +1,74 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.view + +import android.content.Context +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +/** + * Vertical LinearLayoutManager that will ensure an item at a position specified through + * [StickyItemsAdapter.isStickyItem] will not scroll past the list bottom. + * + * The list would otherwise scroll normally with the other elements being scrolled beneath the sticky item. + * + * @param context [Context] needed for various Android interactions. + * @param reverseLayout When set to true, layouts from end to start. + */ +open class StickyFooterLinearLayoutManager<T> constructor( + context: Context, + reverseLayout: Boolean = false, +) : StickyItemsLinearLayoutManager<T>( + context, + StickyItemPlacement.BOTTOM, + reverseLayout, +) where T : RecyclerView.Adapter<*>, T : StickyItemsAdapter { + + override fun scrollToIndicatedPositionWithOffset( + position: Int, + offset: Int, + actuallyScrollToPositionWithOffset: (Int, Int) -> Unit, + ) { + // The following scenarios are handled: + // - if position is bigger than [stickyItemPosition] + // -> the default behavior will have the list scrolled downwards enough to show that. + // - if position is the one of the stickyItem + // -> the default behavior will scroll to exactly the header. Perfect match. + // - if position is before that of the [stickyItem] and does not fit the screen + // -> only scenario we need to handle: the sticky footer must be shown and the default implementation + // would scroll to show as the last item in the list the item at [position]. But that is where the sticky + // item is anchored. Need to scroll to the next position so that that item will be obscured by the sticky + // item and not the now above item at [position]. + // + // Providing any offsets with the stickyView shown and the above scenarios handles means they are handled also. + + if (position < stickyItemPosition && getChildAt(position) == null) { + actuallyScrollToPositionWithOffset(position + 1, offset) + return + } + + actuallyScrollToPositionWithOffset(position, offset) + } + + override fun shouldStickyItemBeShownForCurrentPosition(): Boolean { + if (stickyItemPosition == RecyclerView.NO_POSITION) { + return false + } + + // The item at [stickyItemPosition] should be anchored to the top if: + // - it or a lower indexed item is shown at the bottom of the list + // - the last shown item is translated downwards off screen + // (happens when [scrollToPositionWithOffset] was called with a big enough offset) + val lastVisibleElement = stickyItemView?.let { childCount - 2 } ?: childCount - 1 + return getAdapterPositionForItemIndex(lastVisibleElement) <= stickyItemPosition + } + + override fun getY(itemView: View): Float { + return when (reverseLayout) { + true -> 0f + false -> height - itemView.height.toFloat() + } + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/StickyHeaderLinearLayoutManager.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/StickyHeaderLinearLayoutManager.kt new file mode 100644 index 0000000000..27a758a891 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/StickyHeaderLinearLayoutManager.kt @@ -0,0 +1,73 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.view + +import android.content.Context +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +/** + * Vertical LinearLayoutManager that will ensure an item at a position specified through + * [StickyItemsAdapter.isStickyItem] will not scroll past the list's top. + * + * The list would otherwise scroll normally with the other elements being scrolled beneath the sticky item. + * + * @param context [Context] needed for various Android interactions. + * @param reverseLayout When set to true, layouts from end to start. + */ +open class StickyHeaderLinearLayoutManager<T> constructor( + context: Context, + reverseLayout: Boolean = false, +) : StickyItemsLinearLayoutManager<T>( + context, + StickyItemPlacement.TOP, + reverseLayout, +) where T : RecyclerView.Adapter<*>, T : StickyItemsAdapter { + + override fun scrollToIndicatedPositionWithOffset( + position: Int, + offset: Int, + actuallyScrollToPositionWithOffset: (Int, Int) -> Unit, + ) { + // The following scenarios are handled: + // - if position is smaller than [stickyItemPosition] + // -> the default behavior will have the list scrolled upwards enough to show that. + // - if position is the one of the stickyItem + // -> the default behavior will scroll to exactly the header. Perfect match. + // - if position is bigger than [stickyItemPosition] + // -> only scenario we need to handle: default implementation would scroll to show at the top of the list + // the item at that position. But that is where the sticky item is anchored. Need to ask for the item at + // the before position being shown at the top of the list and let that be obscured by the sticky item. + // + // Providing any offsets with the stickyView shown and the above scenarios handles means they are handled also. + + if (position + 1 > stickyItemPosition) { + actuallyScrollToPositionWithOffset(position - 1, offset) + return + } + + actuallyScrollToPositionWithOffset(position, offset) + } + + override fun shouldStickyItemBeShownForCurrentPosition(): Boolean { + if (stickyItemPosition == RecyclerView.NO_POSITION) { + return false + } + + // The item at [stickyItemPosition] should be anchored to the top if: + // - it or a below item is shown at the top of the list + // - the first shown item is translated upwards off screen + // (happens when [scrollToPositionWithOffset] was called with a big enough offset) + return getAdapterPositionForItemIndex(0) >= stickyItemPosition || + getChildAt(0)?.bottom ?: 1 <= 0 // return false if there is no item at index 0 + } + + override fun getY(itemView: View): Float { + return when (reverseLayout) { + true -> height - itemView.height.toFloat() + false -> 0f + } + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/StickyItemLayoutManager.kt b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/StickyItemLayoutManager.kt new file mode 100644 index 0000000000..f49ec0bf2c --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/StickyItemLayoutManager.kt @@ -0,0 +1,483 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.view + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.PointF +import android.os.Parcelable +import android.view.View +import android.view.ViewTreeObserver +import androidx.annotation.VisibleForTesting +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import kotlinx.parcelize.Parcelize + +// Inspired from +// https://github.com/qiujayen/sticky-layoutmanager/blob/b1ddb086db5b04ff3c5357dabe1bff47a935dd37/ +// sticky-layoutmanager/src/main/java/com/jay/widget/StickyHeadersLinearLayoutManager.java + +/** + * Contract needed to be implemented by all [RecyclerView.Adapter]s + * that want to display a list with a sticky header / footer. + */ +interface StickyItemsAdapter { + /** + * Whether this should be considered a sticky item. + * + * All items will be checked. Only the last one presenting as sticky will be used as such. + */ + fun isStickyItem(position: Int): Boolean + + /** + * Callback allowing any customization for the view that will become sticky. + */ + fun setupStickyItem(stickyItem: View) {} + + /** + * Callback allowing cleanup after the previous sticky view becomes a regular view. + */ + fun tearDownStickyItem(stickyItem: View) {} +} + +/** + * Whether the sticky item should be a header or a footer. + */ +enum class StickyItemPlacement { + /** + * The sticky item will be fixed at the top of the list. + * + * If the list is scrolled down until past the sticky item's position that view + * will become a regular view and will be scrolled down as the others. + * + * If the list is scrolled up past the sticky item's position that view + * will be anchored to the top of the list, always being shown as the first item. + */ + TOP, + + /** + * The sticky item will be fixed at the bottom of the list. + * + * If the list is scrolled up until past the sticky item's position that view + * will become a regular view and will be scrolled up as the others. + * + * If the list is scrolled down past the sticky item's position that view + * will be anchored to the bottom of the list, always being shown as the last item. + */ + BOTTOM, +} + +/** + * Vertical LinearLayoutManager that will prevent certain items from being scrolled off-screen. + * + * @param context [Context] needed for various Android interactions. + * @param stickyItemPlacement whether the sticky item should be blocked from being scrolled off + * to the top of the screen or off to the bottom of the screen. + * @param reverseLayout When set to true, layouts from end to start. + */ +@Suppress("TooManyFunctions") +abstract class StickyItemsLinearLayoutManager<T> constructor( + context: Context, + private val stickyItemPlacement: StickyItemPlacement, + reverseLayout: Boolean = false, +) : LinearLayoutManager(context, RecyclerView.VERTICAL, reverseLayout) + where T : RecyclerView.Adapter<*>, T : StickyItemsAdapter { + + @VisibleForTesting + internal var listAdapter: T? = null + + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + internal var stickyItemPosition = RecyclerView.NO_POSITION + + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + internal var stickyItemView: View? = null + + // Allows to re-evaluate and display a possibly new sticky item if data / adapter changed. + @VisibleForTesting + internal var stickyItemPositionsObserver = ItemPositionsAdapterDataObserver() + + // Save / Restore scroll state + @VisibleForTesting + internal var scrollPosition = RecyclerView.NO_POSITION + + @VisibleForTesting + internal var scrollOffset = 0 + + /** + * @see [LinearLayoutManager.scrollToPositionWithOffset] + * + * @param position list item index which needs to be shown. + * @param offset optional distance offset from the top of the list to be applied after scrolling to [position] + * @param actuallyScrollToPositionWithOffset callback to be used for actually scrolling to an updated position + * ad offset based on the relation with the sticky item. + * + * Use [setScrollState] before and after + */ + abstract fun scrollToIndicatedPositionWithOffset( + position: Int, + offset: Int, + actuallyScrollToPositionWithOffset: (Int, Int) -> Unit, + ) + + /** + * Whether the sticky item should be shown. + * + * Expected to return if the sticky header item is scrolled past the list top or the sticky bottom item + * is scrolled past the list bottom. + */ + abstract fun shouldStickyItemBeShownForCurrentPosition(): Boolean + + /** + * Returns the position in the Y axis to position the header appropriately, + * depending on direction and [android.R.attr.clipToPadding]. + */ + abstract fun getY(itemView: View): Float + + override fun onAttachedToWindow(recyclerView: RecyclerView) { + super.onAttachedToWindow(recyclerView) + setAdapter(recyclerView.adapter) + } + + override fun onAdapterChanged( + oldAdapter: RecyclerView.Adapter<*>?, + newAdapter: RecyclerView.Adapter<*>?, + ) { + super.onAdapterChanged(oldAdapter, newAdapter) + setAdapter(newAdapter) + } + + override fun onSaveInstanceState(): Parcelable { + return SavedState( + superState = super.onSaveInstanceState(), + scrollPosition = scrollPosition, + scrollOffset = scrollOffset, + ) + } + + override fun onRestoreInstanceState(state: Parcelable?) { + (state as? mozilla.components.browser.menu.view.SavedState)?.let { + scrollPosition = it.scrollPosition + scrollOffset = it.scrollOffset + super.onRestoreInstanceState(it.superState) + } + } + + override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) { + restoreView { super.onLayoutChildren(recycler, state) } + + if (!state.isPreLayout) { + updateStickyItem(recycler, true) + } + } + + override fun scrollVerticallyBy( + dy: Int, + recycler: RecyclerView.Recycler, + state: RecyclerView.State?, + ): Int { + val distanceScrolled = restoreView { super.scrollVerticallyBy(dy, recycler, state) } + if (distanceScrolled != 0) { + updateStickyItem(recycler, false) + } + return distanceScrolled + } + + override fun findLastVisibleItemPosition(): Int = + restoreView { super.findLastVisibleItemPosition() } + + override fun findFirstVisibleItemPosition(): Int = + restoreView { super.findFirstVisibleItemPosition() } + + override fun findFirstCompletelyVisibleItemPosition(): Int = + restoreView { super.findFirstCompletelyVisibleItemPosition() } + + override fun findLastCompletelyVisibleItemPosition(): Int = + restoreView { super.findLastCompletelyVisibleItemPosition() } + + override fun computeVerticalScrollExtent(state: RecyclerView.State): Int = + restoreView { super.computeVerticalScrollExtent(state) } + + override fun computeVerticalScrollOffset(state: RecyclerView.State): Int = + restoreView { super.computeVerticalScrollOffset(state) } + + override fun computeVerticalScrollRange(state: RecyclerView.State): Int = + restoreView { super.computeVerticalScrollRange(state) } + + override fun computeScrollVectorForPosition(targetPosition: Int): PointF? = + restoreView { super.computeScrollVectorForPosition(targetPosition) } + + override fun scrollToPosition(position: Int) { + if (stickyItemView != null) { + scrollToPositionWithOffset(position, INVALID_OFFSET) + } else { + super.scrollToPosition(position) + } + } + + override fun scrollToPositionWithOffset(position: Int, offset: Int) { + if (stickyItemView != null) { + // Reset pending scroll. + setScrollState(RecyclerView.NO_POSITION, INVALID_OFFSET) + + scrollToIndicatedPositionWithOffset(position, offset) { updatedPosition, updatedOffset -> + super.scrollToPositionWithOffset(updatedPosition, updatedOffset) + } + + // Remember this position and offset and scroll to it to trigger creating the sticky view. + setScrollState(position, offset) + } else { + super.scrollToPositionWithOffset(position, offset) + } + } + + override fun onFocusSearchFailed( + focused: View, + focusDirection: Int, + recycler: RecyclerView.Recycler, + state: RecyclerView.State, + ): View? = restoreView { super.onFocusSearchFailed(focused, focusDirection, recycler, state) } + + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + internal fun getAdapterPositionForItemIndex(index: Int): Int { + return (getChildAt(index)?.layoutParams as? RecyclerView.LayoutParams) + ?.absoluteAdapterPosition ?: RecyclerView.NO_POSITION + } + + @Suppress("UNCHECKED_CAST") + @VisibleForTesting + internal fun setAdapter(newAdapter: RecyclerView.Adapter<*>?) { + listAdapter?.unregisterAdapterDataObserver(stickyItemPositionsObserver) + + (newAdapter as? T)?.let { + listAdapter = newAdapter + listAdapter?.registerAdapterDataObserver(stickyItemPositionsObserver) + stickyItemPositionsObserver.onChanged() + } ?: run { + listAdapter = null + stickyItemView = null + } + } + + /** + * Perform any [operation] ignoring the sticky item. Accomplished by: + * - detaching the sticky view + * - performing the [operation] + * - reattaching the sticky view. + */ + @VisibleForTesting + internal fun <T> restoreView(operation: () -> T): T { + stickyItemView?.let(this::detachView) + val result = operation() + stickyItemView?.let(this::attachView) + return result + } + + /** + * Updates the sticky item state (creation, binding, display). + * + * To be called whenever there's a layout or scroll. + * + * @param recycler [RecyclerView.Recycler] instance handling views recycling + * @param layout whether this is called while layout or while scrolling. + */ + @VisibleForTesting + internal fun updateStickyItem(recycler: RecyclerView.Recycler, layout: Boolean) { + if (shouldStickyItemBeShownForCurrentPosition()) { + if (stickyItemView == null) { + createStickyView(recycler, stickyItemPosition) + } + + if (layout) { + bindStickyItem(stickyItemView!!) + } + + stickyItemView?.let { + it.translationY = getY(it) + } + } else { + stickyItemView?.let { + recycleStickyItem(recycler) + } + } + } + + /** + * Construct and configure a [RecyclerView.ViewHolder] for [position], + * including measure, layout, and data binding and assigns this to [stickyItemView]. + */ + @VisibleForTesting + internal fun createStickyView(recycler: RecyclerView.Recycler, position: Int) { + val stickyItem = recycler.getViewForPosition(position) + + listAdapter?.setupStickyItem(stickyItem) + + // Add sticky item as a child view, to be detached / reattached whenever + // LinearLayoutManager#fill() is called, which happens on layout and scroll (see overrides). + addView(stickyItem) + measureAndLayout(stickyItem) + + // Hide this new sticky item from the parent LayoutManager, as it's fully managed by this LayoutManager. + ignoreView(stickyItem) + + stickyItemView = stickyItem + } + + /** + * Binds a new [stickyItem]. + */ + @VisibleForTesting + internal fun bindStickyItem(stickyItem: View) { + measureAndLayout(stickyItem) + + // If we have a pending scroll wait until the end of layout and scroll again. + if (scrollPosition != RecyclerView.NO_POSITION) { + stickyItem.viewTreeObserver.addOnGlobalLayoutListener( + object : + ViewTreeObserver.OnGlobalLayoutListener { + override fun onGlobalLayout() { + stickyItem.viewTreeObserver.removeOnGlobalLayoutListener(this) + if (scrollPosition != RecyclerView.NO_POSITION) { + scrollToPositionWithOffset(scrollPosition, scrollOffset) + setScrollState(RecyclerView.NO_POSITION, INVALID_OFFSET) + } + } + }, + ) + } + } + + /** + * Measures and lays out [stickyItemView]. + */ + @VisibleForTesting + internal fun measureAndLayout(stickyItem: View) { + measureChildWithMargins(stickyItem, 0, 0) + stickyItem.layout( + paddingLeft, + 0, + width - paddingRight, + stickyItem.measuredHeight, + ) + } + + /** + * Returns a no longer needed [stickyItemView] View to the [RecyclerView]'s [RecyclerView.RecycledViewPool] + * allowing it to be recycled and reused later after being re-binded in the Adapter. + * + * @param recycler [RecyclerView.Recycler] instance handling views recycling. + */ + @VisibleForTesting + internal fun recycleStickyItem(recycler: RecyclerView.Recycler?) { + val stickyItem = stickyItemView ?: return + stickyItemView = null + + stickyItem.translationY = 0f + + listAdapter?.tearDownStickyItem(stickyItem) + + // Stop ignoring sticky header so that it can be recycled. + stopIgnoringView(stickyItem) + + removeView(stickyItem) + recycler?.recycleView(stickyItem) + } + + @VisibleForTesting + internal fun setScrollState(position: Int, offset: Int) { + scrollPosition = position + scrollOffset = offset + } + + /** + * Observer for any changes in the items displayed or even when the Adapter changes. + */ + @VisibleForTesting + internal inner class ItemPositionsAdapterDataObserver : RecyclerView.AdapterDataObserver() { + override fun onChanged() = handleChange() + + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) = handleChange() + + override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) = handleChange() + + override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) = handleChange() + + @VisibleForTesting + internal fun handleChange() { + listAdapter?.let { + stickyItemPosition = calculateNewStickyItemPosition(it) + + // Remove sticky header immediately. A layout will follow. + if (stickyItemView != null) { + recycleStickyItem(null) + } + } + } + + /** + * Get the position of the closest to the anchor sticky item. + * + * @return sticky item's index in the adapter or RecyclerView.NO_POSITION is such an item doesn't exists. + */ + @VisibleForTesting + internal fun calculateNewStickyItemPosition(adapter: T): Int { + var newStickyItemPosition = RecyclerView.NO_POSITION + + if (stickyItemPlacement == StickyItemPlacement.TOP) { + for (i in (itemCount - 1) downTo 0) { + if (adapter.isStickyItem(i)) { + newStickyItemPosition = i + } + } + } else { + for (i in 0 until itemCount) { + if (adapter.isStickyItem(i)) { + newStickyItemPosition = i + } + } + } + + return newStickyItemPosition + } + } + + companion object { + /** + * Get a new instance of a vertical [LinearLayoutManager] that can show one specific item + * as a fixed header / footer in the list, be that reversed or not. + * + * @param stickyItemPlacement whether the sticky item should be anchored to the top or bottom of the list + * @param reverseLayout when set to true, layouts from end to start. + */ + fun <T> get( + context: Context, + stickyItemPlacement: StickyItemPlacement = StickyItemPlacement.TOP, + reverseLayout: Boolean = false, + ): StickyItemsLinearLayoutManager<T> + where T : RecyclerView.Adapter<*>, T : StickyItemsAdapter { + return when (stickyItemPlacement) { + StickyItemPlacement.TOP -> StickyHeaderLinearLayoutManager( + context, + reverseLayout, + ) + StickyItemPlacement.BOTTOM -> StickyFooterLinearLayoutManager( + context, + reverseLayout, + ) + } + } + } +} + +/** + * Save / restore existing [RecyclerView] state and scrolling position and offset. + */ +@SuppressLint("ParcelCreator") +@Parcelize +@VisibleForTesting +internal data class SavedState( + val superState: Parcelable?, + val scrollPosition: Int, + val scrollOffset: Int, +) : Parcelable diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/anim-ldrtl/menu_enter_bottom.xml b/mobile/android/android-components/components/browser/menu/src/main/res/anim-ldrtl/menu_enter_bottom.xml new file mode 100644 index 0000000000..6d27c410ea --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/anim-ldrtl/menu_enter_bottom.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<set xmlns:android="http://schemas.android.com/apk/res/android"> + <scale android:interpolator="@android:anim/accelerate_decelerate_interpolator" + android:fromXScale="0" + android:toXScale="1" + android:fromYScale="0" + android:toYScale="1" + android:pivotX="5%" + android:pivotY="100%" + android:duration="@android:integer/config_shortAnimTime" /> + <alpha android:interpolator="@android:anim/linear_interpolator" + android:fromAlpha="0" + android:toAlpha="1" + android:duration="@android:integer/config_shortAnimTime" /> + <translate android:interpolator="@android:anim/accelerate_decelerate_interpolator" + android:fromYDelta="0" + android:toYDelta="0" + android:duration="@android:integer/config_shortAnimTime" /> +</set> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/anim-ldrtl/menu_enter_top.xml b/mobile/android/android-components/components/browser/menu/src/main/res/anim-ldrtl/menu_enter_top.xml new file mode 100644 index 0000000000..fc141bdbd0 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/anim-ldrtl/menu_enter_top.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<set xmlns:android="http://schemas.android.com/apk/res/android"> + <scale android:interpolator="@android:anim/accelerate_decelerate_interpolator" + android:fromXScale="0" + android:toXScale="1" + android:fromYScale="0" + android:toYScale="1" + android:pivotX="5%" + android:pivotY="5%" + android:duration="@android:integer/config_shortAnimTime" /> + <alpha android:interpolator="@android:anim/linear_interpolator" + android:fromAlpha="0" + android:toAlpha="1" + android:duration="@android:integer/config_shortAnimTime" /> + <translate android:interpolator="@android:anim/accelerate_decelerate_interpolator" + android:fromYDelta="0" + android:toYDelta="0" + android:duration="@android:integer/config_shortAnimTime" /> +</set> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/anim/menu_enter_bottom.xml b/mobile/android/android-components/components/browser/menu/src/main/res/anim/menu_enter_bottom.xml new file mode 100644 index 0000000000..89153ca6e5 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/anim/menu_enter_bottom.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<set xmlns:android="http://schemas.android.com/apk/res/android"> + <scale android:interpolator="@android:anim/accelerate_decelerate_interpolator" + android:fromXScale="0" + android:toXScale="1" + android:fromYScale="0" + android:toYScale="1" + android:pivotX="95%" + android:pivotY="100%" + android:duration="@android:integer/config_shortAnimTime" /> + <alpha android:interpolator="@android:anim/linear_interpolator" + android:fromAlpha="0" + android:toAlpha="1" + android:duration="@android:integer/config_shortAnimTime" /> + <translate android:interpolator="@android:anim/accelerate_decelerate_interpolator" + android:fromYDelta="0" + android:toYDelta="0" + android:duration="@android:integer/config_shortAnimTime" /> +</set> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/anim/menu_enter_top.xml b/mobile/android/android-components/components/browser/menu/src/main/res/anim/menu_enter_top.xml new file mode 100644 index 0000000000..f0485403ef --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/anim/menu_enter_top.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<set xmlns:android="http://schemas.android.com/apk/res/android"> + <scale android:interpolator="@android:anim/accelerate_decelerate_interpolator" + android:fromXScale="0" + android:toXScale="1" + android:fromYScale="0" + android:toYScale="1" + android:pivotX="95%" + android:pivotY="5%" + android:duration="@android:integer/config_shortAnimTime" /> + <alpha android:interpolator="@android:anim/linear_interpolator" + android:fromAlpha="0" + android:toAlpha="1" + android:duration="@android:integer/config_shortAnimTime" /> + <translate android:interpolator="@android:anim/accelerate_decelerate_interpolator" + android:fromYDelta="0" + android:toYDelta="0" + android:duration="@android:integer/config_shortAnimTime" /> +</set> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/anim/menu_exit.xml b/mobile/android/android-components/components/browser/menu/src/main/res/anim/menu_exit.xml new file mode 100644 index 0000000000..226166c109 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/anim/menu_exit.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<set xmlns:android="http://schemas.android.com/apk/res/android"> + <alpha android:interpolator="@android:anim/linear_interpolator" + android:fromAlpha="1" + android:toAlpha="0" + android:duration="@android:integer/config_shortAnimTime" /> +</set> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/drawable/mozac_browser_menu_notification_icon.xml b/mobile/android/android-components/components/browser/menu/src/main/res/drawable/mozac_browser_menu_notification_icon.xml new file mode 100644 index 0000000000..8afb175afe --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/drawable/mozac_browser_menu_notification_icon.xml @@ -0,0 +1,15 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="8dp" + android:height="8dp" + android:viewportWidth="10" + android:viewportHeight="10"> + <path + android:pathData="M1,5a4,4 0 1,0 8,0a4,4 0 1,0 -8,0" + android:strokeWidth="1" + android:strokeAlpha=".2" + android:fillColor="#fff" + android:strokeColor="#000" /> +</vector> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/drawable/mozac_menu_indicator.xml b/mobile/android/android-components/components/browser/menu/src/main/res/drawable/mozac_menu_indicator.xml new file mode 100644 index 0000000000..33d8ad19b4 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/drawable/mozac_menu_indicator.xml @@ -0,0 +1,29 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="40dp" + android:height="40dp" + android:viewportWidth="40" + android:viewportHeight="40"> + <group + android:scaleX="0.714" + android:scaleY="0.714" + android:pivotX="20" + android:pivotY="20"> + <path + android:pathData="m38.622,27.309a8,8 0,0 0,-11.314 11.314,19.949 19.949,0 0,1 -7.308,1.377c-11.046,0 -20,-8.954 -20,-20s8.954,-20 20,-20 20,8.954 20,20c0,2.58 -0.488,5.045 -1.378,7.309z" + android:fillColor="#ffffff" + android:fillAlpha=".4" /> + <path + android:pathData="M33,33m-6.4,0a6.4,6.4 0,1 1,12.8 0a6.4,6.4 0,1 1,-12.8 0" + android:fillColor="#ffffff" + android:fillAlpha=".4" /> + <path + android:pathData="M33,33m-4.3,0a4.3,4.3 0,1 1,8.6 0a4.3,4.3 0,1 1,-8.6 0" + android:strokeWidth="1" + android:strokeAlpha=".2" + android:fillColor="#ffffff" + android:strokeColor="#000000" /> + </group> +</vector> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/drawable/mozac_menu_notification.xml b/mobile/android/android-components/components/browser/menu/src/main/res/drawable/mozac_menu_notification.xml new file mode 100644 index 0000000000..b7b5ced0c0 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/drawable/mozac_menu_notification.xml @@ -0,0 +1,21 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="40dp" + android:height="40dp" + android:viewportWidth="40" + android:viewportHeight="40"> + <group + android:translateX="25.55" + android:translateY="5.55" + android:scaleX="0.75" + android:scaleY="0.75"> + <path + android:pathData="M1,5a4,4 0 1,0 8,0a4,4 0 1,0 -8,0" + android:strokeWidth="1" + android:strokeAlpha=".2" + android:fillColor="#fff" + android:strokeColor="#000" /> + </group> +</vector> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/drawable/rounded_corner.xml b/mobile/android/android-components/components/browser/menu/src/main/res/drawable/rounded_corner.xml new file mode 100644 index 0000000000..892c34799d --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/drawable/rounded_corner.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <corners android:radius="4dp" /> +</shape>
\ No newline at end of file diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu.xml new file mode 100644 index 0000000000..83e075a2e0 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + style="@style/Mozac.Browser.Menu" + android:id="@+id/mozac_browser_menu_menuView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:cardCornerRadius="@dimen/mozac_browser_menu_corner_radius" + app:cardElevation="@dimen/mozac_browser_menu_elevation" + app:cardUseCompatPadding="true"> + + <mozilla.components.browser.menu.view.DynamicWidthRecyclerView + android:id="@+id/mozac_browser_menu_recyclerView" + android:paddingTop="@dimen/mozac_browser_menu_padding_vertical" + android:paddingBottom="@dimen/mozac_browser_menu_padding_vertical" + android:overScrollMode="never" + android:layout_width="@dimen/mozac_browser_menu_width" + android:layout_height="wrap_content" + tools:listitem="@layout/mozac_browser_menu_item_simple" /> + +</androidx.cardview.widget.CardView> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_button.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_button.xml new file mode 100644 index 0000000000..78193dce10 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_button.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<merge xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_height="match_parent" + android:layout_width="match_parent" + android:clickable="true" + android:focusable="true" + android:background="?android:selectableItemBackgroundBorderless" + tools:parentTag="android.widget.FrameLayout"> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/highlight" + app:srcCompat="@drawable/mozac_menu_indicator" + android:layout_height="match_parent" + android:layout_width="match_parent" + android:scaleType="center" + android:contentDescription="@string/mozac_browser_menu_highlighted" + android:visibility="gone" /> + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/icon" + app:srcCompat="@drawable/mozac_ic_ellipsis_vertical_24" + android:layout_height="match_parent" + android:layout_width="match_parent" + android:scaleType="center" + android:contentDescription="@string/mozac_browser_menu_button" /> + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/notification_dot" + app:srcCompat="@drawable/mozac_menu_notification" + android:layout_height="match_parent" + android:layout_width="match_parent" + android:scaleType="center" + android:contentDescription="@string/mozac_browser_menu_highlighted" + android:visibility="gone" /> + +</merge> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_category.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_category.xml new file mode 100644 index 0000000000..15591618df --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_category.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/category_text" + style="@style/Mozac.Browser.Menu.Item.Category" + android:layout_width="match_parent" + android:gravity="center_vertical" + tools:text="Category" /> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_highlightable_item.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_highlightable_item.xml new file mode 100644 index 0000000000..b956d85bd7 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_highlightable_item.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<androidx.constraintlayout.widget.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + style="@style/Mozac.Browser.Menu.Item.Container" + tools:ignore="UnusedAttribute" + android:foreground="?android:attr/selectableItemBackground" + android:layout_width="match_parent" + android:orientation="horizontal" + android:clickable="true" + android:focusable="true"> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/image" + style="@style/Mozac.Browser.Menu.Item.ImageText.Icon" + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_gravity="center" + android:background="@android:color/transparent" + android:importantForAccessibility="no" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:src="@android:drawable/screen_background_dark" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/notification_dot" + style="@style/Mozac.Browser.Menu.Item.ImageText.Icon" + android:layout_width="8dp" + android:layout_height="8dp" + android:translationX="@dimen/mozac_browser_menu_highlightable_notification_translate_x" + android:translationY="@dimen/mozac_browser_menu_highlightable_notification_translate_y" + android:background="@android:color/transparent" + android:contentDescription="@string/mozac_browser_menu_highlighted" + android:visibility="gone" + app:layout_constraintTop_toTopOf="@id/image" + app:layout_constraintEnd_toEndOf="@id/image" + app:srcCompat="@drawable/mozac_browser_menu_notification_icon" + tools:visibility="visible" /> + + <TextView + android:id="@+id/text" + style="@style/Mozac.Browser.Menu.Item.ImageText.Label" + android:layout_width="wrap_content" + android:layout_centerVertical="true" + android:background="@android:color/transparent" + android:clickable="false" + android:focusable="false" + android:gravity="center_vertical" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@+id/end_image" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintStart_toEndOf="@+id/image" + app:layout_constraintTop_toTopOf="parent" + tools:text="Item" /> + + <TextView + android:id="@+id/highlight_text" + style="@style/Mozac.Browser.Menu.Item.ImageText.Label" + android:layout_width="wrap_content" + android:layout_centerVertical="true" + android:background="@android:color/transparent" + android:clickable="false" + android:focusable="false" + android:gravity="center_vertical" + android:visibility="invisible" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@+id/end_image" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintStart_toEndOf="@+id/image" + app:layout_constraintTop_toTopOf="parent" + tools:text="Highlighted Item" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/end_image" + style="@style/Mozac.Browser.Menu.Item.ImageText.Icon" + android:layout_alignParentEnd="true" + android:layout_centerVertical="true" + android:layout_gravity="center_vertical" + android:background="@android:color/transparent" + android:clickable="false" + android:focusable="false" + android:importantForAccessibility="no" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:src="@android:drawable/screen_background_dark" /> +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_highlightable_switch.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_highlightable_switch.xml new file mode 100644 index 0000000000..58d6fcef50 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_highlightable_switch.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<androidx.constraintlayout.widget.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + style="@style/Mozac.Browser.Menu.Item.Container" + tools:ignore="UnusedAttribute" + android:foreground="?android:attr/selectableItemBackground" + android:layout_width="match_parent" + android:orientation="horizontal" + android:clickable="true" + android:focusable="true"> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/image" + style="@style/Mozac.Browser.Menu.Item.ImageText.Icon" + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_gravity="center" + android:background="@android:color/transparent" + android:importantForAccessibility="no" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:src="@android:drawable/screen_background_dark" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/notification_dot" + style="@style/Mozac.Browser.Menu.Item.ImageText.Icon" + android:layout_width="8dp" + android:layout_height="8dp" + android:translationX="@dimen/mozac_browser_menu_highlightable_notification_translate_x" + android:translationY="@dimen/mozac_browser_menu_highlightable_notification_translate_y" + android:background="@android:color/transparent" + android:importantForAccessibility="no" + android:visibility="gone" + app:layout_constraintTop_toTopOf="@id/image" + app:layout_constraintEnd_toEndOf="@id/image" + app:srcCompat="@drawable/mozac_browser_menu_notification_icon" + tools:visibility="visible" /> + + <androidx.appcompat.widget.SwitchCompat + android:id="@+id/switch_widget" + style="@style/Mozac.Browser.Menu.Item.Text" + android:layout_width="0dp" + android:layout_height="@dimen/mozac_browser_menu_item_container_layout_height" + android:background="?android:attr/selectableItemBackgroundBorderless" + android:orientation="vertical" + android:paddingStart="@dimen/mozac_browser_menu_item_image_text_label_padding_start" + android:paddingEnd="0dp" + android:textAlignment="viewStart" + tools:text="Item" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/image" + app:layout_constraintTop_toTopOf="parent" /> +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_checkbox.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_checkbox.xml new file mode 100644 index 0000000000..ebd0991816 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_checkbox.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<androidx.appcompat.widget.AppCompatCheckBox xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + style="@style/Mozac.Browser.Menu.Item.Text" + android:layout_width="match_parent" + android:layout_height="@dimen/mozac_browser_menu_item_container_layout_height" + android:button="@null" + android:drawableEnd="?android:attr/listChoiceIndicatorMultiple" + android:drawablePadding="@dimen/mozac_browser_menu_checkbox_padding" + android:gravity="center_vertical" + android:paddingStart="16dp" + android:paddingEnd="16dp" + tools:text="Item" /> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_divider.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_divider.xml new file mode 100644 index 0000000000..819ead6066 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_divider.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<View xmlns:android="http://schemas.android.com/apk/res/android" + style="@style/Mozac.Browser.Menu.Item.Divider.Horizontal" + android:importantForAccessibility="no" + android:clickable="false"/> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_image_switch.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_image_switch.xml new file mode 100644 index 0000000000..56a20c16e6 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_image_switch.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?><!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<androidx.appcompat.widget.SwitchCompat xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/switch_widget" + style="@style/Mozac.Browser.Menu.Item.Text" + android:layout_width="match_parent" + android:layout_height="@dimen/mozac_browser_menu_item_container_layout_height" + android:drawablePadding="@dimen/mozac_browser_menu_item_image_text_icon_padding" + android:paddingStart="@dimen/mozac_browser_menu_item_container_padding_start" + android:paddingEnd="@dimen/mozac_browser_menu_item_container_padding_end" + android:textAlignment="viewStart" + app:drawableStartCompat="@android:drawable/screen_background_dark" /> + diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_image_text.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_image_text.xml new file mode 100644 index 0000000000..fc72b27707 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_image_text.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + style="@style/Mozac.Browser.Menu.Item.Container" + android:layout_width="match_parent" + android:orientation="horizontal" + android:clickable="true" + android:focusable="true" + android:gravity="center_vertical" + tools:ignore="UseCompoundDrawables"> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/image" + style="@style/Mozac.Browser.Menu.Item.ImageText.Icon" + android:clickable="false" + android:background="@android:color/transparent" + android:importantForAccessibility="no" + tools:src="@android:drawable/screen_background_dark"/> + + <TextView + android:id="@+id/text" + style="@style/Mozac.Browser.Menu.Item.ImageText.Label" + android:clickable="false" + android:focusable="false" + android:background="@android:color/transparent" + android:gravity="center_vertical" + tools:text="Item" + android:importantForAccessibility="no"/> +</LinearLayout> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_image_text_checkbox_button.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_image_text_checkbox_button.xml new file mode 100644 index 0000000000..062d8efd0b --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_image_text_checkbox_button.xml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="utf-8"?><!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + style="@style/Mozac.Browser.Menu.Item.Checkbox.Container" + android:layout_width="match_parent" + android:clickable="true" + android:focusable="true" + android:gravity="center_vertical" + android:orientation="horizontal" + tools:ignore="UseCompoundDrawables"> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/image" + style="@style/Mozac.Browser.Menu.Item.ImageText.Icon" + android:background="@android:color/transparent" + android:importantForAccessibility="no" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:src="@android:drawable/screen_background_dark" /> + + <TextView + android:id="@+id/text" + style="@style/Mozac.Browser.Menu.Item.Checkbox.Label" + android:background="@android:color/transparent" + android:gravity="center_vertical" + android:importantForAccessibility="no" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@+id/checkbox" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintHorizontal_chainStyle="spread_inside" + app:layout_constraintStart_toEndOf="@id/image" + app:layout_constraintTop_toTopOf="parent" + tools:text="Item" /> + + <View + android:id="@+id/accessibilityRegion" + android:layout_width="0dp" + android:layout_height="0dp" + android:clickable="true" + android:focusable="true" + android:importantForAccessibility="yes" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="@id/text" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <androidx.appcompat.widget.AppCompatCheckBox + android:id="@+id/checkbox" + style="@style/Mozac.Browser.Menu.Item.Checkbox.Text" + android:button="@null" + android:drawablePadding="7dp" + android:textAlignment="gravity" + android:gravity="center_vertical" + app:layout_constraintStart_toEndOf="@id/text" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintWidth_default="wrap" + tools:drawableStartCompat="@android:drawable/star_big_off" + tools:text="Add" + tools:textOff="Edit" + tools:textOn="Add" /> +</androidx.constraintlayout.widget.ConstraintLayout> + + diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_parent_menu.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_parent_menu.xml new file mode 100644 index 0000000000..3d361dcfbc --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_parent_menu.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto" + style="@style/Mozac.Browser.Menu.Item.Container" + android:layout_width="match_parent" + android:orientation="horizontal" + android:clickable="true" + android:focusable="true" + android:gravity="center_vertical" + tools:ignore="UseCompoundDrawables"> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/image" + style="@style/Mozac.Browser.Menu.Item.ImageText.Icon" + android:clickable="false" + android:background="@android:color/transparent" + android:importantForAccessibility="no" + tools:src="@android:drawable/screen_background_dark"/> + + <TextView + android:id="@+id/text" + style="@style/Mozac.Browser.Menu.Item.ImageText.Label" + android:clickable="false" + android:focusable="false" + android:background="@android:color/transparent" + android:gravity="center_vertical" + tools:text="Item" + android:importantForAccessibility="no"/> + + <View + android:layout_width="0dp" + android:layout_height="1dp" + android:layout_weight="1"/> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/overflowImage" + android:layout_width="24dp" + android:layout_height="24dp" + android:clickable="false" + android:visibility="gone" + android:background="@android:color/transparent" + android:importantForAccessibility="no" + app:srcCompat="@drawable/mozac_ic_chevron_right_24"/> +</LinearLayout> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_simple.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_simple.xml new file mode 100644 index 0000000000..a7afa796fd --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_simple.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/simple_text" + style="@style/Mozac.Browser.Menu.Item.Text" + android:layout_width="match_parent" + android:layout_height="@dimen/mozac_browser_menu_item_container_layout_height" + android:gravity="start|center_vertical" + android:paddingStart="16dp" + android:paddingEnd="16dp" + android:textAlignment="viewStart" + tools:text="Item" /> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_switch.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_switch.xml new file mode 100644 index 0000000000..9d0ee5c56e --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_switch.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<androidx.appcompat.widget.SwitchCompat xmlns:android="http://schemas.android.com/apk/res/android" + style="@style/Mozac.Browser.Menu.Item.Text" + android:layout_width="match_parent" + android:layout_height="@dimen/mozac_browser_menu_item_container_layout_height" + android:paddingStart="16dp" + android:paddingEnd="16dp"/> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_toolbar.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_toolbar.xml new file mode 100644 index 0000000000..00897559de --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_toolbar.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + style="@android:style/TextAppearance.Material.Menu" + android:layout_width="match_parent" + android:layout_height="@dimen/mozac_browser_menu_item_toolbar_height" + android:gravity="center_vertical" + android:orientation="horizontal" /> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_web_extension.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_web_extension.xml new file mode 100644 index 0000000000..1ccecbf48d --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_web_extension.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="@dimen/mozac_browser_menu_item_toolbar_height" + android:id="@+id/container" + style="@style/Mozac.Browser.Menu.Item.Container" + android:gravity="center_vertical"> + + <ImageView + android:id="@+id/action_image" + android:layout_width="@dimen/mozac_browser_menu_item_web_extension_icon_width" + android:layout_height="@dimen/mozac_browser_menu_item_web_extension_icon_height" + android:layout_gravity="center" + android:importantForAccessibility="no" + app:srcCompat="@drawable/mozac_browser_menu_notification_icon"/> + + <TextView + android:id="@+id/action_label" + style="@style/Mozac.Browser.Menu.Item.Text" + android:background="@android:color/transparent" + android:layout_width="wrap_content" + android:layout_height="@dimen/mozac_browser_menu_item_container_layout_height" + android:gravity="center_vertical" + android:textAlignment="viewStart" + android:paddingEnd="16dp" + android:paddingStart="16dp" + android:clickable="false" + android:focusable="false" + tools:ignore="RtlCompat" + tools:text="uBlock Origin" /> + + <View + android:layout_width="0dp" + android:layout_height="1dp" + android:layout_weight="1" /> + + <TextView + android:id="@+id/badge_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minWidth="22dp" + android:gravity="center" + android:textAlignment="center" + android:textStyle="bold" + android:textColor="@color/photonWhite" + android:background="@drawable/rounded_corner" + android:visibility="invisible" + android:padding="3dp" + android:textSize="12sp" + tools:text="18" /> +</LinearLayout> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_tooltip_layout.xml b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_tooltip_layout.xml new file mode 100644 index 0000000000..aa69465b9a --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_tooltip_layout.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <TextView + android:id="@+id/mozac_browser_tooltip_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="@dimen/tooltip_margin" + android:background="?attr/tooltipFrameBackground" + android:ellipsize="end" + android:maxWidth="256dp" + android:maxLines="2" + android:paddingStart="16dp" + android:paddingTop="6dp" + android:paddingEnd="16dp" + android:paddingBottom="6dp" + android:textAppearance="@style/TextAppearance.AppCompat.Tooltip" + android:textColor="?attr/tooltipForegroundColor" + tools:ignore="PrivateResource" /> +</LinearLayout> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-am/strings.xml new file mode 100644 index 0000000000..cec57d8d8a --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-am/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">ምናሌ</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">የተተኮረበት</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">ተጨማሪዎች</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">ቅጥያዎች</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">ተጨማሪዎች አስተዳዳሪ</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">የቅጥያዎች ማስተዳደሪያ</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">ወደ ላይ አስስ</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">ተጨማሪዎች፣ ወደ ላይ ዳስስ</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">ቅጥያዎች፣ ወደ ላይ ዳስስ</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-an/strings.xml new file mode 100644 index 0000000000..20c8245203 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-an/strings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menú</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Destacaus</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Complementos</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Chestor de complementos</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ar/strings.xml new file mode 100644 index 0000000000..5b80338dce --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ar/strings.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">القائمة</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">عليها الإبراز</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">الإضافات</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">مدير الإضافات</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">انتقل لأعلى</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ast/strings.xml new file mode 100644 index 0000000000..d30d41ab85 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ast/strings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menú</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Rescamplóse</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Complementos</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Xestor de complementos</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-az/strings.xml new file mode 100644 index 0000000000..1a5d4fd289 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-az/strings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menyu</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Vurğulanmış</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Əlavələr</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Əlavə idarəçisi</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-azb/strings.xml new file mode 100644 index 0000000000..89ca673be7 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-azb/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">منو</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">هایلایت اولدو</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">تاخیلان</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">اوزانتیلار</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">تاخیلان مودیریتی</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">اوزانتی مودیری</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">یوخاریا گئت</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">تاخیلانلار، یوخاری گئت</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">اوزانتیْلار، یوخاریْ گئت</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ban/strings.xml new file mode 100644 index 0000000000..6955c0d4a9 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ban/strings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menu</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Kasorot</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Pengaya</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Manajer Pengaya</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-be/strings.xml new file mode 100644 index 0000000000..f3b3bb1755 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-be/strings.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Меню</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Вылучаны(я)</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Дадаткі</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Пашырэнні</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Менеджар дадаткаў</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Перайсці ўверх</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-bg/strings.xml new file mode 100644 index 0000000000..99bd63bc40 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-bg/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Меню</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Откроено</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Добавки</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Разширения</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Управление на добавки</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Управление на разширения</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Придвижване нагоре</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Добавки, навигиране нагоре</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Разширения, навигиране нагоре</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-bn/strings.xml new file mode 100644 index 0000000000..76d355b1e4 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-bn/strings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">মেনু</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">হাইলাইট করা হয়েছে</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">অ্যাড-অন</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">অ্যাড-অন ব্যবস্থাপক</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-br/strings.xml new file mode 100644 index 0000000000..548cbd7785 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-br/strings.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Lañser</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Usskedet</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Askouezhioù</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Askouezhioù</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Ardoer an askouezhioù</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Merañ an askouezhioù</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Adpignat</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-bs/strings.xml new file mode 100644 index 0000000000..a0c32fdba8 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-bs/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Meni</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Istaknuto</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Add-oni</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Ekstenzije</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Upravnik add-onima</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Menadžer ekstenzija</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Idi prema gore</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Dodaci, idite gore</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Ekstenzije, idi gore</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ca/strings.xml new file mode 100644 index 0000000000..85424c3ba1 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ca/strings.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menú</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">S’ha ressaltat</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Complements</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Gestor de complements</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Navega amunt</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-cak/strings.xml new file mode 100644 index 0000000000..7ccb1be616 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-cak/strings.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">K\'utsamaj</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Ya\'on ruq\'ij</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Taq tz\'aqat</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Kinuk\'samajel taq Tz\'aqat</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Tib\'an okem ajsik</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description">Taq tz\'aqat, tijote\' chi rokem</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ceb/strings.xml new file mode 100644 index 0000000000..e02f99d729 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ceb/strings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menu</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Gipasiugda</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Add-on</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Add-on Manager</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ckb/strings.xml new file mode 100644 index 0000000000..ee5821c024 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ckb/strings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">پێڕست</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">ئاماژەپێکراو</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">پێوەکراوەکان</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">بەڕێوەبەری پێوەکراوەکان</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-co/strings.xml new file mode 100644 index 0000000000..fb805d3142 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-co/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Listinu</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Sopralineatu</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Moduli addiziunali</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Estensioni</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Ghjestiunariu di moduli addiziunali</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Ghjestiunariu d’estensioni</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Navigà insù</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Moduli addiziunali, ricullà</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Estensioni, ricullà</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-cs/strings.xml new file mode 100644 index 0000000000..450256d049 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-cs/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Nabídka</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Zvýrazněné</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Doplňky</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Rozšíření</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Správce doplňků</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Správce rozšíření</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Přejít nahoru</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Doplňky, přejít nahoru</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Rozšíření, přejít nahoru</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-cy/strings.xml new file mode 100644 index 0000000000..7543b08a9e --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-cy/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Dewislen</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Amlygwyd</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Ychwanegion</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Estyniadau</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Rheolwr Ychwanegion</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Rheolwr Estyniadau</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Llywio i fyny</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Ychwanegion, symud i fyny</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Estyniadau, llywio i fyny</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-da/strings.xml new file mode 100644 index 0000000000..31fd32a217 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-da/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menu</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Fremhævet</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Tilføjelser</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Udvidelser</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Håndtering af tilføjelser</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Håndtering af udvidelser</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Naviger op</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Tilføjelser, naviger op</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Udvidelser, naviger op</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-de/strings.xml new file mode 100644 index 0000000000..4d0ec0bccc --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-de/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menü</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Hervorgehoben</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Erweiterungen</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons-Verwaltung</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Erweiterungs-Manager</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Nach oben navigieren</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons, nach oben navigieren</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Erweiterungen, nach oben navigieren</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-dsb/strings.xml new file mode 100644 index 0000000000..868f5ca76e --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-dsb/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Meni</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Wuzwignjony</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Dodanki</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Rozšyrjenja</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Zastojnik dodankow</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Zastojnik rozšyrjenjow</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Górjej</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Dodanki, górjej nawigěrowaś</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Rozšyrjenja, górjej nawigěrowaś</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-el/strings.xml new file mode 100644 index 0000000000..37c1c075af --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-el/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Μενού</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Επισημασμένο</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Πρόσθετα</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Επεκτάσεις</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Διαχείριση προσθέτων</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Διαχείριση επεκτάσεων</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Πλοήγηση προς τα πάνω</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Πρόσθετα, πλοήγηση προς τα πάνω</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Επεκτάσεις, πλοήγηση προς τα πάνω</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-en-rCA/strings.xml new file mode 100644 index 0000000000..9e33b4ffcd --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-en-rCA/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menu</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Highlighted</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Extensions</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons Manager</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Extensions Manager</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Navigate up</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons, navigate up</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Extensions, navigate up</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-en-rGB/strings.xml new file mode 100644 index 0000000000..9e33b4ffcd --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-en-rGB/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menu</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Highlighted</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Extensions</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons Manager</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Extensions Manager</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Navigate up</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons, navigate up</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Extensions, navigate up</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-eo/strings.xml new file mode 100644 index 0000000000..63b30a2150 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-eo/strings.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menuo</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Elstarigitaj</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Aldonaĵoj</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Administrilo de aldonaĵoj</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Iri supren</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description">Aldonaĵoj, supren</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-es-rAR/strings.xml new file mode 100644 index 0000000000..b527c2e9bb --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-es-rAR/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menú</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Resaltado</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Complementos</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Complementos</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Administrador de complementos</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Administrador de complementos</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Navegar hacia arriba</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Complementos, navegar hacia arriba</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Complementos, navegar hacia arriba</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-es-rCL/strings.xml new file mode 100644 index 0000000000..954a4a0678 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-es-rCL/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menú</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Destacado</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Complementos</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Extensiones</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Administrador de complementos</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Administrador de extensiones</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Navegar hacia arriba</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Complementos, navegar hacia arriba</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Extensiones, navegar hacia arriba</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-es-rES/strings.xml new file mode 100644 index 0000000000..971fcb13c1 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-es-rES/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menú</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Resaltado</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Complementos</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Extensiones</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Administrador de complementos</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Administrador de extensiones</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Navegar hacia arriba</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Complementos, navega hacia arriba</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Extensiones, navega hacia arriba</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-es-rMX/strings.xml new file mode 100644 index 0000000000..b699871b37 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-es-rMX/strings.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menú</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Destacado</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Complementos</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Administrador de complementos</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Navegar hacia arriba</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-es/strings.xml new file mode 100644 index 0000000000..971fcb13c1 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-es/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menú</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Resaltado</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Complementos</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Extensiones</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Administrador de complementos</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Administrador de extensiones</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Navegar hacia arriba</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Complementos, navega hacia arriba</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Extensiones, navega hacia arriba</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-et/strings.xml new file mode 100644 index 0000000000..e21a050061 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-et/strings.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menüü</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Esiletõstetud</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Lisad</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Lisade haldur</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Liigu üles</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-eu/strings.xml new file mode 100644 index 0000000000..53f1a556a3 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-eu/strings.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menua</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Nabarmendua</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Gehigarriak</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Gehigarrien kudeatzailea</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Nabigatu gora</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description">Gehigarriak, nabigatu gora</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-fa/strings.xml new file mode 100644 index 0000000000..af1bd783a6 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-fa/strings.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">منو</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">برجستهشده</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">افزونهها</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">مدیریت افزونهها</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">ناوش به بالا</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ff/strings.xml new file mode 100644 index 0000000000..2ad3a89319 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ff/strings.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Ɓeyditte</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Topitorde Ɓeyditte</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-fi/strings.xml new file mode 100644 index 0000000000..e78d8971b6 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-fi/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Valikko</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Korostettu</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Lisäosat</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Laajennukset</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Lisäosien hallinta</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Laajennusten hallinta</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Liiku ylöspäin</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Lisäosat, liiku ylös</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Laajennukset, liiku ylös</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-fr/strings.xml new file mode 100644 index 0000000000..00c13d5fe1 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-fr/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menu</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Sélectionné</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Modules complémentaires</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Extensions</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Gestionnaire de modules</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Gestionnaire d’extensions</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Remonter</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Modules complémentaires, remonter</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Extensions, remonter</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-fur/strings.xml new file mode 100644 index 0000000000..211c5d879f --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-fur/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menù</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Evidenziât</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Components adizionâi</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Estensions</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Gjestôr comp. adizionâi</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Gjestôr estensions</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Navighe in sù</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Components adizionâi, torne indaûr</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Estensions, torne indaûr</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-fy-rNL/strings.xml new file mode 100644 index 0000000000..39e9c4e032 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-fy-rNL/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menu</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Markearre</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Utwreidingen</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Add-onbehearder</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Utwreidingsbehearder</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Omheech</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons, omheech navigearje</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Utwreidingen, omheech navigearje</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-gd/strings.xml new file mode 100644 index 0000000000..89524181f6 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-gd/strings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">An clàr-taice</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Soillsichte</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Tuilleadain</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Manaidsear nan tuilleadan</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-gl/strings.xml new file mode 100644 index 0000000000..20db3e0dc0 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-gl/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menú</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Realzado</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Complementos</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Extensións</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Xestor de complementos</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Xestor de extensións</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Navegar cara arriba</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Complementos, navegar cara arriba</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Extensións, navega cara arriba</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-gn/strings.xml new file mode 100644 index 0000000000..25fc596f2d --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-gn/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Poravorã</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Hechaukaveha</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Moĩmbaha</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Jepysokue</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Moĩmbaha ñangarekohára</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Jepysokue ñangarekoha</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Eikundaha yvate gotyo</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Moĩmbaha, eikundaha yvate gotyo</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Jepysokue, eikundaha yvate gotyo</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-gu-rIN/strings.xml new file mode 100644 index 0000000000..fde91325c9 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-gu-rIN/strings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">મેનુ</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">પ્રકાશિત કરેલ</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">ઍડ-ઑન્સ</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">એડ-ઑન્સ સંચાલક</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-hi-rIN/strings.xml new file mode 100644 index 0000000000..c5eab9cbd7 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-hi-rIN/strings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">मेन्यू</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">दर्शाए गए</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">ऐड-ऑन</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">ऐड-ऑन प्रबंधक</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-hil/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-hil/strings.xml new file mode 100644 index 0000000000..c6b11e8350 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-hil/strings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menu</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Highlighted</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Mga Add-on</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Mga add-on sang Manager</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-hr/strings.xml new file mode 100644 index 0000000000..ee6f3922ce --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-hr/strings.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Izbornik</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Istaknuto</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Dodaci</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Upravljač dodataka</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Navigiraj gore</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-hsb/strings.xml new file mode 100644 index 0000000000..efb7ef3879 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-hsb/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Meni</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Wuzběhnjeny</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Přidatki</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Rozšěrjenja</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Zrjadowak přidatkow</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Zrjadowak rozšěrjenjow</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Horje</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Přidatki, horje nawigěrować</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Rozšěrjenja, horje nawigěrować</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-hu/strings.xml new file mode 100644 index 0000000000..5f0c7c32af --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-hu/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menü</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Kiemelt</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Kiegészítők</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Kiegészítők</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Kiegészítőkezelő</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Kiegészítőkezelő</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Navigálás fel</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Kiegészítők, navigáció felfelé</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Kiegészítők, navigáció felfelé</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-hy-rAM/strings.xml new file mode 100644 index 0000000000..1eb1bf7ab9 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-hy-rAM/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Ցանկ</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Գունանշված</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Հավելումներ</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Ընդլայնումներ</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Հավելումների կառավարիչ</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Ընդլայնումների կառավարիչ</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Նավարկել վերև</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Հավելումներ, նավարկեք վերև</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Ընդլայնումներ, նավարկեք վերև</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ia/strings.xml new file mode 100644 index 0000000000..97991d144c --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ia/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menu</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Evidentiate</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Additivos</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Extensiones</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Gestor de additivos</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Gestor de extensiones</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Navigar</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Additivos, navigar retro</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Extensiones, remontar</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-in/strings.xml new file mode 100644 index 0000000000..f23b41f900 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-in/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menu</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Tersorot</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Pengaya</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Ekstensi</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Pengelola Pengaya</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Pengelola Ekstensi</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Arahkan ke atas</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Pengaya, navigasikan ke atas</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Ekstensi, navigasikan ke atas</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-is/strings.xml new file mode 100644 index 0000000000..3b1d78f3b5 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-is/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Valmynd</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Undirstrikað</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Viðbætur</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Forritsaukar</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Viðbótastjóri</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Umsýsla forritsauka</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Flakka upp</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Viðbætur, fara upp</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Forritsaukar, fara upp</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-it/strings.xml new file mode 100644 index 0000000000..f7e7f9720c --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-it/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menu</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Evidenziato</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Componenti aggiuntivi</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Estensioni</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Gestore comp. aggiuntivi</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Gestione estensioni</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Passa a livello superiore</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Componenti aggiuntivi, torna indietro</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Estensioni, torna indietro</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-iw/strings.xml new file mode 100644 index 0000000000..e164d78059 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-iw/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">תפריט</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">מודגש</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">תוספות</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">הרחבות</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">מנהל התוספות</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">מנהל ההרחבות</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">ניווט למעלה</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">תוספות, ניווט למעלה</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">הרחבות, ניווט למעלה</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ja/strings.xml new file mode 100644 index 0000000000..a5f5383eae --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ja/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">メニュー</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">強調</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">アドオン</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">拡張機能</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">アドオンマネージャー</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">拡張機能マネージャー</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">上へ移動します</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">アドオン、上へ移動します</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">拡張機能、上へ移動します</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ka/strings.xml new file mode 100644 index 0000000000..5971c5f9fd --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ka/strings.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">მენიუ</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">მონიშნული</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">დამატებები</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">დამატებების მმართველი</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">ზემოთ გადასვლა</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-kaa/strings.xml new file mode 100644 index 0000000000..4a3bd0164c --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-kaa/strings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menyu</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Belgilengen</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Qosımshalar</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Qosımshalar menedjeri</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-kab/strings.xml new file mode 100644 index 0000000000..f36a1014b1 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-kab/strings.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Umuɣ</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">ittwag deg uqerru</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Izegrar</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Isiɣzaf</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Amsefrak n izegrar</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Inig d asawen</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-kk/strings.xml new file mode 100644 index 0000000000..6a25654110 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-kk/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Мәзір</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Ерекшеленген</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Қосымшалар</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Кеңейтулер</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Қосымшалар басқарушысы</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Кеңейтулер басқарушысы</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Жоғары жылжу</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Қосымшалар, жоғары өту</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Кеңейтулер, жоғары өту</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-kmr/strings.xml new file mode 100644 index 0000000000..c977c1edc4 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-kmr/strings.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menû</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Berbiçavkirî</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Pêvek</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Rêvebera pêvekan</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Here jorê</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description">Pêvek, bi jorê ve here</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-kn/strings.xml new file mode 100644 index 0000000000..ca1bfb47be --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-kn/strings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">ಮೆನು</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">ಹೈಲೈಟ್ ಮಾಡಲಾಗಿದೆ</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">ಆಡ್-ಆನ್ಗಳು</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">ಆಡ್-ಆನ್ಗಳ ವ್ಯವಸ್ಥಾಪಕ</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ko/strings.xml new file mode 100644 index 0000000000..f661f30b1e --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ko/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">메뉴</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">강조 표시됨</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">부가 기능</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">확장 기능</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">부가 기능 관리자</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">확장 기능 관리자</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">위로 이동</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">부가 기능, 위로 이동</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">확장 기능, 위로 이동</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-kw/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-kw/strings.xml new file mode 100644 index 0000000000..2dc45ab574 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-kw/strings.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Rol</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Golowboyntys</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Keworansow</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Restrer Keworansow</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Brennya war-vann</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ldrtl/dimens.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ldrtl/dimens.xml new file mode 100644 index 0000000000..10219fc900 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ldrtl/dimens.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<resources> + <dimen name="mozac_browser_menu_highlightable_notification_translate_x">-4dp</dimen> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-lij/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-lij/strings.xml new file mode 100644 index 0000000000..c07f67427c --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-lij/strings.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">In evidensa</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Conponenti azonti</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-lo/strings.xml new file mode 100644 index 0000000000..82a2c4998a --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-lo/strings.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">ເມນູ</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">ຈຸດເດັ່ນ</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Add-ons</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">ຕົວຈັດການ Add-ons</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">ນຳທາງຂຶ້ນໄປທາງເທິງ</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-lt/strings.xml new file mode 100644 index 0000000000..4fbc9bf311 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-lt/strings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Meniu</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Paryškinta</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Priedai</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Priedų tvarkytuvė</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-mix/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-mix/strings.xml new file mode 100644 index 0000000000..228f3e66cf --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-mix/strings.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Katsi</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Tu^un nchichi</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Add-ons</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Komplementos</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Ku\'ntyeé kutyi</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description">Complemento, ku\'ntyeé kutyi</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-mr/strings.xml new file mode 100644 index 0000000000..69f21eed4c --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-mr/strings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">मेनू</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">ठळक</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">ॲड-ऑन</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">अॅड-ऑन व्यवस्थापक</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-my/strings.xml new file mode 100644 index 0000000000..f01b0ddac4 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-my/strings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">မီနူး</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">အသားပေးအရာ</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">အတ်အွန်များ</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">အတ်အွန် စီမံရေး</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-nb-rNO/strings.xml new file mode 100644 index 0000000000..d6dbb1debb --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-nb-rNO/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Meny</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Uthevet</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Tillegg</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Utvidelser</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Tilleggsbehandler</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Behandling av utvidelser</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Naviger opp</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Tillegg, naviger opp</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Utvidelser, naviger opp</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ne-rNP/strings.xml new file mode 100644 index 0000000000..d7ca7d7694 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ne-rNP/strings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">मेनु</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">हाइलाइट गरियो</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">एड-अनहरू</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">एड-अन म्यानेजर</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-nl/strings.xml new file mode 100644 index 0000000000..b11eaaa5e2 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-nl/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menu</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Gemarkeerd</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Extensies</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Add-onbeheerder</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Extensiebeheerder</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Omhoog</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons, omhoog navigeren</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Extensies, omhoog navigeren</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-nn-rNO/strings.xml new file mode 100644 index 0000000000..2bdfe10f1c --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-nn-rNO/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Meny</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Utheva</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Tillegg</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Utvidingar</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Tilleggshandsamar</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Utvidingshandsamar</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Naviger opp</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Tillegg, naviger opp</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Utvidingar, naviger opp</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-oc/strings.xml new file mode 100644 index 0000000000..ad88a95a37 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-oc/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menú</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Notables</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Moduls complementaris</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Extensions</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Gestionari de moduls complementaris</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Gestionari d’extensions</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Remontar</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Moduls complementaris, montar</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Extension, montar</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-or/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-or/strings.xml new file mode 100644 index 0000000000..94b86a71fe --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-or/strings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">ମେନୁ</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">ହାଇଲାଇଟ୍ କରାଯାଇଥିବା</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">ଆଡ଼-ଅନସମୂହ</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">ଆଡ-ଅନ ପରିଚାଳକ</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-pa-rIN/strings.xml new file mode 100644 index 0000000000..342ab91a44 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-pa-rIN/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">ਮੀਨੂ</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">ਉਘਾੜੇ</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">ਐਡ-ਆਨ</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">ਇਕਸਟੈਨਸ਼ਨਾਂ</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">ਐਡ-ਆਨ ਮੈਨੇਜਰ</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">ਇਕਸਟੈਨਸ਼ਨ ਮੈਨੇਜਰ</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">ਉੱਤੇ ਜਾਓ</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">ਐਡ-ਆਨ, ਉੱਤੇ</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">ਇਕਸਟੈਨਸ਼ਨਾਂ, ਉੱਤੇ ਵੱਲ</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-pa-rPK/strings.xml new file mode 100644 index 0000000000..295865d138 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-pa-rPK/strings.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">مینو</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">اُگھاڑے</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">وادھے والے</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">وادھیاں والیاں دیاں سیٹنگاں</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">اُتے جاؤ</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-pl/strings.xml new file mode 100644 index 0000000000..1929685c94 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-pl/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menu</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Wyróżnione</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Dodatki</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Rozszerzenia</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Zarządzaj dodatkami</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Zarządzaj rozszerzeniami</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Przejdź w górę</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Dodatki, przejdź w górę</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Rozszerzenia, przejdź w górę</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-pt-rBR/strings.xml new file mode 100644 index 0000000000..44cbedefab --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-pt-rBR/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menu</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Destacado</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Extensões</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Extensões</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Gerenciador de extensões</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Gerenciador de extensões</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Ir para o topo</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Extensões, voltar</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Extensões, voltar</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-pt-rPT/strings.xml new file mode 100644 index 0000000000..75b9647a29 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-pt-rPT/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menu</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Destacado</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Extras</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Extensões</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Gestor de extras</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Gestor de Extensões</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Navegar para cima</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Extras, navegar para cima</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Extensões, navegar para cima</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-rm/strings.xml new file mode 100644 index 0000000000..8c8152c946 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-rm/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menu</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Cun emfasa</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Supplements</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Extensiuns</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Administraziun da supplements</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Administraziun dad extensiuns</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Navigar ensi</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Supplements, returnar ensi</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Extensiuns, turnar ensi</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ro/strings.xml new file mode 100644 index 0000000000..ab9f724ab0 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ro/strings.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Meniu</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Evidențiat</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Suplimente</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Manager de suplimente</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Navighează în sus</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ru/strings.xml new file mode 100644 index 0000000000..3a7c4eceab --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ru/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Меню</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Выделено</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Дополнения</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Расширения</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Менеджер дополнений</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Менеджер расширений</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Перейти наверх</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Дополнения, перейти вверх</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Расширения, перейти вверх</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-sat/strings.xml new file mode 100644 index 0000000000..4650592f55 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-sat/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">ᱢᱤᱱᱭᱩ</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">ᱩᱪᱷᱟᱹᱱᱟᱜ</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">ᱮᱰᱼᱚᱱᱥ</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">ᱮᱠᱥᱴᱮᱱᱥᱚᱱᱠᱚ</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">ᱮᱰᱼᱚᱱᱥ ᱵᱮᱵᱚᱥᱛᱟ</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">ᱮᱠᱥᱴᱮᱱᱥᱚᱱ ᱢᱮᱱᱮᱡᱚᱨ</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">ᱪᱮᱛᱟᱱ ᱥᱮᱱ ᱱᱮᱣᱤᱜᱮᱴ ᱢᱮ</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">ᱮᱰ-ᱚᱱᱥ, ᱪᱮᱛᱟᱱ ᱥᱮᱫ</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">ᱮᱠᱥᱴᱮᱱᱥᱚᱱ, ᱪᱮᱛᱟᱱ ᱛᱮ ᱥᱮᱱᱚᱜ ᱢᱮ</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-sc/strings.xml new file mode 100644 index 0000000000..4fd82314b3 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-sc/strings.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menù</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">In evidèntzia</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Cumplementos</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Gestore de cumplementos</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Nàviga in artu</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-si/strings.xml new file mode 100644 index 0000000000..08f69d548f --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-si/strings.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">වට්ටෝරුව</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">ත්රීවාලෝකිත</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">එක්කහු</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">දිගු</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">එක්කහු කළමනාකරු</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">දිගු කළමනාකරු</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">ඉහළට යාත්රණය</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-sk/strings.xml new file mode 100644 index 0000000000..3a119e7b06 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-sk/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Ponuka</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Zvýraznené</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Doplnky</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Rozšírenia</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Správca doplnkov</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Správca rozšírení</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Prejsť nahor</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Doplnky, prejsť nahor</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Rozšírenia, prejsť nahor</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-skr/strings.xml new file mode 100644 index 0000000000..cd91a8395b --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-skr/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">مینیو</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">نمایاں کیتا ڳیا</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">ایڈ ــ آن</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">ایکسٹینشنز</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">ایڈ ــ آن منیجر</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">ایکسٹنشنز منیجر</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">اُتے ون٘ڄو</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">ایڈ آن, اُتے نیویگیٹ کرو</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">ایکسٹنشناں, اُتے نیویگیٹ کرو</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-sl/strings.xml new file mode 100644 index 0000000000..f08c3d07ce --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-sl/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Meni</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Označeno</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Dodatki</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Razširitve</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Upravitelj dodatkov</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Upravitelj razširitev</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Pojdi gor</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Dodatki, pomakni se gor</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Razširitve, pomakni se gor</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-sq/strings.xml new file mode 100644 index 0000000000..54f6a4112c --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-sq/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menu</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">E theksuar</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Shtesa</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Zgjerime</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Përgjegjës Shtesash</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Përgjegjës Zgjerimesh</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Lëvizni për sipër</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Shtesa, shkoni sipër</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Zgjerime, shkoni sipër</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-sr/strings.xml new file mode 100644 index 0000000000..5853163b79 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-sr/strings.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Мени</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Истакнуто</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Додаци</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Управник додатака</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Иди нагоре</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-su/strings.xml new file mode 100644 index 0000000000..1a786bee7f --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-su/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menu</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Disorot</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Émboh</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Éksténsi</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Manajer Émboh</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Pangatur éksténsi</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Pindah ka luhur</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Add-ins, tuduhkeun ka luhur</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Éksténsi, tuduhkeun ka luhur</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-sv-rSE/strings.xml new file mode 100644 index 0000000000..002fa3390f --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-sv-rSE/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Meny</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Markerad</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Tillägg</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Tillägg</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Tilläggshanterare</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Tilläggshanterare</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Navigera uppåt</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Tillägg, navigera uppåt</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Tillägg, navigera upp</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-szl/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-szl/strings.xml new file mode 100644 index 0000000000..497be7b9bf --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-szl/strings.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Myni</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Ôbznoczōne</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Rozszyrzynia</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Regiyrowanie rozszyrzyniami</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Zōńdź na wiyrch</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ta/strings.xml new file mode 100644 index 0000000000..a00d99cf17 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ta/strings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">பட்டி</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">மிளிர்ப்புகள்</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">துணை நிரல்கள்</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">துணை நிரல் நிர்வாகி</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-te/strings.xml new file mode 100644 index 0000000000..c951083b0a --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-te/strings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">మెనూ</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">హైలైట్ చేసినవి</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">పొడగింతలు</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">పొడగింతల నిర్వాహకి</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-tg/strings.xml new file mode 100644 index 0000000000..e3c7eecde1 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-tg/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Меню</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Таъкид</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Ҷузъи иловагӣ</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Васеъшавиҳо</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Мудири ҷузъи иловагӣ</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Мудири васеъшавиҳо</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Ба боло гузаред</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Ҷузъҳои иловагӣ, гузариш ба боло</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Васеъшавиҳо, гузариш ба боло</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-th/strings.xml new file mode 100644 index 0000000000..4ab63a3cdd --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-th/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">เมนู</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">เน้น</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">ส่วนเสริม</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">ส่วนขยาย</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">ตัวจัดการส่วนเสริม</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">ตัวจัดการส่วนขยาย</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">นำทางขึ้นไปด้านบน</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">ส่วนเสริม, นำทางไปด้านบน</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">ส่วนขยาย, นำทางไปด้านบน</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-tl/strings.xml new file mode 100644 index 0000000000..45b5f15738 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-tl/strings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menu</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Mga naka-highlight</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Mga Add-on</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Add-on Manager</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-tok/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-tok/strings.xml new file mode 100644 index 0000000000..f946769c8c --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-tok/strings.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">ilo pi kepeken sin</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">lawa pi kepeken sin</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-tr/strings.xml new file mode 100644 index 0000000000..7e4c87c3f3 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-tr/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menü</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Vurgulu</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Eklentiler</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Uzantılar</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Eklenti yöneticisi</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Uzantı yöneticisi</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Yukarı git</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Eklentiler, yukarı git</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Uzantılar, yukarı git</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-trs/strings.xml new file mode 100644 index 0000000000..25b10e1df4 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-trs/strings.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menû</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Sa ña\'āan</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Sa gā\'ue nūtò\'</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Sa nīkāj ñu\'ūnj nej sa gā\'ue nūtò\'</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Gāchē nun gan’ānj nāhuīt</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-tt/strings.xml new file mode 100644 index 0000000000..68f7abc358 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-tt/strings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Меню</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Аерылган</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Кушымчалар</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Кушымчалар менеджеры</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-tzm/strings.xml new file mode 100644 index 0000000000..0c2827eb8f --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-tzm/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Umuɣ</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ug/strings.xml new file mode 100644 index 0000000000..df31e6911e --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ug/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">تىزىملىك</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">ئالاھىدە</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">قىستۇرما</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">كېڭەيتمە</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">قىستۇرما باشقۇرغۇچ</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">كېڭەيتمە باشقۇرۇش</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">ئۈستىگە يول باشلا</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">قوشۇلما، ئۈستىگە يول باشلا</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">كېڭەيتمە، ئۈستىگە يول باشلا</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-uk/strings.xml new file mode 100644 index 0000000000..3788a0d5c8 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-uk/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Меню</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Виділено</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Додатки</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Розширення</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Керувати додатками</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Менеджер розширень</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Вгору</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Додатки, перейти вгору</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Розширення, перейти вгору</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-ur/strings.xml new file mode 100644 index 0000000000..c6070a6ec2 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-ur/strings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">مینیو</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">نمایاں کیا گیا</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">ایڈ اون</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">ایڈ اون مینیجر</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-uz/strings.xml new file mode 100644 index 0000000000..940434e75a --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-uz/strings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menyu</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Belgilangan</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Qoʻshimcha dasturlar</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Qoʻshimcha dasturlar boshqaruvchisi</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-vec/strings.xml new file mode 100644 index 0000000000..c999198e19 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-vec/strings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menù</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Evidensià</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Conponenti che se pole xontare</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Gestion Estension</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-vi/strings.xml new file mode 100644 index 0000000000..8100e82a1c --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-vi/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menu</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Đã tô sáng</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Tiện ích</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Tiện ích</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Quản lí tiện ích</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Quản lý tiện ích</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Điều hướng lên</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Tiện ích, điều hướng lên</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Tiện ích, điều hướng lên</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-yo/strings.xml new file mode 100644 index 0000000000..6cacf9d220 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-yo/strings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Mẹ́nù</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Afàmìsí</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons">Àfikún</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager">Àsàmójútó Àfikún</string> + </resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-zh-rCN/strings.xml new file mode 100644 index 0000000000..bd7fd2ad36 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-zh-rCN/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">菜单</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">高亮</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">附加组件</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">扩展</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">附加组件管理器</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">扩展管理器</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">向上导航</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">附加组件,向上导航</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">扩展,向上导航</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000000..e566379f79 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">選單</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">強調</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">附加元件</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">擴充套件</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">附加元件管理員</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">擴充套件管理員</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">向上導航</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">附加元件,向上導航</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">擴充套件,向上導航</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values/colors.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values/colors.xml new file mode 100644 index 0000000000..8b76cc158d --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values/colors.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<resources> + <!-- Empty by default, allows others to theme as they see fit --> + <color name="mozac_browser_menu_background"></color> +</resources>
\ No newline at end of file diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values/dimens.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values/dimens.xml new file mode 100644 index 0000000000..c815522515 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values/dimens.xml @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<resources> + <dimen name="mozac_browser_menu_corner_radius">4dp</dimen> + <dimen name="mozac_browser_menu_elevation">8dp</dimen> + <dimen name="mozac_browser_menu_width">250dp</dimen> + <dimen name="mozac_browser_menu_width_min">250dp</dimen> + <dimen name="mozac_browser_menu_width_max">250dp</dimen> + <dimen name="mozac_browser_menu_padding_vertical">0dp</dimen> + + <!--Menu Item --> + <dimen name="mozac_browser_menu_item_text_size">16sp</dimen> + <dimen name="mozac_browser_menu_item_container_layout_height">48dp</dimen> + <dimen name="mozac_browser_menu_item_container_padding_start">16dp</dimen> + <dimen name="mozac_browser_menu_item_container_padding_end">16dp</dimen> + <!--Menu Item --> + + <!--Checkbox Menu Item --> + <dimen name="mozac_browser_menu_item_checkbox_container_layout_height">48dp</dimen> + <dimen name="mozac_browser_menu_item_checkbox_container_padding_start">16dp</dimen> + <dimen name="mozac_browser_menu_item_checkbox_container_padding_end">16dp</dimen> + <!--Checkbox Menu Item --> + + <!--DynamicWidthRecyclerView --> + <dimen name="mozac_browser_menu_material_min_tap_area">48dp</dimen> + <dimen name="mozac_browser_menu_material_min_item_width">112dp</dimen> + <!--DynamicWidthRecyclerView --> + + <!--BrowserMenuDivider --> + <dimen name="mozac_browser_menu_item_divider_height">1dp</dimen> + <!--BrowserMenuDivider --> + + <!--BrowserMenuHighlightableItem --> + <dimen name="mozac_browser_menu_highlightable_notification_translate_x">4dp</dimen> + <dimen name="mozac_browser_menu_highlightable_notification_translate_y">-4dp</dimen> + <dimen name="mozac_browser_menu_highlightable_notification_dot_size">8dp</dimen> + <!--BrowserMenuHighlightableItem --> + + <!-- BrowserMenuCategory --> + <dimen name="mozac_browser_menu_category_text_size">14sp</dimen> + <dimen name="mozac_browser_menu_category_layout_height">40dp</dimen> + <dimen name="mozac_browser_menu_category_padding_start">16dp</dimen> + <dimen name="mozac_browser_menu_category_padding_end">16dp</dimen> + <!-- BrowserMenuCategory --> + + <!--BrowserMenuCheckbox --> + <dimen name="mozac_browser_menu_checkbox_padding">12dp</dimen> + <!--BrowserMenuCheckbox --> + + <!--WebExtensionBrowserMenuItem --> + <dimen name="mozac_browser_menu_item_web_extension_icon_width">24dp</dimen> + <dimen name="mozac_browser_menu_item_web_extension_icon_height">24dp</dimen> + <!--WebExtensionBrowserMenuItem --> + + <!--BrowserMenuImageText--> + + <!--Icon--> + <dimen name="mozac_browser_menu_item_image_text_icon_width">24dp</dimen> + <dimen name="mozac_browser_menu_item_image_text_icon_height">24dp</dimen> + <dimen name="mozac_browser_menu_item_image_text_icon_padding">20dp</dimen> + <!--Icon--> + + <!--Label--> + <dimen name="mozac_browser_menu_item_image_text_label_padding_start">20dp</dimen> + <dimen name="mozac_browser_menu_item_checkbox_text_label_padding_start">20dp</dimen> + <dimen name="mozac_browser_menu_item_checkbox_text_label_padding_end">20dp</dimen> + <!--Label--> + + <!--Checkbox--> + <dimen name="mozac_browser_menu_item_image_checkbox_padding_start">12dp</dimen> + <dimen name="mozac_browser_menu_item_image_checkbox_padding_end">12dp</dimen> + <!--Checkbox--> + + <!--BrowserMenuImageText--> + + <!-- BrowserMenuItemToolbar --> + <dimen name="mozac_browser_menu_item_toolbar_height">56dp</dimen> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values/strings.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values/strings.xml new file mode 100644 index 0000000000..dae4c81308 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values/strings.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://mozac.org/tools"> + <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. --> + <string name="mozac_browser_menu_button">Menu</string> + <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight --> + <string name="mozac_browser_menu_highlighted">Highlighted</string> + <!-- Label for add-ons submenu section --> + <string name="mozac_browser_menu_addons" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons</string> + <!-- Label for extensions submenu section --> + <string name="mozac_browser_menu_extensions">Extensions</string> + <!-- Label for add-ons sub menu item for add-ons manager --> + <string name="mozac_browser_menu_addons_manager" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons Manager</string> + <!-- Label for extensions sub menu item for extensions manager --> + <string name="mozac_browser_menu_extensions_manager">Extensions Manager</string> + <!-- Content description for the action bar "up" button --> + <string name="action_bar_up_description">Navigate up</string> + <!-- Content description for the action bar "up" button of the add-ons sub menu item --> + <string name="mozac_browser_menu_addons_description" moz:removedIn="126" tools:ignore="UnusedResources">Add-ons, navigate up</string> + <!-- Content description for the action bar "up" button of the extensions sub menu item --> + <string name="mozac_browser_menu_extensions_content_description">Extensions, navigate up</string> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/main/res/values/style.xml b/mobile/android/android-components/components/browser/menu/src/main/res/values/style.xml new file mode 100644 index 0000000000..078d14e7fa --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/main/res/values/style.xml @@ -0,0 +1,106 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<resources> + + <style name="Mozac.Browser.Menu" parent=""> + <item name="cardBackgroundColor">@color/mozac_browser_menu_background</item> + </style> + + <!-- Item Divider --> + <style name="Mozac.Browser.Menu.Item.Divider" parent=""> + <item name="android:background">?android:attr/listDivider</item> + </style> + + <style name="Mozac.Browser.Menu.Item.Divider.Horizontal"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">@dimen/mozac_browser_menu_item_divider_height</item> + </style> + <!-- Item Divider --> + + <style name="Mozac.Browser.Menu.Item.Container" parent=""> + <item name="android:layout_height">@dimen/mozac_browser_menu_item_container_layout_height</item> + <item name="android:paddingStart">@dimen/mozac_browser_menu_item_container_padding_start</item> + <item name="android:paddingEnd">@dimen/mozac_browser_menu_item_container_padding_end</item> + <item name="android:background">?android:attr/selectableItemBackground</item> + </style> + + <style name="Mozac.Browser.Menu.Item.CandidateContainer" parent="Mozac.Browser.Menu.Item.Container"> + <item name="android:paddingStart">16dp</item> + <item name="android:paddingEnd">16dp</item> + </style> + + <style name="Mozac.Browser.Menu.Item.Text" parent="@android:style/TextAppearance.Material.Menu"> + <item name="android:background">?android:attr/selectableItemBackground</item> + <item name="android:textSize">@dimen/mozac_browser_menu_item_text_size</item> + <item name="android:ellipsize">end</item> + <item name="android:lines">1</item> + <item name="android:focusable">true</item> + <item name="android:clickable">true</item> + </style> + + <!-- BrowserMenuCategory --> + <style name="Mozac.Browser.Menu.Item.Category" parent=""> + <item name="android:layout_height">@dimen/mozac_browser_menu_category_layout_height</item> + <item name="android:textSize">@dimen/mozac_browser_menu_category_text_size</item> + <item name="android:paddingStart">@dimen/mozac_browser_menu_category_padding_start</item> + <item name="android:paddingEnd">@dimen/mozac_browser_menu_category_padding_end</item> + <item name="android:background">?android:attr/selectableItemBackground</item> + </style> + <!-- BrowserMenuCategory --> + + <!-- BrowserMenuImageText --> + <style name="Mozac.Browser.Menu.Item.ImageText.Icon" parent=""> + <item name="android:layout_width">@dimen/mozac_browser_menu_item_image_text_icon_width</item> + <item name="android:layout_height">@dimen/mozac_browser_menu_item_image_text_icon_height</item> + </style> + + <style name="Mozac.Browser.Menu.Item.CandidateIcon" parent="Mozac.Browser.Menu.Item.ImageText.Icon"> + <item name="android:layout_width">24dp</item> + <item name="android:layout_height">24dp</item> + </style> + + <style name="Mozac.Browser.Menu.Item.ImageText.Label" parent="Mozac.Browser.Menu.Item.Text"> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:paddingStart">@dimen/mozac_browser_menu_item_image_text_label_padding_start</item> + <item name="android:paddingEnd">@dimen/mozac_browser_menu_item_image_text_label_padding_start</item> + </style> + + <style name="Mozac.Browser.Menu.Item.Checkbox.Label" parent="Mozac.Browser.Menu.Item.ImageText.Label"> + <item name="android:paddingStart">@dimen/mozac_browser_menu_item_checkbox_text_label_padding_start</item> + <item name="android:paddingEnd">@dimen/mozac_browser_menu_item_checkbox_text_label_padding_end</item> + </style> + + <style name="Mozac.Browser.Menu.Item.Checkbox.Text" parent="Mozac.Browser.Menu.Item.Text"> + <item name="android:layout_width">0dp</item> + <item name="android:layout_height">0dp</item> + <item name="android:paddingStart">@dimen/mozac_browser_menu_item_image_checkbox_padding_start</item> + <item name="android:paddingEnd">@dimen/mozac_browser_menu_item_image_checkbox_padding_end</item> + </style> + + <style name="Mozac.Browser.Menu.Item.Checkbox.Container" parent="Mozac.Browser.Menu.Item.Container"> + <item name="android:layout_height">@dimen/mozac_browser_menu_item_checkbox_container_layout_height</item> + <item name="android:paddingStart">@dimen/mozac_browser_menu_item_checkbox_container_padding_start</item> + <item name="android:paddingEnd">@dimen/mozac_browser_menu_item_checkbox_container_padding_end</item> + </style> + + <style name="Mozac.Browser.Menu.Item.CandidateLabel" parent="Mozac.Browser.Menu.Item.Text"> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">wrap_content</item> + </style> + <!-- BrowserMenuImageText --> + + <!-- Animation --> + <style name="Mozac.Browser.Menu.Animation.OverflowMenuTop" parent=""> + <item name="android:windowEnterAnimation">@anim/menu_enter_top</item> + <item name="android:windowExitAnimation">@anim/menu_exit</item> + </style> + + <style name="Mozac.Browser.Menu.Animation.OverflowMenuBottom" parent=""> + <item name="android:windowEnterAnimation">@anim/menu_enter_bottom</item> + <item name="android:windowExitAnimation">@anim/menu_exit</item> + </style> + <!-- Animation --> +</resources> diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuAdapterTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuAdapterTest.kt new file mode 100644 index 0000000000..64d7c41260 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuAdapterTest.kt @@ -0,0 +1,210 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.reset +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` + +@RunWith(AndroidJUnit4::class) +class BrowserMenuAdapterTest { + + @Test + fun `items that return false from the visible lambda will be filtered out`() { + val items = listOf( + createMenuItem(1, { true }), + createMenuItem(2, { true }), + createMenuItem(3, { false }), + createMenuItem(4, { false }), + createMenuItem(5, { true }), + ) + + val adapter = BrowserMenuAdapter(testContext, items) + + assertEquals(3, adapter.visibleItems.size) + + adapter.visibleItems.assertTrueForOne { it.getLayoutResource() == 1 } + adapter.visibleItems.assertTrueForOne { it.getLayoutResource() == 2 } + adapter.visibleItems.assertTrueForOne { it.getLayoutResource() == 5 } + + adapter.visibleItems.assertTrueForAll { it.visible() } + + assertEquals(3, adapter.itemCount) + } + + @Test + fun `layout resource ID is used as view type`() { + val items = listOf( + createMenuItem(23), + createMenuItem(42), + ) + + val adapter = BrowserMenuAdapter(testContext, items) + + assertEquals(2, adapter.itemCount) + + assertEquals(23, adapter.getItemViewType(0)) + assertEquals(42, adapter.getItemViewType(1)) + } + + @Test + fun `bind will be forwarded to item implementation`() { + val item1 = spy(createMenuItem()) + val item2 = spy(createMenuItem()) + + val menu = mock(BrowserMenu::class.java) + + val adapter = BrowserMenuAdapter(testContext, listOf(item1, item2)) + adapter.menu = menu + + val view = mock(View::class.java) + val holder = BrowserMenuItemViewHolder(view) + + adapter.onBindViewHolder(holder, 0) + + verify(item1).bind(menu, view) + verify(item2, never()).bind(menu, view) + + reset(item1) + reset(item2) + + adapter.onBindViewHolder(holder, 1) + + verify(item1, never()).bind(menu, view) + verify(item2).bind(menu, view) + } + + @Test + fun `invalidate will be forwarded to item implementation`() { + val item1 = spy(createMenuItem()) + val item2 = spy(createMenuItem()) + + val menu = mock(BrowserMenu::class.java) + + val adapter = BrowserMenuAdapter(testContext, listOf(item1, item2)) + adapter.menu = menu + val recyclerView = mock(RecyclerView::class.java) + + val view = mock(View::class.java) + val holder = BrowserMenuItemViewHolder(view) + `when`(recyclerView.findViewHolderForAdapterPosition(0)).thenReturn(holder) + `when`(recyclerView.findViewHolderForAdapterPosition(1)).thenReturn(null) + + adapter.invalidate(recyclerView) + + verify(item1).invalidate(view) + verify(item2, never()).invalidate(view) + } + + @Test + fun `total interactive item count is given provided adapter`() { + val items = listOf( + createMenuItem(1, { true }, { 1 }), + createMenuItem(2, { true }, { 0 }), + createMenuItem(3, { false }, { 10 }), + createMenuItem(4, { true }, { 5 }), + ) + + val adapter = BrowserMenuAdapter(testContext, items) + + assertEquals(6, adapter.interactiveCount) + } + + @Test + fun `GIVEN a stickyItem exists in the visible items WHEN isStickyItem is called THEN it returns true`() { + val items = listOf( + createMenuItem(1, { true }, { 1 }), + createMenuItem(3, { true }, { 10 }, true), + createMenuItem(4, { true }, { 5 }), + createMenuItem(3, { false }, { 3 }, true), + ) + + val adapter = BrowserMenuAdapter(testContext, items) + + assertFalse(adapter.isStickyItem(0)) + assertTrue(adapter.isStickyItem(1)) + assertFalse(adapter.isStickyItem(2)) + assertFalse(adapter.isStickyItem(3)) + assertFalse(adapter.isStickyItem(4)) + } + + @Test + fun `GIVEN a BrowserMenu exists WHEN setupStickyItem is called THEN the item background color is set for the View parameter`() { + val adapter = BrowserMenuAdapter(testContext, emptyList()) + val menu: BrowserMenu = mock() + menu.backgroundColor = Color.CYAN + adapter.menu = menu + val view = View(testContext) + + adapter.setupStickyItem(view) + + assertEquals(menu.backgroundColor, (view.background as ColorDrawable).color) + } + + @Test + fun `GIVEN BrowserMenuAdapter WHEN tearDownStickyItem is called THEN the item background is reset to transparent`() { + val adapter = BrowserMenuAdapter(testContext, emptyList()) + val view = View(testContext) + view.setBackgroundColor(Color.CYAN) + + adapter.tearDownStickyItem(view) + + assertEquals(Color.TRANSPARENT, (view.background as ColorDrawable).color) + } + + private fun List<BrowserMenuItem>.assertTrueForOne(predicate: (BrowserMenuItem) -> Boolean) { + for (item in this) { + if (predicate(item)) { + return + } + } + fail("Predicate false for all items") + } + + private fun List<BrowserMenuItem>.assertTrueForAll(predicate: (BrowserMenuItem) -> Boolean) { + for (item in this) { + if (!predicate(item)) { + fail("Predicate not true for all items") + } + } + } + + private fun createMenuItem( + layout: Int = 0, + visible: () -> Boolean = { true }, + interactiveCount: () -> Int = { 1 }, + isSticky: Boolean = false, + ): BrowserMenuItem { + return object : BrowserMenuItem { + override val visible = visible + + override val interactiveCount = interactiveCount + + override fun getLayoutResource() = layout + + override fun bind(menu: BrowserMenu, view: View) {} + + override fun invalidate(view: View) {} + + override val isSticky = isSticky + } + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuBuilderTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuBuilderTest.kt new file mode 100644 index 0000000000..6f39e7cd64 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuBuilderTest.kt @@ -0,0 +1,44 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu + +import android.view.View +import android.widget.ImageButton +import androidx.recyclerview.widget.RecyclerView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class BrowserMenuBuilderTest { + + @Test + fun `items are forwarded from builder to menu`() { + val builder = BrowserMenuBuilder(listOf(mockMenuItem(), mockMenuItem())) + + val menu = builder.build(testContext) + + val anchor = ImageButton(testContext) + val popup = menu.show(anchor) + + val recyclerView: RecyclerView = popup.contentView.findViewById(R.id.mozac_browser_menu_recyclerView) + assertNotNull(recyclerView) + + val recyclerAdapter = recyclerView.adapter!! + assertNotNull(recyclerAdapter) + assertEquals(2, recyclerAdapter.itemCount) + } + + private fun mockMenuItem() = object : BrowserMenuItem { + override val visible: () -> Boolean = { true } + + override fun getLayoutResource() = R.layout.mozac_browser_menu_item_simple + + override fun bind(menu: BrowserMenu, view: View) {} + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuHighlightTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuHighlightTest.kt new file mode 100644 index 0000000000..aa4f6d33cd --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuHighlightTest.kt @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu + +import android.graphics.Color +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect +import mozilla.components.concept.menu.candidate.LowPriorityHighlightEffect +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import mozilla.components.ui.colors.R as colorsR + +@RunWith(AndroidJUnit4::class) +class BrowserMenuHighlightTest { + + @Test + fun `low priority effect keeps notification tint`() { + val highlight = BrowserMenuHighlight.LowPriority( + notificationTint = Color.RED, + ) + assertEquals(LowPriorityHighlightEffect(Color.RED), highlight.asEffect(mock())) + } + + @Test + fun `high priority effect keeps background tint`() { + val highlight = BrowserMenuHighlight.HighPriority( + backgroundTint = Color.RED, + ) + assertEquals(HighPriorityHighlightEffect(Color.RED), highlight.asEffect(mock())) + } + + @Suppress("Deprecation") + @Test + fun `classic highlight effect converts background tint`() { + val colorId = colorsR.color.photonRed50 + val highlight = BrowserMenuHighlight.ClassicHighlight( + startImageResource = 0, + endImageResource = 0, + backgroundResource = 0, + colorResource = colorId, + ) + assertEquals(HighPriorityHighlightEffect(testContext.getColor(colorId)), highlight.asEffect(testContext)) + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuPositioningTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuPositioningTest.kt new file mode 100644 index 0000000000..898733464a --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuPositioningTest.kt @@ -0,0 +1,163 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu + +import android.view.View +import android.view.ViewGroup +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import mozilla.components.support.test.whenever +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.Mockito.spy +import org.robolectric.Shadows +import org.robolectric.shadows.ShadowDisplay + +@RunWith(AndroidJUnit4::class) +class BrowserMenuPositioningTest { + + @Test + fun `GIVEN inferMenuPositioningData WHEN called with the menu layout, anchor and current menu data THEN it returns a new MenuPositioningData populated with all data needed to show a PopupWindow`() { + val view: ViewGroup = mock() + Mockito.doReturn(70).`when`(view).measuredHeight + val anchor = View(testContext) + anchor.layoutParams = ViewGroup.LayoutParams(20, 40) + setScreenHeight(100) + + val result = inferMenuPositioningData(view, anchor, MenuPositioningData()) + + val expected = MenuPositioningData( + BrowserMenuPlacement.AnchoredToTop.Dropdown(anchor), // orientation DOWN and fitsDown + askedOrientation = BrowserMenu.Orientation.DOWN, // default + fitsUp = false, // availableHeightToTop(0) is smaller than containerHeight(70) + fitsDown = true, // availableHeightToBottom(470) is bigger than containerHeight(70) + availableHeightToTop = 0, + availableHeightToBottom = 100, // mocked by us above + containerViewHeight = 70, // mocked by us above + ) + Assert.assertEquals(expected, result) + } + + @Test + fun `GIVEN inferMenuPositioningData WHEN availableHeightToBottom is bigger than availableHeightToTop THEN it returns a new MenuPositioningData populated with all data needed to show a PopupWindow that fits down`() { + val view: ViewGroup = mock() + Mockito.doReturn(70).`when`(view).measuredHeight + val anchor = View(testContext) + anchor.layoutParams = ViewGroup.LayoutParams(20, 40) + + setScreenHeight(50) + + val result = inferMenuPositioningData(view, anchor, MenuPositioningData()) + + val expected = MenuPositioningData( + BrowserMenuPlacement.AnchoredToTop.Dropdown(anchor), // orientation DOWN and fitsDown + askedOrientation = BrowserMenu.Orientation.DOWN, // default + fitsUp = false, // availableHeightToTop(0) is smaller than containerHeight(70) and smaller than availableHeightToBottom(50) + fitsDown = true, // availableHeightToBottom(50) is smaller than containerHeight(70) and bigger than availableHeightToTop(0) + availableHeightToTop = 0, + availableHeightToBottom = 50, // mocked by us above + containerViewHeight = 70, // mocked by us above + ) + Assert.assertEquals(expected, result) + } + + @Test + fun `GIVEN inferMenuPositioningData WHEN availableHeightToTop is bigger than availableHeightToBottom THEN it returns a new MenuPositioningData populated with all data needed to show a PopupWindow that fits up`() { + val view: ViewGroup = mock() + Mockito.doReturn(70).`when`(view).measuredHeight + val anchor = spy(View(testContext)) + anchor.layoutParams = ViewGroup.LayoutParams(20, 40) + + whenever(anchor.getLocationOnScreen(IntArray(2))).thenAnswer { invocation -> + val args = invocation.arguments + val location = args[0] as IntArray + location[0] = 0 + location[1] = 60 + location + } + + setScreenHeight(100) + + val result = inferMenuPositioningData(view, anchor, MenuPositioningData()) + + val expected = MenuPositioningData( + BrowserMenuPlacement.AnchoredToBottom.Dropdown(anchor), // orientation UP and fitsUp + askedOrientation = BrowserMenu.Orientation.DOWN, // default + fitsUp = true, // availableHeightToTop(60) is smaller than containerHeight(70) and bigger than availableHeightToBottom(40) + fitsDown = false, // availableHeightToBottom(40) is smaller than containerHeight(70) and smaller than availableHeightToTop(60) + availableHeightToTop = 60, // mocked by us above + availableHeightToBottom = 40, + containerViewHeight = 70, // mocked by us above + ) + + Assert.assertEquals(expected, result) + } + + @Test + fun `GIVEN inferMenuPosition WHEN called with an anchor and the current menu data THEN it returns a new MenuPositioningData with data about positioning the menu`() { + val view: View = mock() + + var data = MenuPositioningData(askedOrientation = BrowserMenu.Orientation.DOWN, fitsDown = true) + var result = inferMenuPosition(view, data) + Assert.assertEquals( + BrowserMenuPlacement.AnchoredToTop.Dropdown(view), + result.inferredMenuPlacement, + ) + + data = MenuPositioningData(askedOrientation = BrowserMenu.Orientation.UP, fitsUp = true) + result = inferMenuPosition(view, data) + Assert.assertEquals( + BrowserMenuPlacement.AnchoredToBottom.Dropdown(view), + result.inferredMenuPlacement, + ) + + data = MenuPositioningData( + fitsUp = false, + fitsDown = false, + availableHeightToTop = 1, + availableHeightToBottom = 2, + ) + result = inferMenuPosition(view, data) + Assert.assertEquals( + BrowserMenuPlacement.AnchoredToTop.ManualAnchoring(view), + result.inferredMenuPlacement, + ) + + data = MenuPositioningData( + fitsUp = false, + fitsDown = false, + availableHeightToTop = 1, + availableHeightToBottom = 0, + ) + result = inferMenuPosition(view, data) + Assert.assertEquals( + BrowserMenuPlacement.AnchoredToBottom.ManualAnchoring(view), + result.inferredMenuPlacement, + ) + + data = MenuPositioningData(askedOrientation = BrowserMenu.Orientation.DOWN, fitsUp = true) + result = inferMenuPosition(view, data) + Assert.assertEquals( + BrowserMenuPlacement.AnchoredToBottom.Dropdown(view), + result.inferredMenuPlacement, + ) + + data = MenuPositioningData(askedOrientation = BrowserMenu.Orientation.UP, fitsDown = true) + result = inferMenuPosition(view, data) + Assert.assertEquals( + BrowserMenuPlacement.AnchoredToTop.Dropdown(view), + result.inferredMenuPlacement, + ) + } + + private fun setScreenHeight(value: Int) { + val display = ShadowDisplay.getDefaultDisplay() + val shadow = Shadows.shadowOf(display) + shadow.setHeight(value) + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuTest.kt new file mode 100644 index 0000000000..aff7c709db --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuTest.kt @@ -0,0 +1,496 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu + +import android.content.res.ColorStateList +import android.graphics.Color +import android.os.Build +import android.view.Gravity +import android.view.View +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.widget.Button +import android.widget.FrameLayout +import androidx.cardview.widget.CardView +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.menu.BrowserMenu.Orientation.DOWN +import mozilla.components.browser.menu.item.SimpleBrowserMenuItem +import mozilla.components.browser.menu.view.DynamicWidthRecyclerView +import mozilla.components.browser.menu.view.ExpandableLayout +import mozilla.components.browser.menu.view.StickyHeaderLinearLayoutManager +import mozilla.components.concept.menu.MenuStyle +import mozilla.components.support.test.any +import mozilla.components.support.test.eq +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNotSame +import org.junit.Assert.assertSame +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.Mockito.doNothing +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import org.robolectric.Shadows +import org.robolectric.annotation.Config +import org.robolectric.shadows.ShadowDisplay + +@RunWith(AndroidJUnit4::class) +class BrowserMenuTest { + + @Test + fun `show returns non-null popup window`() { + val items = listOf( + SimpleBrowserMenuItem("Hello") {}, + SimpleBrowserMenuItem("World") {}, + ) + + val adapter = BrowserMenuAdapter(testContext, items) + + val menu = BrowserMenu(adapter) + + val anchor = Button(testContext) + val popup = menu.show(anchor) + + assertNotNull(popup) + } + + @Test + fun `show assigns currAnchor and isShown`() { + val items = listOf( + SimpleBrowserMenuItem("Hello") {}, + SimpleBrowserMenuItem("World") {}, + ) + + val adapter = BrowserMenuAdapter(testContext, items) + + val menu = BrowserMenu(adapter) + + val anchor = Button(testContext) + val popup = menu.show(anchor) + + assertNotNull(popup) + assertEquals(anchor, menu.currAnchor) + assertTrue(menu.isShown) + } + + @Test + fun `show assigns width and background color`() { + val items = listOf(SimpleBrowserMenuItem("Hello") {}) + + val adapter = BrowserMenuAdapter(testContext, items) + + val menu = spy(BrowserMenu(adapter)) + + val anchor = Button(testContext) + val menuStyle = MenuStyle( + backgroundColor = Color.RED, + minWidth = 20, + maxWidth = 500, + ) + val popup = menu.show(anchor, style = menuStyle) + + assertNotNull(popup) + assertEquals(anchor, menu.currAnchor) + assertTrue(menu.isShown) + + val cardView = popup.contentView.findViewById<CardView>(R.id.mozac_browser_menu_menuView) + val recyclerView = popup.contentView.findViewById<DynamicWidthRecyclerView>(R.id.mozac_browser_menu_recyclerView) + + verify(menu).setColors(any(), eq(menuStyle)) + assertEquals(ColorStateList.valueOf(Color.RED), cardView.cardBackgroundColor) + assertEquals(20, recyclerView.minWidth) + assertEquals(500, recyclerView.maxWidth) + } + + @Test + fun `dismiss sets isShown to false`() { + val items = listOf( + SimpleBrowserMenuItem("Hello") {}, + SimpleBrowserMenuItem("World") {}, + ) + + val adapter = BrowserMenuAdapter(testContext, items) + + val menu = BrowserMenu(adapter) + + val anchor = Button(testContext) + val popup = menu.show(anchor) + popup.dismiss() + + assertFalse(menu.isShown) + } + + @Test + fun `recyclerview adapter will have items for every menu item`() { + val items = listOf( + SimpleBrowserMenuItem("Hello") {}, + SimpleBrowserMenuItem("World") {}, + ) + + val adapter = BrowserMenuAdapter(testContext, items) + + val menu = BrowserMenu(adapter) + + val anchor = Button(testContext) + val popup = menu.show(anchor) + + val recyclerView: RecyclerView = popup.contentView.findViewById(R.id.mozac_browser_menu_recyclerView) + assertNotNull(recyclerView) + + val recyclerAdapter = recyclerView.adapter!! + assertNotNull(recyclerAdapter) + assertEquals(2, recyclerAdapter.itemCount) + } + + @Test + fun `endOfMenuAlwaysVisible will be forwarded to recyclerview layoutManager`() { + val items = listOf( + SimpleBrowserMenuItem("Hello") {}, + SimpleBrowserMenuItem("World") {}, + ) + + val adapter = spy(BrowserMenuAdapter(testContext, items)) + val menu = BrowserMenu(adapter) + + val anchor = Button(testContext) + val popup = menu.show(anchor, endOfMenuAlwaysVisible = true) + + val recyclerView: RecyclerView = popup.contentView.findViewById(R.id.mozac_browser_menu_recyclerView) + assertNotNull(recyclerView) + + val layoutManager = recyclerView.layoutManager as LinearLayoutManager + assertTrue(layoutManager.stackFromEnd) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.M]) + fun `endOfMenuAlwaysVisible will be forwarded to scrollOnceToTheBottom on devices with Android M and below`() { + val items = listOf( + SimpleBrowserMenuItem("Hello") {}, + SimpleBrowserMenuItem("World") {}, + ) + + val adapter = BrowserMenuAdapter(testContext, items) + val menu = spy(BrowserMenu(adapter)) + doNothing().`when`(menu).scrollOnceToTheBottom(any()) + + val anchor = Button(testContext) + val popup = menu.show(anchor, endOfMenuAlwaysVisible = true) + + val recyclerView: RecyclerView = popup.contentView.findViewById(R.id.mozac_browser_menu_recyclerView) + val layoutManager = recyclerView.layoutManager as LinearLayoutManager + + assertFalse(layoutManager.stackFromEnd) + verify(menu).scrollOnceToTheBottom(any()) + } + + @Test + fun `invalidate will be forwarded to recyclerview adapter`() { + val items = listOf( + SimpleBrowserMenuItem("Hello") {}, + SimpleBrowserMenuItem("World") {}, + ) + + val adapter = spy(BrowserMenuAdapter(testContext, items)) + + val menu = BrowserMenu(adapter) + + val anchor = Button(testContext) + val popup = menu.show(anchor) + + val recyclerView: RecyclerView = popup.contentView.findViewById(R.id.mozac_browser_menu_recyclerView) + assertNotNull(recyclerView) + assertNotNull(recyclerView.adapter) + + menu.invalidate() + Mockito.verify(adapter).invalidate(recyclerView) + } + + @Test + fun `invalidate is a no-op if the menu is closed`() { + val items = listOf(SimpleBrowserMenuItem("Hello") {}) + val menu = BrowserMenu(BrowserMenuAdapter(testContext, items)) + + menu.invalidate() + } + + @Test + fun `created popup window is displayed automatically`() { + val items = listOf( + SimpleBrowserMenuItem("Hello") {}, + SimpleBrowserMenuItem("World") {}, + ) + + val adapter = BrowserMenuAdapter(testContext, items) + + val menu = BrowserMenu(adapter) + + val anchor = Button(testContext) + val popup = menu.show(anchor) + + assertTrue(popup.isShowing) + } + + @Test + fun `dismissing the browser menu will dismiss the popup`() { + val items = listOf( + SimpleBrowserMenuItem("Hello") {}, + SimpleBrowserMenuItem("World") {}, + ) + + val adapter = BrowserMenuAdapter(testContext, items) + + val menu = BrowserMenu(adapter) + + val anchor = Button(testContext) + val popup = menu.show(anchor) + + assertTrue(popup.isShowing) + + menu.dismiss() + + assertFalse(popup.isShowing) + } + + @Test + fun `determineMenuOrientation returns Orientation-DOWN by default`() { + assertEquals( + BrowserMenu.Orientation.DOWN, + BrowserMenu.determineMenuOrientation(mock()), + ) + } + + @Test + fun `determineMenuOrientation returns Orientation-UP for views with bottom gravity in CoordinatorLayout`() { + val params = CoordinatorLayout.LayoutParams(100, 100) + params.gravity = Gravity.BOTTOM + + val view = View(testContext) + view.layoutParams = params + + assertEquals( + BrowserMenu.Orientation.UP, + BrowserMenu.determineMenuOrientation(view), + ) + } + + @Test + fun `determineMenuOrientation returns Orientation-DOWN for views with top gravity in CoordinatorLayout`() { + val params = CoordinatorLayout.LayoutParams(100, 100) + params.gravity = Gravity.TOP + + val view = View(testContext) + view.layoutParams = params + + assertEquals( + BrowserMenu.Orientation.DOWN, + BrowserMenu.determineMenuOrientation(view), + ) + } + + @Test + fun `Popup#show will initialize the menuPositioningData`() { + val adapter = BrowserMenuAdapter(testContext, emptyList()) + val menu = BrowserMenu(adapter) + val anchor = Button(testContext) + setScreenHeight(100) + + menu.show(anchor) + + val expected = MenuPositioningData( + BrowserMenuPlacement.AnchoredToTop.Dropdown(anchor), + DOWN, + false, + true, + 0, + 100, + 28, + ) + assertEquals(expected, menu.menuPositioningData) + } + + @Test + fun `configureExpandableMenu will setup a new ExpandabeLayout for a AnchoredToBottom#ManualAnchoring menu`() { + val items = listOf( + SimpleBrowserMenuItem("Hello") {}, + SimpleBrowserMenuItem("World", isCollapsingMenuLimit = true) {}, + ) + val adapter = BrowserMenuAdapter(testContext, items) + val menu = BrowserMenu(adapter) + val view = FrameLayout(testContext) + val anchor = Button(testContext) + menu.menuPositioningData = MenuPositioningData(BrowserMenuPlacement.AnchoredToBottom.Dropdown(anchor)) + + val result = menu.configureExpandableMenu(view, true) + + assertTrue(result is ExpandableLayout) + assertTrue(result.getChildAt(0) == view) + } + + @Test + fun `configureExpandableMenu will setup a new ExpandabeLayout for a AnchoredToBottom#Dropdown menu`() { + val items = listOf( + SimpleBrowserMenuItem("Hello") {}, + SimpleBrowserMenuItem("World", isCollapsingMenuLimit = true) {}, + ) + val adapter = BrowserMenuAdapter(testContext, items) + val menu = BrowserMenu(adapter) + val view = FrameLayout(testContext) + val anchor = Button(testContext) + menu.menuPositioningData = MenuPositioningData(BrowserMenuPlacement.AnchoredToBottom.ManualAnchoring(anchor)) + + val result = menu.configureExpandableMenu(view, true) + + assertTrue(result is ExpandableLayout) + assertTrue(result.getChildAt(0) == view) + } + + @Test + fun `configureExpandableMenu will not setup a new ExpandableLayout if none of the items can serve as a collapsingMenuLimit`() { + val items = listOf( + SimpleBrowserMenuItem("Hello") {}, + SimpleBrowserMenuItem("World") {}, + ) + val adapter = BrowserMenuAdapter(testContext, items) + val menu = BrowserMenu(adapter) + val view = FrameLayout(testContext) + val anchor = Button(testContext) + menu.menuPositioningData = MenuPositioningData(BrowserMenuPlacement.AnchoredToBottom.ManualAnchoring(anchor)) + + val result = menu.configureExpandableMenu(view, true) + + assertFalse(result is ExpandableLayout) + assertTrue(result == view) + } + + @Test + fun `GIVEN a top anchored menu WHEN configureExpandableMenu is called THEN it a new layout manager with sticky item at top is set`() { + val menu = spy(BrowserMenu(mock())) + // Call show to have a default layout manager set + menu.show(View(testContext)) + val initialLayoutManager = menu.menuList!!.layoutManager + menu.menuPositioningData = MenuPositioningData(BrowserMenuPlacement.AnchoredToTop.Dropdown(mock())) + + menu.configureExpandableMenu(menu.menuList!!, false) + + assertNotSame(initialLayoutManager, menu.menuList!!.layoutManager) + assertTrue(menu.menuList!!.layoutManager is StickyHeaderLinearLayoutManager<*>) + } + + @Test + fun `GIVEN a top anchored menu WHEN configureExpandableMenu is called THEN stackFromEnd is false`() { + val menu = spy(BrowserMenu(mock())) + // Call show to have a default layout manager set + menu.show(View(testContext)) + menu.menuPositioningData = MenuPositioningData(BrowserMenuPlacement.AnchoredToTop.Dropdown(mock())) + + menu.configureExpandableMenu(menu.menuList!!, false) + + assertFalse((menu.menuList!!.layoutManager as LinearLayoutManager).stackFromEnd) + } + + @Test + fun `GIVEN a top anchored menu WHEN configureExpandableMenu is called THEN stackFromEnd is true`() { + val menu = spy(BrowserMenu(mock())) + // Call show to have a default layout manager set + menu.show(View(testContext)) + menu.menuPositioningData = MenuPositioningData(BrowserMenuPlacement.AnchoredToTop.Dropdown(mock())) + + menu.configureExpandableMenu(menu.menuList!!, true) + + assertTrue((menu.menuList!!.layoutManager as LinearLayoutManager).stackFromEnd) + } + + @Test + fun `getNewPopupWindow will return a PopupWindow with MATCH_PARENT height if the view is ExpandableLayout`() { + val expandableLayout = ExpandableLayout.wrapContentInExpandableView(FrameLayout(testContext), 0) { } + + val result = BrowserMenu(mock()).getNewPopupWindow(expandableLayout) + + assertSame(expandableLayout, result.contentView) + assertTrue(result.height == MATCH_PARENT) + assertTrue(result.width == WRAP_CONTENT) + } + + @Test + fun `getNewPopupWindow will return a PopupWindow with WRAP_CONTENT height if the view is not ExpandableLayout`() { + val notExpandableLayout = FrameLayout(testContext) + + val result = BrowserMenu(mock()).getNewPopupWindow(notExpandableLayout) + + assertSame(notExpandableLayout, result.contentView) + assertTrue(result.height == WRAP_CONTENT) + assertTrue(result.width == WRAP_CONTENT) + } + + @Test + fun `popup is dismissed when anchor is detached`() { + val items = listOf( + SimpleBrowserMenuItem("Mock") {}, + SimpleBrowserMenuItem("Menu") {}, + ) + val adapter = BrowserMenuAdapter(testContext, items) + val menu = BrowserMenu(adapter) + val anchor = Button(testContext) + val popupWindow = menu.show(anchor) + + assertTrue(popupWindow.isShowing) + + menu.onViewDetachedFromWindow(anchor) + + assertFalse(popupWindow.isShowing) + } + + @Test + fun `GIVEN BrowserMenu WHEN setColor is called with a null MenuStyle THEN the color of the menuView is not changed but cached in backgroundColor`() { + val menu = BrowserMenu(mock()) + val menuParent = CardView(testContext).apply { + id = R.id.mozac_browser_menu_menuView + setCardBackgroundColor(Color.YELLOW) + } + val menuLayout = FrameLayout(testContext).also { it.addView(menuParent) } + assertEquals(Color.RED, menu.backgroundColor) + + menu.setColors(menuLayout, null) + + assertEquals(Color.YELLOW, menuParent.cardBackgroundColor.defaultColor) + assertEquals(Color.YELLOW, menu.backgroundColor) + } + + @Test + fun `GIVEN BrowserMenu WHEN setColor is called with a valid MenuStyle THEN the color of the menuView is changed and cached in backgroundColor`() { + val menu = BrowserMenu(mock()) + val menuParent = CardView(testContext).apply { + id = R.id.mozac_browser_menu_menuView + setCardBackgroundColor(Color.YELLOW) + } + val menuLayout = FrameLayout(testContext).also { it.addView(menuParent) } + val menuStyle = MenuStyle( + backgroundColor = Color.GREEN, + minWidth = 20, + maxWidth = 500, + ) + assertEquals(Color.RED, menu.backgroundColor) + + menu.setColors(menuLayout, menuStyle) + + assertEquals(menuStyle.backgroundColor!!.defaultColor, menuParent.cardBackgroundColor.defaultColor) + assertEquals(menuStyle.backgroundColor!!.defaultColor, menu.backgroundColor) + } + + private fun setScreenHeight(value: Int) { + val display = ShadowDisplay.getDefaultDisplay() + val shadow = Shadows.shadowOf(display) + shadow.setHeight(value) + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/WebExtensionBrowserMenuBuilderTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/WebExtensionBrowserMenuBuilderTest.kt new file mode 100644 index 0000000000..0c5feb0d5b --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/WebExtensionBrowserMenuBuilderTest.kt @@ -0,0 +1,510 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu + +import android.view.LayoutInflater +import android.view.View +import android.widget.ImageButton +import androidx.recyclerview.widget.RecyclerView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.menu.item.BackPressMenuItem +import mozilla.components.browser.menu.item.BrowserMenuImageText +import mozilla.components.browser.menu.item.ParentBrowserMenuItem +import mozilla.components.browser.menu.item.WebExtensionBrowserMenuItem +import mozilla.components.browser.menu.item.WebExtensionPlaceholderMenuItem +import mozilla.components.browser.menu.item.WebExtensionPlaceholderMenuItem.Companion.MAIN_EXTENSIONS_MENU_ID +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.WebExtensionState +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.webextension.WebExtensionBrowserAction +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import androidx.appcompat.R as appcompatR +import mozilla.components.ui.icons.R as iconsR + +@RunWith(AndroidJUnit4::class) +class WebExtensionBrowserMenuBuilderTest { + + private val submenuPlaceholderMenuItem = WebExtensionPlaceholderMenuItem(id = MAIN_EXTENSIONS_MENU_ID) + + @Test + fun `WHEN there are no web extension actions THEN add-ons menu item invokes onAddonsManagerTapped`() { + var isAddonsManagerTapped = false + val store = BrowserStore() + val builder = WebExtensionBrowserMenuBuilder( + listOf(mockMenuItem(), submenuPlaceholderMenuItem, mockMenuItem()), + store = store, + onAddonsManagerTapped = { isAddonsManagerTapped = true }, + appendExtensionSubMenuAtStart = true, + ) + + val menu = builder.build(testContext) + + val addonsManagerItem = menu.adapter.visibleItems[1] as? BrowserMenuImageText + val addonsManagerItemView = + LayoutInflater.from(testContext).inflate(addonsManagerItem!!.getLayoutResource(), null) + addonsManagerItem.bind(menu, addonsManagerItemView) + assertFalse(isAddonsManagerTapped) + addonsManagerItemView.performClick() + assertTrue(isAddonsManagerTapped) + } + + @Test + fun `GIVEN style is provided WHEN creating extension menu THEN styles should be applied to items`() { + val browserAction = WebExtensionBrowserAction("browser_action", true, mock(), "", 0, 0) {} + val extensions = mapOf( + "id" to WebExtensionState( + "id", + "url", + "name", + true, + browserAction = browserAction, + ), + ) + + val store = BrowserStore(BrowserState(extensions = extensions)) + val style = WebExtensionBrowserMenuBuilder.Style( + addonsManagerMenuItemDrawableRes = iconsR.drawable.mozac_ic_extension_24, + backPressMenuItemDrawableRes = iconsR.drawable.mozac_ic_back_24, + ) + val builder = WebExtensionBrowserMenuBuilder( + listOf(mockMenuItem()), + store = store, + style = style, + appendExtensionSubMenuAtStart = true, + ) + + val menu = builder.build(testContext) + val anchor = ImageButton(testContext) + menu.show(anchor) + + val parentMenuItem = menu.adapter.visibleItems[0] as ParentBrowserMenuItem + val subMenuItemIndex = parentMenuItem.subMenu.adapter.visibleItems.lastIndex + val backPressMenuItem = parentMenuItem.subMenu.adapter.visibleItems[0] as BackPressMenuItem + val addonsManagerItem = parentMenuItem.subMenu.adapter.visibleItems[subMenuItemIndex] as BrowserMenuImageText + + assertEquals(style.backPressMenuItemDrawableRes, backPressMenuItem.imageResource) + assertEquals(style.webExtIconTintColorResource, backPressMenuItem.iconTintColorResource) + + assertEquals(style.webExtIconTintColorResource, addonsManagerItem.iconTintColorResource) + assertEquals(style.webExtIconTintColorResource, addonsManagerItem.iconTintColorResource) + } + + @Test + fun `web extension sub menu add-ons manager sub menu item invokes onAddonsManagerTapped when clicked`() { + val browserAction = WebExtensionBrowserAction("browser_action", true, mock(), "", 0, 0) {} + val pageAction = WebExtensionBrowserAction("page_action", true, mock(), "", 0, 0) {} + + val extensions = mapOf( + "id" to WebExtensionState( + "id", + "url", + "name", + true, + browserAction = browserAction, + pageAction = pageAction, + ), + ) + + val store = BrowserStore( + BrowserState( + extensions = extensions, + ), + ) + + var isAddonsManagerTapped = false + val builder = WebExtensionBrowserMenuBuilder( + listOf(mockMenuItem(), submenuPlaceholderMenuItem, mockMenuItem()), + store = store, + onAddonsManagerTapped = { isAddonsManagerTapped = true }, + appendExtensionSubMenuAtStart = true, + ) + + val menu = builder.build(testContext) + + val parentMenuItem = menu.adapter.visibleItems[1] as? ParentBrowserMenuItem + val subMenuItemSize = parentMenuItem!!.subMenu.adapter.visibleItems.size + assertEquals(6, subMenuItemSize) + val addOnsManagerMenuItem = + parentMenuItem.subMenu.adapter.visibleItems[subMenuItemSize - 1] as? BrowserMenuImageText + val addonsManagerItemView = + LayoutInflater.from(testContext).inflate(addOnsManagerMenuItem!!.getLayoutResource(), null) + addOnsManagerMenuItem.bind(menu, addonsManagerItemView) + assertFalse(isAddonsManagerTapped) + addonsManagerItemView.performClick() + assertTrue(isAddonsManagerTapped) + assertNotNull(addOnsManagerMenuItem) + } + + @Test + fun `web extension submenu is added at the top when usingBottomToolbar is true with no placeholder for submenu`() { + val browserAction = WebExtensionBrowserAction("browser_action", true, mock(), "", 0, 0) {} + val pageAction = WebExtensionBrowserAction("page_action", true, mock(), "", 0, 0) {} + + val extensions = mapOf( + "id" to WebExtensionState( + "id", + "url", + "name", + true, + browserAction = browserAction, + pageAction = pageAction, + ), + ) + + val store = BrowserStore( + BrowserState( + extensions = extensions, + ), + ) + + val builder = WebExtensionBrowserMenuBuilder( + listOf(mockMenuItem(), mockMenuItem(), mockMenuItem()), + store = store, + appendExtensionSubMenuAtStart = true, + ) + + val menu = builder.build(testContext) + val anchor = ImageButton(testContext) + val popup = menu.show(anchor) + + val recyclerView: RecyclerView = popup.contentView.findViewById(R.id.mozac_browser_menu_recyclerView) + assertNotNull(recyclerView) + + val recyclerAdapter = recyclerView.adapter!! + assertNotNull(recyclerAdapter) + assertEquals(4, recyclerAdapter.itemCount) + + val parentMenuItem = menu.adapter.visibleItems[0] as? ParentBrowserMenuItem + val subMenuItemSize = parentMenuItem!!.subMenu.adapter.visibleItems.size + assertEquals(6, subMenuItemSize) + val backMenuItem = parentMenuItem.subMenu.adapter.visibleItems[0] as? BackPressMenuItem + val subMenuExtItemBrowserAction = parentMenuItem.subMenu.adapter.visibleItems[2] as? WebExtensionBrowserMenuItem + val subMenuExtItemPageAction = parentMenuItem.subMenu.adapter.visibleItems[3] as? WebExtensionBrowserMenuItem + val addOnsManagerMenuItem = parentMenuItem.subMenu.adapter.visibleItems[subMenuItemSize - 1] as? BrowserMenuImageText + assertNotNull(backMenuItem) + assertEquals("browser_action", subMenuExtItemBrowserAction!!.action.title) + assertEquals("page_action", subMenuExtItemPageAction!!.action.title) + assertNotNull(addOnsManagerMenuItem) + } + + @Test + fun `web extension submenu is added at the bottom when usingBottomToolbar is false with no placeholder for submenu `() { + val browserAction = WebExtensionBrowserAction("browser_action", true, mock(), "", 0, 0) {} + val pageAction = WebExtensionBrowserAction("page_action", true, mock(), "", 0, 0) {} + + val extensions = mapOf( + "id" to WebExtensionState( + "id", + "url", + "name", + true, + browserAction = browserAction, + pageAction = pageAction, + ), + ) + + val store = BrowserStore( + BrowserState( + extensions = extensions, + ), + ) + + val builder = + WebExtensionBrowserMenuBuilder( + listOf(mockMenuItem(), mockMenuItem(), mockMenuItem()), + store = store, + appendExtensionSubMenuAtStart = false, + ) + + val menu = builder.build(testContext) + val anchor = ImageButton(testContext) + val popup = menu.show(anchor) + + val recyclerView: RecyclerView = popup.contentView.findViewById(R.id.mozac_browser_menu_recyclerView) + assertNotNull(recyclerView) + + val recyclerAdapter = recyclerView.adapter!! + assertNotNull(recyclerAdapter) + assertEquals(4, recyclerAdapter.itemCount) + + val parentMenuItem = menu.adapter.visibleItems[recyclerAdapter.itemCount - 1] as? ParentBrowserMenuItem + val subMenuItemSize = parentMenuItem!!.subMenu.adapter.visibleItems.size + assertEquals(6, subMenuItemSize) + val backMenuItem = parentMenuItem.subMenu.adapter.visibleItems[subMenuItemSize - 1] as? BackPressMenuItem + val subMenuExtItemBrowserAction = parentMenuItem.subMenu.adapter.visibleItems[2] as? WebExtensionBrowserMenuItem + val subMenuExtItemPageAction = parentMenuItem.subMenu.adapter.visibleItems[3] as? WebExtensionBrowserMenuItem + val addOnsManagerMenuItem = parentMenuItem.subMenu.adapter.visibleItems[0] as? BrowserMenuImageText + assertNotNull(backMenuItem) + assertEquals("browser_action", subMenuExtItemBrowserAction!!.action.title) + assertEquals("page_action", subMenuExtItemPageAction!!.action.title) + assertNotNull(addOnsManagerMenuItem) + } + + @Test + fun `web extension is moved to main menu when extension id equals WebExtensionPlaceholderMenuItem id`() { + val promotableWebExtensionId = "promotable extension id" + val promotableWebExtensionTitle = "promotable extension action title" + val testIconTintColorResource = appcompatR.color.accent_material_dark + + val pageAction = WebExtensionBrowserAction("page_action", true, mock(), "", 0, 0) {} + val pageActionPromotableWebExtension = WebExtensionBrowserAction(promotableWebExtensionTitle, true, mock(), "", 0, 0) {} + + // just 2 extensions in the extension menu + val extensions = mapOf( + "id" to WebExtensionState( + "id", + "url", + "name", + true, + browserAction = null, + pageAction = pageAction, + ), + promotableWebExtensionId to WebExtensionState( + promotableWebExtensionId, + "url", + "name", + true, + browserAction = null, + pageAction = pageActionPromotableWebExtension, + ), + ) + val store = BrowserStore( + BrowserState( + extensions = extensions, + ), + ) + + // 4 items initially on the main menu + val items = listOf( + WebExtensionPlaceholderMenuItem( + id = promotableWebExtensionId, + iconTintColorResource = testIconTintColorResource, + ), + mockMenuItem(), + submenuPlaceholderMenuItem, + mockMenuItem(), + ) + + val builder = + WebExtensionBrowserMenuBuilder( + items, + store = store, + appendExtensionSubMenuAtStart = false, + ) + + val menu = builder.build(testContext) + val anchor = ImageButton(testContext) + val popup = menu.show(anchor) + + val recyclerView: RecyclerView = popup.contentView.findViewById(R.id.mozac_browser_menu_recyclerView) + assertNotNull(recyclerView) + + val recyclerAdapter = recyclerView.adapter!! as BrowserMenuAdapter + assertNotNull(recyclerAdapter) + + // main menu should have the 4 initial items, one replaced by web extension one replaced by the extensions menu + assertEquals(4, recyclerAdapter.itemCount) + + val replacedItem = recyclerAdapter.visibleItems[0] + // the replaced item should be a WebExtensionBrowserMenuItem + assertEquals("WebExtensionBrowserMenuItem", replacedItem.javaClass.simpleName) + + // the replaced item should have the action title of the WebExtensionBrowserMenuItem + assertEquals(promotableWebExtensionTitle, (replacedItem as WebExtensionBrowserMenuItem).action.title) + + // the replaced item should have the icon tint set by placeholder + assertEquals(testIconTintColorResource, replacedItem.iconTintColorResource) + + val parentMenuItem = menu.adapter.visibleItems[2] as? ParentBrowserMenuItem + val subMenuItemSize = parentMenuItem!!.subMenu.adapter.visibleItems.size + + // add-ons should only have one extension, 2 dividers, 1 add-on manager item and 1 back menu item + assertEquals(5, subMenuItemSize) + + val backMenuItem = parentMenuItem.subMenu.adapter.visibleItems[subMenuItemSize - 1] as? BackPressMenuItem + val subMenuExtItemPageAction = parentMenuItem.subMenu.adapter.visibleItems[2] as? WebExtensionBrowserMenuItem + val addOnsManagerMenuItem = parentMenuItem.subMenu.adapter.visibleItems[0] as? BrowserMenuImageText + + assertNotNull(backMenuItem) + assertEquals("page_action", subMenuExtItemPageAction!!.action.title) + assertNotNull(addOnsManagerMenuItem) + } + + @Test + fun `GIVEN a placeholder with the id MAIN_EXTENSIONS_MENU_ID WHEN the menu is built THEN the extensions sub-menu is inserted in its place`() { + val pageAction = WebExtensionBrowserAction("page_action", true, mock(), "", 0, 0) {} + + val extensions = mapOf( + "id" to WebExtensionState( + "id", + "url", + "name", + true, + browserAction = null, + pageAction = pageAction, + ), + ) + val store = BrowserStore( + BrowserState( + extensions = extensions, + ), + ) + + // 3 items initially on the main menu + val items = listOf( + mockMenuItem(), + submenuPlaceholderMenuItem, + mockMenuItem(), + ) + + val builder = + WebExtensionBrowserMenuBuilder( + items, + store = store, + appendExtensionSubMenuAtStart = false, + ) + + val menu = builder.build(testContext) + val anchor = ImageButton(testContext) + val popup = menu.show(anchor) + + val recyclerView: RecyclerView = popup.contentView.findViewById(R.id.mozac_browser_menu_recyclerView) + assertNotNull(recyclerView) + + val recyclerAdapter = recyclerView.adapter!! as BrowserMenuAdapter + assertNotNull(recyclerAdapter) + + // main menu should have the 3 initial items, one replaced by extensions sub-menu + assertEquals(3, recyclerAdapter.itemCount) + + val parentMenuItem = recyclerAdapter.visibleItems[1] as ParentBrowserMenuItem + // the replaced item should be a ParentBrowserMenuItem + assertEquals("ParentBrowserMenuItem", parentMenuItem.javaClass.simpleName) + + // the replaced item should have the action title of the WebExtensionBrowserMenuItem + assertEquals(testContext.getString(R.string.mozac_browser_menu_extensions), parentMenuItem.label) + + val subMenuItemSize = parentMenuItem.subMenu.adapter.visibleItems.size + + // add-ons should only have one extension, 2 dividers, 1 add-on manager item and 1 back menu item + assertEquals(5, subMenuItemSize) + + val backMenuItem = parentMenuItem.subMenu.adapter.visibleItems[subMenuItemSize - 1] as? BackPressMenuItem + val subMenuExtItemPageAction = parentMenuItem.subMenu.adapter.visibleItems[2] as? WebExtensionBrowserMenuItem + val addOnsManagerMenuItem = parentMenuItem.subMenu.adapter.visibleItems[0] as? BrowserMenuImageText + + assertNotNull(backMenuItem) + assertEquals("page_action", subMenuExtItemPageAction!!.action.title) + assertNotNull(addOnsManagerMenuItem) + } + + @Test + fun `GIVEN showAddonsInMenu with value true WHEN the menu is built THEN the Add-ons item is added at the bottom`() { + val pageAction = WebExtensionBrowserAction("page_action", true, mock(), "", 0, 0) {} + + val extensions = mapOf( + "id" to WebExtensionState( + "id", + "url", + "name", + true, + browserAction = null, + pageAction = pageAction, + ), + ) + val store = BrowserStore(BrowserState(extensions = extensions)) + + // 2 items initially on the main menu + val items = listOf( + mockMenuItem(), + mockMenuItem(), + ) + + val builder = + WebExtensionBrowserMenuBuilder( + items, + store = store, + showAddonsInMenu = true, + ) + + val menu = builder.build(testContext) + val anchor = ImageButton(testContext) + val popup = menu.show(anchor) + + val recyclerView: RecyclerView = popup.contentView.findViewById(R.id.mozac_browser_menu_recyclerView) + assertNotNull(recyclerView) + + val recyclerAdapter = recyclerView.adapter as BrowserMenuAdapter + assertNotNull(recyclerAdapter) + + // main menu should have 3 items and the last one should be the "Add-ons" item + assertEquals(3, recyclerAdapter.itemCount) + + val lastItem = recyclerAdapter.visibleItems[2] + assert(lastItem is ParentBrowserMenuItem && lastItem.label == testContext.getString(R.string.mozac_browser_menu_extensions)) + } + + @Test + fun `GIVEN showAddonsInMenu with value false WHEN the menu is built THEN the Add-ons item is not added`() { + val pageAction = WebExtensionBrowserAction("page_action", true, mock(), "", 0, 0) {} + + val extensions = mapOf( + "id" to WebExtensionState( + "id", + "url", + "name", + true, + browserAction = null, + pageAction = pageAction, + ), + ) + val store = BrowserStore(BrowserState(extensions = extensions)) + + // 2 items initially on the main menu + val items = listOf( + mockMenuItem(), + mockMenuItem(), + ) + + val builder = + WebExtensionBrowserMenuBuilder( + items, + store = store, + showAddonsInMenu = false, + ) + + val menu = builder.build(testContext) + val anchor = ImageButton(testContext) + val popup = menu.show(anchor) + + val recyclerView: RecyclerView = popup.contentView.findViewById(R.id.mozac_browser_menu_recyclerView) + assertNotNull(recyclerView) + + val recyclerAdapter = recyclerView.adapter as BrowserMenuAdapter + assertNotNull(recyclerAdapter) + + // main menu should have 2 items + assertEquals(2, recyclerAdapter.itemCount) + + recyclerAdapter.visibleItems.forEach { item -> + assert(item !is ParentBrowserMenuItem) + } + } + + private fun mockMenuItem() = object : BrowserMenuItem { + override val visible: () -> Boolean = { true } + + override fun getLayoutResource() = R.layout.mozac_browser_menu_item_simple + + override fun bind(menu: BrowserMenu, view: View) {} + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/WebExtensionBrowserMenuTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/WebExtensionBrowserMenuTest.kt new file mode 100644 index 0000000000..77769db9c4 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/WebExtensionBrowserMenuTest.kt @@ -0,0 +1,525 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu + +import android.graphics.Bitmap +import android.graphics.Color +import android.view.View +import android.widget.Button +import android.widget.ImageView +import android.widget.TextView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.menu.WebExtensionBrowserMenu.Companion.getOrUpdateWebExtensionMenuItems +import mozilla.components.browser.menu.WebExtensionBrowserMenu.Companion.webExtensionBrowserActions +import mozilla.components.browser.menu.WebExtensionBrowserMenu.Companion.webExtensionPageActions +import mozilla.components.browser.menu.facts.BrowserMenuFacts.Items.WEB_EXTENSION_MENU_ITEM +import mozilla.components.browser.menu.item.SimpleBrowserMenuItem +import mozilla.components.browser.state.selector.selectedTab +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.TabSessionState +import mozilla.components.browser.state.state.WebExtensionState +import mozilla.components.browser.state.state.createTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.webextension.Action +import mozilla.components.concept.engine.webextension.WebExtensionBrowserAction +import mozilla.components.concept.engine.webextension.WebExtensionPageAction +import mozilla.components.support.base.facts.processor.CollectionProcessor +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import mozilla.components.support.test.rule.MainCoroutineRule +import mozilla.components.support.test.whenever +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import mozilla.components.support.base.facts.Action as FactsAction + +@RunWith(AndroidJUnit4::class) +@kotlinx.coroutines.ExperimentalCoroutinesApi +class WebExtensionBrowserMenuTest { + + @get:Rule + val coroutinesTestRule = MainCoroutineRule() + + @Before + fun setup() { + webExtensionBrowserActions.clear() + webExtensionPageActions.clear() + } + + @Test + fun `actions are only updated when the menu is shown`() { + webExtensionBrowserActions.clear() + val browserAction = WebExtensionBrowserAction("browser_action", true, mock(), "", 0, 0) {} + val pageAction = WebExtensionPageAction("browser_action", true, mock(), "", 0, 0) {} + val extensions = mapOf( + "browser_action" to WebExtensionState( + "browser_action", + "url", + "name", + true, + browserAction = browserAction, + pageAction = pageAction, + ), + ) + + val store = + BrowserStore( + BrowserState( + extensions = extensions, + ), + ) + val items = listOf( + SimpleBrowserMenuItem("Hello") {}, + SimpleBrowserMenuItem("World") {}, + ) + + val adapter = BrowserMenuAdapter(testContext, items) + val menu = WebExtensionBrowserMenu(adapter, store) + + val anchor = Button(testContext) + val popup = menu.show(anchor) + + assertNotNull(popup) + + val defaultBrowserAction = + WebExtensionBrowserAction("default_title", true, mock(), "", 0, 0) {} + val defaultPageAction = + WebExtensionPageAction("default_title", true, mock(), "", 0, 0) {} + val defaultExtensions: Map<String, WebExtensionState> = mapOf( + "id" to WebExtensionState( + "id", + "url", + "name", + true, + browserAction = defaultBrowserAction, + pageAction = defaultPageAction, + ), + ) + + createTab( + "https://www.example.org", + id = "tab1", + extensions = defaultExtensions, + ) + assertEquals(1, webExtensionBrowserActions.size) + assertEquals(1, webExtensionPageActions.size) + + menu.dismiss() + val anotherBrowserAction = + WebExtensionBrowserAction("another_title", true, mock(), "", 0, 0) {} + val anotherPageAction = + WebExtensionBrowserAction("another_title", true, mock(), "", 0, 0) {} + val anotherExtension: Map<String, WebExtensionState> = mapOf( + "id2" to WebExtensionState( + "id2", + "url", + "name", + true, + browserAction = anotherBrowserAction, + pageAction = anotherPageAction, + ), + ) + + createTab( + "https://www.example2.org", + id = "tab2", + extensions = anotherExtension, + ) + assertEquals(0, webExtensionBrowserActions.size) + assertEquals(0, webExtensionPageActions.size) + } + + @Test + fun `render web extension actions from browser state`() { + val defaultBrowserAction = + WebExtensionBrowserAction("default_browser_action_title", true, mock(), "", 0, 0) {} + val defaultPageAction = + WebExtensionPageAction("default_page_action_title", true, mock(), "", 0, 0) {} + val overriddenBrowserAction = + WebExtensionBrowserAction("overridden_browser_action_title", true, mock(), "", 0, 0) {} + val overriddenPageAction = + WebExtensionBrowserAction("overridden_page_action_title", true, mock(), "", 0, 0) {} + + val extensions: Map<String, WebExtensionState> = mapOf( + "id" to WebExtensionState( + "id", + "url", + "name", + true, + browserAction = defaultBrowserAction, + pageAction = defaultPageAction, + ), + ) + val overriddenExtensions: Map<String, WebExtensionState> = mapOf( + "id" to WebExtensionState( + "id", + "url", + "name", + true, + browserAction = overriddenBrowserAction, + pageAction = overriddenPageAction, + ), + ) + val store = + BrowserStore( + BrowserState( + tabs = listOf( + createTab( + "https://www.example.org", + id = "tab1", + extensions = overriddenExtensions, + ), + ), + selectedTabId = "tab1", + extensions = extensions, + ), + ) + + val browserMenuItems = + getOrUpdateWebExtensionMenuItems(store.state, store.state.selectedTab) + assertEquals(2, browserMenuItems.size) + + var actionMenu = browserMenuItems[0] + assertEquals( + "overridden_browser_action_title", + actionMenu.action.title, + ) + + actionMenu = browserMenuItems[1] + assertEquals( + "overridden_page_action_title", + actionMenu.action.title, + ) + } + + @Test + fun `getOrUpdateWebExtensionMenuItems does not include actions from disabled extensions`() { + val enabledPageAction = + WebExtensionBrowserAction("enabled_page_action", true, mock(), "", 0, 0) {} + val disabledPageAction = + WebExtensionBrowserAction("disabled_page_action", true, mock(), "", 0, 0) {} + val enabledBrowserAction = + WebExtensionBrowserAction("enabled_browser_action", true, mock(), "", 0, 0) {} + val disabledBrowserAction = + WebExtensionBrowserAction("disabled_browser_action", true, mock(), "", 0, 0) {} + + val extensions = mapOf( + "enabled" to WebExtensionState( + "enabled", + "url", + "name", + true, + browserAction = enabledBrowserAction, + pageAction = enabledPageAction, + ), + "disabled" to WebExtensionState( + "disabled", + "url", + "name", + false, + browserAction = disabledBrowserAction, + pageAction = disabledPageAction, + ), + ) + + val store = + BrowserStore( + BrowserState( + extensions = extensions, + ), + ) + + val browserMenuItems = getOrUpdateWebExtensionMenuItems(store.state) + assertEquals(2, browserMenuItems.size) + + var menuAction = browserMenuItems[0] + assertEquals( + "enabled_browser_action", + menuAction.action.title, + ) + menuAction = browserMenuItems[1] + assertEquals( + "enabled_page_action", + menuAction.action.title, + ) + } + + @Test + fun `browser actions can be overridden per tab`() { + webExtensionBrowserActions.clear() + val loadIcon: (suspend (Int) -> Bitmap?)? = { mock() } + val pageAction = Action( + title = "title", + loadIcon = loadIcon, + enabled = true, + badgeText = "badgeText", + badgeTextColor = Color.WHITE, + badgeBackgroundColor = Color.BLUE, + ) {} + + val pageActionOverride = Action( + title = "updatedTitle", + loadIcon = null, + enabled = false, + badgeText = "updatedText", + badgeTextColor = Color.RED, + badgeBackgroundColor = Color.GREEN, + ) {} + + val browserAction = Action( + title = "title", + loadIcon = loadIcon, + enabled = true, + badgeText = "badgeText", + badgeTextColor = Color.WHITE, + badgeBackgroundColor = Color.BLUE, + ) {} + + val browserActionOverride = Action( + title = "updatedTitle", + loadIcon = null, + enabled = false, + badgeText = "updatedText", + badgeTextColor = Color.RED, + badgeBackgroundColor = Color.GREEN, + ) {} + + val browserExtensions = HashMap<String, WebExtensionState>() + browserExtensions["1"] = + WebExtensionState(id = "1", browserAction = browserAction, pageAction = pageAction) + + val browserState = BrowserState(extensions = browserExtensions) + getOrUpdateWebExtensionMenuItems(browserState, mock()) + + // Verifying global browser action + assertTrue(webExtensionBrowserActions.size == 1) + var ext1 = webExtensionBrowserActions["1"] + assertTrue(ext1?.action?.enabled!!) + assertEquals("badgeText", ext1.action.badgeText!!) + assertEquals("title", ext1.action.title!!) + assertEquals(loadIcon, ext1.action.loadIcon!!) + assertEquals(Color.WHITE, ext1.action.badgeTextColor!!) + assertEquals(Color.BLUE, ext1.action.badgeBackgroundColor!!) + + // Verifying global page action + assertTrue(webExtensionPageActions.size == 1) + ext1 = webExtensionPageActions["1"]!! + assertTrue(ext1.action.enabled!!) + assertEquals("badgeText", ext1.action.badgeText!!) + assertEquals("title", ext1.action.title!!) + assertEquals(loadIcon, ext1.action.loadIcon!!) + assertEquals(Color.WHITE, ext1.action.badgeTextColor!!) + assertEquals(Color.BLUE, ext1.action.badgeBackgroundColor!!) + + val tabExtensions = HashMap<String, WebExtensionState>() + tabExtensions["1"] = WebExtensionState( + id = "1", + browserAction = browserActionOverride, + pageAction = pageActionOverride, + ) + + val tabSessionState = TabSessionState( + content = mock(), + extensionState = tabExtensions, + ) + + getOrUpdateWebExtensionMenuItems(browserState, tabSessionState) + + // Verify rendering session-specific browser action override + assertTrue(webExtensionBrowserActions.size == 1) + var updatedExt1 = webExtensionBrowserActions["1"] + assertFalse(updatedExt1?.action?.enabled!!) + assertEquals("updatedText", updatedExt1.action.badgeText!!) + assertEquals("updatedTitle", updatedExt1.action.title!!) + assertEquals(loadIcon, updatedExt1.action.loadIcon!!) + assertEquals(Color.RED, updatedExt1.action.badgeTextColor!!) + assertEquals(Color.GREEN, updatedExt1.action.badgeBackgroundColor!!) + + // Verify rendering session-specific page action override + assertTrue(webExtensionPageActions.size == 1) + updatedExt1 = webExtensionBrowserActions["1"]!! + assertFalse(updatedExt1.action.enabled!!) + assertEquals("updatedText", updatedExt1.action.badgeText!!) + assertEquals("updatedTitle", updatedExt1.action.title!!) + assertEquals(loadIcon, updatedExt1.action.loadIcon!!) + assertEquals(Color.RED, updatedExt1.action.badgeTextColor!!) + assertEquals(Color.GREEN, updatedExt1.action.badgeBackgroundColor!!) + } + + @Test + fun `actions are sorted per extension name`() { + val loadIcon: (suspend (Int) -> Bitmap?)? = { mock() } + + val actionExt1 = Action( + title = "title", + loadIcon = loadIcon, + enabled = true, + badgeText = "badgeText", + badgeTextColor = Color.WHITE, + badgeBackgroundColor = Color.BLUE, + ) {} + + val actionExt2 = Action( + title = "title", + loadIcon = loadIcon, + enabled = true, + badgeText = "badgeText", + badgeTextColor = Color.WHITE, + badgeBackgroundColor = Color.BLUE, + ) {} + + val browserExtensions = HashMap<String, WebExtensionState>() + browserExtensions["1"] = + WebExtensionState(id = "1", name = "extensionA", browserAction = actionExt1) + browserExtensions["2"] = + WebExtensionState(id = "2", name = "extensionB", browserAction = actionExt2) + + val tabSessionState = TabSessionState( + content = mock(), + extensionState = emptyMap(), + ) + + val browserState = BrowserState(extensions = browserExtensions) + val actionItems = getOrUpdateWebExtensionMenuItems(browserState, tabSessionState) + assertEquals(2, actionItems.size) + assertEquals(actionExt1, actionItems[0].action) + assertEquals(actionExt2, actionItems[1].action) + } + + @Test + fun `clicking on the menu item should emit a BrowserMenuFacts with the web extension id`() { + val imageView: ImageView = mock() + val badgeView: TextView = mock() + val labelView = TextView(testContext) + val container = View(testContext) + val view: View = mock() + + whenever(view.findViewById<ImageView>(R.id.action_image)).thenReturn(imageView) + whenever(view.findViewById<TextView>(R.id.badge_text)).thenReturn(badgeView) + whenever(view.findViewById<TextView>(R.id.action_label)).thenReturn(labelView) + whenever(view.findViewById<View>(R.id.container)).thenReturn(container) + whenever(view.context).thenReturn(mock()) + + val browserAction = + WebExtensionBrowserAction("title", true, mock(), "", 0, 0) {} + val pageAction = + WebExtensionPageAction("title", true, mock(), "", 0, 0) {} + val extensions: Map<String, WebExtensionState> = mapOf( + "some_example_id" to WebExtensionState( + "some_example_id", + "url", + "name", + true, + browserAction = browserAction, + pageAction = pageAction, + ), + ) + + val store = + BrowserStore( + BrowserState( + extensions = extensions, + ), + ) + + val browserMenuItems = getOrUpdateWebExtensionMenuItems(store.state) + val menuItem = browserMenuItems[1] + val menu: WebExtensionBrowserMenu = mock() + + menuItem.bind(menu, view) + + CollectionProcessor.withFactCollection { facts -> + container.performClick() + + val fact = facts[0] + assertEquals(FactsAction.CLICK, fact.action) + assertEquals(WEB_EXTENSION_MENU_ITEM, fact.item) + assertEquals(1, fact.metadata?.size) + assertTrue(fact.metadata?.containsKey("id")!!) + assertEquals("some_example_id", fact.metadata?.get("id")) + } + } + + @Test + fun `hides browser and page actions in private tabs if extension is not allowed to run`() { + val loadIcon: (suspend (Int) -> Bitmap?)? = { mock() } + + val actionExt1 = Action( + title = "title", + loadIcon = loadIcon, + enabled = true, + badgeText = "badgeText", + badgeTextColor = Color.WHITE, + badgeBackgroundColor = Color.BLUE, + ) {} + + val tabSessionState = TabSessionState( + content = mock(), + extensionState = emptyMap(), + ) + whenever(tabSessionState.content.private).thenReturn(true) + + val browserExtensions = HashMap<String, WebExtensionState>() + browserExtensions["1"] = + WebExtensionState(id = "1", name = "extensionA", browserAction = actionExt1) + val browserState = BrowserState(extensions = browserExtensions) + val actionItems = getOrUpdateWebExtensionMenuItems(browserState, tabSessionState) + assertEquals(0, actionItems.size) + + val browserExtensionsAllowedInPrivateBrowsing = HashMap<String, WebExtensionState>() + browserExtensionsAllowedInPrivateBrowsing["1"] = + WebExtensionState(id = "1", allowedInPrivateBrowsing = true, name = "extensionA", browserAction = actionExt1) + val browserStateAllowedInPrivateBrowsing = BrowserState(extensions = browserExtensionsAllowedInPrivateBrowsing) + val actionItemsAllowedInPrivateBrowsing = getOrUpdateWebExtensionMenuItems(browserStateAllowedInPrivateBrowsing, tabSessionState) + assertEquals(1, actionItemsAllowedInPrivateBrowsing.size) + assertEquals(actionExt1, actionItemsAllowedInPrivateBrowsing[0].action) + } + + @Test + fun `does not include menu item for disabled paged actions`() { + val enabledPageAction = + WebExtensionBrowserAction("enabled_page_action", true, mock(), "", 0, 0) {} + val disabledPageAction = + WebExtensionBrowserAction("disabled_page_action", false, mock(), "", 0, 0) {} + + val extensions = mapOf( + "ext1" to WebExtensionState( + "ext1", + "url", + "name", + true, + pageAction = enabledPageAction, + ), + "ext2" to WebExtensionState( + "ext2", + "url", + "name", + true, + pageAction = disabledPageAction, + ), + ) + + val store = + BrowserStore( + BrowserState( + extensions = extensions, + ), + ) + + val browserMenuItems = getOrUpdateWebExtensionMenuItems(store.state) + assertEquals(1, browserMenuItems.size) + + var menuAction = browserMenuItems[0] + assertEquals( + "enabled_page_action", + menuAction.action.title, + ) + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/ext/BrowserMenuItemTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/ext/BrowserMenuItemTest.kt new file mode 100644 index 0000000000..36e7fb15ba --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/ext/BrowserMenuItemTest.kt @@ -0,0 +1,113 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.ext + +import android.graphics.Color +import mozilla.components.browser.menu.BrowserMenuHighlight +import mozilla.components.browser.menu.item.BrowserMenuHighlightableItem +import mozilla.components.browser.menu.item.BrowserMenuImageText +import org.junit.Assert +import org.junit.Test + +class BrowserMenuItemTest { + + @Test + fun `highest prio item gets selected`() { + val highlightLow1 = BrowserMenuHighlight.LowPriority(Color.YELLOW) + val highlightLow2 = BrowserMenuHighlight.LowPriority(Color.RED) + val highlightHigh = BrowserMenuHighlight.HighPriority(Color.GREEN) + + val list = listOf( + BrowserMenuHighlightableItem( + label = "Test1", + startImageResource = 0, + highlight = highlightLow1, + isHighlighted = { true }, + ), + BrowserMenuHighlightableItem( + label = "Test2", + startImageResource = 0, + highlight = highlightLow2, + isHighlighted = { true }, + ), + BrowserMenuImageText( + label = "Test3", + imageResource = 0, + ), + BrowserMenuHighlightableItem( + label = "Test4", + startImageResource = 0, + highlight = highlightHigh, + isHighlighted = { true }, + ), + ) + Assert.assertEquals(highlightHigh, list.getHighlight()) + } + + @Test + fun `invisible item does not get selected`() { + val highlightedVisible = BrowserMenuHighlightableItem( + label = "Test1", + startImageResource = 0, + highlight = BrowserMenuHighlight.LowPriority(Color.YELLOW), + isHighlighted = { true }, + ) + val highlightedInvisible = BrowserMenuHighlightableItem( + label = "Test2", + startImageResource = 0, + highlight = BrowserMenuHighlight.HighPriority(Color.GREEN), + isHighlighted = { true }, + ) + highlightedInvisible.visible = { false } + + val list = listOf(highlightedVisible, highlightedInvisible) + Assert.assertEquals(highlightedVisible.highlight, list.getHighlight()) + } + + @Test + fun `non highlightable item does not get selected`() { + val highlight = BrowserMenuHighlight.LowPriority(Color.YELLOW) + val highlight2 = BrowserMenuHighlight.HighPriority(Color.GREEN) + val list = listOf( + BrowserMenuHighlightableItem( + label = "Test1", + startImageResource = 0, + highlight = highlight, + isHighlighted = { true }, + ), + BrowserMenuHighlightableItem( + label = "Test2", + startImageResource = 0, + highlight = highlight2, + isHighlighted = { false }, + ), + ) + Assert.assertEquals(highlight, list.getHighlight()) + } + + @Test + fun `higher prio highlight which cannot propagate does not get selected`() { + val highlight = BrowserMenuHighlight.LowPriority(Color.YELLOW) + val highlightNonPropagate = BrowserMenuHighlight.HighPriority( + Color.GREEN, + canPropagate = false, + ) + val list = listOf( + BrowserMenuHighlightableItem( + label = "Test1", + startImageResource = 0, + highlight = highlight, + isHighlighted = { true }, + ), + BrowserMenuHighlightableItem( + label = "Test2", + startImageResource = 0, + highlight = highlightNonPropagate, + isHighlighted = { true }, + ), + ) + Assert.assertEquals(highlight, list.getHighlight()) + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/AbstractParentBrowserMenuItemTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/AbstractParentBrowserMenuItemTest.kt new file mode 100644 index 0000000000..3b22a871cc --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/AbstractParentBrowserMenuItemTest.kt @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.view.View +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.BrowserMenuAdapter +import mozilla.components.browser.menu.R +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class AbstractParentBrowserMenuItemTest { + + @Test + fun bind() { + val view = View(testContext) + var subMenuShowCalled = false + var subMenuDismissCalled = false + + val subMenuItem = SimpleBrowserMenuItem("test") + val subMenuAdapter = BrowserMenuAdapter(testContext, listOf(subMenuItem)) + val subMenu = BrowserMenu(subMenuAdapter) + val parentMenuItem = DummyParentBrowserMenuItem(subMenu) + val nestedMenuAdapter = BrowserMenuAdapter(testContext, listOf(parentMenuItem)) + val nestedMenu = BrowserMenu(nestedMenuAdapter) + + parentMenuItem.onSubMenuShow = { + subMenuShowCalled = true + } + parentMenuItem.onSubMenuDismiss = { + subMenuDismissCalled = true + } + + parentMenuItem.bind(nestedMenu, view) + nestedMenu.show(view) + assertTrue(nestedMenu.isShown) + + view.performClick() + assertFalse(nestedMenu.isShown) + assertTrue(subMenu.isShown) + assertTrue(subMenuShowCalled) + + subMenu.dismiss() + assertTrue(subMenuDismissCalled) + } + + @Test + fun onBackPressed() { + val view = View(testContext) + var subMenuDismissCalled = false + + val subMenuItem = SimpleBrowserMenuItem("test") + val subMenuAdapter = BrowserMenuAdapter(testContext, listOf(subMenuItem)) + val subMenu = BrowserMenu(subMenuAdapter) + val parentMenuItem = DummyParentBrowserMenuItem(subMenu) + val nestedMenuAdapter = BrowserMenuAdapter(testContext, listOf(parentMenuItem)) + val nestedMenu = BrowserMenu(nestedMenuAdapter) + + parentMenuItem.onSubMenuDismiss = { + subMenuDismissCalled = true + } + + parentMenuItem.bind(nestedMenu, view) + // verify onBackPressed while sub menu is not shown does nothing. + parentMenuItem.onBackPressed(nestedMenu, view) + assertFalse(subMenuDismissCalled) + + nestedMenu.show(view) + view.performClick() + parentMenuItem.onBackPressed(nestedMenu, view) + assertTrue(nestedMenu.isShown) + assertFalse(subMenu.isShown) + assertTrue(subMenuDismissCalled) + } +} + +class DummyParentBrowserMenuItem( + subMenu: BrowserMenu, + endOfMenuAlwaysVisible: Boolean = false, +) : AbstractParentBrowserMenuItem(subMenu, endOfMenuAlwaysVisible) { + override var visible: () -> Boolean = { true } + override fun getLayoutResource(): Int = R.layout.mozac_browser_menu_item_simple +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuCategoryTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuCategoryTest.kt new file mode 100644 index 0000000000..8c94ffe6e9 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuCategoryTest.kt @@ -0,0 +1,183 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.content.Context +import android.graphics.Typeface +import android.view.LayoutInflater +import android.view.View +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.R +import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate +import mozilla.components.concept.menu.candidate.TextStyle +import mozilla.components.support.test.mock +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.never +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class BrowserMenuCategoryTest { + private lateinit var menuCategory: BrowserMenuCategory + private val context: Context get() = ApplicationProvider.getApplicationContext() + private val label = "test" + + @Before + fun setup() { + menuCategory = BrowserMenuCategory(label) + } + + @Test + fun `menu category uses correct layout`() { + assertEquals(R.layout.mozac_browser_menu_category, menuCategory.getLayoutResource()) + } + + @Test + fun `menu category has correct label`() { + assertEquals(label, menuCategory.label) + } + + @Test + fun `menu category should handle initialization with text size`() { + val menuCategoryWithTextSize = BrowserMenuCategory(label, 12f) + + val view = inflate(menuCategoryWithTextSize) + val textView = view.findViewById<TextView>(R.id.category_text) + + assertEquals(12f, textView.textSize) + } + + @Test + fun `menu category should handle initialization with text colour resource`() { + val menuCategoryWithTextColour = BrowserMenuCategory(label, textColorResource = android.R.color.holo_red_dark) + + val view = inflate(menuCategoryWithTextColour) + val textView = view.findViewById<TextView>(R.id.category_text) + val expectedColour = ContextCompat.getColor(textView.context, android.R.color.holo_red_dark) + + assertEquals(expectedColour, textView.currentTextColor) + } + + @Test + fun `GIVEN a BrowserMenuCategory, WHEN backgroundColorResource is provided, THEN the background resource is set to that value`() { + val expectedColour = android.R.color.holo_red_dark + val menuCategoryWithBackgroundColour = BrowserMenuCategory(label, backgroundColorResource = expectedColour) + val view: TextView = mock() + val menu: BrowserMenu = mock() + + menuCategoryWithBackgroundColour.bind(menu, view) + + verify(view).setBackgroundResource(expectedColour) + } + + @Test + fun `GIVEN a BrowserMenuCategory, WHEN backgroundColorResource is not provided, THEN no background is set`() { + val menuCategoryWithNoBackgroundColour = BrowserMenuCategory(label) + val view: TextView = mock() + val menu: BrowserMenu = mock() + + menuCategoryWithNoBackgroundColour.bind(menu, view) + + verify(view, never()).setBackgroundResource(anyInt()) + } + + @Test + fun `menu category should handle initialization with text style`() { + val menuCategoryWithTextStyle = BrowserMenuCategory(label, textStyle = Typeface.ITALIC) + + val view = inflate(menuCategoryWithTextStyle) + val textView = view.findViewById<TextView>(R.id.category_text) + + assertEquals(Typeface.ITALIC, textView.typeface.style) + } + + @Test + fun `menu category should handle initialization with text alignment`() { + val menuCategoryWithTextAlignment = BrowserMenuCategory(label, textAlignment = View.TEXT_ALIGNMENT_VIEW_END) + + val view = inflate(menuCategoryWithTextAlignment) + val textView = view.findViewById<TextView>(R.id.category_text) + + assertEquals(View.TEXT_ALIGNMENT_VIEW_END, textView.textAlignment) + } + + @Test + fun `menu category can be converted to candidate`() { + assertEquals( + DecorativeTextMenuCandidate( + label, + textStyle = TextStyle( + textStyle = Typeface.BOLD, + textAlignment = View.TEXT_ALIGNMENT_VIEW_START, + ), + ), + BrowserMenuCategory(label).asCandidate(context), + ) + + assertEquals( + DecorativeTextMenuCandidate( + label, + textStyle = TextStyle( + size = 12f, + textStyle = Typeface.BOLD, + textAlignment = View.TEXT_ALIGNMENT_VIEW_START, + ), + ), + BrowserMenuCategory(label, 12f).asCandidate(context), + ) + + assertEquals( + DecorativeTextMenuCandidate( + label, + textStyle = TextStyle( + color = ContextCompat.getColor(context, android.R.color.holo_red_dark), + textStyle = Typeface.BOLD, + textAlignment = View.TEXT_ALIGNMENT_VIEW_START, + ), + ), + BrowserMenuCategory( + label, + textColorResource = android.R.color.holo_red_dark, + ).asCandidate(context), + ) + + assertEquals( + DecorativeTextMenuCandidate( + label, + textStyle = TextStyle( + textStyle = Typeface.ITALIC, + textAlignment = View.TEXT_ALIGNMENT_VIEW_START, + ), + ), + BrowserMenuCategory(label, textStyle = Typeface.ITALIC).asCandidate(context), + ) + + assertEquals( + DecorativeTextMenuCandidate( + label, + textStyle = TextStyle( + textStyle = Typeface.BOLD, + textAlignment = View.TEXT_ALIGNMENT_VIEW_END, + ), + ), + BrowserMenuCategory(label, textAlignment = View.TEXT_ALIGNMENT_VIEW_END).asCandidate(context), + ) + } + + private fun inflate(browserMenuCategory: BrowserMenuCategory): View { + val view = LayoutInflater.from(context).inflate(browserMenuCategory.getLayoutResource(), null) + val mockMenu = Mockito.mock(BrowserMenu::class.java) + browserMenuCategory.bind(mockMenu, view) + return view + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuCheckboxTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuCheckboxTest.kt new file mode 100644 index 0000000000..38bcb45c89 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuCheckboxTest.kt @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import mozilla.components.browser.menu.R +import mozilla.components.concept.menu.candidate.CompoundMenuCandidate +import mozilla.components.support.test.mock +import org.junit.Assert.assertEquals +import org.junit.Test + +class BrowserMenuCheckboxTest { + + @Test + fun `browser checkbox uses correct layout`() { + val item = BrowserMenuCheckbox("Hello") {} + assertEquals(R.layout.mozac_browser_menu_item_checkbox, item.getLayoutResource()) + } + + @Test + fun `checkbox can be converted to candidate with correct end type`() { + val listener = { _: Boolean -> } + + assertEquals( + CompoundMenuCandidate( + "Hello", + isChecked = false, + end = CompoundMenuCandidate.ButtonType.CHECKBOX, + onCheckedChange = listener, + ), + BrowserMenuCheckbox( + "Hello", + listener = listener, + ).asCandidate(mock()), + ) + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuCompoundButtonTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuCompoundButtonTest.kt new file mode 100644 index 0000000000..25a09bd77c --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuCompoundButtonTest.kt @@ -0,0 +1,190 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewTreeObserver +import android.widget.CheckBox +import androidx.appcompat.widget.SwitchCompat +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.R +import mozilla.components.concept.menu.candidate.CompoundMenuCandidate +import mozilla.components.support.test.any +import mozilla.components.support.test.argumentCaptor +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class BrowserMenuCompoundButtonTest { + + @Test + fun `simple menu items are always visible by default`() { + val item = SimpleTestBrowserCompoundButton("Hello") { + // do nothing + } + + assertTrue(item.visible()) + } + + @Test + fun `layout resource can be inflated`() { + val item = SimpleTestBrowserCompoundButton("Hello") { + // do nothing + } + + val view = LayoutInflater.from(testContext) + .inflate(item.getLayoutResource(), null) + + assertNotNull(view) + } + + @Test + fun `clicking bound view will invoke callback and dismiss menu`() { + var callbackInvoked = false + + val item = SimpleTestBrowserCompoundButton("Hello") { checked -> + callbackInvoked = checked + } + + val menu = mock(BrowserMenu::class.java) + val view = CheckBox(testContext) + + item.bind(menu, view) + + view.isChecked = true + + assertTrue(callbackInvoked) + verify(menu).dismiss() + } + + @Test + fun `initialState is invoked on bind`() { + val initialState: () -> Boolean = { true } + val item = SimpleTestBrowserCompoundButton("Hello", initialState) {} + + val menu = mock(BrowserMenu::class.java) + val view = spy(CheckBox(testContext)) + item.bind(menu, view) + + verify(view).isChecked = true + } + + @Test + fun `hitting default methods`() { + val item = SimpleTestBrowserCompoundButton("") {} + item.invalidate(mock(View::class.java)) + } + + @Test + fun `menu compound button can be converted to candidate`() { + val listener = { _: Boolean -> } + + assertEquals( + CompoundMenuCandidate( + "Hello", + isChecked = false, + end = CompoundMenuCandidate.ButtonType.CHECKBOX, + onCheckedChange = listener, + ), + SimpleTestBrowserCompoundButton( + "Hello", + listener = listener, + ).asCandidate(testContext), + ) + + assertEquals( + CompoundMenuCandidate( + "Hello", + isChecked = true, + end = CompoundMenuCandidate.ButtonType.CHECKBOX, + onCheckedChange = listener, + ), + SimpleTestBrowserCompoundButton( + "Hello", + initialState = { true }, + listener = listener, + ).asCandidate(testContext), + ) + } + + @Test + fun `GIVEN the View is attached to Window WHEN bind is called THEN the layout direction is not updated`() { + val item = SimpleTestBrowserCompoundButton("Hello") {} + val menu = mock(BrowserMenu::class.java) + val view: SwitchCompat = mock() + doReturn(true).`when`(view).isAttachedToWindow + doReturn(mock<ViewTreeObserver>()).`when`(view).viewTreeObserver + + item.bind(menu, view) + + verify(view, never()).layoutDirection = ArgumentMatchers.anyInt() + } + + @Test + fun `GIVEN the View is not attached to Window WHEN bind is called THEN the layout direction is changed to locale`() { + val item = SimpleTestBrowserCompoundButton("Hello") {} + val menu = mock(BrowserMenu::class.java) + val view: SwitchCompat = mock() + doReturn(false).`when`(view).isAttachedToWindow + doReturn(mock<ViewTreeObserver>()).`when`(view).viewTreeObserver + + item.bind(menu, view) + + verify(view).layoutDirection = View.LAYOUT_DIRECTION_LOCALE + } + + @Test + fun `GIVEN the View is not attached to Window WHEN bind is called THEN the a viewTreeObserver for preDraw is set`() { + val item = SimpleTestBrowserCompoundButton("Hello") {} + val menu = mock(BrowserMenu::class.java) + val view: SwitchCompat = mock() + val viewTreeObserver: ViewTreeObserver = mock() + doReturn(false).`when`(view).isAttachedToWindow + doReturn(viewTreeObserver).`when`(view).viewTreeObserver + + item.bind(menu, view) + + verify(viewTreeObserver).addOnPreDrawListener(any()) + } + + @Test + fun `GIVEN a view with updated layout direction WHEN it is about to be drawn THEN the layout direction reset`() { + val item = SimpleTestBrowserCompoundButton("Hello") {} + val menu = mock(BrowserMenu::class.java) + val view: SwitchCompat = mock() + val viewTreeObserver: ViewTreeObserver = mock() + doReturn(false).`when`(view).isAttachedToWindow + doReturn(viewTreeObserver).`when`(view).viewTreeObserver + val captor = argumentCaptor<ViewTreeObserver.OnPreDrawListener>() + + item.bind(menu, view) + verify(viewTreeObserver).addOnPreDrawListener(captor.capture()) + + captor.value.onPreDraw() + verify(viewTreeObserver).removeOnPreDrawListener(captor.value) + verify(view).layoutDirection = View.LAYOUT_DIRECTION_INHERIT + } + + class SimpleTestBrowserCompoundButton( + label: String, + initialState: () -> Boolean = { false }, + listener: (Boolean) -> Unit, + ) : BrowserMenuCompoundButton(label, false, false, initialState, listener) { + override fun getLayoutResource(): Int = R.layout.mozac_browser_menu_item_simple + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuDividerTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuDividerTest.kt new file mode 100644 index 0000000000..1a3979dd1e --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuDividerTest.kt @@ -0,0 +1,47 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import mozilla.components.browser.menu.R +import mozilla.components.concept.menu.candidate.ContainerStyle +import mozilla.components.concept.menu.candidate.DividerMenuCandidate +import mozilla.components.support.test.mock +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class BrowserMenuDividerTest { + + @Test + fun `browser divider uses correct layout`() { + val item = BrowserMenuDivider() + assertEquals(R.layout.mozac_browser_menu_item_divider, item.getLayoutResource()) + } + + @Test + fun `hitting default methods`() { + val item = BrowserMenuDivider() + assertTrue(item.visible()) + item.bind(mock(), mock()) + item.invalidate(mock()) + } + + @Test + fun `menu divider can be converted to candidate`() { + assertEquals( + DividerMenuCandidate(), + BrowserMenuDivider().asCandidate(mock()), + ) + + assertEquals( + DividerMenuCandidate( + containerStyle = ContainerStyle(isVisible = true), + ), + BrowserMenuDivider().apply { + visible = { true } + }.asCandidate(mock()), + ) + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableItemTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableItemTest.kt new file mode 100644 index 0000000000..49bd310a00 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableItemTest.kt @@ -0,0 +1,334 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.view.LayoutInflater +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.appcompat.widget.AppCompatImageView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat.getColor +import androidx.core.view.isVisible +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.BrowserMenuHighlight +import mozilla.components.browser.menu.R +import mozilla.components.concept.menu.candidate.DrawableMenuIcon +import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect +import mozilla.components.concept.menu.candidate.LowPriorityHighlightEffect +import mozilla.components.concept.menu.candidate.TextMenuCandidate +import mozilla.components.concept.menu.candidate.TextStyle +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.robolectric.Shadows +import mozilla.components.ui.colors.R as colorsR + +@RunWith(AndroidJUnit4::class) +class BrowserMenuHighlightableItemTest { + + private val colorId = colorsR.color.photonRed50 + + @Suppress("Deprecation") + @Test + fun `browser menu highlightable item should be inflated`() { + var onClickWasPress = false + val item = BrowserMenuHighlightableItem( + "label", + imageResource = android.R.drawable.ic_menu_report_image, + iconTintColorResource = android.R.color.black, + textColorResource = android.R.color.black, + highlight = BrowserMenuHighlightableItem.Highlight( + endImageResource = android.R.drawable.ic_menu_report_image, + backgroundResource = colorId, + colorResource = colorId, + ), + ) { + onClickWasPress = true + } + + val view = inflate(item) + + view.performClick() + assertTrue(onClickWasPress) + } + + @Suppress("Deprecation") + @Test + fun `browser menu highlightable item should properly handle classic highlighting`() { + var shouldHighlight = false + val item = BrowserMenuHighlightableItem( + label = "label", + startImageResource = android.R.drawable.ic_menu_report_image, + iconTintColorResource = android.R.color.black, + textColorResource = android.R.color.black, + highlight = BrowserMenuHighlightableItem.Highlight( + startImageResource = android.R.drawable.ic_menu_camera, + endImageResource = android.R.drawable.ic_menu_add, + backgroundResource = colorId, + colorResource = colorId, + ), + isHighlighted = { shouldHighlight }, + ) + + val view = inflate(item) + + assertEquals("label", view.textView.text) + + // Highlight should not exist before set + val oldDrawable = view.startImageView.drawable + assertEquals(android.R.drawable.ic_menu_report_image, Shadows.shadowOf(oldDrawable).createdFromResId) + assertFalse(view.endImageView.isVisible) + + shouldHighlight = true + item.invalidate(view) + + // Highlight should now exist + assertTrue(view.endImageView.isVisible) + assertNotEquals(oldDrawable, view.startImageView.drawable) + assertEquals(android.R.drawable.ic_menu_camera, Shadows.shadowOf(view.startImageView.drawable).createdFromResId) + assertEquals(android.R.drawable.ic_menu_add, Shadows.shadowOf(view.endImageView.drawable).createdFromResId) + assertNotNull(view.endImageView.imageTintList) + assertEquals(colorId, Shadows.shadowOf(view.background).createdFromResId) + } + + @Test + fun `browser menu highlightable item should properly handle high priority highlighting`() { + var shouldHighlight = false + val item = BrowserMenuHighlightableItem( + label = "label", + startImageResource = android.R.drawable.ic_menu_report_image, + iconTintColorResource = android.R.color.black, + textColorResource = android.R.color.black, + highlight = BrowserMenuHighlight.HighPriority( + endImageResource = android.R.drawable.ic_menu_add, + backgroundTint = Color.RED, + label = "highlight", + ), + isHighlighted = { shouldHighlight }, + ) + + val view = inflate(item) + + assertEquals("label", view.textView.text) + assertEquals("highlight", view.highlightedTextView.text) + + // Highlight should not exist before set + assertEquals(android.R.drawable.ic_menu_report_image, Shadows.shadowOf(view.startImageView.drawable).createdFromResId) + assertFalse(view.highlightedTextView.isVisible) + assertFalse(view.endImageView.isVisible) + + shouldHighlight = true + item.invalidate(view) + + // Highlight should now exist + assertTrue(view.highlightedTextView.isVisible) + assertEquals(android.R.drawable.ic_menu_report_image, Shadows.shadowOf(view.startImageView.drawable).createdFromResId) + assertEquals(android.R.drawable.ic_menu_add, Shadows.shadowOf(view.endImageView.drawable).createdFromResId) + assertNotNull(view.endImageView.imageTintList) + assertTrue(view.endImageView.isVisible) + } + + @Test + fun `browser menu highlightable item should properly handle low priority highlighting`() { + var shouldHighlight = false + val item = BrowserMenuHighlightableItem( + label = "label", + startImageResource = android.R.drawable.ic_menu_report_image, + iconTintColorResource = android.R.color.black, + textColorResource = android.R.color.black, + highlight = BrowserMenuHighlight.LowPriority( + notificationTint = Color.RED, + label = "highlight", + ), + isHighlighted = { shouldHighlight }, + ) + + val view = inflate(item) + + assertEquals("label", view.textView.text) + assertEquals("highlight", view.highlightedTextView.text) + + val startImageView = view.findViewById<AppCompatImageView>(R.id.image) + val highlightImageView = view.findViewById<AppCompatImageView>(R.id.end_image) + + // Highlight should not exist before set + assertEquals(android.R.drawable.ic_menu_report_image, Shadows.shadowOf(startImageView.drawable).createdFromResId) + assertFalse(view.highlightedTextView.isVisible) + assertFalse(highlightImageView.isVisible) + + shouldHighlight = true + item.invalidate(view) + + // Highlight should now exist + assertTrue(view.findViewById<TextView>(R.id.highlight_text).isVisible) + assertFalse(view.findViewById<AppCompatImageView>(R.id.end_image).isVisible) + assertNull(view.background) + } + + @Test + fun `browser menu highlightable item with with no iconTintColorResource must not have a tinted icon`() { + val item = BrowserMenuHighlightableItem( + "label", + startImageResource = android.R.drawable.ic_menu_report_image, + highlight = BrowserMenuHighlight.HighPriority( + endImageResource = android.R.drawable.ic_menu_report_image, + backgroundTint = Color.RED, + ), + ) + + val view = inflate(item) + + assertEquals("label", view.textView.text) + assertNull(view.startImageView.drawable!!.colorFilter) + assertNull(view.endImageView.imageTintList) + } + + @Test + fun `bind highlightable item with with default high priority`() { + val item = BrowserMenuHighlightableItem( + "label", + startImageResource = android.R.drawable.ic_menu_report_image, + highlight = BrowserMenuHighlight.HighPriority( + backgroundTint = Color.RED, + ), + ) + + val view = inflate(item) + + assertEquals("label", view.textView.text) + assertEquals("label", view.highlightedTextView.text) + assertTrue(view.highlightedTextView.isVisible) + assertTrue(view.background is ColorDrawable) + assertNull(view.endImageView.drawable) + } + + @Suppress("Deprecation") + @Test + fun `browser menu highlightable item with with no highlight must not have highlightImageView visible`() { + val item = BrowserMenuHighlightableItem( + "label", + android.R.drawable.ic_menu_report_image, + highlight = null, + ) + + val view = inflate(item) + val endImageView = view.findViewById<AppCompatImageView>(R.id.end_image) + val textView = view.findViewById<TextView>(R.id.text) + assertEquals("label", textView.text) + assertEquals(endImageView.visibility, View.GONE) + } + + @Test + fun `menu item can be converted to candidate`() { + val listener = {} + + var shouldHighlight = false + val highPriorityItem = BrowserMenuHighlightableItem( + label = "label", + startImageResource = android.R.drawable.ic_menu_report_image, + iconTintColorResource = android.R.color.black, + textColorResource = android.R.color.black, + highlight = BrowserMenuHighlight.HighPriority( + endImageResource = android.R.drawable.ic_menu_add, + backgroundTint = Color.RED, + label = "highlight", + ), + isHighlighted = { shouldHighlight }, + listener = listener, + ) + + assertEquals( + TextMenuCandidate( + "label", + start = DrawableMenuIcon( + null, + tint = getColor(testContext, android.R.color.black), + ), + textStyle = TextStyle( + color = getColor(testContext, android.R.color.black), + ), + onClick = listener, + ), + highPriorityItem.asCandidate(testContext).removeDrawables(), + ) + + shouldHighlight = true + assertEquals( + TextMenuCandidate( + "highlight", + start = DrawableMenuIcon( + null, + tint = getColor(testContext, android.R.color.black), + ), + end = DrawableMenuIcon(null), + textStyle = TextStyle( + color = getColor(testContext, android.R.color.black), + ), + effect = HighPriorityHighlightEffect( + backgroundTint = Color.RED, + ), + onClick = listener, + ), + highPriorityItem.asCandidate(testContext).removeDrawables(), + ) + + assertEquals( + TextMenuCandidate( + "highlight", + start = DrawableMenuIcon( + null, + tint = getColor(testContext, android.R.color.black), + effect = LowPriorityHighlightEffect( + notificationTint = Color.RED, + ), + ), + textStyle = TextStyle( + color = getColor(testContext, android.R.color.black), + ), + onClick = listener, + ), + BrowserMenuHighlightableItem( + label = "label", + startImageResource = android.R.drawable.ic_menu_report_image, + iconTintColorResource = android.R.color.black, + textColorResource = android.R.color.black, + highlight = BrowserMenuHighlight.LowPriority( + notificationTint = Color.RED, + label = "highlight", + ), + isHighlighted = { true }, + listener = listener, + ).asCandidate(testContext).removeDrawables(), + ) + } + + private fun inflate(item: BrowserMenuHighlightableItem): ConstraintLayout { + val view = LayoutInflater.from(testContext).inflate(item.getLayoutResource(), null) + val mockMenu = mock(BrowserMenu::class.java) + item.bind(mockMenu, view) + return view as ConstraintLayout + } + + private val ConstraintLayout.startImageView: ImageView get() = findViewById(R.id.image) + private val ConstraintLayout.endImageView: ImageView get() = findViewById(R.id.end_image) + private val ConstraintLayout.textView: TextView get() = findViewById(R.id.text) + private val ConstraintLayout.highlightedTextView: TextView get() = findViewById(R.id.highlight_text) + + private fun TextMenuCandidate.removeDrawables() = copy( + start = (start as? DrawableMenuIcon)?.copy(drawable = null), + end = (end as? DrawableMenuIcon)?.copy(drawable = null), + ) +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableSwitchTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableSwitchTest.kt new file mode 100644 index 0000000000..5df2dab9a8 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableSwitchTest.kt @@ -0,0 +1,124 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.graphics.Color +import android.view.LayoutInflater +import android.widget.ImageView +import androidx.appcompat.widget.AppCompatImageView +import androidx.appcompat.widget.SwitchCompat +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.BrowserMenuHighlight +import mozilla.components.browser.menu.R +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.robolectric.Shadows +import mozilla.components.ui.colors.R as colorsR + +@RunWith(AndroidJUnit4::class) +class BrowserMenuHighlightableSwitchTest { + + @Test + fun `menu item uses correct layout`() { + val item = BrowserMenuHighlightableSwitch( + label = "label", + startImageResource = android.R.drawable.ic_menu_report_image, + highlight = BrowserMenuHighlight.LowPriority( + notificationTint = Color.RED, + ), + ) {} + + assertEquals(R.layout.mozac_browser_menu_highlightable_switch, item.getLayoutResource()) + } + + @Suppress("Deprecation") + @Test + fun `browser menu highlightable item should be inflated`() { + var onClickWasPress = false + val item = BrowserMenuHighlightableSwitch( + label = "label", + startImageResource = android.R.drawable.ic_menu_report_image, + highlight = BrowserMenuHighlight.LowPriority( + notificationTint = Color.RED, + ), + ) { + onClickWasPress = true + } + + val view = inflate(item) + + view.switch.performClick() + assertTrue(onClickWasPress) + } + + @Test + fun `browser menu highlightable item should properly handle low priority highlighting`() { + var shouldHighlight = false + val item = BrowserMenuHighlightableSwitch( + label = "label", + startImageResource = android.R.drawable.ic_menu_report_image, + iconTintColorResource = android.R.color.black, + textColorResource = android.R.color.black, + highlight = BrowserMenuHighlight.LowPriority( + notificationTint = colorsR.color.photonRed50, + label = "highlight", + ), + isHighlighted = { shouldHighlight }, + ) {} + + val view = inflate(item) + + assertEquals("label", view.switch.text) + + val startImageView = view.findViewById<AppCompatImageView>(R.id.image) + + // Highlight should not exist before set + assertEquals(android.R.drawable.ic_menu_report_image, Shadows.shadowOf(startImageView.drawable).createdFromResId) + assertFalse(view.notificationDot.isVisible) + + shouldHighlight = true + item.invalidate(view) + + // Highlight should now exist + assertEquals("highlight", view.switch.text) + assertTrue(view.notificationDot.isVisible) + } + + @Test + fun `browser menu highlightable item with with no iconTintColorResource must not have a tinted icon`() { + val item = BrowserMenuHighlightableSwitch( + "label", + startImageResource = android.R.drawable.ic_menu_report_image, + highlight = BrowserMenuHighlight.LowPriority( + notificationTint = Color.RED, + ), + ) {} + + val view = inflate(item) + + assertEquals("label", view.switch.text) + assertNull(view.startImageView.drawable!!.colorFilter) + } + + private fun inflate(item: BrowserMenuHighlightableSwitch): ConstraintLayout { + val view = LayoutInflater.from(testContext).inflate(item.getLayoutResource(), null) + val mockMenu = mock(BrowserMenu::class.java) + item.bind(mockMenu, view) + return view as ConstraintLayout + } + + private val ConstraintLayout.startImageView: ImageView get() = findViewById(R.id.image) + private val ConstraintLayout.notificationDot: ImageView get() = findViewById(R.id.notification_dot) + private val ConstraintLayout.switch: SwitchCompat get() = findViewById(R.id.switch_widget) +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuImageSwitchTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuImageSwitchTest.kt new file mode 100644 index 0000000000..ae3c82aa7b --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuImageSwitchTest.kt @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.menu.R +import mozilla.components.concept.menu.candidate.CompoundMenuCandidate +import mozilla.components.concept.menu.candidate.DrawableMenuIcon +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class BrowserMenuImageSwitchTest { + private lateinit var menuItem: BrowserMenuImageSwitch + private val label = "test" + private val icon = android.R.drawable.ic_menu_call + + @Before + fun setup() { + menuItem = BrowserMenuImageSwitch(icon, label) {} + } + + @Test + fun `menu item uses correct layout`() { + assertEquals(R.layout.mozac_browser_menu_item_image_switch, menuItem.getLayoutResource()) + } + + @Test + fun `menu item has correct label`() { + assertEquals(label, menuItem.label) + } + + @Test + fun `menu item has correct icon`() { + assertEquals(icon, menuItem.imageResource) + } + + @Test + fun `menu switch can be converted to candidate`() { + val listener = { _: Boolean -> } + + assertEquals( + CompoundMenuCandidate( + label, + isChecked = false, + start = DrawableMenuIcon(null), + end = CompoundMenuCandidate.ButtonType.SWITCH, + onCheckedChange = listener, + ), + BrowserMenuImageSwitch(icon, label, listener = listener).asCandidate(testContext).run { + copy(start = (start as DrawableMenuIcon?)?.copy(drawable = null)) + }, + ) + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuImageTextCheckboxButtonTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuImageTextCheckboxButtonTest.kt new file mode 100644 index 0000000000..8969fcd94e --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuImageTextCheckboxButtonTest.kt @@ -0,0 +1,204 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.view.LayoutInflater +import android.view.View +import android.widget.CheckBox +import android.widget.ImageView +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.widget.ImageViewCompat.getImageTintList +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.R +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class BrowserMenuImageTextCheckboxButtonTest { + + private lateinit var item: BrowserMenuImageTextCheckboxButton + private lateinit var secondaryItem: BrowserMenuImageTextCheckboxButton + + private val label = "Bookmarks" + private val imageResource = android.R.drawable.ic_menu_report_image + private val iconTintColorResource = android.R.color.holo_red_dark + + private val tintColorResource = android.R.color.holo_purple + private val labelListener = { } + private val primaryLabel = "Add" + private val secondaryLabel = "Edit" + private val primaryStateIconResource = android.R.drawable.star_big_off + private val secondaryStateIconResource = android.R.drawable.star_big_on + private val isInPrimaryState: () -> Boolean = { true } + private val onCheckedChangedListener: (Boolean) -> Unit = { } + + @Before + fun setUp() { + item = spy( + BrowserMenuImageTextCheckboxButton( + imageResource = imageResource, + label = label, + iconTintColorResource = iconTintColorResource, + textColorResource = tintColorResource, + labelListener = labelListener, + primaryLabel = primaryLabel, + secondaryLabel = secondaryLabel, + primaryStateIconResource = primaryStateIconResource, + secondaryStateIconResource = secondaryStateIconResource, + tintColorResource = tintColorResource, + isInPrimaryState = isInPrimaryState, + onCheckedChangedListener = onCheckedChangedListener, + ), + ) + + secondaryItem = spy( + BrowserMenuImageTextCheckboxButton( + imageResource = imageResource, + label = label, + iconTintColorResource = iconTintColorResource, + textColorResource = tintColorResource, + labelListener = labelListener, + primaryLabel = primaryLabel, + secondaryLabel = secondaryLabel, + primaryStateIconResource = primaryStateIconResource, + secondaryStateIconResource = secondaryStateIconResource, + tintColorResource = tintColorResource, + isInPrimaryState = { false }, + onCheckedChangedListener = onCheckedChangedListener, + ), + ) + } + + @Test + fun `layout resource can be inflated`() { + val view = LayoutInflater.from(testContext) + .inflate(item.getLayoutResource(), null) + + assertNotNull(view) + } + + @Test + fun `item uses correct layout`() { + assertEquals(R.layout.mozac_browser_menu_item_image_text_checkbox_button, item.getLayoutResource()) + } + + @Test + fun `item is visible by default`() { + assertTrue(item.visible()) + } + + @Test + fun `initialState is invoked on bind and properly sets label`() { + val menu = mock(BrowserMenu::class.java) + val view = LayoutInflater.from(testContext) + .inflate(item.getLayoutResource(), null) + + item.bind(menu, view) + val checkBox = view.findViewById<CheckBox>(R.id.checkbox) + var expectedLabel = if (item.isInPrimaryState()) primaryLabel else secondaryLabel + + assertEquals(expectedLabel, primaryLabel) + assertEquals(expectedLabel, checkBox.text) + + secondaryItem.bind(menu, view) + val secondaryCheckBox = view.findViewById<CheckBox>(R.id.checkbox) + expectedLabel = if (secondaryItem.isInPrimaryState()) primaryLabel else secondaryLabel + + assertEquals(expectedLabel, secondaryLabel) + assertEquals(expectedLabel, secondaryCheckBox.text) + } + + @Test + fun `item has the correct text color`() { + val view = inflate(item) + + val textView = view.findViewById<TextView>(R.id.text) + val expectedColour = ContextCompat.getColor(view.context, item.textColorResource) + + assertEquals(textView.text, label) + assertEquals(expectedColour, textView.currentTextColor) + } + + @Test + fun `item has icon with correct resource and tint`() { + val view = inflate(item) + + val icon = view.findViewById<ImageView>(R.id.image) + val expectedColour = ContextCompat.getColor(view.context, item.iconTintColorResource) + + assertNotNull(icon.drawable) + assertEquals(getImageTintList(icon)?.defaultColor, expectedColour) + } + + @Test + fun `item accessibilityRegion has label text as content description`() { + val view = inflate(item) + + val accessibilityRegion = view.findViewById<View>(R.id.accessibilityRegion) + + assertEquals(label, accessibilityRegion.contentDescription) + } + + @Test + fun `item accessibilityRegion is clickable`() { + val view = inflate(item) + + val accessibilityRegion = view.findViewById<View>(R.id.accessibilityRegion) + + assertTrue(accessibilityRegion.isClickable) + assertTrue(accessibilityRegion.callOnClick()) + } + + @Test + fun `clicking item dismisses menu`() { + val view = inflate(item) + val menu = mock(BrowserMenu::class.java) + + item.bind(menu, view) + view.callOnClick() + + verify(menu).dismiss() + } + + @Test + fun `clicking checkbox dismisses menu`() { + val view = inflate(item) + val menu = mock(BrowserMenu::class.java) + + item.bind(menu, view) + val checkBox = view.findViewById<CheckBox>(R.id.checkbox) + + checkBox.performClick() + + verify(menu).dismiss() + } + + @Test + fun `item checkbox has text with correct tint`() { + val view = inflate(item) + + val checkbox = view.findViewById<CheckBox>(R.id.checkbox) + val expectedColour = ContextCompat.getColor(view.context, tintColorResource) + + assertEquals(expectedColour, checkbox.currentTextColor) + } + + private fun inflate(item: BrowserMenuImageText): View { + val view = LayoutInflater.from(testContext).inflate(item.getLayoutResource(), null) + val mockMenu = mock(BrowserMenu::class.java) + item.bind(mockMenu, view) + return view + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuImageTextTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuImageTextTest.kt new file mode 100644 index 0000000000..951a68e2df --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuImageTextTest.kt @@ -0,0 +1,131 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.widget.TextView +import androidx.appcompat.widget.AppCompatImageView +import androidx.core.content.ContextCompat.getColor +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.R +import mozilla.components.concept.menu.candidate.DrawableMenuIcon +import mozilla.components.concept.menu.candidate.TextMenuCandidate +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock + +@RunWith(AndroidJUnit4::class) +class BrowserMenuImageTextTest { + + private val context: Context get() = ApplicationProvider.getApplicationContext() + + @Test + fun `browser menu ImageText should be inflated`() { + var onClickWasPress = false + val item = BrowserMenuImageText( + "label", + android.R.drawable.ic_menu_report_image, + android.R.color.black, + ) { + onClickWasPress = true + } + + val view = inflate(item) + + view.performClick() + assertTrue(onClickWasPress) + } + + @Test + fun `browser menu ImageText should have the right text, image, and iconTintColorResource`() { + val item = BrowserMenuImageText( + "label", + android.R.drawable.ic_menu_report_image, + android.R.color.black, + ) { + } + + val view = inflate(item) + + val textView = view.findViewById<TextView>(R.id.text) + assertEquals(textView.text, "label") + + val imageView = view.findViewById<AppCompatImageView>(R.id.image) + + assertNotNull(imageView.drawable) + + assertNotNull(imageView.imageTintList) + } + + @Test + fun `browser menu ImageText with with no iconTintColorResource must not have an imageTintList`() { + val item = BrowserMenuImageText( + "label", + android.R.drawable.ic_menu_report_image, + ) + + val view = inflate(item) + + val imageView = view.findViewById<AppCompatImageView>(R.id.image) + + assertNull(imageView.imageTintList) + } + + @Test + fun `menu image text item can be converted to candidate`() { + val listener = {} + + assertEquals( + TextMenuCandidate( + "label", + start = DrawableMenuIcon(null), + onClick = listener, + ), + BrowserMenuImageText( + "label", + android.R.drawable.ic_menu_report_image, + listener = listener, + ).asCandidate(context).let { + val text = it as TextMenuCandidate + text.copy(start = (text.start as? DrawableMenuIcon)?.copy(drawable = null)) + }, + ) + + assertEquals( + TextMenuCandidate( + "label", + start = DrawableMenuIcon( + null, + tint = getColor(context, android.R.color.black), + ), + onClick = listener, + ), + BrowserMenuImageText( + "label", + android.R.drawable.ic_menu_report_image, + android.R.color.black, + listener = listener, + ).asCandidate(context).let { + val text = it as TextMenuCandidate + text.copy(start = (text.start as? DrawableMenuIcon)?.copy(drawable = null)) + }, + ) + } + + private fun inflate(item: BrowserMenuImageText): View { + val view = LayoutInflater.from(context).inflate(item.getLayoutResource(), null) + val mockMenu = mock(BrowserMenu::class.java) + item.bind(mockMenu, view) + return view + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuItemToolbarTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuItemToolbarTest.kt new file mode 100644 index 0000000000..a5f729a321 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuItemToolbarTest.kt @@ -0,0 +1,439 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.view.LayoutInflater +import android.widget.ImageButton +import android.widget.LinearLayout +import android.widget.TextView +import androidx.appcompat.R +import androidx.appcompat.widget.AppCompatImageView +import androidx.core.content.ContextCompat.getColor +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.concept.menu.candidate.ContainerStyle +import mozilla.components.concept.menu.candidate.DrawableMenuIcon +import mozilla.components.concept.menu.candidate.RowMenuCandidate +import mozilla.components.concept.menu.candidate.SmallMenuCandidate +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.reset +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class BrowserMenuItemToolbarTest { + + @Test + fun `toolbar is visible by default`() { + val toolbar = BrowserMenuItemToolbar(emptyList()) + + assertTrue(toolbar.visible()) + } + + @Test + fun `layout resource can be inflated`() { + val toolbar = BrowserMenuItemToolbar(emptyList()) + + val view = LayoutInflater.from(testContext) + .inflate(toolbar.getLayoutResource(), null) + + assertNotNull(view) + } + + @Test + fun `empty toolbar does not add anything to view group`() { + val layout = LinearLayout(testContext) + + val menu = mock(BrowserMenu::class.java) + + val toolbar = BrowserMenuItemToolbar(emptyList()) + toolbar.bind(menu, layout) + + assertEquals(0, layout.childCount) + } + + @Test + fun `toolbar removes previously existing child views from view group`() { + val layout = LinearLayout(testContext) + layout.addView(TextView(testContext)) + layout.addView(TextView(testContext)) + + assertEquals(2, layout.childCount) + + val menu = mock(BrowserMenu::class.java) + + val toolbar = BrowserMenuItemToolbar(emptyList()) + toolbar.bind(menu, layout) + + assertEquals(0, layout.childCount) + } + + @Test + fun `items are added as ImageButton to view group`() { + val buttons = listOf( + BrowserMenuItemToolbar.Button( + R.drawable.abc_ic_ab_back_material, + "Button01", + ) {}, + BrowserMenuItemToolbar.Button( + R.drawable.abc_ic_ab_back_material, + "Button02", + ) {}, + BrowserMenuItemToolbar.TwoStateButton( + primaryImageResource = R.drawable.abc_ic_go_search_api_material, + primaryContentDescription = "TwoStatePrimary", + secondaryImageResource = R.drawable.abc_ic_clear_material, + secondaryContentDescription = "TwoStateSecondary", + ) {}, + ) + + val menu = mock(BrowserMenu::class.java) + val layout = LinearLayout(testContext) + + val toolbar = BrowserMenuItemToolbar(buttons) + toolbar.bind(menu, layout) + + assertEquals(3, layout.childCount) + + val child1 = layout.getChildAt(0) + val child2 = layout.getChildAt(1) + val child3 = layout.getChildAt(2) + + assertTrue(child1 is ImageButton) + assertTrue(child2 is ImageButton) + assertTrue(child3 is ImageButton) + + assertEquals("Button01", child1.contentDescription) + assertEquals("Button02", child2.contentDescription) + assertEquals("TwoStatePrimary", child3.contentDescription) + } + + @Test + fun `Disabled Button is not enabled`() { + val buttons = listOf( + BrowserMenuItemToolbar.Button( + imageResource = R.drawable.abc_ic_go_search_api_material, + contentDescription = "Button01", + iconTintColorResource = R.color.accent_material_light, + isEnabled = { false }, + ) {}, + ) + + val menu = mock(BrowserMenu::class.java) + val layout = LinearLayout(testContext) + + val toolbar = BrowserMenuItemToolbar(buttons) + toolbar.bind(menu, layout) + + val child1 = layout.getChildAt(0) + assertEquals("Button01", child1.contentDescription) + assertFalse(child1.isEnabled) + } + + @Test + fun `Button redraws when invalidate is triggered`() { + var isEnabled = false + val buttons = listOf( + BrowserMenuItemToolbar.Button( + imageResource = R.drawable.abc_ic_go_search_api_material, + contentDescription = "Button01", + isEnabled = { isEnabled }, + ) {}, + ) + + val menu = mock(BrowserMenu::class.java) + val layout = LinearLayout(testContext) + + val toolbar = BrowserMenuItemToolbar(buttons) + toolbar.bind(menu, layout) + + val child1 = layout.getChildAt(0) + assertEquals("Button01", child1.contentDescription) + assertFalse(child1.isEnabled) + + isEnabled = true + toolbar.invalidate(layout) + assertTrue(child1.isEnabled) + } + + @Test + fun `Disabled TwoState Button in secondary state is disabled`() { + val buttons = listOf( + BrowserMenuItemToolbar.TwoStateButton( + primaryImageResource = R.drawable.abc_ic_go_search_api_material, + primaryContentDescription = "TwoStateEnabled", + secondaryImageResource = R.drawable.abc_ic_clear_material, + secondaryContentDescription = "TwoStateDisabled", + isInPrimaryState = { false }, + disableInSecondaryState = true, + ) {}, + ) + + val menu = mock(BrowserMenu::class.java) + val layout = LinearLayout(testContext) + + val toolbar = BrowserMenuItemToolbar(buttons) + toolbar.bind(menu, layout) + + val child1 = layout.getChildAt(0) + assertEquals("TwoStateDisabled", child1.contentDescription) + assertFalse(child1.isEnabled) + } + + @Test + fun `TwoStateButton has primary and secondary state invoked`() { + val primaryResource = R.drawable.abc_ic_go_search_api_material + val secondaryResource = R.drawable.abc_ic_clear_material + + var reloadPageAction = BrowserMenuItemToolbar.TwoStateButton( + primaryImageResource = primaryResource, + primaryContentDescription = "primary", + primaryImageTintResource = R.color.accent_material_dark, + secondaryImageResource = secondaryResource, + secondaryContentDescription = "secondary", + secondaryImageTintResource = R.color.accent_material_light, + ) {} + assertTrue(reloadPageAction.isInPrimaryState.invoke()) + + reloadPageAction = BrowserMenuItemToolbar.TwoStateButton( + primaryImageResource = primaryResource, + primaryContentDescription = "primary", + primaryImageTintResource = R.color.accent_material_dark, + secondaryImageResource = secondaryResource, + secondaryContentDescription = "secondary", + secondaryImageTintResource = R.color.accent_material_light, + isInPrimaryState = { false }, + ) {} + assertFalse(reloadPageAction.isInPrimaryState.invoke()) + } + + @Test + fun `TwoStateButton redraws when invalidate is triggered`() { + var isInPrimaryState = true + val buttons = listOf( + BrowserMenuItemToolbar.TwoStateButton( + primaryImageResource = R.drawable.abc_ic_go_search_api_material, + primaryContentDescription = "TwoStateEnabled", + secondaryImageResource = R.drawable.abc_ic_clear_material, + secondaryContentDescription = "TwoStateDisabled", + isInPrimaryState = { isInPrimaryState }, + ) {}, + ) + + val menu = mock(BrowserMenu::class.java) + val layout = LinearLayout(testContext) + + val toolbar = BrowserMenuItemToolbar(buttons) + toolbar.bind(menu, layout) + + val child1 = layout.getChildAt(0) + assertEquals("TwoStateEnabled", child1.contentDescription) + + isInPrimaryState = false + toolbar.invalidate(layout) + assertEquals("TwoStateDisabled", child1.contentDescription) + } + + @Test + fun `TwoStateButton doesn't redraw if state hasn't changed`() { + val isInPrimaryState = true + val button = BrowserMenuItemToolbar.TwoStateButton( + primaryImageResource = R.drawable.abc_ic_go_search_api_material, + primaryContentDescription = "TwoStateEnabled", + secondaryImageResource = R.drawable.abc_ic_clear_material, + secondaryContentDescription = "TwoStateDisabled", + isInPrimaryState = { isInPrimaryState }, + disableInSecondaryState = true, + ) {} + + val view = mock(AppCompatImageView::class.java) + + button.bind(view) + verify(view).contentDescription = "TwoStateEnabled" + + reset(view) + + button.invalidate(view) + verify(view, never()).contentDescription = "TwoStateEnabled" + } + + @Test + fun `clicking item view invokes callback and dismisses menu`() { + var callbackInvoked = false + + val button = BrowserMenuItemToolbar.Button( + R.drawable.abc_ic_ab_back_material, + "Test", + ) { + callbackInvoked = true + } + + assertFalse(callbackInvoked) + + val menu = mock(BrowserMenu::class.java) + val layout = LinearLayout(testContext) + + val toolbar = BrowserMenuItemToolbar(listOf(button)) + toolbar.bind(menu, layout) + + assertEquals(1, layout.childCount) + + val view = layout.getChildAt(0) + + assertFalse(callbackInvoked) + verify(menu, never()).dismiss() + + view.performClick() + + assertTrue(callbackInvoked) + verify(menu).dismiss() + } + + @Test + fun `long clicking item view invokes callback and dismisses menu`() { + var callbackInvoked = false + + val button = BrowserMenuItemToolbar.Button( + R.drawable.abc_ic_ab_back_material, + "Test", + longClickListener = { + callbackInvoked = true + }, + ) {} + + assertFalse(callbackInvoked) + + val menu = mock(BrowserMenu::class.java) + val layout = LinearLayout(testContext) + + val toolbar = BrowserMenuItemToolbar(listOf(button)) + toolbar.bind(menu, layout) + + assertEquals(1, layout.childCount) + + val view = layout.getChildAt(0) + + assertFalse(callbackInvoked) + verify(menu, never()).dismiss() + + view.performLongClick() + + assertTrue(callbackInvoked) + verify(menu).dismiss() + } + + @Test + fun `toolbar can be converted to candidate`() { + val listener = {} + + assertEquals( + RowMenuCandidate(emptyList()), + BrowserMenuItemToolbar(emptyList()).asCandidate(testContext), + ) + + var isEnabled = false + var isInPrimaryState = true + val toolbarWithTwoState = BrowserMenuItemToolbar( + listOf( + BrowserMenuItemToolbar.Button( + R.drawable.abc_ic_ab_back_material, + "Button01", + isEnabled = { isEnabled }, + listener = listener, + ), + BrowserMenuItemToolbar.Button( + R.drawable.abc_ic_ab_back_material, + "Button02", + iconTintColorResource = R.color.accent_material_light, + listener = listener, + ), + BrowserMenuItemToolbar.TwoStateButton( + primaryImageResource = R.drawable.abc_ic_go_search_api_material, + primaryContentDescription = "TwoStatePrimary", + secondaryImageResource = R.drawable.abc_ic_clear_material, + secondaryContentDescription = "TwoStateSecondary", + isInPrimaryState = { isInPrimaryState }, + listener = listener, + ), + ), + ) + + assertEquals( + RowMenuCandidate( + listOf( + SmallMenuCandidate( + "Button01", + icon = DrawableMenuIcon(null), + containerStyle = ContainerStyle(isEnabled = false), + onClick = listener, + ), + SmallMenuCandidate( + "Button02", + icon = DrawableMenuIcon( + null, + tint = getColor(testContext, R.color.accent_material_light), + ), + onClick = listener, + ), + SmallMenuCandidate( + "TwoStatePrimary", + icon = DrawableMenuIcon(null), + onClick = listener, + ), + ), + ), + toolbarWithTwoState.asCandidate(testContext).run { + copy( + items = items.map { + it.copy(icon = it.icon.copy(drawable = null)) + }, + ) + }, + ) + + isEnabled = true + isInPrimaryState = false + + assertEquals( + RowMenuCandidate( + listOf( + SmallMenuCandidate( + "Button01", + icon = DrawableMenuIcon(null), + containerStyle = ContainerStyle(isEnabled = true), + onClick = listener, + ), + SmallMenuCandidate( + "Button02", + icon = DrawableMenuIcon( + null, + tint = getColor(testContext, R.color.accent_material_light), + ), + onClick = listener, + ), + SmallMenuCandidate( + "TwoStateSecondary", + icon = DrawableMenuIcon(null), + onClick = listener, + ), + ), + ), + toolbarWithTwoState.asCandidate(testContext).run { + copy( + items = items.map { + it.copy(icon = it.icon.copy(drawable = null)) + }, + ) + }, + ) + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuSwitchTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuSwitchTest.kt new file mode 100644 index 0000000000..9943e11212 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuSwitchTest.kt @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import mozilla.components.browser.menu.R +import mozilla.components.concept.menu.candidate.CompoundMenuCandidate +import mozilla.components.support.test.mock +import org.junit.Assert.assertEquals +import org.junit.Test + +class BrowserMenuSwitchTest { + + @Test + fun `browser switch uses correct layout`() { + val item = BrowserMenuSwitch("Hello") {} + assertEquals(R.layout.mozac_browser_menu_item_switch, item.getLayoutResource()) + } + + @Test + fun `switch can be converted to candidate with correct end type`() { + val listener = { _: Boolean -> } + + assertEquals( + CompoundMenuCandidate( + "Hello", + isChecked = false, + end = CompoundMenuCandidate.ButtonType.SWITCH, + onCheckedChange = listener, + ), + BrowserMenuSwitch( + "Hello", + listener = listener, + ).asCandidate(mock()), + ) + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/ParentBrowserMenuItemTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/ParentBrowserMenuItemTest.kt new file mode 100644 index 0000000000..2335571a25 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/ParentBrowserMenuItemTest.kt @@ -0,0 +1,150 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.view.LayoutInflater +import android.widget.TextView +import androidx.appcompat.widget.AppCompatImageView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.BrowserMenuAdapter +import mozilla.components.browser.menu.R +import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate +import mozilla.components.concept.menu.candidate.DrawableMenuIcon +import mozilla.components.concept.menu.candidate.NestedMenuCandidate +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import mozilla.components.ui.icons.R as iconsR + +@RunWith(AndroidJUnit4::class) +class ParentBrowserMenuItemTest { + + @Test + fun `menu item ImageText should have the right text, image, and iconTintColorResource`() { + val subMenuItem = SimpleBrowserMenuItem("test") + val subMenuAdapter = BrowserMenuAdapter(testContext, listOf(subMenuItem)) + val subMenu = BrowserMenu(subMenuAdapter) + val parentMenuItem = ParentBrowserMenuItem( + label = "label", + imageResource = android.R.drawable.ic_menu_report_image, + iconTintColorResource = android.R.color.black, + textColorResource = android.R.color.black, + subMenu = subMenu, + ) + val view = LayoutInflater.from(testContext).inflate(parentMenuItem.getLayoutResource(), null) + val nestedMenuAdapter = BrowserMenuAdapter(testContext, listOf(parentMenuItem)) + val nestedMenu = BrowserMenu(nestedMenuAdapter) + + parentMenuItem.bind(nestedMenu, view) + val textView = view.findViewById<TextView>(R.id.text) + val imageView = view.findViewById<AppCompatImageView>(R.id.image) + val overflowView = view.findViewById<AppCompatImageView>(R.id.overflowImage) + + assertEquals("label", textView.text) + assertNotNull(imageView.drawable) + assertNotNull(imageView.imageTintList) + assertNotNull(overflowView.imageTintList) + } + + @Test + fun `menu item ImageText with no iconTintColorResource must not have an imageTintList`() { + val subMenuItem = SimpleBrowserMenuItem("test") + val subMenuAdapter = BrowserMenuAdapter(testContext, listOf(subMenuItem)) + val subMenu = BrowserMenu(subMenuAdapter) + val parentMenuItem = ParentBrowserMenuItem( + label = "label", + imageResource = android.R.drawable.ic_menu_report_image, + subMenu = subMenu, + ) + val view = LayoutInflater.from(testContext).inflate(parentMenuItem.getLayoutResource(), null) + val nestedMenuAdapter = BrowserMenuAdapter(testContext, listOf(parentMenuItem)) + val nestedMenu = BrowserMenu(nestedMenuAdapter) + + parentMenuItem.bind(nestedMenu, view) + val imageView = view.findViewById<AppCompatImageView>(R.id.image) + + assertNull(imageView.imageTintList) + } + + @Test + fun `onBackPressed after sub menu is shown will dismiss the sub menu`() { + val backPressMenuItem = BackPressMenuItem( + contentDescription = "Navigate up", + label = "back", + imageResource = iconsR.drawable.mozac_ic_back_24, + ) + val backPressView = LayoutInflater.from(testContext).inflate(backPressMenuItem.getLayoutResource(), null) + val subMenuItem = SimpleBrowserMenuItem("test") + val subMenuAdapter = BrowserMenuAdapter(testContext, listOf(backPressMenuItem, subMenuItem)) + val subMenu = BrowserMenu(subMenuAdapter) + backPressMenuItem.bind(subMenu, backPressView) + + val parentMenuItem = ParentBrowserMenuItem( + label = "label", + imageResource = android.R.drawable.ic_menu_report_image, + subMenu = subMenu, + ) + val view = LayoutInflater.from(testContext).inflate(parentMenuItem.getLayoutResource(), null) + val nestedMenuAdapter = BrowserMenuAdapter(testContext, listOf(parentMenuItem)) + val nestedMenu = BrowserMenu(nestedMenuAdapter) + parentMenuItem.bind(nestedMenu, view) + + nestedMenu.show(view) + view.performClick() + assertTrue(subMenu.isShown) + assertFalse(nestedMenu.isShown) + + backPressView.performClick() + assertFalse(subMenu.isShown) + assertTrue(nestedMenu.isShown) + } + + @Test + fun `menu item image text item can be converted to candidate`() { + val backPressMenuItem = BackPressMenuItem( + contentDescription = "Navigate up", + label = "back", + imageResource = iconsR.drawable.mozac_ic_back_24, + ) + val subMenuItem = SimpleBrowserMenuItem("test") + val subMenuAdapter = BrowserMenuAdapter(testContext, listOf(backPressMenuItem, subMenuItem)) + val subMenu = BrowserMenu(subMenuAdapter) + val menuItem = ParentBrowserMenuItem( + "label", + android.R.drawable.ic_menu_report_image, + subMenu = subMenu, + ) + + val candidate = menuItem.asCandidate(testContext) + + assertEquals(menuItem.hashCode(), candidate.id) + assertEquals("label", candidate.text) + assertEquals(2, candidate.subMenuItems!!.size) + + val backCandidate = candidate.subMenuItems!![0] as NestedMenuCandidate + val testCandidate = candidate.subMenuItems!![1] as DecorativeTextMenuCandidate + assertEquals( + NestedMenuCandidate( + id = backPressMenuItem.hashCode(), + text = "back", + start = DrawableMenuIcon(null), + subMenuItems = null, + ), + backCandidate.run { + copy(start = (start as? DrawableMenuIcon)?.copy(drawable = null)) + }, + ) + assertEquals( + DecorativeTextMenuCandidate("test"), + testCandidate, + ) + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/SimpleBrowserMenuHighlightableItemTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/SimpleBrowserMenuHighlightableItemTest.kt new file mode 100644 index 0000000000..7a2fe33d20 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/SimpleBrowserMenuHighlightableItemTest.kt @@ -0,0 +1,152 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.view.LayoutInflater +import android.view.View +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.R +import mozilla.components.concept.menu.candidate.ContainerStyle +import mozilla.components.concept.menu.candidate.TextMenuCandidate +import mozilla.components.concept.menu.candidate.TextStyle +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class SimpleBrowserMenuHighlightableItemTest { + + @Test + fun `GIVEN a simple item, WHEN we try to inflate it in the menu, THEN the item should be inflated`() { + var onClickWasPress = false + val item = SimpleBrowserMenuHighlightableItem( + "label", + textColorResource = android.R.color.black, + backgroundTint = Color.RED, + ) { + onClickWasPress = true + } + + val view = inflate(item) + view.performClick() + + assertTrue(onClickWasPress) + } + + @Test + fun `GIVEN a simple item, WHEN we inflate it, THEN it should be visible by default`() { + val listener = {} + val item = SimpleBrowserMenuHighlightableItem( + label = "label", + textColorResource = android.R.color.black, + backgroundTint = Color.RED, + listener = listener, + ) + + assertTrue(item.visible()) + } + + @Test + fun `GIVEN a simple item, WHEN clicking bound view, THEN callback is invoked and the menu dismissed`() { + var callbackInvoked = false + + val item = SimpleBrowserMenuHighlightableItem( + label = "label", + textColorResource = android.R.color.black, + backgroundTint = Color.RED, + ) { + callbackInvoked = true + } + + val menu = mock(BrowserMenu::class.java) + val view = TextView(testContext) + + item.bind(menu, view) + + view.performClick() + + assertTrue(callbackInvoked) + verify(menu).dismiss() + } + + @Test + fun `GIVEN a simple item, WHEN we inflate it, THEN it should have the right properties`() { + val listener = {} + val item = SimpleBrowserMenuHighlightableItem( + label = "label", + textSize = 10f, + textColorResource = android.R.color.black, + backgroundTint = Color.RED, + isHighlighted = { false }, + listener = listener, + ) + + var view = inflate(item) + var textView = view.findViewById<TextView>(R.id.simple_text) + + assertEquals(textView.text, "label") + assertEquals(textView.textSize, 10f) + assertEquals(textView.currentTextColor, testContext.getColor(android.R.color.black)) + + val highlightedItem = SimpleBrowserMenuHighlightableItem( + label = "label", + textSize = 10f, + textColorResource = android.R.color.black, + backgroundTint = Color.RED, + isHighlighted = { true }, + listener = listener, + ) + + view = inflate(highlightedItem) + textView = view.findViewById(R.id.simple_text) + + assertEquals(textView.text, "label") + assertEquals(textView.textSize, 10f) + assertEquals(textView.currentTextColor, testContext.getColor(android.R.color.black)) + assertEquals((textView.background as ColorDrawable).color, Color.RED) + } + + @Test + fun `GIVEN a simple item, WHEN it converts to candidate, THEN it should have the correct properties`() { + val listener = {} + val shouldHighlight = false + val item = SimpleBrowserMenuHighlightableItem( + label = "label", + textColorResource = android.R.color.black, + backgroundTint = Color.RED, + isHighlighted = { shouldHighlight }, + listener = listener, + ) + + assertEquals( + TextMenuCandidate( + "label", + textStyle = TextStyle( + color = ContextCompat.getColor(testContext, android.R.color.black), + ), + containerStyle = ContainerStyle(true), + onClick = listener, + ), + item.asCandidate(testContext), + ) + } + + private fun inflate(item: SimpleBrowserMenuHighlightableItem): View { + val view = LayoutInflater.from(testContext).inflate(item.getLayoutResource(), null) + val mockMenu = Mockito.mock(BrowserMenu::class.java) + item.bind(mockMenu, view) + return view + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/SimpleBrowserMenuItemTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/SimpleBrowserMenuItemTest.kt new file mode 100644 index 0000000000..bd5da09b52 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/SimpleBrowserMenuItemTest.kt @@ -0,0 +1,122 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.view.LayoutInflater +import android.view.View +import android.widget.TextView +import androidx.core.content.ContextCompat.getColor +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.R +import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate +import mozilla.components.concept.menu.candidate.TextMenuCandidate +import mozilla.components.concept.menu.candidate.TextStyle +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class SimpleBrowserMenuItemTest { + + @Test + fun `simple menu items are always visible by default`() { + val item = SimpleBrowserMenuItem("Hello") { + // do nothing + } + + assertTrue(item.visible()) + } + + @Test + fun `layout resource can be inflated`() { + val item = SimpleBrowserMenuItem("Hello") { + // do nothing + } + + val view = LayoutInflater.from(testContext) + .inflate(item.getLayoutResource(), null) + + assertNotNull(view) + } + + @Test + fun `clicking bound view will invoke callback and dismiss menu`() { + var callbackInvoked = false + + val item = SimpleBrowserMenuItem("Hello") { + callbackInvoked = true + } + + val menu = mock(BrowserMenu::class.java) + val view = TextView(testContext) + + item.bind(menu, view) + + view.performClick() + + assertTrue(callbackInvoked) + verify(menu).dismiss() + } + + @Test + fun `simple browser menu item should have the right text, textSize, and textColorResource`() { + val item = SimpleBrowserMenuItem( + "Powered by Mozilla", + 10f, + android.R.color.holo_green_dark, + ) + + val view = inflate(item) + + val textView = view.findViewById<TextView>(R.id.simple_text) + assertEquals(textView.text, "Powered by Mozilla") + assertEquals(textView.textSize, 10f) + assertEquals(textView.currentTextColor, testContext.getColor(android.R.color.holo_green_dark)) + } + + @Test + fun `simple menu item can be converted to candidate`() { + val listener = {} + + assertEquals( + TextMenuCandidate( + "Hello", + onClick = listener, + ), + SimpleBrowserMenuItem( + "Hello", + listener = listener, + ).asCandidate(testContext), + ) + + assertEquals( + DecorativeTextMenuCandidate( + "Powered by Mozilla", + textStyle = TextStyle( + size = 10f, + color = getColor(testContext, android.R.color.holo_green_dark), + ), + ), + SimpleBrowserMenuItem( + "Powered by Mozilla", + 10f, + android.R.color.holo_green_dark, + ).asCandidate(testContext), + ) + } + + private fun inflate(item: SimpleBrowserMenuItem): View { + val view = LayoutInflater.from(testContext).inflate(item.getLayoutResource(), null) + val mockMenu = mock(BrowserMenu::class.java) + item.bind(mockMenu, view) + return view + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/TwoStateBrowserMenuImageTextTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/TwoStateBrowserMenuImageTextTest.kt new file mode 100644 index 0000000000..c8030091cc --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/TwoStateBrowserMenuImageTextTest.kt @@ -0,0 +1,87 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.widget.TextView +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.R +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock + +@RunWith(AndroidJUnit4::class) +class TwoStateBrowserMenuImageTextTest { + + private val context: Context get() = ApplicationProvider.getApplicationContext() + private lateinit var menuItemPrimary: TwoStateBrowserMenuImageText + private lateinit var menuItemSecondary: TwoStateBrowserMenuImageText + + private var primaryPressed = false + private var secondaryPressed = false + + private val primaryLabel: String = "primaryLabel" + private val secondaryLabel: String = "secondaryLabel" + + @Before + fun setup() { + menuItemPrimary = TwoStateBrowserMenuImageText( + primaryLabel = primaryLabel, + secondaryLabel = secondaryLabel, + primaryStateIconResource = android.R.drawable.ic_delete, + secondaryStateIconResource = android.R.drawable.ic_input_add, + isInPrimaryState = { true }, + primaryStateAction = { primaryPressed = true }, + ) + + menuItemSecondary = TwoStateBrowserMenuImageText( + primaryLabel = primaryLabel, + secondaryLabel = secondaryLabel, + primaryStateIconResource = android.R.drawable.ic_delete, + secondaryStateIconResource = android.R.drawable.ic_input_add, + isInPrimaryState = { false }, + isInSecondaryState = { true }, + secondaryStateAction = { secondaryPressed = true }, + ) + } + + @Test + fun `browser menu should be inflated`() { + val view = inflate(menuItemPrimary) + view.performClick() + assertTrue(primaryPressed) + + val secondView = inflate(menuItemSecondary) + secondView.performClick() + assertTrue(secondaryPressed) + } + + @Test + fun `browser menu should have the right text`() { + val view = inflate(menuItemPrimary) + val textView = view.findViewById<TextView>(R.id.text) + + assertEquals(textView.text, primaryLabel) + + val secondView = inflate(menuItemSecondary) + val secondTextView = secondView.findViewById<TextView>(R.id.text) + + assertEquals(secondTextView.text, secondaryLabel) + } + + private fun inflate(item: BrowserMenuImageText): View { + val view = LayoutInflater.from(context).inflate(item.getLayoutResource(), null) + val mockMenu = mock(BrowserMenu::class.java) + item.bind(mockMenu, view) + return view + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/WebExtensionBrowserMenuItemTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/WebExtensionBrowserMenuItemTest.kt new file mode 100644 index 0000000000..60419f4bfb --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/WebExtensionBrowserMenuItemTest.kt @@ -0,0 +1,355 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.item + +import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.view.LayoutInflater +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.test.runTest +import mozilla.components.browser.menu.R +import mozilla.components.browser.menu.WebExtensionBrowserMenu +import mozilla.components.concept.engine.webextension.Action +import mozilla.components.support.test.argumentCaptor +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import mozilla.components.support.test.rule.MainCoroutineRule +import mozilla.components.support.test.whenever +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.notNull +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import androidx.appcompat.R as appcompatR + +@RunWith(AndroidJUnit4::class) +class WebExtensionBrowserMenuItemTest { + + @get:Rule + val coroutinesTestRule = MainCoroutineRule() + private val dispatcher = coroutinesTestRule.testDispatcher + + @Test + fun `web extension menu item is visible by default`() { + val webExtMenuItem = WebExtensionBrowserMenuItem(mock(), mock()) + + assertTrue(webExtMenuItem.visible()) + } + + @Test + fun `layout resource can be inflated`() { + val webExtMenuItem = WebExtensionBrowserMenuItem(mock(), mock()) + + val view = LayoutInflater.from(testContext) + .inflate(webExtMenuItem.getLayoutResource(), null) + + assertNotNull(view) + } + + @Test + fun `view is disabled if browser action is disabled`() { + val icon: Bitmap = mock() + val imageView: ImageView = mock() + val badgeView: TextView = mock() + val labelView: TextView = mock() + val container = View(testContext) + val view: View = mock() + + whenever(view.findViewById<ImageView>(R.id.action_image)).thenReturn(imageView) + whenever(view.findViewById<TextView>(R.id.badge_text)).thenReturn(badgeView) + whenever(view.findViewById<TextView>(R.id.action_label)).thenReturn(labelView) + whenever(view.findViewById<View>(R.id.container)).thenReturn(container) + whenever(view.context).thenReturn(mock()) + + val browserAction = Action( + title = "title", + loadIcon = { icon }, + enabled = false, + badgeText = "badgeText", + badgeTextColor = Color.WHITE, + badgeBackgroundColor = Color.BLUE, + ) {} + + val action = WebExtensionBrowserMenuItem(browserAction, {}) + action.bind(mock(), view) + dispatcher.scheduler.advanceUntilIdle() + + assertFalse(view.isEnabled) + } + + @Test + fun bind() { + val icon: Bitmap = mock() + val imageView: ImageView = mock() + val badgeView: TextView = mock() + val background: Drawable = mock() + val labelView: TextView = mock() + val container = View(testContext) + val view: View = mock() + + whenever(badgeView.background).thenReturn(background) + whenever(view.findViewById<ImageView>(R.id.action_image)).thenReturn(imageView) + whenever(view.findViewById<TextView>(R.id.badge_text)).thenReturn(badgeView) + whenever(view.findViewById<TextView>(R.id.action_label)).thenReturn(labelView) + whenever(view.findViewById<View>(R.id.container)).thenReturn(container) + whenever(view.context).thenReturn(testContext) + + val browserAction = Action( + title = "title", + loadIcon = { icon }, + enabled = true, + badgeText = "badgeText", + badgeTextColor = Color.WHITE, + badgeBackgroundColor = Color.BLUE, + ) {} + + val action = WebExtensionBrowserMenuItem(browserAction, {}) + action.bind(mock(), view) + dispatcher.scheduler.advanceUntilIdle() + + val iconCaptor = argumentCaptor<BitmapDrawable>() + verify(imageView).setImageDrawable(iconCaptor.capture()) + assertEquals(icon, iconCaptor.value.bitmap) + + verify(imageView).contentDescription = "title" + verify(labelView).text = "title" + verify(badgeView).text = "badgeText" + verify(badgeView).setTextColor(Color.WHITE) + verify(background).setTint(Color.BLUE) + } + + @Test + fun `badge text view is invisible if action badge text is empty`() { + val icon: Bitmap = mock() + val imageView: ImageView = mock() + val badgeView: TextView = spy(TextView(testContext)) + val labelView: TextView = mock() + val container = View(testContext) + val view: View = mock() + + whenever(view.findViewById<ImageView>(R.id.action_image)).thenReturn(imageView) + whenever(view.findViewById<TextView>(R.id.badge_text)).thenReturn(badgeView) + whenever(view.findViewById<TextView>(R.id.action_label)).thenReturn(labelView) + whenever(view.findViewById<View>(R.id.container)).thenReturn(container) + whenever(view.context).thenReturn(testContext) + + val badgeText = "" + val browserAction = Action( + title = "title", + loadIcon = { icon }, + enabled = true, + badgeText = badgeText, + badgeTextColor = Color.WHITE, + badgeBackgroundColor = Color.BLUE, + ) {} + + val action = WebExtensionBrowserMenuItem(browserAction, {}) + action.bind(mock(), view) + dispatcher.scheduler.advanceUntilIdle() + + verify(badgeView).setBadgeText(badgeText) + assertEquals(View.INVISIBLE, badgeView.visibility) + } + + @Test + fun fallbackToDefaultIcon() { + val imageView: ImageView = mock() + val badgeView: TextView = mock() + val labelView: TextView = mock() + val container = View(testContext) + val view: View = mock() + + whenever(view.findViewById<ImageView>(R.id.action_image)).thenReturn(imageView) + whenever(view.findViewById<TextView>(R.id.badge_text)).thenReturn(badgeView) + whenever(view.findViewById<TextView>(R.id.action_label)).thenReturn(labelView) + whenever(view.findViewById<View>(R.id.container)).thenReturn(container) + whenever(view.context).thenReturn(testContext) + + val browserAction = Action( + title = "title", + loadIcon = { throw IllegalArgumentException() }, + enabled = true, + badgeText = "badgeText", + badgeTextColor = Color.WHITE, + badgeBackgroundColor = Color.BLUE, + ) {} + + val action = WebExtensionBrowserMenuItem(browserAction, {}) + action.bind(mock(), view) + dispatcher.scheduler.advanceUntilIdle() + + verify(imageView).setImageDrawable(notNull()) + } + + @Test + fun `clicking item view invokes callback and dismisses menu`() { + var callbackInvoked = false + + val icon: Bitmap = mock() + val imageView: ImageView = mock() + val badgeView: TextView = mock() + val labelView = TextView(testContext) + val container = View(testContext) + val view: View = mock() + + whenever(view.findViewById<ImageView>(R.id.action_image)).thenReturn(imageView) + whenever(view.findViewById<TextView>(R.id.badge_text)).thenReturn(badgeView) + whenever(view.findViewById<TextView>(R.id.action_label)).thenReturn(labelView) + whenever(view.findViewById<View>(R.id.container)).thenReturn(container) + whenever(view.context).thenReturn(mock()) + + val browserAction = Action( + title = "title", + loadIcon = { icon }, + enabled = true, + badgeText = "badgeText", + badgeTextColor = Color.WHITE, + badgeBackgroundColor = Color.BLUE, + ) {} + + val item = WebExtensionBrowserMenuItem(browserAction, { callbackInvoked = true }) + + val menu: WebExtensionBrowserMenu = mock() + + item.bind(menu, view) + dispatcher.scheduler.advanceUntilIdle() + + container.performClick() + + assertTrue(callbackInvoked) + verify(menu).dismiss() + } + + @Test + fun `labelView and badgeView redraws when invalidate is triggered`() { + val icon: Bitmap = mock() + val imageView: ImageView = mock() + val badgeView: TextView = mock() + val labelView: TextView = mock() + val container = View(testContext) + val view: View = mock() + + whenever(view.findViewById<ImageView>(R.id.action_image)).thenReturn(imageView) + whenever(view.findViewById<TextView>(R.id.badge_text)).thenReturn(badgeView) + whenever(view.findViewById<TextView>(R.id.action_label)).thenReturn(labelView) + whenever(view.findViewById<View>(R.id.container)).thenReturn(container) + whenever(view.context).thenReturn(mock()) + + val browserAction = Action( + title = "title", + loadIcon = { icon }, + enabled = true, + badgeText = "badgeText", + badgeTextColor = Color.WHITE, + badgeBackgroundColor = Color.BLUE, + ) {} + + val item = WebExtensionBrowserMenuItem(browserAction, {}) + + val menu: WebExtensionBrowserMenu = mock() + + item.bind(menu, view) + dispatcher.scheduler.advanceUntilIdle() + + verify(labelView).text = "title" + verify(badgeView).text = "badgeText" + + val browserActionOverride = Action( + title = "override", + loadIcon = { icon }, + enabled = true, + badgeText = "overrideBadge", + badgeTextColor = Color.WHITE, + badgeBackgroundColor = Color.BLUE, + ) {} + + item.action = browserActionOverride + item.invalidate(view) + + verify(labelView).text = "override" + verify(badgeView).text = "overrideBadge" + verify(labelView).invalidate() + verify(badgeView).invalidate() + } + + @Test + fun `GIVEN setIcon was called, WHEN bind is called, icon setup uses the tint set`() = runTest { + val webExtMenuItem = spy(WebExtensionBrowserMenuItem(mock(), mock())) + val testIconTintColorResource = appcompatR.color.accent_material_dark + val menu: WebExtensionBrowserMenu = mock() + val imageView: ImageView = mock() + val badgeView: TextView = mock() + val labelView: TextView = mock() + val container = View(testContext) + val view: View = mock() + + whenever(view.findViewById<ImageView>(R.id.action_image)).thenReturn(imageView) + whenever(view.findViewById<TextView>(R.id.badge_text)).thenReturn(badgeView) + whenever(view.findViewById<TextView>(R.id.action_label)).thenReturn(labelView) + whenever(view.findViewById<View>(R.id.container)).thenReturn(container) + whenever(view.context).thenReturn(mock()) + whenever(imageView.measuredHeight).thenReturn(2) + + webExtMenuItem.setIconTint(testIconTintColorResource) + webExtMenuItem.bind(menu, view) + + val viewCaptor = argumentCaptor<View>() + val imageViewCaptor = argumentCaptor<ImageView>() + val tintCaptor = argumentCaptor<Int>() + + verify(webExtMenuItem).setupIcon(viewCaptor.capture(), imageViewCaptor.capture(), tintCaptor.capture()) + + assertEquals(testIconTintColorResource, tintCaptor.value) + assertEquals(view, viewCaptor.value) + assertEquals(imageView, imageViewCaptor.value) + + assertEquals(testIconTintColorResource, webExtMenuItem.iconTintColorResource) + } + + @Test + fun `WHEN invalidate is called THEN setupIcon is called`() = runTest { + val webExtMenuItem = spy(WebExtensionBrowserMenuItem(mock(), mock())) + val imageView: ImageView = mock() + val badgeView: TextView = mock() + val labelView: TextView = mock() + val container = View(testContext) + val testIconTintColorResource = appcompatR.color.accent_material_dark + val view: View = mock() + + whenever(view.findViewById<ImageView>(R.id.action_image)).thenReturn(imageView) + whenever(view.findViewById<TextView>(R.id.badge_text)).thenReturn(badgeView) + whenever(view.findViewById<TextView>(R.id.action_label)).thenReturn(labelView) + whenever(view.findViewById<View>(R.id.container)).thenReturn(container) + whenever(view.context).thenReturn(mock()) + whenever(imageView.measuredHeight).thenReturn(2) + + webExtMenuItem.setIconTint(testIconTintColorResource) + webExtMenuItem.invalidate(view) + + val viewCaptor = argumentCaptor<View>() + val imageViewCaptor = argumentCaptor<ImageView>() + val tintCaptor = argumentCaptor<Int>() + + verify(webExtMenuItem).setupIcon( + viewCaptor.capture(), + imageViewCaptor.capture(), + tintCaptor.capture(), + ) + + assertEquals(view, viewCaptor.value) + assertEquals(imageView, imageViewCaptor.value) + assertEquals(testIconTintColorResource, webExtMenuItem.iconTintColorResource) + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/DynamicWidthRecyclerViewTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/DynamicWidthRecyclerViewTest.kt new file mode 100644 index 0000000000..48d8cf9ee3 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/DynamicWidthRecyclerViewTest.kt @@ -0,0 +1,226 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.view + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.menu.R +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import org.robolectric.Robolectric +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +class DynamicWidthRecyclerViewTest { + + @Test + fun `minWidth and maxWidth should be initialized from xml attributes`() { + val dynamicRecyclerView = buildRecyclerView(100) + dynamicRecyclerView.minWidth = 123 + dynamicRecyclerView.maxWidth = 456 + + assertEquals(123, dynamicRecyclerView.minWidth) + assertEquals(456, dynamicRecyclerView.maxWidth) + } + + @Test + fun `If minWidth and maxWidth are not provided view should use layout_width`() { + val dynamicRecyclerView = buildRecyclerView(100) + + dynamicRecyclerView.measure(100, 100) + + // View should not try calculate/reconcile new dimensions, it should just call super.onMeasure(..) + verify(dynamicRecyclerView).callParentOnMeasure(100, 100) + verify(dynamicRecyclerView, never()).setReconciledDimensions(anyInt(), anyInt()) + } + + @Test + fun `If only minWidth is provided view should use layout_width`() { + val dynamicRecyclerView = buildRecyclerView(100) + dynamicRecyclerView.minWidth = 50 + + dynamicRecyclerView.measure(100, 100) + + // View should not try calculate/reconcile new dimensions, it should just call super.onMeasure(..) + verify(dynamicRecyclerView).callParentOnMeasure(100, 100) + verify(dynamicRecyclerView, never()).setReconciledDimensions(anyInt(), anyInt()) + } + + @Test + fun `If only maxWidth is provided view should use layout_width`() { + val dynamicRecyclerView = buildRecyclerView(100) + dynamicRecyclerView.maxWidth = 300 + + dynamicRecyclerView.measure(100, 100) + + // View should not try calculate/reconcile new dimensions, it should just call super.onMeasure(..) + verify(dynamicRecyclerView).callParentOnMeasure(100, 100) + verify(dynamicRecyclerView, never()).setReconciledDimensions(anyInt(), anyInt()) + } + + @Test + fun `Should only allow for dynamic width if minWidth has a positive value`() { + val dynamicRecyclerView = buildRecyclerView(100) + dynamicRecyclerView.minWidth = -1 + dynamicRecyclerView.maxWidth = 100 + + dynamicRecyclerView.measure(1, 2) + + // If minWidth has a negative value we should only just call super.onMeasure(..) + verify(dynamicRecyclerView).callParentOnMeasure(1, 2) + verify(dynamicRecyclerView, never()).setReconciledDimensions(anyInt(), anyInt()) + } + + @Test + fun `Should only allow for dynamic width if minWidth is smaller than maxWidth`() { + val dynamicRecyclerView = buildRecyclerView(100) + dynamicRecyclerView.minWidth = 100 + dynamicRecyclerView.maxWidth = 100 + + dynamicRecyclerView.measure(1, 2) + + // If minWidth is >= to maxWidth we should only just call super.onMeasure(..) + verify(dynamicRecyclerView).callParentOnMeasure(1, 2) + verify(dynamicRecyclerView, never()).setReconciledDimensions(anyInt(), anyInt()) + } + + @Test + fun `To allow for dynamic width children can expand entirely between minWidth and maxWidth`() { + val dynamicRecyclerView = buildRecyclerView(100) + dynamicRecyclerView.minWidth = 50 + dynamicRecyclerView.maxWidth = 100 + + dynamicRecyclerView.measure(10, 10) + + // To allow for children to be as wide as they want widthSpec should be 0 + verify(dynamicRecyclerView).callParentOnMeasure(0, 10) + // Robolectric doesn't do any kind of measuring and always returns 0 for View measurements. + verify(dynamicRecyclerView).setReconciledDimensions(0, 0) + } + + @Test + @Config(qualifiers = "w333dp") + fun `getScreenWidth() should return display's width in pixels`() { + val dynamicRecyclerView = DynamicWidthRecyclerView(testContext, null) + + assertEquals(333, dynamicRecyclerView.getScreenWidth()) + } + + @Test + fun `setReconciledDimensions() must set material minimum width even if childs are smaller`() { + val childrenWidth = 20 + val materialMinWidth = testContext.resources.getDimensionPixelSize(R.dimen.mozac_browser_menu_material_min_item_width) + // Layout width is *2 to allow bigger sizes. minWidth is /2 to verify the material min width is used. + val dynamicRecyclerView = buildRecyclerView(materialMinWidth * 2) + dynamicRecyclerView.minWidth = materialMinWidth / 2 + dynamicRecyclerView.maxWidth = 500 + + dynamicRecyclerView.setReconciledDimensions(childrenWidth, 500) + + verify(dynamicRecyclerView).callSetMeasuredDimension(materialMinWidth, 500) + } + + @Test + fun `setReconciledDimensions() must set minWidth even if children width is smaller`() { + val childrenWidth = 20 + // minWidth set in xml. Ensure it is bigger than the default. + val minWidth = testContext.resources.getDimensionPixelSize(R.dimen.mozac_browser_menu_material_min_item_width) + 10 + val dynamicRecyclerView = buildRecyclerView(minWidth * 2) + dynamicRecyclerView.minWidth = minWidth + dynamicRecyclerView.maxWidth = 500 + + dynamicRecyclerView.setReconciledDimensions(childrenWidth, 500) + + verify(dynamicRecyclerView).callSetMeasuredDimension(minWidth, 500) + } + + @Test + fun `setReconciledDimensions() will set children width if it is bigger than minWidth and smaller than maxWidth`() { + val materialMinWidth = testContext.resources.getDimensionPixelSize(R.dimen.mozac_browser_menu_material_min_item_width) + val childrenWidth = materialMinWidth + 10 + // Layout width is *2 to allow bigger sizes. minWidth is /2 to verify the material min width is used. + val dynamicRecyclerView = buildRecyclerView(materialMinWidth * 2) + dynamicRecyclerView.minWidth = materialMinWidth + dynamicRecyclerView.maxWidth = 500 + + dynamicRecyclerView.setReconciledDimensions(childrenWidth, 500) + + verify(dynamicRecyclerView).callSetMeasuredDimension(childrenWidth, 500) + } + + @Test + @Config(qualifiers = "w500dp") + @Suppress("UnnecessaryVariable") + fun `setReconciledDimensions() must set maxWidth when children width is bigger`() { + val materialMaxWidth = 500 - testContext.resources.getDimensionPixelSize(R.dimen.mozac_browser_menu_material_min_tap_area) + val childrenWidth = materialMaxWidth + val maxWidth = materialMaxWidth - 10 + val dynamicRecyclerView = buildRecyclerView(1000) + dynamicRecyclerView.minWidth = 100 + dynamicRecyclerView.maxWidth = maxWidth + + dynamicRecyclerView.setReconciledDimensions(childrenWidth, 100) + + verify(dynamicRecyclerView).callSetMeasuredDimension(maxWidth, 100) + } + + @Test + @Config(qualifiers = "w500dp") + fun `setReconciledDimensions must set material maximum width when children width is bigger`() { + val materialMaxWidth = 500 - testContext.resources.getDimensionPixelSize(R.dimen.mozac_browser_menu_material_min_tap_area) + val maxWidth = 500 + val childrenWidth = maxWidth + 10 + val dynamicRecyclerView = buildRecyclerView(1000) + dynamicRecyclerView.minWidth = 100 + dynamicRecyclerView.maxWidth = maxWidth + + dynamicRecyclerView.setReconciledDimensions(childrenWidth, 100) + + verify(dynamicRecyclerView).callSetMeasuredDimension(materialMaxWidth, 100) + } + + @Test + fun `maxWidthOfAllChildren can only be initialized once with a positive value`() { + val dynamicRecyclerView = DynamicWidthRecyclerView(testContext) + + assertEquals(0, dynamicRecyclerView.maxWidthOfAllChildren) + + dynamicRecyclerView.maxWidthOfAllChildren = 42 + assertEquals(42, dynamicRecyclerView.maxWidthOfAllChildren) + + dynamicRecyclerView.maxWidthOfAllChildren = 24 + assertEquals(42, dynamicRecyclerView.maxWidthOfAllChildren) + } + + @Test + fun `onMeasure will call setReconciledDimensions with maxWidthOfAllChildren`() { + val dynamicRecyclerView = spy(DynamicWidthRecyclerView(testContext)) + doReturn(100).`when`(dynamicRecyclerView).measuredHeight + doReturn(100).`when`(dynamicRecyclerView).measuredWidth + doReturn(100).`when`(dynamicRecyclerView).height + dynamicRecyclerView.maxWidthOfAllChildren = 42 + dynamicRecyclerView.minWidth = 10 + dynamicRecyclerView.maxWidth = Int.MAX_VALUE + + dynamicRecyclerView.measure(0, 0) + + verify(dynamicRecyclerView).setReconciledDimensions(42, 100) + } + + private fun buildRecyclerView(layoutWidth: Int): DynamicWidthRecyclerView { + val customAttributeSet = Robolectric.buildAttributeSet().apply { + // android.R.attr.layout_width needs to always be set + addAttribute(android.R.attr.layout_width, "${layoutWidth}dp") + }.build() + + return spy(DynamicWidthRecyclerView(testContext, customAttributeSet)) + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/ExpandableLayoutTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/ExpandableLayoutTest.kt new file mode 100644 index 0000000000..4a5dfa1bd1 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/ExpandableLayoutTest.kt @@ -0,0 +1,822 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.view + +import android.animation.ValueAnimator +import android.graphics.Rect +import android.view.MotionEvent +import android.view.MotionEvent.ACTION_CANCEL +import android.view.MotionEvent.ACTION_DOWN +import android.view.MotionEvent.ACTION_MOVE +import android.view.MotionEvent.ACTION_UP +import android.view.View +import android.view.ViewConfiguration +import android.view.ViewGroup +import android.view.animation.AccelerateDecelerateInterpolator +import android.widget.FrameLayout +import androidx.core.view.marginBottom +import androidx.core.view.marginLeft +import androidx.core.view.marginRight +import androidx.core.view.marginTop +import androidx.core.view.updateLayoutParams +import androidx.recyclerview.widget.RecyclerView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.test.any +import mozilla.components.support.test.eq +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertSame +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class ExpandableLayoutTest { + @Test + fun `GIVEN ExpandableLayout WHEN wrapContentInExpandableView is called THEN it should properly setup a new ExpandableLayout`() { + val wrappedView = FrameLayout(testContext) + val blankTouchListener: (() -> Unit) = mock() + wrappedView.layoutParams = ViewGroup.MarginLayoutParams(11, 12).apply { + setMargins(13, 14, 15, 16) + } + + val result = ExpandableLayout.wrapContentInExpandableView( + wrappedView, + 42, + 33, + blankTouchListener, + ) + + assertEquals(FrameLayout.LayoutParams.WRAP_CONTENT, result.wrappedView.layoutParams.height) + assertEquals(FrameLayout.LayoutParams.WRAP_CONTENT, result.wrappedView.layoutParams.width) + assertEquals(13, result.wrappedView.marginLeft) + assertEquals(14, result.wrappedView.marginTop) + assertEquals(15, result.wrappedView.marginRight) + assertEquals(16, result.wrappedView.marginBottom) + assertEquals(1, result.childCount) + assertSame(wrappedView, result.wrappedView) + assertSame(blankTouchListener, result.blankTouchListener) + + // Also test the default configuration of a newly built ExpandableLayout. + assertEquals(42, result.lastVisibleItemIndexWhenCollapsed) + assertEquals(33, result.stickyItemIndex) + assertEquals(ExpandableLayout.NOT_CALCULATED_DEFAULT_HEIGHT, result.collapsedHeight) + assertEquals(ExpandableLayout.NOT_CALCULATED_DEFAULT_HEIGHT, result.expandedHeight) + assertEquals(ExpandableLayout.NOT_CALCULATED_DEFAULT_HEIGHT, result.parentHeight) + assertTrue(result.isCollapsed) + assertFalse(result.isExpandInProgress) + assertEquals(ViewConfiguration.get(testContext).scaledTouchSlop.toFloat(), result.touchSlop) + assertEquals(ExpandableLayout.NOT_CALCULATED_Y_TOUCH_COORD, result.initialYCoord) + } + + @Test + fun `GIVEN ExpandableLayout WHEN onMeasure is called THEN it delegates the parent for measuring`() { + val expandableLayout = spy( + ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + ) { }, + ) + expandableLayout.isCollapsed = false + + expandableLayout.measure(123, 123) + + verify(expandableLayout).callParentOnMeasure(123, 123) + } + + @Test + fun `GIVEN ExpandableLayout in the collapsed state and the height values available WHEN onMeasure is called THEN it will trigger collapse()`() { + val expandableLayout = spy( + ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + ) { }, + ) + expandableLayout.isCollapsed = true + doReturn(100).`when`(expandableLayout).getOrCalculateCollapsedHeight() + doReturn(100).`when`(expandableLayout).getOrCalculateExpandedHeight(anyInt()) + + expandableLayout.measure(123, 123) + + verify(expandableLayout).collapse() + } + + @Test + fun `GIVEN ExpandableLayout not in the collapsed state but all height values known WHEN onMeasure is called THEN collapse() is not called`() { + val expandableLayout = spy( + ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + ) { }, + ) + expandableLayout.isCollapsed = false + doReturn(100).`when`(expandableLayout).getOrCalculateCollapsedHeight() + doReturn(100).`when`(expandableLayout).getOrCalculateExpandedHeight(anyInt()) + + expandableLayout.measure(123, 123) + + verify(expandableLayout, never()).collapse() + } + + @Test + fun `GIVEN ExpandableLayout in the collapsed state but with collapsedHeight unknown WHEN onMeasure is called THEN collapse() is be called`() { + val expandableLayout = spy( + ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + ) { }, + ) + expandableLayout.isCollapsed = true + doReturn(ExpandableLayout.NOT_CALCULATED_DEFAULT_HEIGHT).`when`(expandableLayout).getOrCalculateCollapsedHeight() + doReturn(100).`when`(expandableLayout).getOrCalculateExpandedHeight(anyInt()) + + expandableLayout.measure(123, 123) + + verify(expandableLayout, never()).collapse() + } + + @Test + fun `GIVEN ExpandableLayout in the collapsed state but with expandedHeight unknown WHEN onMeasure is called THEN collapse() is not be called`() { + val expandableLayout = spy( + ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + ) { }, + ) + expandableLayout.isCollapsed = true + doReturn(100).`when`(expandableLayout).getOrCalculateCollapsedHeight() + doReturn(ExpandableLayout.NOT_CALCULATED_DEFAULT_HEIGHT).`when`(expandableLayout).getOrCalculateExpandedHeight(anyInt()) + + expandableLayout.measure(123, 123) + + verify(expandableLayout, never()).collapse() + } + + @Test + fun `GIVEN an expanded menu WHEN onInterceptTouchEvent is called for a touch on the menu THEN super is called`() { + val blankTouchListener = spy {} + val expandableLayout = spy( + ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + blankTouchListener = blankTouchListener, + ), + ) + val event: MotionEvent = mock() + doReturn(false).`when`(expandableLayout).shouldInterceptTouches() + doReturn(true).`when`(expandableLayout).isTouchingTheWrappedView(any()) + + expandableLayout.onInterceptTouchEvent(event) + + verify(blankTouchListener, never()).invoke() + verify(expandableLayout).callParentOnInterceptTouchEvent(event) + } + + @Test + fun `GIVEN a menu currently expanding WHEN onInterceptTouchEvent is called for a touch on the menu THEN the touch is swallowed`() { + val blankTouchListener = spy {} + val expandableLayout = spy( + ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + blankTouchListener = blankTouchListener, + ), + ) + val event: MotionEvent = mock() + doReturn(false).`when`(expandableLayout).shouldInterceptTouches() + doReturn(true).`when`(expandableLayout).isTouchingTheWrappedView(any()) + expandableLayout.isExpandInProgress = true + + expandableLayout.onInterceptTouchEvent(event) + + verify(blankTouchListener, never()).invoke() + verify(expandableLayout, never()).callParentOnInterceptTouchEvent(event) + } + + @Test + fun `GIVEN an expanded menu WHEN onInterceptTouchEvent is called for a outside the menu THEN super blankTouchListener is invoked`() { + val blankTouchListener = spy {} + val expandableLayout = spy( + ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + blankTouchListener = blankTouchListener, + ), + ) + val event: MotionEvent = mock() + doReturn(false).`when`(expandableLayout).shouldInterceptTouches() + doReturn(false).`when`(expandableLayout).isTouchingTheWrappedView(any()) + + expandableLayout.onInterceptTouchEvent(event) + + verify(blankTouchListener).invoke() + verify(expandableLayout, never()).callParentOnInterceptTouchEvent(event) + } + + @Test + fun `GIVEN ExpandableLayout WHEN onInterceptTouchEvent is called for ACTION_CANCEL or ACTION_UP THEN the events are not intercepted`() { + val expandableLayout = spy( + ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + ) { }, + ) + val actionCancel = MotionEvent.obtain(0, 0, ACTION_UP, 0f, 0f, 0) + val actionUp = MotionEvent.obtain(0, 0, ACTION_CANCEL, 0f, 0f, 0) + doReturn(true).`when`(expandableLayout).shouldInterceptTouches() + + assertFalse(expandableLayout.onInterceptTouchEvent(actionCancel)) + assertFalse(expandableLayout.onInterceptTouchEvent(actionUp)) + + verify(expandableLayout, never()).callParentOnInterceptTouchEvent(any()) + } + + @Test + fun `GIVEN the wrappedView is in the expand process WHEN onInterceptTouchEvent is called while for new touches THEN they are intercepted`() { + val expandableLayout = spy( + ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + ) { }, + ) + val actionDown = MotionEvent.obtain(0, 0, ACTION_DOWN, 0f, 0f, 0) + doReturn(true).`when`(expandableLayout).shouldInterceptTouches() + expandableLayout.isExpandInProgress = true + + assertTrue(expandableLayout.onInterceptTouchEvent(actionDown)) + verify(expandableLayout, never()).callParentOnInterceptTouchEvent(any()) + } + + @Test + fun `GIVEN the wrappedView not in the expand process WHEN onInterceptTouchEvent is called for new touches THEN they are not intercepted`() { + val expandableLayout = spy( + ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + ) { }, + ) + val actionDown = MotionEvent.obtain(0, 0, ACTION_DOWN, 0f, 0f, 0) + doReturn(true).`when`(expandableLayout).shouldInterceptTouches() + expandableLayout.isExpandInProgress = false + + assertFalse(expandableLayout.onInterceptTouchEvent(actionDown)) + verify(expandableLayout, never()).callParentOnInterceptTouchEvent(any()) + } + + @Test + fun `GIVEN blankTouchListener set WHEN onInterceptTouchEvent is called for a touch that does not intersect wrappedView's bounds THEN blankTouchListener is called`() { + var listenerCalled = false + val listener = spy { listenerCalled = true } + val expandableLayout = spy( + ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + blankTouchListener = listener, + ), + ) + doReturn(false).`when`(expandableLayout).isTouchingTheWrappedView(any()) + val actionDown = MotionEvent.obtain(0, 0, ACTION_DOWN, 0f, 0f, 0) + + expandableLayout.onInterceptTouchEvent(actionDown) + + assertTrue(listenerCalled) + } + + @Test + fun `GIVEN blankTouchListener set WHEN onInterceptTouchEvent is called for a touch that intersects wrappedView's bounds THEN blankTouchListener is not called`() { + var listenerCalled = false + val listener = spy { listenerCalled = true } + val expandableLayout = spy( + ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + blankTouchListener = listener, + ), + ) + doReturn(true).`when`(expandableLayout).isTouchingTheWrappedView(any()) + val actionDown = MotionEvent.obtain(0, 0, ACTION_DOWN, 0f, 0f, 0) + + expandableLayout.onInterceptTouchEvent(actionDown) + + assertFalse(listenerCalled) + } + + @Test + fun `GIVEN initialYCoord set WHEN onInterceptTouchEvent is called for ACTION_DOWN THEN initialYCoord will be reset to the new value`() { + val expandableLayout = ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + ) { } + + val actionDown1 = MotionEvent.obtain(0, 0, ACTION_DOWN, 0f, 22f, 0) + expandableLayout.onInterceptTouchEvent(actionDown1) + assertEquals(22f, expandableLayout.initialYCoord) + + val actionDown2 = MotionEvent.obtain(0, 0, ACTION_DOWN, 0f, -33f, 0) + expandableLayout.onInterceptTouchEvent(actionDown2) + assertEquals(-33f, expandableLayout.initialYCoord) + } + + @Test + fun `GIVEN ExpandableLayout is in the expand process WHEN onInterceptTouchEvent is called for scroll events THEN these events are intercepted`() { + val expandableLayout = spy( + ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + ) { }, + ) + val actionDown = MotionEvent.obtain(0, 0, ACTION_MOVE, 0f, 0f, 0) + doReturn(false).`when`(expandableLayout).shouldInterceptTouches() + doReturn(false).`when`(expandableLayout).isTouchingTheWrappedView(any()) + + assertTrue(expandableLayout.onInterceptTouchEvent(actionDown)) + verify(expandableLayout, never()).expand() + verify(expandableLayout, never()).callParentOnInterceptTouchEvent(any()) + } + + @Test + fun `GIVEN the wrappedView is not expanding WHEN onInterceptTouchEvent is called for an event that is not a scroll THEN this event is not intercepted`() { + val expandableLayout = spy( + ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + ) { }, + ) + val actionDown = MotionEvent.obtain(0, 0, ACTION_MOVE, 0f, 0f, 0) + doReturn(true).`when`(expandableLayout).shouldInterceptTouches() + doReturn(false).`when`(expandableLayout).isScrollingUp(any()) + + assertFalse(expandableLayout.onInterceptTouchEvent(actionDown)) + verify(expandableLayout, never()).expand() + verify(expandableLayout, never()).callParentOnInterceptTouchEvent(any()) + } + + @Test + fun `GIVEN the wrappedView is not expanding WHEN onInterceptTouchEvent is called for scroll up events THEN the events are intercepted and expand() is called`() { + val expandableLayout = spy( + ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + ) { }, + ) + val actionDown = MotionEvent.obtain(0, 0, ACTION_MOVE, 0f, 0f, 0) + doReturn(true).`when`(expandableLayout).shouldInterceptTouches() + doReturn(true).`when`(expandableLayout).isScrollingUp(any()) + + assertTrue(expandableLayout.onInterceptTouchEvent(actionDown)) + verify(expandableLayout).expand() + verify(expandableLayout, never()).callParentOnInterceptTouchEvent(any()) + } + + @Test + fun `GIVEN isTouchForWrappedView() WHEN called with a touch event that is within the wrappedView bounds THEN it returns true`() { + val wrappedView = spy(FrameLayout(testContext)) + doAnswer { + val rect = it.arguments[0] as Rect + rect.set(0, 0, 100, 100) + }.`when`(wrappedView).getHitRect(any()) + val expandableLayout = ExpandableLayout.wrapContentInExpandableView( + wrappedView, + 1, + ) { } + val inBoundsEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 5f, 5f, 0) + + assertTrue(expandableLayout.isTouchingTheWrappedView(inBoundsEvent)) + } + + @Test + fun `GIVEN isTouchForWrappedView WHEN called with a touch event that is not within the wrappedView bounds THEN it returns false`() { + val wrappedView = spy(FrameLayout(testContext)) + doAnswer { + val rect = it.arguments[0] as Rect + rect.set(0, 0, 100, 100) + }.`when`(wrappedView).getHitRect(any()) + val expandableLayout = ExpandableLayout.wrapContentInExpandableView( + wrappedView, + 1, + ) { } + val outOfBoundsEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 105f, 105f, 0) + + assertFalse(expandableLayout.isTouchingTheWrappedView(outOfBoundsEvent)) + } + + @Test + fun `GIVEN ExpandableLayout in the collapsed state WHEN shouldInterceptTouches is called THEN it returns true`() { + val expandableLayout = ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + ) { } + expandableLayout.isCollapsed = true + + assertTrue(expandableLayout.shouldInterceptTouches()) + } + + @Test + fun `GIVEN ExpandableLayout not collapsed WHEN shouldInterceptTouches is called THEN it returns false`() { + val expandableLayout = ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + ) { } + expandableLayout.isCollapsed = false + + assertFalse(expandableLayout.shouldInterceptTouches()) + } + + @Test + fun `GIVEN ExpandableLayout currently expanding WHEN shouldInterceptTouches is called THEN it returns false`() { + val expandableLayout = ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + ) { } + expandableLayout.isCollapsed = true + expandableLayout.isExpandInProgress = false + + assertTrue(expandableLayout.shouldInterceptTouches()) + } + + @Test + fun `GIVEN ExpandableLayout WHEN collapse is called THEN it sets a positive translation and a smaller height for the wrappedView`() { + val expandableLayout = ExpandableLayout.wrapContentInExpandableView( + spy(FrameLayout(testContext)), + 1, + ) { } + expandableLayout.wrappedView.updateLayoutParams { + height = 100 + } + expandableLayout.parentHeight = 200 + expandableLayout.collapsedHeight = 50 + + expandableLayout.collapse() + + // If the available height is 200, with the layout starting at 0,0 + // to properly "anchor" a 50px height wrappedView we need to translate it 150px. + verify(expandableLayout.wrappedView).translationY = 150f + assertEquals(50, expandableLayout.wrappedView.layoutParams.height) + } + + @Test + fun `GIVEN ExpandableLayout WHEN getExpandViewAnimator is called THEN it returns a new ValueAnimator`() { + val expandableLayout = ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + ) { } + + val result = expandableLayout.getExpandViewAnimator(100) + + assertTrue(result.interpolator is AccelerateDecelerateInterpolator) + assertEquals(ExpandableLayout.DEFAULT_DURATION_EXPAND_ANIMATOR, result.duration) + } + + @Test + fun `GIVEN ExpandableLayout WHEN expand is called THEN it updates the translationY and height to show the wrappedView with expandedHeight`() { + val expandableLayout = spy( + ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + ) { }, + ) + val animator = ValueAnimator.ofInt(0, 100) + doAnswer { + animator + }.`when`(expandableLayout).getExpandViewAnimator(anyInt()) + expandableLayout.expandedHeight = 100 + expandableLayout.collapsedHeight = 50 + expandableLayout.wrappedView.translationY = 50f + + expandableLayout.expand() + animator.end() + + verify(expandableLayout).getExpandViewAnimator(50) + assertEquals(-50f, expandableLayout.wrappedView.translationY) + assertEquals(150, expandableLayout.wrappedView.layoutParams.height) + assertTrue(System.currentTimeMillis() > 0) + } + + @Test + fun `GIVEN collapsedHeight if already calculated WHEN getOrCalculateCollapsedHeight is called THEN it returns collapsedHeight`() { + val expandableLayout = spy( + ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + ) { }, + ) + expandableLayout.collapsedHeight = 123 + + val result = expandableLayout.getOrCalculateCollapsedHeight() + + verify(expandableLayout, never()).calculateCollapsedHeight() + assertEquals(123, result) + } + + @Test + fun `GIVEN collapsedHeight is not already calculated WHEN getOrCalculateCollapsedHeight is called THEN it delegates calculateCollapsedHeight and returns the value from that`() { + val expandableLayout = spy( + ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + ) { }, + ) + doReturn(42).`when`(expandableLayout).calculateCollapsedHeight() + + val result = expandableLayout.getOrCalculateCollapsedHeight() + + verify(expandableLayout).calculateCollapsedHeight() + assertEquals(42, result) + } + + @Test + fun `GIVEN expandedHeight not calculated WHEN getOrCalculateExpandedHeight is called THEN it sets expandedHeight with the value of measuredHeight`() { + val wrappedView = spy(FrameLayout(testContext)) + val expandableLayout = spy( + ExpandableLayout.wrapContentInExpandableView( + wrappedView, + 1, + ) { }, + ) + + doReturn(100).`when`(wrappedView).measuredHeight + assertEquals(100, expandableLayout.getOrCalculateExpandedHeight(0)) + assertEquals(100, expandableLayout.expandedHeight) + + doReturn(200).`when`(wrappedView).measuredHeight + assertEquals(100, expandableLayout.getOrCalculateExpandedHeight(0)) + assertEquals(100, expandableLayout.expandedHeight) + } + + @Test + fun `GIVEN parentHeight not already calculated WHEN getOrCalculateExpandedHeight is called THEN it sets parentHeight with the value from the heightSpec size`() { + val expandableLayout = spy( + ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + ) { }, + ) + + expandableLayout.getOrCalculateExpandedHeight(View.MeasureSpec.makeMeasureSpec(123, View.MeasureSpec.EXACTLY)) + assertEquals(123, expandableLayout.parentHeight) + + expandableLayout.getOrCalculateExpandedHeight(View.MeasureSpec.makeMeasureSpec(321, View.MeasureSpec.EXACTLY)) + assertEquals(123, expandableLayout.parentHeight) + } + + @Test + fun `GIVEN parentHeight not calculated WHEN getOrCalculateExpandedHeight is called with a parent height THEN it sets the expandedHeight as the minimum of between expandedHeight and parent height`() { + val expandableLayout = spy( + ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + ) { }, + ) + expandableLayout.expandedHeight = 123 + + expandableLayout.getOrCalculateExpandedHeight(View.MeasureSpec.makeMeasureSpec(101, View.MeasureSpec.EXACTLY)) + assertEquals(101, expandableLayout.expandedHeight) + + expandableLayout.getOrCalculateExpandedHeight(View.MeasureSpec.makeMeasureSpec(222, View.MeasureSpec.EXACTLY)) + assertEquals(101, expandableLayout.expandedHeight) + } + + @Test + fun `GIVEN getOrCalculateExpandedHeight() WHEN calculating the collapsed height to be bigger than the available screen height THEN it cancels collapsing`() { + val expandableLayout = spy( + ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + ) { }, + ) + expandableLayout.collapsedHeight = 50 + expandableLayout.expandedHeight = 100 + + var result = expandableLayout.getOrCalculateExpandedHeight(View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)) + assertEquals(100, result) + assertTrue(expandableLayout.isCollapsed) + assertFalse(expandableLayout.isExpandInProgress) + + // Reset parent height. Simulate entirely new calculations to have the code passing an if check + expandableLayout.parentHeight = -1 + expandableLayout.collapsedHeight = 1_000 + result = expandableLayout.getOrCalculateExpandedHeight(View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)) + assertEquals(100, result) + assertFalse(expandableLayout.isCollapsed) + assertFalse(expandableLayout.isExpandInProgress) + } + + @Test + fun `GIVEN a set touchSlop WHEN isScrollingUp calculates that is was exceeded THEN it returns true `() { + val expandableLayout = ExpandableLayout.wrapContentInExpandableView( + FrameLayout(testContext), + 1, + ) { } + expandableLayout.initialYCoord = 0f + expandableLayout.touchSlop = 10f + + var distanceScrolledDown = 11f + assertFalse(expandableLayout.isScrollingUp(MotionEvent.obtain(0, 0, ACTION_MOVE, 0f, distanceScrolledDown, 0))) + distanceScrolledDown = 5f + assertFalse(expandableLayout.isScrollingUp(MotionEvent.obtain(0, 0, ACTION_MOVE, 0f, distanceScrolledDown, 0))) + + var distanceScrolledUp = -11f + assertTrue(expandableLayout.isScrollingUp(MotionEvent.obtain(0, 0, ACTION_MOVE, 0f, distanceScrolledUp, 0))) + distanceScrolledUp = -5f + assertFalse(expandableLayout.isScrollingUp(MotionEvent.obtain(0, 0, ACTION_MOVE, 0f, distanceScrolledUp, 0))) + } + + @Test + fun `GIVEN a list of items WHEN calculateCollapsedHeight is called with an item index not found in the items list THEN it returns the value of measuredHeight`() { + val list = RecyclerView(testContext).apply { + layoutManager = mock() + addView(mock(), mock<RecyclerView.LayoutParams>()) + addView(mock(), mock<RecyclerView.LayoutParams>()) + } + val wrappedView = FrameLayout(testContext).apply { addView(list) } + val measuredHeight = -42 + val expandableLayout = spy( + ExpandableLayout.wrapContentInExpandableView( + wrappedView, + 0, + ) { }, + ) + + doReturn(measuredHeight).`when`(expandableLayout).measuredHeight + + doReturn(0).`when`(expandableLayout).getChildPositionForAdapterIndex(any(), anyInt()) + assertEquals(measuredHeight, expandableLayout.calculateCollapsedHeight()) + + // Here we test the list of two items collapsed to the first. + expandableLayout.lastVisibleItemIndexWhenCollapsed = 1 + doReturn(1).`when`(expandableLayout).getChildPositionForAdapterIndex(any(), anyInt()) + assertEquals(0, expandableLayout.calculateCollapsedHeight()) + + expandableLayout.lastVisibleItemIndexWhenCollapsed = 2 + doReturn(2).`when`(expandableLayout).getChildPositionForAdapterIndex(any(), anyInt()) + assertEquals(measuredHeight, expandableLayout.calculateCollapsedHeight()) + } + + @Test + fun `GIVEN calculateCollapsedHeight() WHEN called without a sticky footer index THEN it returns the distance between parent top and half of the SpecialView`() { + val viewHeightForEachProperty = 1_000 + val listHeightForEachProperty = 100 + val itemHeightForEachProperty = 10 + val layoutManager = mock<RecyclerView.LayoutManager>() + // Although correctly set below (in configureWithHeight) the params get overwritten in RecyclerView when adding items + // So we need to fake RecyclerView's LayoutParams response. + val layoutParams = mock<RecyclerView.LayoutParams>() + .also { it.configureMarginResponse(itemHeightForEachProperty) } + doReturn(layoutParams).`when`(layoutManager).generateLayoutParams(any()) + // Adding Views and creating spies in two stages because otherwise + // the addView call for a spy will not get us the expected result. + var list = RecyclerView(testContext).apply { + this.layoutManager = layoutManager + addView(spy(View(testContext)).configureWithHeight(itemHeightForEachProperty)) + addView(spy(View(testContext)).configureWithHeight(itemHeightForEachProperty)) + addView(spy(View(testContext)).configureWithHeight(itemHeightForEachProperty)) + } + list = spy(list).configureWithHeight(listHeightForEachProperty) + var wrappedView = FrameLayout(testContext).apply { addView(list) } + wrappedView = spy(wrappedView).configureWithHeight(viewHeightForEachProperty) + val expandableLayout = spy(ExpandableLayout.wrapContentInExpandableView(wrappedView, 1) { }) + doReturn(1).`when`(expandableLayout).getChildPositionForAdapterIndex(any(), eq(1)) + + val result = expandableLayout.calculateCollapsedHeight() + + var expected = 0 + expected += viewHeightForEachProperty * 4 // marginTop + marginBottom + paddingTop + paddingBottom + expected += listHeightForEachProperty * 4 // marginTop + marginBottom + paddingTop + paddingBottom + expected += itemHeightForEachProperty * 3 // height + marginTop + marginBottom for the top item shown in entirety + expected += itemHeightForEachProperty // marginTop for the special view + expected += itemHeightForEachProperty / 2 // as per the specs, show only half of the special view + assertEquals(expected, result) + } + + @Test + fun `GIVEN calculateCollapsedHeight() WHEN called with a sticky footer index THEN it returns the distance between parent top and half of the SpecialView + height of sticky`() { + val viewHeightForEachProperty = 1_000 + val listHeightForEachProperty = 100 + val itemHeightForEachProperty = 10 + val layoutManager = mock<RecyclerView.LayoutManager>() + // Although correctly set below (in configureWithHeight) the params get overwritten in RecyclerView when adding items + // So we need to fake RecyclerView's LayoutParams response. + val layoutParams = mock<RecyclerView.LayoutParams>() + .also { it.configureMarginResponse(itemHeightForEachProperty) } + doReturn(layoutParams).`when`(layoutManager).generateLayoutParams(any()) + // Adding Views and creating spies in two stages because otherwise + // the addView call for a spy will not get us the expected result. + var list = RecyclerView(testContext).apply { + this.layoutManager = layoutManager + addView(spy(View(testContext)).configureWithHeight(itemHeightForEachProperty)) + addView(spy(View(testContext)).configureWithHeight(itemHeightForEachProperty)) + addView(spy(View(testContext)).configureWithHeight(itemHeightForEachProperty)) + } + list = spy(list).configureWithHeight(listHeightForEachProperty) + var wrappedView = FrameLayout(testContext).apply { addView(list) } + wrappedView = spy(wrappedView).configureWithHeight(viewHeightForEachProperty) + val expandableLayout = spy(ExpandableLayout.wrapContentInExpandableView(wrappedView, 1, 2) { }) + doReturn(1).`when`(expandableLayout).getChildPositionForAdapterIndex(any(), eq(1)) + doReturn(2).`when`(expandableLayout).getChildPositionForAdapterIndex(any(), eq(2)) + + val result = expandableLayout.calculateCollapsedHeight() + + var expected = 0 + expected += viewHeightForEachProperty * 4 // marginTop + marginBottom + paddingTop + paddingBottom + expected += listHeightForEachProperty * 4 // marginTop + marginBottom + paddingTop + paddingBottom + expected += itemHeightForEachProperty * 3 // height + marginTop + marginBottom for the top item shown in entirety + expected += itemHeightForEachProperty // marginTop for the special view + expected += itemHeightForEachProperty / 2 // as per the specs, show only half of the special view + expected += itemHeightForEachProperty // height of the sticky item + assertEquals(expected, result) + } + + @Test + fun `GIVEN calculateCollapsedHeight() WHEN called the same item as limit and sticky THEN it returns the distance between parent top and bottom of sticky`() { + val viewHeightForEachProperty = 1_000 + val listHeightForEachProperty = 100 + val itemHeightForEachProperty = 10 + val layoutManager = mock<RecyclerView.LayoutManager>() + // Although correctly set below (in configureWithHeight) the params get overwritten in RecyclerView when adding items + // So we need to fake RecyclerView's LayoutParams response. + val layoutParams = mock<RecyclerView.LayoutParams>() + .also { it.configureMarginResponse(itemHeightForEachProperty) } + doReturn(layoutParams).`when`(layoutManager).generateLayoutParams(any()) + // Adding Views and creating spies in two stages because otherwise + // the addView call for a spy will not get us the expected result. + var list = RecyclerView(testContext).apply { + this.layoutManager = layoutManager + addView(spy(View(testContext)).configureWithHeight(itemHeightForEachProperty)) + addView(spy(View(testContext)).configureWithHeight(itemHeightForEachProperty)) + addView(spy(View(testContext)).configureWithHeight(itemHeightForEachProperty)) + } + list = spy(list).configureWithHeight(listHeightForEachProperty) + var wrappedView = FrameLayout(testContext).apply { addView(list) } + wrappedView = spy(wrappedView).configureWithHeight(viewHeightForEachProperty) + val expandableLayout = spy(ExpandableLayout.wrapContentInExpandableView(wrappedView, 1, 1) { }) + doReturn(1).`when`(expandableLayout).getChildPositionForAdapterIndex(any(), eq(1)) + + val result = expandableLayout.calculateCollapsedHeight() + + var expected = 0 + expected += viewHeightForEachProperty * 4 // marginTop + marginBottom + paddingTop + paddingBottom + expected += listHeightForEachProperty * 4 // marginTop + marginBottom + paddingTop + paddingBottom + expected += itemHeightForEachProperty * 3 // height + marginTop + marginBottom for the top item shown in entirety + expected += itemHeightForEachProperty // marginTop for the special view + expected += itemHeightForEachProperty // height of the sticky item + assertEquals(expected, result) + } + + @Test + fun `GIVEN a RecyclerView with a set Adapter WHEN getChildPositionForAdapterIndex is called THEN it returns the list position of the item in Adapter`() { + val layoutManager = mock<RecyclerView.LayoutManager>() + // Although correctly set below (in configureWithHeight) the params get overwritten in RecyclerView when adding items + // So we need to fake RecyclerView's LayoutParams response. + val layoutParams = mock<RecyclerView.LayoutParams>() + doReturn(layoutParams).`when`(layoutManager).generateLayoutParams(any()) + val list = spy( + RecyclerView(testContext).apply { + this.layoutManager = layoutManager + addView(View(testContext).apply { setLayoutParams(ViewGroup.LayoutParams(10, 10)) }) + addView(View(testContext).apply { setLayoutParams(ViewGroup.LayoutParams(10, 10)) }) + addView(View(testContext).apply { setLayoutParams(ViewGroup.LayoutParams(10, 10)) }) + }, + ) + val wrappedView = FrameLayout(testContext).apply { addView(list) } + val expandableLayout = spy(ExpandableLayout.wrapContentInExpandableView(wrappedView)) + doReturn(3).`when`(list).getChildAdapterPosition(any()) + + assertEquals(-1, expandableLayout.getChildPositionForAdapterIndex(list, 2)) + // We'll get a match based on the above "doReturn().." and adapterIndex: 3 for the first child. + assertEquals(0, expandableLayout.getChildPositionForAdapterIndex(list, 3)) + } +} + +/** + * Convenience method to set the same value - the received [height] parameter as the + * height, margins and paddings values for the current View. + */ +fun <V> V.configureWithHeight(height: Int): V where V : View { + doReturn(height).`when`(this).measuredHeight + layoutParams = ViewGroup.MarginLayoutParams(height, height).apply { + setMargins(height, height, height, height) + } + setPadding(height, height, height, height) + + return this +} + +/** + * Convenience method for setting the [margin] value to all LayoutParams margins. + */ +fun <T> T.configureMarginResponse(margin: Int) where T : ViewGroup.MarginLayoutParams { + this.topMargin = margin + this.rightMargin = margin + this.bottomMargin = margin + this.leftMargin = margin +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/FakeStickyItemLayoutManager.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/FakeStickyItemLayoutManager.kt new file mode 100644 index 0000000000..50f87d17d9 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/FakeStickyItemLayoutManager.kt @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.view + +import android.content.Context +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +/** + * A default implementation of a the abstract [StickyItemsLinearLayoutManager] to be used in tests. + */ +open class FakeStickyItemLayoutManager<T> constructor( + context: Context, + internal val stickyItemPlacement: StickyItemPlacement = StickyItemPlacement.TOP, + reverseLayout: Boolean = false, +) : StickyItemsLinearLayoutManager<T>( + context, + stickyItemPlacement, + reverseLayout, +) where T : RecyclerView.Adapter<*>, T : StickyItemsAdapter { + override fun scrollToIndicatedPositionWithOffset( + position: Int, + offset: Int, + actuallyScrollToPositionWithOffset: (Int, Int) -> Unit, + ) { } + + override fun shouldStickyItemBeShownForCurrentPosition() = true + + override fun getY(itemView: View) = 0f +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/FakeStickyItemsAdapter.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/FakeStickyItemsAdapter.kt new file mode 100644 index 0000000000..d2187baad3 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/FakeStickyItemsAdapter.kt @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.view + +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.support.test.mock + +/** + * A default implementation of [StickyItemsAdapter] to be used in tests. + */ +class FakeStickyItemsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>(), StickyItemsAdapter { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + mock() + + override fun getItemCount(): Int = 42 + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {} + + override fun isStickyItem(position: Int): Boolean = false +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/MenuButtonTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/MenuButtonTest.kt new file mode 100644 index 0000000000..3d1b74b49a --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/MenuButtonTest.kt @@ -0,0 +1,172 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.view + +import android.content.res.ColorStateList +import android.graphics.Color +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.widget.ImageView +import androidx.appcompat.widget.AppCompatImageView +import androidx.core.view.children +import androidx.core.view.isVisible +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.BrowserMenuBuilder +import mozilla.components.browser.menu.BrowserMenuHighlight +import mozilla.components.concept.menu.MenuController +import mozilla.components.support.test.any +import mozilla.components.support.test.eq +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class MenuButtonTest { + private lateinit var menuController: MenuController + private lateinit var menuBuilder: BrowserMenuBuilder + private lateinit var menu: BrowserMenu + private lateinit var menuButton: MenuButton + private lateinit var menuIcon: ImageView + private lateinit var highlightView: ImageView + private lateinit var notificationIconView: ImageView + + @Before + fun setup() { + menuController = mock() + menu = mock() + menuBuilder = mock() + doReturn(menu).`when`(menuBuilder).build(testContext) + + menuButton = MenuButton(testContext) + val images = menuButton.children.mapNotNull { it as? AppCompatImageView }.toList() + highlightView = images[0] + menuIcon = images[1] + notificationIconView = images[2] + } + + @Test + fun `changing menu controller dismisses old menu`() { + menuButton.menuController = menuController + menuButton.performClick() + + verify(menuController).show(menuButton) + + menuButton.menuController = mock() + verify(menuController).dismiss() + } + + @Test + fun `changing menu builder dismisses old menu`() { + menuButton.menuBuilder = menuBuilder + menuButton.performClick() + + verify(menu).show(eq(menuButton), any(), any(), anyBoolean(), any()) + + menuButton.menuBuilder = mock() + verify(menu).dismiss() + } + + @Test + fun `opening a new menu will prefer using the controller`() { + menuButton.menuController = menuController + menuButton.menuBuilder = menuBuilder + + menuButton.performClick() + + verify(menuController).show(menuButton) + verify(menuBuilder, never()).build(testContext) + verify(menu, never()).show(any(), any(), any(), anyBoolean(), any()) + } + + @Test + fun `trying to open a new menu when we already have one will dismiss the current`() { + menuButton.menuBuilder = menuBuilder + + menuButton.performClick() + menuButton.performClick() + + verify(menu, times(1)).show(eq(menuButton), any(), any(), anyBoolean(), any()) + verify(menu, times(1)).dismiss() + } + + @Test + fun `icon has content description`() { + assertEquals("Menu", menuIcon.contentDescription) + assertNotNull(menuIcon.drawable) + } + + @Test + fun `icon color filter can be changed`() { + assertNull(menuIcon.colorFilter) + + menuButton.setColorFilter(0xffffff) + assertEquals(PorterDuffColorFilter(0xffffff, PorterDuff.Mode.SRC_ATOP), menuIcon.colorFilter) + } + + @Test + fun `icon can invalidate menu`() { + menuButton.menuBuilder = menuBuilder + menuButton.performClick() + + verify(menu).show(eq(menuButton), any(), any(), anyBoolean(), any()) + + menuButton.invalidateBrowserMenu() + verify(menu).invalidate() + } + + @Test + fun `icon displays high priority highlight`() { + assertFalse(highlightView.isVisible) + assertFalse(notificationIconView.isVisible) + + menuButton.setHighlight( + BrowserMenuHighlight.HighPriority(Color.RED), + ) + + assertTrue(highlightView.isVisible) + assertFalse(notificationIconView.isVisible) + + assertEquals(ColorStateList.valueOf(Color.RED), highlightView.imageTintList) + } + + @Test + fun `icon displays low priority highlight`() { + assertFalse(highlightView.isVisible) + assertFalse(notificationIconView.isVisible) + + menuButton.setHighlight( + BrowserMenuHighlight.LowPriority(Color.BLUE), + ) + + assertFalse(highlightView.isVisible) + assertTrue(notificationIconView.isVisible) + + assertEquals(PorterDuffColorFilter(Color.BLUE, PorterDuff.Mode.SRC_ATOP), notificationIconView.colorFilter) + } + + @Test + fun `menu can be dismissed`() { + menuButton.menuController = menuController + menuButton.menu = menu + + menuButton.dismissMenu() + + verify(menuButton.menuController)?.dismiss() + verify(menuButton.menu)?.dismiss() + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/StickyFooterLinearLayoutManagerTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/StickyFooterLinearLayoutManagerTest.kt new file mode 100644 index 0000000000..f4e095d7dd --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/StickyFooterLinearLayoutManagerTest.kt @@ -0,0 +1,189 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.view + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.support.test.mock +import org.junit.Assert +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify + +class StickyFooterLinearLayoutManagerTest { + private lateinit var manager: StickyFooterLinearLayoutManager<FakeStickyItemsAdapter> + + @Before + fun setup() { + manager = StickyFooterLinearLayoutManager(mock(), false) + } + + @Test + fun `GIVEN scrollToIndicatedPositionWithOffset WHEN called with a position smaller than stickyItemPosition THEN will scroll to after that`() { + manager.stickyItemPosition = 5 + var positionToScrollResult = -1 + var offsetToScrollResult = -1 + val scrollCallback: (Int, Int) -> Unit = { position, offset -> + positionToScrollResult = position + offsetToScrollResult = offset + } + + manager.scrollToIndicatedPositionWithOffset(4, 22, scrollCallback) + Assert.assertEquals(5, positionToScrollResult) + Assert.assertEquals(22, offsetToScrollResult) + + manager.scrollToIndicatedPositionWithOffset(0, 22, scrollCallback) + Assert.assertEquals(1, positionToScrollResult) + Assert.assertEquals(22, offsetToScrollResult) + } + + @Test + fun `GIVEN scrollToIndicatedPositionWithOffset WHEN called with a position smaller than stickyItemPosition which is displayed THEN will scroll to that`() { + manager = spy(manager) + doReturn(mock<View>()).`when`(manager).getChildAt(ArgumentMatchers.anyInt()) + manager.stickyItemPosition = 5 + var positionToScrollResult = -1 + var offsetToScrollResult = -1 + val scrollCallback: (Int, Int) -> Unit = { position, offset -> + positionToScrollResult = position + offsetToScrollResult = offset + } + + manager.scrollToIndicatedPositionWithOffset(4, 22, scrollCallback) + Assert.assertEquals(4, positionToScrollResult) + Assert.assertEquals(22, offsetToScrollResult) + + manager.scrollToIndicatedPositionWithOffset(0, 22, scrollCallback) + Assert.assertEquals(0, positionToScrollResult) + Assert.assertEquals(22, offsetToScrollResult) + } + + @Test + fun `GIVEN scrollToIndicatedPositionWithOffset WHEN called with a position equal to stickyItemPosition THEN will scroll to that position`() { + manager.stickyItemPosition = 6 + var positionToScrollResult = -1 + var offsetToScrollResult = -1 + val scrollCallback: (Int, Int) -> Unit = { position, offset -> + positionToScrollResult = position + offsetToScrollResult = offset + } + + manager.scrollToIndicatedPositionWithOffset(6, 22, scrollCallback) + Assert.assertEquals(6, positionToScrollResult) + Assert.assertEquals(22, offsetToScrollResult) + } + + @Test + fun `GIVEN scrollToIndicatedPositionWithOffset WHEN called with a position bigger than stickyItemPosition THEN will scroll to that position`() { + manager.stickyItemPosition = 6 + var positionToScrollResult = -1 + var offsetToScrollResult = -1 + val scrollCallback: (Int, Int) -> Unit = { position, offset -> + positionToScrollResult = position + offsetToScrollResult = offset + } + + manager.scrollToIndicatedPositionWithOffset(7, 22, scrollCallback) + Assert.assertEquals(7, positionToScrollResult) + Assert.assertEquals(22, offsetToScrollResult) + + manager.scrollToIndicatedPositionWithOffset(10, 22, scrollCallback) + Assert.assertEquals(10, positionToScrollResult) + Assert.assertEquals(22, offsetToScrollResult) + + // Negative positions are handled by Android'd LayoutManager. We should pass any to it. + manager.scrollToIndicatedPositionWithOffset(3333, 22, scrollCallback) + Assert.assertEquals(3333, positionToScrollResult) + Assert.assertEquals(22, offsetToScrollResult) + } + + @Test + fun `GIVEN stickyItemPosition not set WHEN shouldStickyItemBeShownForCurrentPosition is called THEN it returns false`() { + manager.stickyItemPosition = RecyclerView.NO_POSITION + + Assert.assertFalse(manager.shouldStickyItemBeShownForCurrentPosition()) + } + + @Test + fun `GIVEN sticky item shown WHEN shouldStickyItemBeShownForCurrentPosition is called THEN it checks the item above the sticky one`() { + val manager = spy(manager) + manager.stickyItemPosition = 5 + manager.stickyItemView = mock() + doReturn(10).`when`(manager).childCount + + manager.shouldStickyItemBeShownForCurrentPosition() + + verify(manager).getAdapterPositionForItemIndex(8) + } + + @Test + fun `GIVEN sticky item not shown WHEN shouldStickyItemBeShownForCurrentPosition is called THEN it checks the bottom most item`() { + val manager = spy(manager) + manager.stickyItemPosition = 5 + doReturn(10).`when`(manager).childCount + + manager.shouldStickyItemBeShownForCurrentPosition() + + verify(manager).getAdapterPositionForItemIndex(9) + } + + @Test + fun ` GIVEN sticky item being the last shown item WHEN shouldStickyItemBeShownForCurrentPosition is called THEN it returns true`() { + val manager = spy(manager) + manager.stickyItemPosition = 5 + doReturn(5).`when`(manager).getAdapterPositionForItemIndex(ArgumentMatchers.anyInt()) + + assertTrue(manager.shouldStickyItemBeShownForCurrentPosition()) + } + + @Test + fun ` GIVEN sticky item being scrolled upwards from the bottom WHEN shouldStickyItemBeShownForCurrentPosition is called THEN it returns false`() { + val manager = spy(manager) + manager.stickyItemPosition = 5 + + doReturn(6).`when`(manager).getAdapterPositionForItemIndex(ArgumentMatchers.anyInt()) + assertFalse(manager.shouldStickyItemBeShownForCurrentPosition()) + + doReturn(60).`when`(manager).getAdapterPositionForItemIndex(ArgumentMatchers.anyInt()) + assertFalse(manager.shouldStickyItemBeShownForCurrentPosition()) + } + + @Test + fun ` GIVEN sticky item being scrolled downwards offscreen WHEN shouldStickyItemBeShownForCurrentPosition is called THEN it returns true`() { + val manager = spy(manager) + manager.stickyItemPosition = 5 + + doReturn(4).`when`(manager).getAdapterPositionForItemIndex(ArgumentMatchers.anyInt()) + assertTrue(manager.shouldStickyItemBeShownForCurrentPosition()) + + doReturn(0).`when`(manager).getAdapterPositionForItemIndex(ArgumentMatchers.anyInt()) + assertTrue(manager.shouldStickyItemBeShownForCurrentPosition()) + } + + @Test + fun `GIVEN a default layout menu WHEN getY is called THEN it returns the translation needed to push the sticky item to the top`() { + manager = spy(manager) + val stickyView: View = mock() + doReturn(100).`when`(manager).height + doReturn(33).`when`(stickyView).height + + Assert.assertEquals(67f, manager.getY(stickyView)) + } + + @Test + fun `GIVEN a reverseLayout menu WHEN getY is called THEN it returns 0 as the translation to be set for the sticky item`() { + manager = spy(StickyFooterLinearLayoutManager(mock(), true)) + doReturn(100).`when`(manager).height + val stickyView: View = mock() + doReturn(33).`when`(stickyView).height + + Assert.assertEquals(0f, manager.getY(stickyView)) + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/StickyHeaderLinearLayoutManagerTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/StickyHeaderLinearLayoutManagerTest.kt new file mode 100644 index 0000000000..897a8ca8c1 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/StickyHeaderLinearLayoutManagerTest.kt @@ -0,0 +1,155 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.view + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.support.test.mock +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.spy + +class StickyHeaderLinearLayoutManagerTest { + private lateinit var manager: StickyHeaderLinearLayoutManager<FakeStickyItemsAdapter> + + @Before + fun setup() { + manager = StickyHeaderLinearLayoutManager(mock(), false) + } + + @Test + fun `GIVEN scrollToIndicatedPositionWithOffset WHEN called with a position bigger than stickyItemPosition THEN will scroll to before that`() { + manager.stickyItemPosition = 5 + var positionToScrollResult = -1 + var offsetToScrollResult = -1 + val scrollCallback: (Int, Int) -> Unit = { position, offset -> + positionToScrollResult = position + offsetToScrollResult = offset + } + + manager.scrollToIndicatedPositionWithOffset(6, 22, scrollCallback) + assertEquals(5, positionToScrollResult) + assertEquals(22, offsetToScrollResult) + + manager.scrollToIndicatedPositionWithOffset(10, 22, scrollCallback) + assertEquals(9, positionToScrollResult) + assertEquals(22, offsetToScrollResult) + } + + @Test + fun `GIVEN scrollToIndicatedPositionWithOffset WHEN called with a position equal to stickyItemPosition THEN will scroll to before that`() { + manager.stickyItemPosition = 6 + var positionToScrollResult = -1 + var offsetToScrollResult = -1 + val scrollCallback: (Int, Int) -> Unit = { position, offset -> + positionToScrollResult = position + offsetToScrollResult = offset + } + + manager.scrollToIndicatedPositionWithOffset(6, 22, scrollCallback) + assertEquals(5, positionToScrollResult) + assertEquals(22, offsetToScrollResult) + } + + @Test + fun `GIVEN scrollToIndicatedPositionWithOffset WHEN called with a position smaller than stickyItemPosition THEN will scroll to that position`() { + manager.stickyItemPosition = 6 + var positionToScrollResult = -1 + var offsetToScrollResult = -1 + val scrollCallback: (Int, Int) -> Unit = { position, offset -> + positionToScrollResult = position + offsetToScrollResult = offset + } + + manager.scrollToIndicatedPositionWithOffset(5, 22, scrollCallback) + assertEquals(5, positionToScrollResult) + assertEquals(22, offsetToScrollResult) + + manager.scrollToIndicatedPositionWithOffset(0, 22, scrollCallback) + assertEquals(0, positionToScrollResult) + assertEquals(22, offsetToScrollResult) + + // Negative positions are handled by Android'd LayoutManager. We should pass any to it. + manager.scrollToIndicatedPositionWithOffset(-33, 22, scrollCallback) + assertEquals(-33, positionToScrollResult) + assertEquals(22, offsetToScrollResult) + } + + @Test + fun `GIVEN stickyItemPosition not set WHEN shouldStickyItemBeShownForCurrentPosition is called THEN it returns false`() { + manager.stickyItemPosition = RecyclerView.NO_POSITION + + assertFalse(manager.shouldStickyItemBeShownForCurrentPosition()) + } + + @Test + fun `GIVEN the top item is the sticky one WHEN shouldStickyItemBeShownForCurrentPosition is called THEN it returns true`() { + val manager = spy(manager) + manager.stickyItemPosition = 3 + doReturn(3).`when`(manager).getAdapterPositionForItemIndex(0) + + assertTrue(manager.shouldStickyItemBeShownForCurrentPosition()) + } + + @Test + fun `GIVEN items below the sticky item are scrolled upwards offscreen WHEN shouldStickyItemBeShownForCurrentPosition is called THEN it returns true`() { + val manager = spy(manager) + manager.stickyItemPosition = 3 + + doReturn(4).`when`(manager).getAdapterPositionForItemIndex(0) + assertTrue(manager.shouldStickyItemBeShownForCurrentPosition()) + + doReturn(5).`when`(manager).getAdapterPositionForItemIndex(0) + assertTrue(manager.shouldStickyItemBeShownForCurrentPosition()) + } + + @Test + fun `GIVEN items above the sticky item shown at top but ofsetted offscreen WHEN shouldStickyItemBeShownForCurrentPosition is called THEN it returns true`() { + val manager = spy(manager) + manager.stickyItemPosition = 3 + doReturn(1).`when`(manager).getAdapterPositionForItemIndex(0) + val topMostItem: View = mock() + doReturn(topMostItem).`when`(manager).getChildAt(0) + + doReturn(0).`when`(topMostItem).bottom + assertTrue(manager.shouldStickyItemBeShownForCurrentPosition()) + + doReturn(-5).`when`(topMostItem).bottom + assertTrue(manager.shouldStickyItemBeShownForCurrentPosition()) + } + + @Test + fun `GIVEN the sticky item is shown below the top of the list WHEN shouldStickyItemBeShownForCurrentPosition is called THEN it returns false`() { + val manager = spy(manager) + manager.stickyItemPosition = 3 + doReturn(1).`when`(manager).getAdapterPositionForItemIndex(0) + + assertFalse(manager.shouldStickyItemBeShownForCurrentPosition()) + } + + @Test + fun `GIVEN a default layout menu WHEN getY is called THEN it returns 0 as the translation to be set for the sticky item`() { + manager = spy(manager) + val stickyView: View = mock() + doReturn(100).`when`(manager).height + doReturn(33).`when`(stickyView).height + + assertEquals(0f, manager.getY(stickyView)) + } + + @Test + fun `GIVEN a reverseLayout menu WHEN getY is called THEN it returns the translation needed to push the sticky item to the top`() { + manager = spy(StickyHeaderLinearLayoutManager(mock(), true)) + doReturn(100).`when`(manager).height + val stickyView: View = mock() + doReturn(33).`when`(stickyView).height + + assertEquals(67f, manager.getY(stickyView)) + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/StickyItemsLinearLayoutManagerTest.kt b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/StickyItemsLinearLayoutManagerTest.kt new file mode 100644 index 0000000000..5e7d00314f --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/StickyItemsLinearLayoutManagerTest.kt @@ -0,0 +1,631 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu.view + +import android.graphics.PointF +import android.view.View +import android.view.ViewTreeObserver +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager.INVALID_OFFSET +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.support.test.any +import mozilla.components.support.test.argumentCaptor +import mozilla.components.support.test.eq +import mozilla.components.support.test.mock +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertSame +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers +import org.mockito.Mockito +import org.mockito.Mockito.doNothing +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify + +class StickyItemsLinearLayoutManagerTest { + // For shorter test names "StickyItemsLinearLayoutManager" is referred to as SILLM. + + private lateinit var manager: FakeStickyItemLayoutManager<FakeStickyItemsAdapter> + + @Before + fun setup() { + manager = FakeStickyItemLayoutManager(mock()) + } + + @Test + fun `GIVEN a SILLM WHEN a new instance is contructed THEN it has specific default values`() { + assertEquals(RecyclerView.NO_POSITION, manager.stickyItemPosition) + assertEquals(RecyclerView.NO_POSITION, manager.scrollPosition) + assertEquals(0, manager.scrollOffset) + } + + @Test + fun `GIVEN a SILLM WHEN onAttachedToWindow called THEN it calls super and sets the new adapter`() { + manager = spy(manager) + val list = Mockito.mock(RecyclerView::class.java) + + manager.onAttachedToWindow(list) + + verify(manager).setAdapter(list.adapter) + } + + @Test + fun `GIVEN a SILLM WHEN onSaveInstanceState called THEN it returns a new SavedState with the scroll data`() { + manager.scrollPosition = 42 + manager.scrollOffset = 422 + + val result: SavedState = manager.onSaveInstanceState() as SavedState + + assertTrue(result.superState is LinearLayoutManager.SavedState) + assertEquals(42, result.scrollPosition) + assertEquals(422, result.scrollOffset) + } + + @Test + fun `GIVEN a SILLM WHEN onRestoreInstanceState is called with a new state THEN it updates scrollPosition and scrollOffset`() { + val newState = SavedState( + null, + scrollPosition = 222, + scrollOffset = 221, + ) + + manager.onRestoreInstanceState(newState) + + assertEquals(222, manager.scrollPosition) + assertEquals(221, manager.scrollOffset) + } + + @Test + fun `GIVEN a SILLM WHEN onRestoreInstanceState is called with a null state THEN scrollPosition and scrollOffset are left unchanged`() { + manager.onRestoreInstanceState(null) + + assertEquals(RecyclerView.NO_POSITION, manager.scrollPosition) + assertEquals(0, manager.scrollOffset) + } + + @Test + fun `GIVEN a SILLM WHEN onLayoutChildren is called while in preLayout THEN it execute the super with the sticky item detached and not updates stickyItem`() { + manager = spy(manager) + val listState: RecyclerView.State = mock() + doReturn(true).`when`(listState).isPreLayout + + manager.onLayoutChildren(mock(), listState) + + verify(manager).restoreView<Unit>(any()) + verify(manager, never()).updateStickyItem(any(), ArgumentMatchers.anyBoolean()) + } + + @Test + fun `GIVEN a SILLM WHEN onLayoutChildren is called while not in preLayout THEN it execute the super with the sticky item detached and updates stickyItem`() { + manager = spy(manager) + val recycler: RecyclerView.Recycler = mock() + val listState: RecyclerView.State = mock() + doReturn(false).`when`(listState).isPreLayout + // Prevent side effects following the "manager.onLayoutChildren" call + doReturn(false).`when`(manager).shouldStickyItemBeShownForCurrentPosition() + + manager.onLayoutChildren(recycler, listState) + + verify(manager).restoreView<Unit>(any()) + verify(manager).updateStickyItem(recycler, true) + } + + @Test + fun `GIVEN A SILLM WHEN scrollVerticallyBy is called THEN it detaches the sticky item to scroll using parent and not updates the sticky item`() { + manager = spy(manager) + + val result = manager.scrollVerticallyBy(0, mock(), mock()) + + verify(manager).restoreView<Int>(any()) + verify(manager, never()).updateStickyItem(any(), ArgumentMatchers.anyBoolean()) + assertEquals(0, result) + } + + @Test + fun `GIVEN a SILLM WHEN findLastVisibleItemPosition is called THEN it detaches the sticky item to call the super method`() { + manager = spy(manager) + + manager.findLastVisibleItemPosition() + + verify(manager).restoreView<Int>(any()) + } + + @Test + fun `GIVEN a SILLM WHEN findFirstVisibleItemPosition is called THEN it detaches the sticky item to call the super method`() { + manager = spy(manager) + + manager.findFirstVisibleItemPosition() + + verify(manager).restoreView<Int>(any()) + } + + @Test + fun `GIVEN a SILLM WHEN findFirstCompletelyVisibleItemPosition is called THEN it detaches the sticky item to call the super method`() { + manager = spy(manager) + + manager.findFirstCompletelyVisibleItemPosition() + + verify(manager).restoreView<Int>(any()) + } + + @Test + fun `GIVEN a SILLM WHEN findLastCompletelyVisibleItemPosition is called THEN it detaches the sticky item to call the super method`() { + manager = spy(manager) + + manager.findLastCompletelyVisibleItemPosition() + + verify(manager).restoreView<Int>(any()) + } + + @Test + fun `GIVEN a SILLM WHEN computeVerticalScrollExtent is called THEN it detaches the sticky item to call the super method`() { + manager = spy(manager) + + manager.computeVerticalScrollExtent(mock()) + + verify(manager).restoreView<Int>(any()) + } + + @Test + fun `GIVEN a SILLM WHEN computeVerticalScrollOffset is called THEN it detaches the sticky item to call the super method`() { + manager = spy(manager) + + manager.computeVerticalScrollOffset(mock()) + + verify(manager).restoreView<Int>(any()) + } + + @Test + fun `GIVEN a SILLM WHEN computeVerticalScrollRange is called THEN it detaches the sticky item to call the super method`() { + manager = spy(manager) + + manager.computeVerticalScrollRange(mock()) + + verify(manager).restoreView<Int>(any()) + } + + @Test + fun `GIVEN a SILLM WHEN computeScrollVectorForPosition is called THEN it detaches the sticky item to call the super method`() { + manager = spy(manager) + + manager.computeScrollVectorForPosition(33) + + verify(manager).restoreView<PointF>(any()) + } + + @Test + fun `GIVEN sticky item is null WHEN scrollToPosition is called THEN scrollToPositionWithOffset is not called`() { + manager = spy(manager) + + manager.scrollToPosition(32) + + verify(manager, never()).scrollToPositionWithOffset(ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt()) + } + + @Test + fun `GIVEN sticky item is not null WHEN scrollToPosition is called THEN it calls scrollToPositionWithOffset with INVALID_OFFSET`() { + manager = spy(manager) + manager.stickyItemView = mock() + + manager.scrollToPosition(32) + + verify(manager).scrollToPositionWithOffset(32, INVALID_OFFSET) + } + + @Test + fun `GIVEN sticky item is not null WHEN scrollToPositionWithOffset is called THEN scrollToIndicatedPositionWithOffset is delegated`() { + manager = spy(manager) + manager.stickyItemView = mock() + + manager.scrollToPositionWithOffset(23, 9) + + verify(manager).setScrollState(RecyclerView.NO_POSITION, INVALID_OFFSET) + verify(manager).scrollToIndicatedPositionWithOffset(eq(23), eq(9), any()) + verify(manager).setScrollState(23, 9) + } + + @Test + fun `GIVEN sticky item is null WHEN onFocusSearchFailed is called THEN it detaches the sticky item to call the super method`() { + manager = spy(manager) + + manager.onFocusSearchFailed(mock(), 3, mock(), mock()) + + verify(manager).restoreView<View?>(any()) + } + + @Test + fun `GIVEN a SILLM WHEN getAdapterPositionForItemIndex is called with a index for which there is no bound view THEN it returns -1`() { + assertEquals(RecyclerView.NO_POSITION, manager.getAdapterPositionForItemIndex(22)) + } + + @Test + fun `GIVEN a SILLM WHEN getAdapterPositionForItemIndex is called with a index of an existing view THEN it returns it's absoluteAdapterPosition`() { + manager = spy(manager) + val params: RecyclerView.LayoutParams = mock() + doReturn(7).`when`(params).absoluteAdapterPosition + val view: View = mock() + doReturn(params).`when`(view).layoutParams + doReturn(view).`when`(manager).getChildAt(ArgumentMatchers.anyInt()) + + assertEquals(7, manager.getAdapterPositionForItemIndex(22)) + } + + @Test + fun `GIVEN a SILLM WHEN setAdapter is called with a null argument THEN the current adapter and stickyItem are set to null`() { + val initialAdapter = mock<FakeStickyItemsAdapter>() + manager.listAdapter = initialAdapter + + manager.setAdapter(null) + + verify(initialAdapter).unregisterAdapterDataObserver(manager.stickyItemPositionsObserver) + assertNull(manager.listAdapter) + assertNull(manager.stickyItemView) + } + + @Test + fun `GIVEN a SILLM WHEN setAdapter is called with a new valid adapter THEN the current adapter is reset`() { + val initialAdapter: FakeStickyItemsAdapter = mock() + val newAdapter: FakeStickyItemsAdapter = mock() + manager.listAdapter = initialAdapter + manager.stickyItemPositionsObserver = spy(manager.stickyItemPositionsObserver) + + manager.setAdapter(newAdapter) + + verify(initialAdapter).unregisterAdapterDataObserver(manager.stickyItemPositionsObserver) + assertSame(newAdapter, manager.listAdapter) + verify(newAdapter).registerAdapterDataObserver(manager.stickyItemPositionsObserver) + verify(manager.stickyItemPositionsObserver).onChanged() + } + + @Test + fun `GIVEN a SILLM WHEN restoreView is called with a method parameter THEN the sticky item is detached, method executed, item reattached`() { + manager = spy(manager) + val stickyView: View = mock() + manager.stickyItemView = stickyView + doNothing().`when`(manager).detachView(any()) + doNothing().`when`(manager).attachView(any()) + val orderVerifier = Mockito.inOrder(manager) + + val result = manager.restoreView { 3 } + + orderVerifier.verify(manager).detachView(stickyView) + orderVerifier.verify(manager).attachView(stickyView) + assertEquals(3, result) + } + + @Test + fun `GIVEN sticky item should not be shown WHEN updateStickyItem is called THEN the stickyItemView is recycled`() { + manager = spy(manager) + manager.stickyItemView = mock() + doReturn(false).`when`(manager).shouldStickyItemBeShownForCurrentPosition() + doNothing().`when`(manager).recycleStickyItem(any()) + val recycler: RecyclerView.Recycler = mock() + + manager.updateStickyItem(recycler, true) + + verify(manager).recycleStickyItem(recycler) + } + + @Test + fun `GIVEN sticky item should be shown and not exists WHEN updateStickyItem is called THEN a new stickyItemView is created`() { + manager = spy(manager) + doReturn(true).`when`(manager).shouldStickyItemBeShownForCurrentPosition() + doReturn(0f).`when`(manager).getY(any()) + manager.stickyItemPosition = 42 + val recycler: RecyclerView.Recycler = mock() + doNothing().`when`(manager).createStickyView(any(), ArgumentMatchers.anyInt()) + + manager.updateStickyItem(recycler, false) + + verify(manager).createStickyView(recycler, 42) + verify(manager, never()).recycleStickyItem(any()) + } + + @Test + fun `GIVEN sticky item should be shown and exists WHEN updateStickyItem is called THEN another stickyItemView is not created`() { + manager = spy(manager) + val stickyView: View = mock() + manager.stickyItemView = stickyView + doReturn(true).`when`(manager).shouldStickyItemBeShownForCurrentPosition() + doReturn(0f).`when`(manager).getY(any()) + manager.stickyItemPosition = 42 + val recycler: RecyclerView.Recycler = mock() + doNothing().`when`(manager).createStickyView(any(), ArgumentMatchers.anyInt()) + + manager.updateStickyItem(recycler, false) + + verify(manager, never()).createStickyView(any(), ArgumentMatchers.anyInt()) + verify(manager, never()).recycleStickyItem(any()) + } + + @Test + fun `GIVEN sticky item should be shown WHEN updateStickyItem is called while layout THEN bindStickyItem is called`() { + manager = spy(manager) + val stickyView: View = mock() + manager.stickyItemView = stickyView + doReturn(true).`when`(manager).shouldStickyItemBeShownForCurrentPosition() + doReturn(0f).`when`(manager).getY(any()) + doNothing().`when`(manager).createStickyView(any(), ArgumentMatchers.anyInt()) + doNothing().`when`(manager).bindStickyItem(any()) + + manager.updateStickyItem(mock(), true) + + verify(manager).bindStickyItem(stickyView) + verify(manager, never()).recycleStickyItem(any()) + } + + @Test + fun `GIVEN sticky item should be shown WHEN updateStickyItem is called while not layout THEN bindStickyItem is not called`() { + manager = spy(manager) + val stickyView: View = mock() + manager.stickyItemView = stickyView + doReturn(true).`when`(manager).shouldStickyItemBeShownForCurrentPosition() + doReturn(0f).`when`(manager).getY(any()) + doNothing().`when`(manager).createStickyView(any(), ArgumentMatchers.anyInt()) + doNothing().`when`(manager).bindStickyItem(any()) + + manager.updateStickyItem(mock(), false) + + verify(manager, never()).bindStickyItem(any()) + verify(manager, never()).recycleStickyItem(any()) + } + + @Test + fun `GIVEN sticky item should be shown and it's view exists WHEN updateStickyItem is called THEN the stickyItemView gets set a new Y translation`() { + manager = spy(manager) + val stickyView: View = mock() + manager.stickyItemView = stickyView + doReturn(true).`when`(manager).shouldStickyItemBeShownForCurrentPosition() + doReturn(44f).`when`(manager).getY(any()) + doNothing().`when`(manager).createStickyView(any(), ArgumentMatchers.anyInt()) + + manager.updateStickyItem(mock(), false) + + verify(manager).getY(stickyView) + verify(stickyView).translationY = 44f + verify(manager, never()).recycleStickyItem(any()) + } + + @Test + fun `GIVEN SILLM WHEN createStickyView is called THEN a new View is created and cached in stickyItemView`() { + manager = spy(manager) + val adapter: FakeStickyItemsAdapter = mock() + manager.listAdapter = adapter + val recycler: RecyclerView.Recycler = mock() + val newStickyView: View = mock() + doReturn(newStickyView).`when`(recycler).getViewForPosition(ArgumentMatchers.anyInt()) + doNothing().`when`(manager).addView(any()) + doNothing().`when`(manager).measureAndLayout(any()) + doNothing().`when`(manager).ignoreView(any()) + + manager.createStickyView(recycler, 22) + + verify(adapter).setupStickyItem(newStickyView) + verify(manager).addView(newStickyView) + verify(manager).measureAndLayout(newStickyView) + verify(manager).ignoreView(newStickyView) + assertSame(newStickyView, manager.stickyItemView) + } + + @Test + fun `GIVEN a SILLM WHEN bindStickyItem is called for a new View THEN the view is measured and layout`() { + manager = spy(manager) + val view: View = mock() + doNothing().`when`(manager).measureAndLayout(any()) + + manager.bindStickyItem(view) + + verify(manager).measureAndLayout(view) + } + + @Test + fun `GIVEN a pending scroll WHEN bindStickyItem is called for a new View THEN a OnGlobalLayoutListener is set`() { + manager = spy(manager) + manager.scrollPosition = 22 + val view: View = mock() + val viewObserver: ViewTreeObserver = mock() + doReturn(viewObserver).`when`(view).viewTreeObserver + doNothing().`when`(manager).measureAndLayout(any()) + + manager.bindStickyItem(view) + + verify(manager).measureAndLayout(view) + verify(viewObserver).addOnGlobalLayoutListener(any()) + } + + @Test + fun `GIVEN no pending scroll WHEN bindStickyItem is called for a new View THEN no OnGlobalLayoutListener is set`() { + manager = spy(manager) + val view: View = mock() + val viewObserver: ViewTreeObserver = mock() + doReturn(viewObserver).`when`(view).viewTreeObserver + doNothing().`when`(manager).measureAndLayout(any()) + + manager.bindStickyItem(view) + + verify(manager).measureAndLayout(view) + verify(viewObserver, never()).addOnGlobalLayoutListener(any()) + } + + @Test + fun `GIVEN a SILLM WHEN measureAndLayout is called for a new View THEN it is measured and layout`() { + manager = spy(manager) + val newView: View = mock() + doReturn(22).`when`(manager).paddingLeft + doReturn(33).`when`(manager).paddingRight + doReturn(100).`when`(manager).width + doReturn(112).`when`(newView).measuredHeight + + doNothing().`when`(manager).measureChildWithMargins(any(), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt()) + + manager.measureAndLayout(newView) + + verify(manager).measureChildWithMargins(newView, 0, 0) + verify(newView).layout(22, 0, (100 - 33), 112) + } + + @Test + fun `GIVEN a SILLM WHEN recycleStickyItem is called THEN the view holder is reset and allowed to be recycled`() { + manager = spy(manager) + val stickyView: View = mock() + manager.stickyItemView = stickyView + val adapter: FakeStickyItemsAdapter = mock() + manager.listAdapter = adapter + val recycler: RecyclerView.Recycler = mock() + val captor = argumentCaptor<View>() + doNothing().`when`(manager).stopIgnoringView(any()) + doNothing().`when`(manager).removeView(any()) + + manager.recycleStickyItem(recycler) + + verify(adapter).tearDownStickyItem(captor.capture()) + verify(manager).stopIgnoringView(captor.value) + verify(manager).removeView(captor.value) + verify(recycler).recycleView(captor.value) + } + + @Test + fun `GIVEN a SILLM WHEN is called with a new position and offset THEN they are cached in scrollPosition and scrollOffset properties`() { + manager.setScrollState(222, 333) + + assertEquals(222, manager.scrollPosition) + assertEquals(333, manager.scrollOffset) + } + + @Test + fun `GIVEN an ItemPositionsAdapterDataObserver WHEN onChanged is called THEN handleChange() is delegated`() { + val observer = spy(manager.stickyItemPositionsObserver) + manager.stickyItemPositionsObserver = observer + + observer.onChanged() + + verify(observer).handleChange() + } + + @Test + fun `GIVEN an ItemPositionsAdapterDataObserver WHEN onItemRangeInserted is called THEN handleChange() is delegated`() { + val observer = spy(manager.stickyItemPositionsObserver) + manager.stickyItemPositionsObserver = observer + + observer.onItemRangeInserted(22, 33) + + verify(observer).handleChange() + } + + @Test + fun `GIVEN an ItemPositionsAdapterDataObserver WHEN onItemRangeRemoved is called THEN handleChange() is delegated`() { + val observer = spy(manager.stickyItemPositionsObserver) + manager.stickyItemPositionsObserver = observer + + observer.onItemRangeRemoved(22, 33) + + verify(observer).handleChange() + } + + @Test + fun `GIVEN an ItemPositionsAdapterDataObserver WHEN onItemRangeMoved is called THEN handleChange() is delegated`() { + val observer = spy(manager.stickyItemPositionsObserver) + manager.stickyItemPositionsObserver = observer + + observer.onItemRangeMoved(11, 22, 33) + + verify(observer).handleChange() + } + + @Test + fun `GIVEN an ItemPositionsAdapterDataObserver WHEN handleChange is called THEN the sticky item is updated`() { + manager = spy(manager) + val adapter: FakeStickyItemsAdapter = mock() + manager.listAdapter = adapter + val stickyView: View = mock() + manager.stickyItemView = stickyView + val observer = spy(manager.ItemPositionsAdapterDataObserver()) + manager.stickyItemPositionsObserver = observer + doReturn(23).`when`(observer).calculateNewStickyItemPosition(any()) + doNothing().`when`(manager).recycleStickyItem(any()) + + observer.handleChange() + + verify(observer).calculateNewStickyItemPosition(adapter) + verify(manager).recycleStickyItem(null) + } + + @Test + fun `GIVEN an ItemPositionsAdapterDataObserver WHEN calculateNewStickyItemPosition is called for a top item the sticky position is first in adaptor`() { + manager = spy(FakeStickyItemLayoutManager(mock(), StickyItemPlacement.TOP)) + val adapter: FakeStickyItemsAdapter = mock() + doReturn(true).`when`(adapter).isStickyItem(3) + doReturn(true).`when`(adapter).isStickyItem(5) + doReturn(10).`when`(manager).itemCount + manager.stickyItemPositionsObserver = manager.ItemPositionsAdapterDataObserver() + + assertEquals(3, manager.stickyItemPositionsObserver.calculateNewStickyItemPosition(adapter)) + } + + @Test + fun `GIVEN an ItemPositionsAdapterDataObserver WHEN calculateNewStickyItemPosition is called for a bottom item the sticky position is last in adaptor`() { + manager = spy(FakeStickyItemLayoutManager(mock(), StickyItemPlacement.BOTTOM)) + val adapter: FakeStickyItemsAdapter = mock() + doReturn(true).`when`(adapter).isStickyItem(3) + doReturn(true).`when`(adapter).isStickyItem(5) + doReturn(10).`when`(manager).itemCount + manager.stickyItemPositionsObserver = manager.ItemPositionsAdapterDataObserver() + + assertEquals(5, manager.stickyItemPositionsObserver.calculateNewStickyItemPosition(adapter)) + } + + @Test + fun `WHEN get is called for a reversed StickyItemPlacement#TOP layout manager THEN a StickyHeaderLinearLayoutManager is returned`() { + val result = StickyItemsLinearLayoutManager.get<FakeStickyItemsAdapter>( + mock(), + StickyItemPlacement.TOP, + true, + ) + + assertTrue(result is StickyHeaderLinearLayoutManager) + assertTrue(result.reverseLayout) + } + + @Test + fun `WHEN get is called for a not reversed StickyItemPlacement#TOP layout manager THEN a StickyHeaderLinearLayoutManager is returned`() { + val result = StickyItemsLinearLayoutManager.get<FakeStickyItemsAdapter>( + mock(), + StickyItemPlacement.TOP, + false, + ) + + assertTrue(result is StickyHeaderLinearLayoutManager) + assertFalse(result.reverseLayout) + } + + @Test + fun `WHEN get is called for a reversed StickyItemPlacement#BOTTOM layout manager THEN a StickyFooterLinearLayoutManager is returned`() { + val result = StickyItemsLinearLayoutManager.get<FakeStickyItemsAdapter>( + mock(), + StickyItemPlacement.BOTTOM, + true, + ) + + assertTrue(result is StickyFooterLinearLayoutManager) + assertTrue(result.reverseLayout) + } + + @Test + fun `WHEN get is called for a not reversed StickyItemPlacement#BOTTOM layout manager THEN a StickyFooterLinearLayoutManager is returned`() { + val result = StickyItemsLinearLayoutManager.get<FakeStickyItemsAdapter>( + mock(), + StickyItemPlacement.BOTTOM, + false, + ) + + assertTrue(result is StickyFooterLinearLayoutManager) + assertFalse(result.reverseLayout) + } +} diff --git a/mobile/android/android-components/components/browser/menu/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/browser/menu/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..49324d83c5 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1,3 @@ +mock-maker-inline +// This allows mocking final classes (classes are final by default in Kotlin) + diff --git a/mobile/android/android-components/components/browser/menu/src/test/resources/mockito-extensions/robolectric.properties b/mobile/android/android-components/components/browser/menu/src/test/resources/mockito-extensions/robolectric.properties new file mode 100644 index 0000000000..932b01b9eb --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/resources/mockito-extensions/robolectric.properties @@ -0,0 +1 @@ +sdk=28 diff --git a/mobile/android/android-components/components/browser/menu/src/test/resources/robolectric.properties b/mobile/android/android-components/components/browser/menu/src/test/resources/robolectric.properties new file mode 100644 index 0000000000..932b01b9eb --- /dev/null +++ b/mobile/android/android-components/components/browser/menu/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +sdk=28 |