diff options
Diffstat (limited to 'mobile/android/android-components/components/concept/toolbar')
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 |