summaryrefslogtreecommitdiffstats
path: root/mobile/android/android-components/components/concept/toolbar
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/android-components/components/concept/toolbar')
-rw-r--r--mobile/android/android-components/components/concept/toolbar/README.md19
-rw-r--r--mobile/android/android-components/components/concept/toolbar/build.gradle39
-rw-r--r--mobile/android/android-components/components/concept/toolbar/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/concept/toolbar/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/AutocompleteDelegate.kt24
-rw-r--r--mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/AutocompleteProvider.kt36
-rw-r--r--mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/AutocompleteResult.kt22
-rw-r--r--mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/ScrollableToolbar.kt34
-rw-r--r--mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/Toolbar.kt563
-rw-r--r--mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionButtonTest.kt76
-rw-r--r--mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionImageTest.kt84
-rw-r--r--mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionSpaceTest.kt47
-rw-r--r--mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionToggleButtonTest.kt213
-rw-r--r--mobile/android/android-components/components/concept/toolbar/src/test/resources/robolectric.properties1
14 files changed, 1183 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/concept/toolbar/README.md b/mobile/android/android-components/components/concept/toolbar/README.md
new file mode 100644
index 0000000000..60e050ce0d
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Concept > Toolbar
+
+Abstract definition of a browser toolbar component.
+
+## 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:concept-toolbar:{latest-version}"
+```
+
+## 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/concept/toolbar/build.gradle b/mobile/android/android-components/components/concept/toolbar/build.gradle
new file mode 100644
index 0000000000..54e303cd11
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/build.gradle
@@ -0,0 +1,39 @@
+/* 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'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.concept.toolbar'
+}
+
+dependencies {
+ implementation ComponentsDependencies.androidx_annotation
+ implementation ComponentsDependencies.androidx_appcompat
+ implementation ComponentsDependencies.androidx_core_ktx
+ api project(':support-base')
+ implementation project(':support-ktx')
+
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation project(':support-test')
+}
+
+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/concept/toolbar/proguard-rules.pro b/mobile/android/android-components/components/concept/toolbar/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/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/concept/toolbar/src/main/AndroidManifest.xml b/mobile/android/android-components/components/concept/toolbar/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+<!-- 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 />
diff --git a/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/AutocompleteDelegate.kt b/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/AutocompleteDelegate.kt
new file mode 100644
index 0000000000..ec68a17633
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/AutocompleteDelegate.kt
@@ -0,0 +1,24 @@
+/* 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.concept.toolbar
+
+/**
+ * Describes an object to which a [AutocompleteResult] may be applied.
+ * Usually, this will delegate to a specific text view.
+ */
+interface AutocompleteDelegate {
+ /**
+ * @param result Apply result of autocompletion.
+ * @param onApplied a lambda/callback invoked if (and only if) the result has been
+ * applied. A result may be discarded by implementations because it is stale or
+ * the autocomplete request has been cancelled.
+ */
+ fun applyAutocompleteResult(result: AutocompleteResult, onApplied: () -> Unit = { })
+
+ /**
+ * Autocompletion was invoked and no match was returned.
+ */
+ fun noAutocompleteResult(input: String)
+}
diff --git a/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/AutocompleteProvider.kt b/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/AutocompleteProvider.kt
new file mode 100644
index 0000000000..0534bbc007
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/AutocompleteProvider.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.concept.toolbar
+
+/**
+ * Object providing autocomplete suggestions for the toolbar.
+ * More such objects can be set for the same toolbar with each getting results from a different source.
+ * If more providers are used the [autocompletePriority] property allows to easily set an order
+ * for the results and the suggestion of which provider should be tried to be applied first.
+ */
+interface AutocompleteProvider : Comparable<AutocompleteProvider> {
+ /**
+ * Retrieves an autocomplete suggestion which best matches [query].
+ *
+ * @param query Segment of text to be autocompleted.
+ *
+ * @return Optional domain URL which best matches the query.
+ */
+ suspend fun getAutocompleteSuggestion(query: String): AutocompleteResult?
+
+ /**
+ * Order in which this provider will be queried for autocomplete suggestions in relation ot others.
+ * - a lower priority means that this provider must be called before others with a higher priority.
+ * - an equal priority offers no ordering guarantees.
+ *
+ * Defaults to `0`.
+ */
+ val autocompletePriority: Int
+ get() = 0
+
+ override fun compareTo(other: AutocompleteProvider): Int {
+ return autocompletePriority - other.autocompletePriority
+ }
+}
diff --git a/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/AutocompleteResult.kt b/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/AutocompleteResult.kt
new file mode 100644
index 0000000000..145188c4d4
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/AutocompleteResult.kt
@@ -0,0 +1,22 @@
+/* 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.concept.toolbar
+
+/**
+ * Describes an autocompletion result.
+ *
+ * @property input Input for which this AutocompleteResult is being provided.
+ * @property text AutocompleteResult of autocompletion, text to be displayed.
+ * @property url AutocompleteResult of autocompletion, full matching url.
+ * @property source Name of the autocompletion source.
+ * @property totalItems A total number of results also available.
+ */
+data class AutocompleteResult(
+ val input: String,
+ val text: String,
+ val url: String,
+ val source: String,
+ val totalItems: Int,
+)
diff --git a/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/ScrollableToolbar.kt b/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/ScrollableToolbar.kt
new file mode 100644
index 0000000000..86af351c26
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/ScrollableToolbar.kt
@@ -0,0 +1,34 @@
+/* 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.concept.toolbar
+
+/**
+ * Interface to be implemented by components that provide hiding-on-scroll toolbar functionality.
+ */
+interface ScrollableToolbar {
+
+ /**
+ * Enable scrolling of the dynamic toolbar. Restore this functionality after [disableScrolling] stopped it.
+ *
+ * The toolbar may have other intrinsic checks depending on which the toolbar will be animated or not.
+ */
+ fun enableScrolling()
+
+ /**
+ * Completely disable scrolling of the dynamic toolbar.
+ * Use [enableScrolling] to restore the functionality.
+ */
+ fun disableScrolling()
+
+ /**
+ * Force the toolbar to expand.
+ */
+ fun expand()
+
+ /**
+ * Force the toolbar to collapse. Only if dynamic.
+ */
+ fun collapse()
+}
diff --git a/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/Toolbar.kt b/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/Toolbar.kt
new file mode 100644
index 0000000000..55244b4f6b
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/Toolbar.kt
@@ -0,0 +1,563 @@
+/* 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.concept.toolbar
+
+import android.graphics.drawable.Drawable
+import android.view.View
+import android.view.View.NO_ID
+import android.view.ViewGroup
+import android.widget.ImageButton
+import android.widget.ImageView
+import androidx.annotation.ColorRes
+import androidx.annotation.Dimension
+import androidx.annotation.Dimension.Companion.DP
+import androidx.annotation.DrawableRes
+import androidx.appcompat.widget.AppCompatImageButton
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.core.content.ContextCompat
+import mozilla.components.support.base.android.Padding
+import mozilla.components.support.ktx.android.content.res.resolveAttribute
+import mozilla.components.support.ktx.android.view.setPadding
+import java.lang.ref.WeakReference
+
+/**
+ * Interface to be implemented by components that provide browser toolbar functionality.
+ */
+@Suppress("TooManyFunctions")
+interface Toolbar : ScrollableToolbar {
+ /**
+ * Sets/Gets the title to be displayed on the toolbar.
+ */
+ var title: String
+
+ /**
+ * Sets/Gets the URL to be displayed on the toolbar.
+ */
+ var url: CharSequence
+
+ /**
+ * Sets/gets private mode.
+ *
+ * In private mode the IME should not update any personalized data such as typing history and personalized language
+ * model based on what the user typed.
+ */
+ var private: Boolean
+
+ /**
+ * Sets/Gets the site security to be displayed on the toolbar.
+ */
+ var siteSecure: SiteSecurity
+
+ /**
+ * Sets/Gets the highlight icon to be displayed on the toolbar.
+ */
+ var highlight: Highlight
+
+ /**
+ * Sets/Gets the site tracking protection state to be displayed on the toolbar.
+ */
+ var siteTrackingProtection: SiteTrackingProtection
+
+ /**
+ * Displays the currently used search terms as part of this Toolbar.
+ *
+ * @param searchTerms the search terms used by the current session
+ */
+ fun setSearchTerms(searchTerms: String)
+
+ /**
+ * Displays the given loading progress. Expects values in the range [0, 100].
+ */
+ fun displayProgress(progress: Int)
+
+ /**
+ * Should be called by an activity when the user pressed the back key of the device.
+ *
+ * @return Returns true if the back press event was handled and should not be propagated further.
+ */
+ fun onBackPressed(): Boolean
+
+ /**
+ * Should be called by the host activity when it enters the stop state.
+ */
+ fun onStop()
+
+ /**
+ * Registers the given function to be invoked when the user selected a new URL i.e. is done
+ * editing.
+ *
+ * If the function returns `true` then the toolbar will automatically switch to "display mode". Otherwise it
+ * remains in "edit mode".
+ *
+ * @param listener the listener function
+ */
+ fun setOnUrlCommitListener(listener: (String) -> Boolean)
+
+ /**
+ * Registers the given function to be invoked when users changes text in the toolbar.
+ *
+ * @param filter A function which will perform autocompletion and send results to [AutocompleteDelegate].
+ */
+ fun setAutocompleteListener(filter: suspend (String, AutocompleteDelegate) -> Unit)
+
+ /**
+ * Attempt to restart the autocomplete functionality with the current user input.
+ */
+ fun refreshAutocomplete() = Unit
+
+ /**
+ * Adds an action to be displayed on the right side of the toolbar in display mode.
+ *
+ * Related:
+ * https://developer.mozilla.org/en-US/Add-ons/WebExtensions/user_interface/Browser_action
+ */
+ fun addBrowserAction(action: Action)
+
+ /**
+ * Removes a previously added browser action (see [addBrowserAction]). If the the provided
+ * actions was never added, this method has no effect.
+ *
+ * @param action the action to remove.
+ */
+ fun removeBrowserAction(action: Action)
+
+ /**
+ * Removes a previously added page action (see [addBrowserAction]). If the the provided
+ * actions was never added, this method has no effect.
+ *
+ * @param action the action to remove.
+ */
+ fun removePageAction(action: Action)
+
+ /**
+ * Removes a previously added navigation action (see [addNavigationAction]). If the the provided
+ * actions was never added, this method has no effect.
+ *
+ * @param action the action to remove.
+ */
+ fun removeNavigationAction(action: Action)
+
+ /**
+ * Declare that the actions (navigation actions, browser actions, page actions) have changed and
+ * should be updated if needed.
+ */
+ fun invalidateActions()
+
+ /**
+ * Adds an action to be displayed on the right side of the URL in display mode.
+ *
+ * Related:
+ * https://developer.mozilla.org/en-US/Add-ons/WebExtensions/user_interface/Page_actions
+ */
+ fun addPageAction(action: Action)
+
+ /**
+ * Adds an action to be displayed on the far left side of the URL in display mode.
+ */
+ fun addNavigationAction(action: Action)
+
+ /**
+ * Adds an action to be displayed at the start of the URL in edit mode.
+ */
+ fun addEditActionStart(action: Action)
+
+ /**
+ * Adds an action to be displayed at the end of the URL in edit mode.
+ */
+ fun addEditActionEnd(action: Action)
+
+ /**
+ * Removes an action at the end of the URL in edit mode.
+ */
+ fun removeEditActionEnd(action: Action)
+
+ /**
+ * Hides the menu button in display mode.
+ */
+ fun hideMenuButton()
+
+ /**
+ * Shows the menu button in display mode.
+ */
+ fun showMenuButton()
+
+ /**
+ * Sets the horizontal padding in display mode.
+ */
+ fun setDisplayHorizontalPadding(horizontalPadding: Int)
+
+ /**
+ * Hides the page action separator in display mode.
+ */
+ fun hidePageActionSeparator()
+
+ /**
+ * Shows the page action separator in display mode.
+ */
+ fun showPageActionSeparator()
+
+ /**
+ * Casts this toolbar to an Android View object.
+ */
+ fun asView(): View = this as View
+
+ /**
+ * Registers the given listener to be invoked when the user edits the URL.
+ */
+ fun setOnEditListener(listener: OnEditListener)
+
+ /**
+ * Switches to URL displaying mode (from editing mode) if supported by the toolbar implementation.
+ */
+ fun displayMode()
+
+ /**
+ * Switches to URL editing mode (from display mode) if supported by the toolbar implementation,
+ * and focuses the URL input field based on the cursor selection.
+ *
+ * @param cursorPlacement Where the cursor should be set after focusing on the URL input field.
+ */
+ fun editMode(cursorPlacement: CursorPlacement = CursorPlacement.ALL)
+
+ /**
+ * Dismisses the display toolbar popup menu
+ */
+ fun dismissMenu()
+
+ /**
+ * Listener to be invoked when the user edits the URL.
+ */
+ interface OnEditListener {
+ /**
+ * Fired when the toolbar switches to edit mode.
+ */
+ fun onStartEditing() = Unit
+
+ /**
+ * Fired when the user presses the back button while in edit mode.
+ */
+ fun onCancelEditing(): Boolean = true
+
+ /**
+ * Fired when the toolbar switches back to display mode.
+ */
+ fun onStopEditing() = Unit
+
+ /**
+ * Fired whenever the user changes the text in the address bar.
+ */
+ fun onTextChanged(text: String) = Unit
+
+ /**
+ * Fired when user clears input by tapping the clear input button.
+ */
+ fun onInputCleared() = Unit
+ }
+
+ /**
+ * Generic interface for actions to be added to the toolbar.
+ */
+ interface Action {
+ val visible: () -> Boolean
+ get() = { true }
+
+ val autoHide: () -> Boolean
+ get() = { false }
+
+ val weight: () -> Int
+ get() = { -1 }
+
+ fun createView(parent: ViewGroup): View
+
+ fun bind(view: View)
+ }
+
+ /**
+ * An action button to be added to the toolbar.
+ *
+ * @param imageDrawable The drawable to be shown.
+ * @param contentDescription The content description to use.
+ * @param visible Lambda that returns true or false to indicate whether this button should be shown.
+ * @param autoHide Lambda that returns true or false to indicate whether this button should auto hide.
+ * @param weight Lambda that returns an integer to indicate weight of an action. The lesser the weight,
+ * the closer it is to the url. A default weight -1 indicates, the position is not cared for
+ * and action will be appended at the end.
+ * @param padding A optional custom padding.
+ * @param iconTintColorResource Optional ID of color resource to tint the icon.
+ * @param longClickListener Callback that will be invoked whenever the button is long-pressed.
+ * @param listener Callback that will be invoked whenever the button is pressed
+ */
+ open class ActionButton(
+ val imageDrawable: Drawable? = null,
+ val contentDescription: String,
+ override val visible: () -> Boolean = { true },
+ override val autoHide: () -> Boolean = { false },
+ override val weight: () -> Int = { -1 },
+ private val background: Int = 0,
+ private val padding: Padding? = null,
+ @ColorRes val iconTintColorResource: Int = ViewGroup.NO_ID,
+ private val longClickListener: (() -> Unit)? = null,
+ private val listener: () -> Unit,
+ ) : Action {
+ private var view: WeakReference<AppCompatImageButton>? = null
+
+ override fun createView(parent: ViewGroup): View =
+ AppCompatImageButton(parent.context).also { imageButton ->
+ view = WeakReference(imageButton)
+
+ imageButton.setImageDrawable(imageDrawable)
+ imageButton.contentDescription = contentDescription
+ imageButton.setTintResource(iconTintColorResource)
+ imageButton.setOnClickListener { listener.invoke() }
+ imageButton.setOnLongClickListener {
+ longClickListener?.invoke()
+ true
+ }
+ imageButton.isLongClickable = longClickListener != null
+
+ val backgroundResource = if (background == 0) {
+ parent.context.theme.resolveAttribute(android.R.attr.selectableItemBackgroundBorderless)
+ } else {
+ background
+ }
+
+ imageButton.setBackgroundResource(backgroundResource)
+ padding?.let { imageButton.setPadding(it) }
+ }
+
+ /**
+ * Changes the content description and the tint colour of the view.
+ *
+ * @param contentDescription The content description to use.
+ * @param tintColorResource ID of color resource to tint the icon.
+ */
+ fun updateView(
+ contentDescription: String? = null,
+ @ColorRes tintColorResource: Int = ViewGroup.NO_ID,
+ ) {
+ view?.get()?.let {
+ it.contentDescription = contentDescription
+ it.setTintResource(tintColorResource)
+ }
+ }
+
+ override fun bind(view: View) = Unit
+ }
+
+ /**
+ * An action button with two states, selected and unselected. When the button is pressed, the
+ * state changes automatically.
+ *
+ * @param imageDrawable The drawable to be shown if the button is in unselected state.
+ * @param imageSelectedDrawable The drawable to be shown if the button is in selected state.
+ * @param contentDescription The content description to use if the button is in unselected state.
+ * @param contentDescriptionSelected The content description to use if the button is in selected state.
+ * @param visible Lambda that returns true or false to indicate whether this button should be shown.
+ * @param weight Lambda that returns an integer to indicate weight of an action. The lesser the weight,
+ * the closer it is to the url. A default weight -1 indicates, the position is not cared for
+ * and action will be appended at the end.
+ * @param selected Sets whether this button should be selected initially.
+ * @param padding A optional custom padding.
+ * @param listener Callback that will be invoked whenever the checked state changes.
+ */
+ open class ActionToggleButton(
+ internal val imageDrawable: Drawable,
+ internal val imageSelectedDrawable: Drawable,
+ private val contentDescription: String,
+ private val contentDescriptionSelected: String,
+ override val visible: () -> Boolean = { true },
+ override val weight: () -> Int = { -1 },
+ private var selected: Boolean = false,
+ @DrawableRes private val background: Int = 0,
+ private val padding: Padding? = null,
+ private val listener: (Boolean) -> Unit,
+ ) : Action {
+ private var view: WeakReference<ImageButton>? = null
+
+ override fun createView(parent: ViewGroup): View = AppCompatImageButton(parent.context).also { imageButton ->
+ view = WeakReference(imageButton)
+
+ imageButton.scaleType = ImageView.ScaleType.CENTER
+ imageButton.setOnClickListener { toggle() }
+ imageButton.isSelected = selected
+
+ updateViewState()
+
+ val backgroundResource = if (background == 0) {
+ parent.context.theme.resolveAttribute(android.R.attr.selectableItemBackgroundBorderless)
+ } else {
+ background
+ }
+
+ imageButton.setBackgroundResource(backgroundResource)
+ padding?.let { imageButton.setPadding(it) }
+ }
+
+ /**
+ * Changes the selected state of the action to the inverse of its current state.
+ *
+ * @param notifyListener If true (default) the listener will be notified about the state change.
+ */
+ fun toggle(notifyListener: Boolean = true) {
+ setSelected(!selected, notifyListener)
+ }
+
+ /**
+ * Changes the selected state of the action.
+ *
+ * @param selected The new selected state
+ * @param notifyListener If true (default) the listener will be notified about a state change.
+ */
+ fun setSelected(selected: Boolean, notifyListener: Boolean = true) {
+ if (this.selected == selected) {
+ // Nothing to do here.
+ return
+ }
+
+ this.selected = selected
+ updateViewState()
+
+ if (notifyListener) {
+ listener.invoke(selected)
+ }
+ }
+
+ /**
+ * Returns the current selected state of the action.
+ */
+ fun isSelected() = selected
+
+ private fun updateViewState() {
+ view?.get()?.let {
+ it.isSelected = selected
+
+ if (selected) {
+ it.setImageDrawable(imageSelectedDrawable)
+ it.contentDescription = contentDescriptionSelected
+ } else {
+ it.setImageDrawable(imageDrawable)
+ it.contentDescription = contentDescription
+ }
+ }
+ }
+
+ override fun bind(view: View) = Unit
+ }
+
+ /**
+ * An "empty" action with a desired width to be used as "placeholder".
+ *
+ * @param desiredWidth The desired width in density independent pixels for this action.
+ * @param padding A optional custom padding.
+ */
+ open class ActionSpace(
+ @Dimension(unit = DP) private val desiredWidth: Int,
+ private val padding: Padding? = null,
+ ) : Action {
+ override fun createView(parent: ViewGroup): View = View(parent.context).apply {
+ minimumWidth = desiredWidth
+ padding?.let { this.setPadding(it) }
+ }
+
+ override fun bind(view: View) = Unit
+ }
+
+ /**
+ * An action that just shows a static, non-clickable image.
+ *
+ * @param imageDrawable The drawable to be shown.
+ * @param contentDescription Optional content description to be used. If no content description
+ * is provided then this view will be treated as not important for
+ * accessibility.
+ * @param padding A optional custom padding.
+ */
+ open class ActionImage(
+ private val imageDrawable: Drawable,
+ private val contentDescription: String? = null,
+ private val padding: Padding? = null,
+ ) : Action {
+
+ override fun createView(parent: ViewGroup): View = AppCompatImageView(parent.context).also { image ->
+ image.minimumWidth = imageDrawable.intrinsicWidth
+ image.setImageDrawable(imageDrawable)
+
+ image.contentDescription = contentDescription
+ image.importantForAccessibility = if (contentDescription.isNullOrEmpty()) {
+ View.IMPORTANT_FOR_ACCESSIBILITY_NO
+ } else {
+ View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
+ }
+ padding?.let { pd -> image.setPadding(pd) }
+ }
+
+ override fun bind(view: View) = Unit
+ }
+
+ enum class SiteSecurity {
+ INSECURE,
+ SECURE,
+ }
+
+ /**
+ * Indicates which tracking protection status a site has.
+ */
+ enum class SiteTrackingProtection {
+ /**
+ * The site has tracking protection enabled, but none trackers have been blocked or detected.
+ */
+ ON_NO_TRACKERS_BLOCKED,
+
+ /**
+ * The site has tracking protection enabled, and trackers have been blocked or detected.
+ */
+ ON_TRACKERS_BLOCKED,
+
+ /**
+ * Tracking protection has been disabled for a specific site.
+ */
+ OFF_FOR_A_SITE,
+
+ /**
+ * Tracking protection has been disabled for all sites.
+ */
+ OFF_GLOBALLY,
+ }
+
+ /**
+ * Indicates the reason why a highlight icon is shown or hidden.
+ */
+ enum class Highlight {
+ /**
+ * The site has changed its permissions from their default values.
+ */
+ PERMISSIONS_CHANGED,
+
+ /**
+ * The site does not show a dot indicator.
+ */
+ NONE,
+ }
+
+ /**
+ * Indicates where the cursor should be set after focusing on the URL input field.
+ */
+ enum class CursorPlacement {
+ /**
+ * All of the text in the input field should be selected.
+ */
+ ALL,
+
+ /**
+ * No text should be selected and the cursor should be placed at the end of the text.
+ */
+ END,
+ }
+}
+
+private fun AppCompatImageButton.setTintResource(@ColorRes tintColorResource: Int) {
+ if (tintColorResource != NO_ID) {
+ imageTintList = ContextCompat.getColorStateList(context, tintColorResource)
+ }
+}
diff --git a/mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionButtonTest.kt b/mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionButtonTest.kt
new file mode 100644
index 0000000000..ddcbccdfe9
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionButtonTest.kt
@@ -0,0 +1,76 @@
+/* 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.concept.toolbar
+
+import android.widget.LinearLayout
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.base.android.Padding
+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.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ActionButtonTest {
+
+ @Test
+ fun `set padding`() {
+ var button = Toolbar.ActionButton(mock(), "imageResource") {}
+ val linearLayout = LinearLayout(testContext)
+ var view = button.createView(linearLayout)
+
+ assertEquals(view.paddingLeft, 0)
+ assertEquals(view.paddingTop, 0)
+ assertEquals(view.paddingRight, 0)
+ assertEquals(view.paddingBottom, 0)
+
+ button = Toolbar.ActionButton(
+ mock(),
+ "imageResource",
+ padding = Padding(16, 20, 24, 28),
+ ) {}
+
+ view = button.createView(linearLayout)
+ view.paddingLeft
+ assertEquals(view.paddingLeft, 16)
+ assertEquals(view.paddingTop, 20)
+ assertEquals(view.paddingRight, 24)
+ assertEquals(view.paddingBottom, 28)
+ }
+
+ @Test
+ fun `constructor with drawables`() {
+ val visibilityListener = { false }
+ val button = Toolbar.ActionButton(
+ mock(),
+ "image",
+ visibilityListener,
+ { false },
+ { -1 },
+ 0,
+ null,
+ ) { }
+ assertNotNull(button.imageDrawable)
+ assertEquals("image", button.contentDescription)
+ assertEquals(visibilityListener, button.visible)
+ assertEquals(Unit, button.bind(mock()))
+
+ val buttonVisibility = Toolbar.ActionButton(mock(), "image") {}
+ assertEquals(true, buttonVisibility.visible())
+ }
+
+ @Test
+ fun `set contentDescription`() {
+ val button = Toolbar.ActionButton(mock(), "image") { }
+ val linearLayout = LinearLayout(testContext)
+ val view = button.createView(linearLayout)
+
+ button.updateView("contentDescription")
+
+ assertEquals("contentDescription", view.contentDescription)
+ }
+}
diff --git a/mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionImageTest.kt b/mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionImageTest.kt
new file mode 100644
index 0000000000..2992103063
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionImageTest.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.concept.toolbar
+
+import android.graphics.drawable.Drawable
+import android.view.View
+import android.view.ViewGroup
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.base.android.Padding
+import mozilla.components.support.test.mock
+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.`when`
+
+@RunWith(AndroidJUnit4::class)
+class ActionImageTest {
+
+ @Test
+ fun `setting minimumWidth`() {
+ val drawable: Drawable = mock()
+ val image = Toolbar.ActionImage(drawable)
+ val emptyImage = Toolbar.ActionImage(mock())
+
+ val viewGroup: ViewGroup = mock()
+ `when`(viewGroup.context).thenReturn(testContext)
+ `when`(drawable.intrinsicWidth).thenReturn(5)
+
+ val emptyImageView = emptyImage.createView(viewGroup)
+ assertEquals(0, emptyImageView.minimumWidth)
+
+ val imageView = image.createView(viewGroup)
+ assertTrue(imageView.minimumWidth != 0)
+ }
+
+ @Test
+ fun `accessibility description provided`() {
+ val image = Toolbar.ActionImage(mock())
+ var imageAccessible = Toolbar.ActionImage(mock(), "image")
+ val viewGroup: ViewGroup = mock()
+ `when`(viewGroup.context).thenReturn(testContext)
+
+ val imageView = image.createView(viewGroup)
+ assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_NO, imageView.importantForAccessibility)
+
+ var imageViewAccessible = imageAccessible.createView(viewGroup)
+ assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, imageViewAccessible.importantForAccessibility)
+
+ imageAccessible = Toolbar.ActionImage(mock(), "")
+ imageViewAccessible = imageAccessible.createView(viewGroup)
+ assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_NO, imageViewAccessible.importantForAccessibility)
+ }
+
+ @Test
+ fun `bind is not implemented`() {
+ val button = Toolbar.ActionImage(mock())
+ assertEquals(Unit, button.bind(mock()))
+ }
+
+ @Test
+ fun `padding is set`() {
+ var image = Toolbar.ActionImage(mock())
+ val viewGroup: ViewGroup = mock()
+ `when`(viewGroup.context).thenReturn(testContext)
+ var view = image.createView(viewGroup)
+
+ assertEquals(view.paddingLeft, 0)
+ assertEquals(view.paddingTop, 0)
+ assertEquals(view.paddingRight, 0)
+ assertEquals(view.paddingBottom, 0)
+
+ image = Toolbar.ActionImage(mock(), padding = Padding(16, 20, 24, 28))
+
+ view = image.createView(viewGroup)
+ assertEquals(view.paddingLeft, 16)
+ assertEquals(view.paddingTop, 20)
+ assertEquals(view.paddingRight, 24)
+ assertEquals(view.paddingBottom, 28)
+ }
+}
diff --git a/mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionSpaceTest.kt b/mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionSpaceTest.kt
new file mode 100644
index 0000000000..6c4d3da1b9
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionSpaceTest.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.concept.toolbar
+
+import android.widget.LinearLayout
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.base.android.Padding
+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
+
+@RunWith(AndroidJUnit4::class)
+class ActionSpaceTest {
+
+ @Test
+ fun `Toolbar ActionSpace must set padding`() {
+ var space = Toolbar.ActionSpace(0)
+ val linearLayout = LinearLayout(testContext)
+ var view = space.createView(linearLayout)
+
+ assertEquals(view.paddingLeft, 0)
+ assertEquals(view.paddingTop, 0)
+ assertEquals(view.paddingRight, 0)
+ assertEquals(view.paddingBottom, 0)
+
+ space = Toolbar.ActionSpace(
+ 0,
+ padding = Padding(16, 20, 24, 28),
+ )
+
+ view = space.createView(linearLayout)
+ assertEquals(view.paddingLeft, 16)
+ assertEquals(view.paddingTop, 20)
+ assertEquals(view.paddingRight, 24)
+ assertEquals(view.paddingBottom, 28)
+ }
+
+ @Test
+ fun `bind is not implemented`() {
+ val button = Toolbar.ActionSpace(0)
+ assertEquals(Unit, button.bind(mock()))
+ }
+}
diff --git a/mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionToggleButtonTest.kt b/mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionToggleButtonTest.kt
new file mode 100644
index 0000000000..0c47f626c5
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/src/test/java/mozilla/components/concept/toolbar/ActionToggleButtonTest.kt
@@ -0,0 +1,213 @@
+/* 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.concept.toolbar
+
+import android.widget.FrameLayout
+import android.widget.LinearLayout
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.base.android.Padding
+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 java.util.UUID
+
+@RunWith(AndroidJUnit4::class)
+class ActionToggleButtonTest {
+
+ @Test
+ fun `clicking view will toggle state`() {
+ val button =
+ Toolbar.ActionToggleButton(mock(), mock(), UUID.randomUUID().toString(), UUID.randomUUID().toString()) {}
+ val view = button.createView(FrameLayout(testContext))
+
+ assertFalse(button.isSelected())
+
+ view.performClick()
+
+ assertTrue(button.isSelected())
+
+ view.performClick()
+
+ assertFalse(button.isSelected())
+ }
+
+ @Test
+ fun `clicking view will invoke listener`() {
+ var listenerInvoked = false
+
+ val button =
+ Toolbar.ActionToggleButton(mock(), mock(), UUID.randomUUID().toString(), UUID.randomUUID().toString()) {
+ listenerInvoked = true
+ }
+
+ val view = button.createView(FrameLayout(testContext))
+
+ assertFalse(listenerInvoked)
+
+ view.performClick()
+
+ assertTrue(listenerInvoked)
+ }
+
+ @Test
+ fun `toggle will invoke listener`() {
+ var listenerInvoked = false
+
+ val button =
+ Toolbar.ActionToggleButton(mock(), mock(), UUID.randomUUID().toString(), UUID.randomUUID().toString()) {
+ listenerInvoked = true
+ }
+
+ assertFalse(listenerInvoked)
+
+ button.toggle()
+
+ assertTrue(listenerInvoked)
+ }
+
+ @Test
+ fun `toggle will not invoke listener if notifyListener is set to false`() {
+ var listenerInvoked = false
+
+ val button =
+ Toolbar.ActionToggleButton(mock(), mock(), UUID.randomUUID().toString(), UUID.randomUUID().toString()) {
+ listenerInvoked = true
+ }
+
+ assertFalse(listenerInvoked)
+
+ button.toggle(notifyListener = false)
+
+ assertFalse(listenerInvoked)
+ }
+
+ @Test
+ fun `setSelected will invoke listener`() {
+ var listenerInvoked = false
+
+ val button =
+ Toolbar.ActionToggleButton(mock(), mock(), UUID.randomUUID().toString(), UUID.randomUUID().toString()) {
+ listenerInvoked = true
+ }
+
+ assertFalse(button.isSelected())
+ assertFalse(listenerInvoked)
+
+ button.setSelected(true)
+
+ assertTrue(listenerInvoked)
+ }
+
+ @Test
+ fun `setSelected will not invoke listener if value has not changed`() {
+ var listenerInvoked = false
+
+ val button =
+ Toolbar.ActionToggleButton(mock(), mock(), UUID.randomUUID().toString(), UUID.randomUUID().toString()) {
+ listenerInvoked = true
+ }
+
+ assertFalse(button.isSelected())
+ assertFalse(listenerInvoked)
+
+ button.setSelected(false)
+
+ assertFalse(listenerInvoked)
+ }
+
+ @Test
+ fun `setSelected will not invoke listener if notifyListener is set to false`() {
+ var listenerInvoked = false
+
+ val button =
+ Toolbar.ActionToggleButton(mock(), mock(), UUID.randomUUID().toString(), UUID.randomUUID().toString()) {
+ listenerInvoked = true
+ }
+
+ assertFalse(button.isSelected())
+ assertFalse(listenerInvoked)
+
+ button.setSelected(true, notifyListener = false)
+
+ assertFalse(listenerInvoked)
+ }
+
+ @Test
+ fun `isSelected will always return correct state`() {
+ val button =
+ Toolbar.ActionToggleButton(mock(), mock(), UUID.randomUUID().toString(), UUID.randomUUID().toString()) {}
+ assertFalse(button.isSelected())
+
+ button.toggle()
+ assertTrue(button.isSelected())
+
+ button.setSelected(true)
+ assertTrue(button.isSelected())
+
+ button.setSelected(false)
+ assertFalse(button.isSelected())
+
+ button.setSelected(true, notifyListener = false)
+ assertTrue(button.isSelected())
+
+ button.toggle(notifyListener = false)
+ assertFalse(button.isSelected())
+
+ val view = button.createView(FrameLayout(testContext))
+ view.performClick()
+ assertTrue(button.isSelected())
+ }
+
+ @Test
+ fun `Toolbar ActionToggleButton must set padding`() {
+ var button = Toolbar.ActionToggleButton(mock(), mock(), "imageResource", "") {}
+ val linearLayout = LinearLayout(testContext)
+ var view = button.createView(linearLayout)
+ val padding = Padding(16, 20, 24, 28)
+
+ assertEquals(view.paddingLeft, 0)
+ assertEquals(view.paddingTop, 0)
+ assertEquals(view.paddingRight, 0)
+ assertEquals(view.paddingBottom, 0)
+
+ button = Toolbar.ActionToggleButton(mock(), mock(), "imageResource", "", padding = padding) {}
+
+ view = button.createView(linearLayout)
+ view.paddingLeft
+ assertEquals(view.paddingLeft, 16)
+ assertEquals(view.paddingTop, 20)
+ assertEquals(view.paddingRight, 24)
+ assertEquals(view.paddingBottom, 28)
+ }
+
+ @Test
+ fun `default constructor with drawables`() {
+ var selectedValue = false
+ val visibility = { true }
+ val button = Toolbar.ActionToggleButton(mock(), mock(), "image", "selected", visible = visibility) { value ->
+ selectedValue = value
+ }
+ assertEquals(true, button.visible())
+ assertNotNull(button.imageDrawable)
+ assertNotNull(button.imageSelectedDrawable)
+ assertEquals(visibility, button.visible)
+ button.setSelected(true)
+ assertTrue(selectedValue)
+
+ val buttonVisibility = Toolbar.ActionToggleButton(mock(), mock(), "image", "selected", background = 0) { }
+ assertTrue(buttonVisibility.visible())
+ }
+
+ @Test
+ fun `bind is not implemented`() {
+ val button = Toolbar.ActionToggleButton(mock(), mock(), "image", "imageSelected") {}
+ assertEquals(Unit, button.bind(mock()))
+ }
+}
diff --git a/mobile/android/android-components/components/concept/toolbar/src/test/resources/robolectric.properties b/mobile/android/android-components/components/concept/toolbar/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/concept/toolbar/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28