v122
harvey186 2024-02-21 18:22:05 +01:00
parent 9057022ade
commit cd1540afd8
338 changed files with 102502 additions and 2881251 deletions

View File

@ -0,0 +1,473 @@
/* 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("TooManyFunctions")
package org.mozilla.fenix.ui.robots
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.hasAnySibling
import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.closeSoftKeyboard
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.ViewActions.clearText
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withChild
import androidx.test.espresso.matcher.ViewMatchers.withClassName
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiSelector
import org.hamcrest.CoreMatchers
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.endsWith
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.DataGenerationHelper.getAvailableSearchEngines
import org.mozilla.fenix.helpers.DataGenerationHelper.getRegionSearchEnginesList
import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
import org.mozilla.fenix.helpers.TestHelper.hasCousin
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.isChecked
import org.mozilla.fenix.helpers.isEnabled
/**
* Implementation of Robot Pattern for the settings search sub menu.
*/
class SettingsSubMenuSearchRobot {
fun verifyToolbarText(title: String) {
onView(
allOf(
withId(R.id.navigationToolbar),
hasDescendant(withContentDescription(R.string.action_bar_up_description)),
hasDescendant(withText(title)),
),
).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
fun verifySearchEnginesSectionHeader() {
onView(withText("Search engines")).check(matches(isDisplayed()))
}
fun verifyDefaultSearchEngineHeader() {
defaultSearchEngineHeader
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
fun verifyDefaultSearchEngineSummary(engineName: String) {
defaultSearchEngineHeader.check(matches(hasSibling(withText(engineName))))
}
fun verifyManageSearchShortcutsHeader() {
manageSearchShortcutsHeader.check(matches(isDisplayed()))
}
fun verifyManageShortcutsSummary() {
manageSearchShortcutsHeader
.check(matches(hasSibling(withText("Edit engines visible in the search menu"))))
}
fun verifyEnginesShortcutsListHeader() =
assertUIObjectExists(itemWithText("Engines visible on the search menu"))
fun verifyAddressBarSectionHeader() {
onView(withText("Address bar - Firefox Suggest")).check(matches(isDisplayed()))
}
fun verifyDefaultSearchEngineList() {
defaultSearchEngineOption("LeOSearch")
.check(matches(hasSibling(withId(R.id.engine_icon))))
.check(matches(isDisplayed()))
defaultSearchEngineOption("DuckDuckGo")
.check(matches(hasSibling(withId(R.id.engine_icon))))
.check(matches(isDisplayed()))
assertUIObjectExists(addSearchEngineButton)
}
fun verifyManageShortcutsList(testRule: ComposeTestRule) {
val availableShortcutsEngines = getRegionSearchEnginesList() + getAvailableSearchEngines()
availableShortcutsEngines.forEach {
testRule.onNodeWithText(it.name)
.assert(hasAnySibling(hasContentDescription("${it.name} search engine")))
.assertIsDisplayed()
}
assertUIObjectExists(addSearchEngineButton)
}
/**
* Method that verifies the selected engines inside the Manage search shortcuts list.
*/
fun verifySearchShortcutChecked(vararg engineShortcut: EngineShortcut) {
engineShortcut.forEach {
val shortcutIsChecked = mDevice.findObject(UiSelector().text(it.name))
.getFromParent(
UiSelector().index(it.checkboxIndex),
).isChecked
if (it.isChecked) {
assertTrue(shortcutIsChecked)
} else {
assertFalse(shortcutIsChecked)
}
}
}
fun verifyAutocompleteURlsIsEnabled(enabled: Boolean) {
autocompleteSwitchButton()
.check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
}
fun verifyShowSearchSuggestionsEnabled(enabled: Boolean) {
showSearchSuggestionSwitchButton()
.check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
}
fun verifyShowSearchSuggestionsInPrivateEnabled(enabled: Boolean) {
showSuggestionsInPrivateModeSwitch()
.check(
matches(
hasSibling(
withChild(
allOf(
withClassName(endsWith("CheckBox")),
isChecked(enabled),
),
),
),
),
)
}
fun verifyShowClipboardSuggestionsEnabled(enabled: Boolean) {
showClipboardSuggestionSwitch()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
}
fun verifySearchBrowsingHistoryEnabled(enabled: Boolean) {
searchHistorySwitchButton()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
}
fun verifySearchBookmarksEnabled(enabled: Boolean) {
searchBookmarksSwitchButton()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
}
fun verifySearchSyncedTabsEnabled(enabled: Boolean) {
searchSyncedTabsSwitchButton()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
}
fun verifyVoiceSearchEnabled(enabled: Boolean) {
voiceSearchSwitchButton()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
}
fun openDefaultSearchEngineMenu() {
defaultSearchEngineHeader.click()
}
fun openManageShortcutsMenu() {
manageSearchShortcutsHeader.click()
}
fun changeDefaultSearchEngine(searchEngineName: String) {
onView(withText(searchEngineName))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.perform(click())
}
fun selectSearchShortcut(shortcut: EngineShortcut) {
mDevice.findObject(UiSelector().text(shortcut.name))
.getFromParent(UiSelector().index(shortcut.checkboxIndex))
.click()
}
fun toggleAutocomplete() = autocompleteSwitchButton().click()
fun toggleShowSearchSuggestions() = showSearchSuggestionSwitchButton().click()
fun toggleVoiceSearch() {
voiceSearchSwitchButton().perform(click())
}
fun toggleClipboardSuggestion() {
showClipboardSuggestionSwitch().click()
}
fun switchSearchHistoryToggle() = searchHistorySwitchButton().click()
fun switchSearchBookmarksToggle() = searchBookmarksSwitchButton().click()
fun switchShowSuggestionsInPrivateSessionsToggle() =
showSuggestionsInPrivateModeSwitch().click()
fun openAddSearchEngineMenu() = addSearchEngineButton.click()
fun verifyEngineListContains(searchEngineName: String, shouldExist: Boolean) =
assertUIObjectExists(itemWithText(searchEngineName), exists = shouldExist)
fun verifyDefaultSearchEngineSelected(searchEngineName: String) {
defaultSearchEngineOption(searchEngineName).check(matches(isChecked(true)))
}
fun verifySaveSearchEngineButtonEnabled(enabled: Boolean) {
addSearchEngineSaveButton().check(matches(isEnabled(enabled)))
}
fun saveNewSearchEngine() {
closeSoftKeyboard()
addSearchEngineSaveButton().click()
}
fun typeCustomEngineDetails(engineName: String, engineURL: String) {
try {
mDevice.findObject(By.res("$packageName:id/edit_engine_name")).clear()
mDevice.findObject(By.res("$packageName:id/edit_engine_name")).text = engineName
assertUIObjectExists(
itemWithResIdAndText("$packageName:id/edit_engine_name", engineName),
)
mDevice.findObject(By.res("$packageName:id/edit_search_string")).clear()
mDevice.findObject(By.res("$packageName:id/edit_search_string")).text = engineURL
assertUIObjectExists(
itemWithResIdAndText("$packageName:id/edit_search_string", engineURL),
)
} catch (e: AssertionError) {
println("The name or the search string were not set properly")
mDevice.findObject(By.res("$packageName:id/edit_engine_name")).clear()
mDevice.findObject(By.res("$packageName:id/edit_engine_name")).setText(engineName)
assertUIObjectExists(
itemWithResIdAndText("$packageName:id/edit_engine_name", engineName),
)
mDevice.findObject(By.res("$packageName:id/edit_search_string")).clear()
mDevice.findObject(By.res("$packageName:id/edit_search_string")).setText(engineURL)
assertUIObjectExists(
itemWithResIdAndText("$packageName:id/edit_search_string", engineURL),
)
}
}
fun typeSearchEngineSuggestionString(searchSuggestionString: String) {
onView(withId(R.id.edit_suggest_string))
.click()
.perform(clearText())
.perform(typeText(searchSuggestionString))
}
// Used in the non-Compose Default search engines menu
fun openEngineOverflowMenu(searchEngineName: String) {
threeDotMenu(searchEngineName).waitForExists(waitingTimeShort)
threeDotMenu(searchEngineName).click()
}
// Used in the composable Manage shortcuts menu, otherwise the overflow menu is not visible
fun openCustomShortcutOverflowMenu(testRule: ComposeTestRule, searchEngineName: String) {
testRule.onNode(overflowMenuWithSiblingText(searchEngineName)).performClick()
}
fun clickEdit() = onView(withText("Edit")).click()
// Used in the Default search engine menu
fun clickDeleteSearchEngine() =
mDevice.findObject(
UiSelector().textContains(getStringResource(R.string.search_engine_delete)),
).click()
// Used in the composable Manage search shortcuts menu, otherwise the overflow menu is not visible
fun clickDeleteSearchEngine(testRule: ComposeTestRule) =
testRule.onNodeWithText("Delete").performClick()
fun clickUndoSnackBarButton() =
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/snackbar_btn"),
).click()
fun saveEditSearchEngine() {
onView(withId(R.id.save_button)).click()
assertUIObjectExists(itemContainingText("Saved"))
}
fun verifyInvalidTemplateSearchStringFormatError() {
closeSoftKeyboard()
onView(withText(getStringResource(R.string.search_add_custom_engine_error_missing_template)))
.check(matches(isDisplayed()))
}
fun verifyErrorConnectingToSearchString(searchEngineName: String) {
closeSoftKeyboard()
onView(withText(getStringResource(R.string.search_add_custom_engine_error_cannot_reach, searchEngineName)))
.check(matches(isDisplayed()))
}
class Transition {
fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
mDevice.waitForIdle()
goBackButton().perform(click())
SettingsRobot().interact()
return SettingsRobot.Transition()
}
fun clickCustomSearchStringLearnMoreLink(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
onView(withId(R.id.custom_search_engines_learn_more)).click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun clickCustomSearchSuggestionsLearnMoreLink(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
onView(withId(R.id.custom_search_suggestions_learn_more)).click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
}
}
/**
* Matches search shortcut items inside the 'Manage search shortcuts' menu
* @param name, of type String, should be the name of the search engine.
* @param checkboxIndex, of type Int, is the checkbox' index afferent to the search engine.
* @param isChecked, of type Boolean, should show if the checkbox is expected to be checked.
*/
class EngineShortcut(
val name: String,
val checkboxIndex: Int,
val isChecked: Boolean = true,
)
private val defaultSearchEngineHeader = onView(withText("Default search engine"))
private val manageSearchShortcutsHeader = onView(withText("Manage alternative search engines"))
private fun searchHistorySwitchButton(): ViewInteraction {
onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText("Search browsing history")),
),
)
return onView(withText("Search browsing history"))
}
private fun searchBookmarksSwitchButton(): ViewInteraction {
onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText("Search bookmarks")),
),
)
return onView(withText("Search bookmarks"))
}
private fun searchSyncedTabsSwitchButton(): ViewInteraction {
onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText("Search synced tabs")),
),
)
return onView(withText("Search synced tabs"))
}
private fun voiceSearchSwitchButton(): ViewInteraction {
onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText("Show voice search")),
),
)
return onView(withText("Show voice search"))
}
private fun autocompleteSwitchButton(): ViewInteraction {
onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText(getStringResource(R.string.preferences_enable_autocomplete_urls))),
),
)
return onView(withText(getStringResource(R.string.preferences_enable_autocomplete_urls)))
}
private fun showSearchSuggestionSwitchButton(): ViewInteraction {
onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText("Show search suggestions")),
),
)
return onView(withText("Show search suggestions"))
}
private fun showClipboardSuggestionSwitch(): ViewInteraction {
onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText(getStringResource(R.string.preferences_show_clipboard_suggestions))),
),
)
return onView(withText(getStringResource(R.string.preferences_show_clipboard_suggestions)))
}
private fun showSuggestionsInPrivateModeSwitch(): ViewInteraction {
onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText(getStringResource(R.string.preferences_show_search_suggestions_in_private))),
),
)
return onView(withText(getStringResource(R.string.preferences_show_search_suggestions_in_private)))
}
private fun goBackButton() =
onView(CoreMatchers.allOf(withContentDescription("Navigate up")))
private val addSearchEngineButton = mDevice.findObject(UiSelector().text("Add search engine"))
private fun addSearchEngineSaveButton() = onView(withId(R.id.save_button))
private fun threeDotMenu(searchEngineName: String) =
mDevice.findObject(UiSelector().text(searchEngineName))
.getFromParent(UiSelector().description("More options"))
private fun defaultSearchEngineOption(searchEngineName: String) =
onView(
allOf(
withId(R.id.radio_button),
hasSibling(withText(searchEngineName)),
),
)
private fun overflowMenuWithSiblingText(text: String): SemanticsMatcher =
hasAnySibling(hasText(text)) and hasContentDescription("More options")

@ -1 +1 @@
Subproject commit b79bdfd15bedaf5e60d3432134d369e73b03d53b Subproject commit 868ab5a098f62933b83eb70dd41126b3971ef4fc

View File

@ -32,7 +32,7 @@ android {
defaultConfig { defaultConfig {
applicationId "com.leos" applicationId "com.leos"
minSdk = 26 minSdkVersion config.minSdkVersion
compileSdk config.compileSdkVersion compileSdk config.compileSdkVersion
targetSdkVersion config.targetSdkVersion targetSdkVersion config.targetSdkVersion
versionCode 1 versionCode 1
@ -63,7 +63,7 @@ android {
// This should be the base URL used to call the AMO API. // This should be the base URL used to call the AMO API.
buildConfigField "String", "AMO_SERVER_URL", "\"https://services.addons.mozilla.org\"" buildConfigField "String", "AMO_SERVER_URL", "\"https://services.addons.mozilla.org\""
def deepLinkSchemeValue = "leosium" def deepLinkSchemeValue = "fenix-dev"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\"" buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
// This allows overriding the target activity for MozillaOnline builds, which happens // This allows overriding the target activity for MozillaOnline builds, which happens
@ -82,6 +82,8 @@ android {
"targetActivity": targetActivity, "targetActivity": targetActivity,
"deepLinkScheme": deepLinkSchemeValue "deepLinkScheme": deepLinkSchemeValue
] ]
buildConfigField "String[]", "SUPPORTED_LOCALE_ARRAY", getSupportedLocales()
} }
def releaseTemplate = { def releaseTemplate = {
@ -109,14 +111,14 @@ android {
debug { debug {
shrinkResources false shrinkResources false
minifyEnabled false minifyEnabled false
applicationIdSuffix ".leosium.debug" applicationIdSuffix ".fenix.debug"
resValue "bool", "IS_DEBUG", "true" resValue "bool", "IS_DEBUG", "true"
pseudoLocalesEnabled true pseudoLocalesEnabled true
} }
nightly releaseTemplate >> { nightly releaseTemplate >> {
applicationIdSuffix ".fenix" applicationIdSuffix ".fenix"
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true" buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
def deepLinkSchemeValue = "leosium.debug" def deepLinkSchemeValue = "fenix-nightly"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\"" buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders.putAll([ manifestPlaceholders.putAll([
"deepLinkScheme": deepLinkSchemeValue "deepLinkScheme": deepLinkSchemeValue
@ -124,7 +126,7 @@ android {
} }
beta releaseTemplate >> { beta releaseTemplate >> {
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true" buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
applicationIdSuffix ".leosium.beta" applicationIdSuffix ".firefox_beta"
def deepLinkSchemeValue = "fenix-beta" def deepLinkSchemeValue = "fenix-beta"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\"" buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders.putAll([ manifestPlaceholders.putAll([
@ -141,7 +143,7 @@ android {
} }
release releaseTemplate >> { release releaseTemplate >> {
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true" buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
applicationIdSuffix ".leosium" applicationIdSuffix ".firefox"
def deepLinkSchemeValue = "fenix" def deepLinkSchemeValue = "fenix"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\"" buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders.putAll([ manifestPlaceholders.putAll([
@ -239,7 +241,14 @@ android {
reset() reset()
include "armeabi-v7a", "arm64-v8a" // As gradle is unable to pick the right apk to install when multiple apks are generated
// while running benchmark tests or generating baseline profiles. To circumvent this,
// this flag is passed to make sure only one apk is generated so gradle can pick that one.
if (project.hasProperty("benchmarkTest")) {
include "arm64-v8a"
} else {
include "x86", "armeabi-v7a", "arm64-v8a", "x86_64"
}
} }
} }
@ -515,7 +524,6 @@ android.applicationVariants.configureEach { variant ->
} else { } else {
buildConfigField "boolean", "LEAKCANARY", "false" buildConfigField "boolean", "LEAKCANARY", "false"
} }
} }
// Generate Kotlin code for the Fenix Glean metrics. // Generate Kotlin code for the Fenix Glean metrics.
@ -670,13 +678,16 @@ dependencies {
implementation ComponentsDependencies.androidx_fragment implementation ComponentsDependencies.androidx_fragment
implementation FenixDependencies.androidx_navigation_fragment implementation FenixDependencies.androidx_navigation_fragment
implementation FenixDependencies.androidx_navigation_ui implementation FenixDependencies.androidx_navigation_ui
implementation ComponentsDependencies.androidx_compose_navigation
implementation ComponentsDependencies.androidx_recyclerview implementation ComponentsDependencies.androidx_recyclerview
implementation ComponentsDependencies.androidx_lifecycle_common implementation ComponentsDependencies.androidx_lifecycle_common
implementation ComponentsDependencies.androidx_lifecycle_livedata implementation ComponentsDependencies.androidx_lifecycle_livedata
implementation ComponentsDependencies.androidx_lifecycle_process implementation ComponentsDependencies.androidx_lifecycle_process
implementation ComponentsDependencies.androidx_lifecycle_runtime implementation ComponentsDependencies.androidx_lifecycle_runtime
implementation ComponentsDependencies.androidx_lifecycle_viewmodel implementation ComponentsDependencies.androidx_lifecycle_viewmodel
implementation ComponentsDependencies.androidx_lifecycle_service
implementation ComponentsDependencies.androidx_core implementation ComponentsDependencies.androidx_core
implementation ComponentsDependencies.androidx_core_ktx implementation ComponentsDependencies.androidx_core_ktx
implementation FenixDependencies.androidx_core_splashscreen implementation FenixDependencies.androidx_core_splashscreen
@ -837,25 +848,6 @@ tasks.register('printVariants') {
} }
} }
tasks.register('buildTranslationArray') {
// This isn't running as a task, instead the array is build when the gradle file is parsed.
// https://github.com/mozilla-mobile/fenix/issues/14175
def foundLocales = new StringBuilder()
foundLocales.append("new String[]{")
fileTree("src/main/res").visit { FileVisitDetails details ->
if (details.file.path.endsWith("${File.separator}strings.xml")) {
def languageCode = details.file.parent.tokenize(File.separator).last().replaceAll('values-', '').replaceAll('-r', '-')
languageCode = (languageCode == "values") ? "en-US" : languageCode
foundLocales.append("\"").append(languageCode).append("\"").append(",")
}
}
foundLocales.append("}")
def foundLocalesString = foundLocales.toString().replaceAll(',}', '}')
android.defaultConfig.buildConfigField "String[]", "SUPPORTED_LOCALE_ARRAY", foundLocalesString
}
afterEvaluate { afterEvaluate {
// Format test output. Ported from AC #2401 // Format test output. Ported from AC #2401
@ -922,5 +914,24 @@ android.applicationVariants.configureEach { variant ->
} }
} }
def getSupportedLocales() {
// This isn't running as a task, instead the array is build when the gradle file is parsed.
// https://github.com/mozilla-mobile/fenix/issues/14175
def foundLocales = new StringBuilder()
foundLocales.append("new String[]{")
fileTree("src/main/res").visit { FileVisitDetails details ->
if (details.file.path.endsWith("${File.separator}strings.xml")) {
def languageCode = details.file.parent.tokenize(File.separator).last().replaceAll('values-', '').replaceAll('-r', '-')
languageCode = (languageCode == "values") ? "en-US" : languageCode
foundLocales.append("\"").append(languageCode).append("\"").append(",")
}
}
foundLocales.append("}")
def foundLocalesString = foundLocales.toString().replaceAll(',}', '}')
return foundLocalesString
}
// Enable expiration by major version. // Enable expiration by major version.
ext.gleanExpireByVersion = 26 ext.gleanExpireByVersion = 26

File diff suppressed because it is too large Load Diff

View File

@ -135,12 +135,12 @@ events:
description: | description: |
A string containing the name of the item the user tapped. These items A string containing the name of the item the user tapped. These items
include: include:
add_to_homescreen, add_to_top_sites, addons_manager, back, bookmark, add_to_homescreen, add_to_top_sites, addons_manager, back, back_long_press,
bookmarks, desktop_view_off, desktop_view_on, downloads, bookmark, bookmarks, desktop_view_off, desktop_view_on, downloads,
find_in_page, forward, history, new_tab, open_in_app, open_in_fenix, find_in_page, forward, forward_long_press, history, new_tab, open_in_app,
quit, reader_mode_appearance, reload, remove_from_top_sites, open_in_fenix, quit, reader_mode_appearance, reload, remove_from_top_sites,
save_to_collection, set_default_browser, settings, share, stop, save_to_collection, set_default_browser, settings, share, stop,
sync_account, and print_content. sync_account, translate and print_content.
type: string type: string
bugs: bugs:
- https://github.com/mozilla-mobile/fenix/issues/1024 - https://github.com/mozilla-mobile/fenix/issues/1024
@ -475,6 +475,23 @@ events:
notification_emails: notification_emails:
- android-probes@mozilla.com - android-probes@mozilla.com
expires: never expires: never
browser_toolbar_security_indicator_tapped:
type: event
description: |
An event that indicates that a user has tapped
the security indicator icon (at the start of the domain name).
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1869664
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/5019#issuecomment-1876329933
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: never
metadata:
tags:
- Toolbar
browser_toolbar_erase_tapped: browser_toolbar_erase_tapped:
type: event type: event
description: | description: |
@ -489,6 +506,22 @@ events:
notification_emails: notification_emails:
- android-probes@mozilla.com - android-probes@mozilla.com
expires: never expires: never
browser_toolbar_input_cleared:
type: event
description: |
A user pressed the circle cross icon, clearing the input in the toolbar.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1869664
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/5019#issuecomment-1876329933
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: never
metadata:
tags:
- Toolbar
browser_toolbar_qr_scan_tapped: browser_toolbar_qr_scan_tapped:
type: event type: event
description: | description: |
@ -506,6 +539,22 @@ events:
metadata: metadata:
tags: tags:
- Toolbar - Toolbar
browser_toolbar_qr_scan_completed:
type: event
description: |
An event that indicates that a QR code has been scanned successfully.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1869664
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/5019#issuecomment-1876329933
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: never
metadata:
tags:
- Toolbar
toolbar_tab_swipe: toolbar_tab_swipe:
type: event type: event
description: | description: |

View File

@ -214,14 +214,6 @@ features:
type: Boolean type: Boolean
default: true default: true
extensions-process:
description: A feature to rollout the extensions process.
variables:
enabled:
description: If true, the extensions process is enabled.
type: Boolean
default: true
growth-data: growth-data:
description: A feature measuring campaign growth data description: A feature measuring campaign growth data
variables: variables:
@ -341,6 +333,26 @@ features:
type: Map<String, String> type: Map<String, String>
default: {} default: {}
fx-strong-password:
description: A feature that provides a generated strong password on sign up.
variables:
enabled:
description: >
When the feature is enabled and Firefox receives a Login event with an
empty saved logins list, a suggested strong password prompt will be shown,
allowing the user to use the generated password to fill in the password field
for the new account that will be created. When the feature is disabled,
there won't be any prompt displayed that would allow using a generated password.
type: Boolean
default: false
defaults:
- channel: developer
value:
enabled: true
- channel: nightly
value:
enabled: true
fx-suggest: fx-suggest:
description: A feature that provides Firefox Suggest search suggestions. description: A feature that provides Firefox Suggest search suggestions.
variables: variables:
@ -361,6 +373,17 @@ features:
- channel: nightly - channel: nightly
value: value:
enabled: true enabled: true
nimbus-is-ready:
description: >
A feature that provides the number of Nimbus is_ready events to send
when Nimbus finishes launching.
variables:
event-count:
description: The number of events that should be sent.
type: Int
default: 1
types: types:
objects: {} objects: {}

View File

@ -25,8 +25,7 @@ features:
card-type: default-browser card-type: default-browser
title: juno_onboarding_default_browser_title_nimbus_2 title: juno_onboarding_default_browser_title_nimbus_2
ordering: 10 ordering: 10
body: juno_onboarding_default_browser_description_nimbus_2 body: juno_onboarding_default_browser_description_nimbus_3
link-text: juno_onboarding_default_browser_description_link_text
image-res: ic_onboarding_welcome image-res: ic_onboarding_welcome
primary-button-label: juno_onboarding_default_browser_positive_button primary-button-label: juno_onboarding_default_browser_positive_button
secondary-button-label: juno_onboarding_default_browser_negative_button secondary-button-label: juno_onboarding_default_browser_negative_button
@ -83,13 +82,6 @@ objects:
description: The message text displayed to the user. May contain linkable text. description: The message text displayed to the user. May contain linkable text.
# This should never be defaulted. # This should never be defaulted.
default: "" default: ""
link-text:
type: Option<Text>
description: >
The text to link from the body text. This should match the linkable text from the body text exactly.
e.g. body: This is a policy link
link-text: policy link
default: null
image-res: image-res:
type: Image type: Image
description: The resource id of the image to be displayed. description: The resource id of the image to be displayed.

View File

@ -92,7 +92,6 @@ class AppRequestInterceptor(
// This method is the only difference from the production code. // This method is the only difference from the production code.
// Otherwise the code should be kept identical // Otherwise the code should be kept identical
@Suppress("LongParameterList")
private fun interceptFxaRequest( private fun interceptFxaRequest(
engineSession: EngineSession, engineSession: EngineSession,
uri: String, uri: String,

View File

@ -146,11 +146,12 @@
}, },
"jinja2": { "jinja2": {
"hashes": [ "hashes": [
"sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa",
"sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"
], ],
"index": "pypi",
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==3.1.2" "version": "==3.1.3"
}, },
"markupsafe": { "markupsafe": {
"hashes": [ "hashes": [

View File

@ -6,19 +6,15 @@ package org.mozilla.fenix.extensions
import android.content.Context import android.content.Context
import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineSession
import org.json.JSONObject
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mozilla.experiments.nimbus.HardcodedNimbusFeatures
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.gecko.GeckoProvider import org.mozilla.fenix.gecko.GeckoProvider
import org.mozilla.fenix.helpers.TestHelper import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.nimbus.FxNimbus
/** /**
* Instrumentation test for verifying that the extensions process can be controlled with Nimbus. * Instrumentation test for verifying that the extensions process is enabled unconditionally.
*/ */
class ExtensionProcessTest { class ExtensionProcessTest {
private lateinit var context: Context private lateinit var context: Context
@ -27,49 +23,12 @@ class ExtensionProcessTest {
@Before @Before
fun setUp() { fun setUp() {
context = TestHelper.appContext context = TestHelper.appContext
policy = policy = context.components.core.trackingProtectionPolicyFactory.createTrackingProtectionPolicy()
context.components.core.trackingProtectionPolicyFactory.createTrackingProtectionPolicy()
} }
@Test @Test
fun test_extension_process_can_be_enabled_by_nimbus() { fun test_extension_process_is_enabled() {
val hardcodedNimbus = HardcodedNimbusFeatures(
context,
"extensions-process" to JSONObject(
"""
{
"enabled": true
}
""".trimIndent(),
),
)
hardcodedNimbus.connectWith(FxNimbus)
val runtime = GeckoProvider.createRuntimeSettings(context, policy) val runtime = GeckoProvider.createRuntimeSettings(context, policy)
assertTrue(FxNimbus.features.extensionsProcess.value().enabled)
assertTrue(runtime.extensionsProcessEnabled!!) assertTrue(runtime.extensionsProcessEnabled!!)
} }
@Test
fun test_extension_process_can_be_disabled_by_nimbus() {
val hardcodedNimbus = HardcodedNimbusFeatures(
context,
"extensions-process" to JSONObject(
"""
{
"enabled": false
}
""".trimIndent(),
),
)
hardcodedNimbus.connectWith(FxNimbus)
val runtime = GeckoProvider.createRuntimeSettings(context, policy)
assertFalse(FxNimbus.features.extensionsProcess.value().enabled)
assertFalse(runtime.extensionsProcessEnabled!!)
}
} }

View File

@ -249,6 +249,7 @@ object AppAndSystemHelper {
* Runs on Debug variant as we don't want to adjust Release permission manifests * Runs on Debug variant as we don't want to adjust Release permission manifests
* Runs the test in its testBlock. * Runs the test in its testBlock.
* Cleans up and sets the default locale after it's done. * Cleans up and sets the default locale after it's done.
* As a safety measure, always add the resetSystemLocaleToEnUS() method in the tearDown method of your Class.
*/ */
fun runWithSystemLocaleChanged(locale: Locale, testRule: ActivityTestRule<HomeActivity>, testBlock: () -> Unit) { fun runWithSystemLocaleChanged(locale: Locale, testRule: ActivityTestRule<HomeActivity>, testBlock: () -> Unit) {
if (Config.channel.isDebug) { if (Config.channel.isDebug) {
@ -274,6 +275,21 @@ object AppAndSystemHelper {
} }
} }
/**
* Resets the default language of the entire device back to EN-US.
* In case of a test instrumentation crash, the finally statement in the
* runWithSystemLocaleChanged(locale: Locale) method, will not be reached.
* Add this method inside the tearDown method of your test class, where the above method is used.
* Note: If set inside the ActivityTestRule's afterActivityFinished() method, this also won't work,
* as the methods inside it are not always executed: https://github.com/android/android-test/issues/498
*/
fun resetSystemLocaleToEnUS() {
if (Locale.getDefault() != Locale.US) {
Log.i(TAG, "Resetting system locale to EN US")
setSystemLocale(Locale.US)
}
}
/** /**
* Changes the default language of the entire device, not just the app. * Changes the default language of the entire device, not just the app.
*/ */

View File

@ -22,6 +22,8 @@ import org.junit.Assert
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.utils.IntentUtils import org.mozilla.fenix.utils.IntentUtils
import java.time.LocalDate
import java.time.LocalTime
object DataGenerationHelper { object DataGenerationHelper {
val appContext: Context = InstrumentationRegistry.getInstrumentation().targetContext val appContext: Context = InstrumentationRegistry.getInstrumentation().targetContext
@ -75,6 +77,28 @@ object DataGenerationHelper {
clipBoard.setPrimaryClip(clipData) clipBoard.setPrimaryClip(clipData)
} }
/**
* Constructs a date and time placeholder string for sponsored Fx suggest links.
* The format of the datetime is YYYYMMDDHH, where YYYY is the four-digit year,
* MM is the two-digit month, DD is the two-digit day, and HH is the two-digit hour.
* Single-digit months, days, and hours are padded with a leading zero to ensure
* the correct format. For example, a date and time of January 10, 2024, at 3 PM
* would be represented as "2024011015".
*
* @return A string representing the current date and time in the specified format.
*/
fun getSponsoredFxSuggestPlaceHolder(): String {
val currentDate = LocalDate.now()
val currentTime = LocalTime.now()
val currentDay = currentDate.dayOfMonth.toString().padStart(2, '0')
val currentMonth = currentDate.monthValue.toString().padStart(2, '0')
val currentYear = currentDate.year.toString()
val currentHour = currentTime.hour.toString().padStart(2, '0')
return currentYear + currentMonth + currentDay + currentHour
}
/** /**
* Returns sponsored shortcut title based on the index. * Returns sponsored shortcut title based on the index.
*/ */

View File

@ -82,6 +82,11 @@ interface FeatureSettingsHelper {
*/ */
var composeTopSitesEnabled: Boolean var composeTopSitesEnabled: Boolean
/**
* Enable or disable translations flow.
*/
var isTranslationsEnabled: Boolean
fun applyFlagUpdates() fun applyFlagUpdates()
fun resetAllFeatureFlags() fun resetAllFeatureFlags()

View File

@ -37,6 +37,7 @@ class FeatureSettingsHelperDelegate() : FeatureSettingsHelper {
etpPolicy = getETPPolicy(settings), etpPolicy = getETPPolicy(settings),
tabsTrayRewriteEnabled = settings.enableTabsTrayToCompose, tabsTrayRewriteEnabled = settings.enableTabsTrayToCompose,
composeTopSitesEnabled = settings.enableComposeTopSites, composeTopSitesEnabled = settings.enableComposeTopSites,
translationsEnabled = settings.enableTranslations,
) )
/** /**
@ -66,6 +67,7 @@ class FeatureSettingsHelperDelegate() : FeatureSettingsHelper {
override var etpPolicy: ETPPolicy by updatedFeatureFlags::etpPolicy override var etpPolicy: ETPPolicy by updatedFeatureFlags::etpPolicy
override var tabsTrayRewriteEnabled: Boolean by updatedFeatureFlags::tabsTrayRewriteEnabled override var tabsTrayRewriteEnabled: Boolean by updatedFeatureFlags::tabsTrayRewriteEnabled
override var composeTopSitesEnabled: Boolean by updatedFeatureFlags::composeTopSitesEnabled override var composeTopSitesEnabled: Boolean by updatedFeatureFlags::composeTopSitesEnabled
override var isTranslationsEnabled: Boolean by updatedFeatureFlags::translationsEnabled
override fun applyFlagUpdates() { override fun applyFlagUpdates() {
applyFeatureFlags(updatedFeatureFlags) applyFeatureFlags(updatedFeatureFlags)
@ -91,6 +93,7 @@ class FeatureSettingsHelperDelegate() : FeatureSettingsHelper {
settings.shouldShowOpenInAppBanner = featureFlags.isOpenInAppBannerEnabled settings.shouldShowOpenInAppBanner = featureFlags.isOpenInAppBannerEnabled
settings.enableTabsTrayToCompose = featureFlags.tabsTrayRewriteEnabled settings.enableTabsTrayToCompose = featureFlags.tabsTrayRewriteEnabled
settings.enableComposeTopSites = featureFlags.composeTopSitesEnabled settings.enableComposeTopSites = featureFlags.composeTopSitesEnabled
settings.enableTranslations = featureFlags.translationsEnabled
setETPPolicy(featureFlags.etpPolicy) setETPPolicy(featureFlags.etpPolicy)
} }
} }
@ -110,6 +113,7 @@ private data class FeatureFlags(
var etpPolicy: ETPPolicy, var etpPolicy: ETPPolicy,
var tabsTrayRewriteEnabled: Boolean, var tabsTrayRewriteEnabled: Boolean,
var composeTopSitesEnabled: Boolean, var composeTopSitesEnabled: Boolean,
var translationsEnabled: Boolean,
) )
internal fun getETPPolicy(settings: Settings): ETPPolicy { internal fun getETPPolicy(settings: Settings): ETPPolicy {

View File

@ -165,6 +165,7 @@ class HomeActivityIntentTestRule internal constructor(
etpPolicy: ETPPolicy = getETPPolicy(settings), etpPolicy: ETPPolicy = getETPPolicy(settings),
tabsTrayRewriteEnabled: Boolean = false, tabsTrayRewriteEnabled: Boolean = false,
composeTopSitesEnabled: Boolean = false, composeTopSitesEnabled: Boolean = false,
translationsEnabled: Boolean = false,
) : this(initialTouchMode, launchActivity, skipOnboarding) { ) : this(initialTouchMode, launchActivity, skipOnboarding) {
this.isHomeOnboardingDialogEnabled = isHomeOnboardingDialogEnabled this.isHomeOnboardingDialogEnabled = isHomeOnboardingDialogEnabled
this.isPocketEnabled = isPocketEnabled this.isPocketEnabled = isPocketEnabled
@ -179,6 +180,7 @@ class HomeActivityIntentTestRule internal constructor(
this.etpPolicy = etpPolicy this.etpPolicy = etpPolicy
this.tabsTrayRewriteEnabled = tabsTrayRewriteEnabled this.tabsTrayRewriteEnabled = tabsTrayRewriteEnabled
this.composeTopSitesEnabled = composeTopSitesEnabled this.composeTopSitesEnabled = composeTopSitesEnabled
this.isTranslationsEnabled = translationsEnabled
} }
private val longTapUserPreference = getLongPressTimeout() private val longTapUserPreference = getLongPressTimeout()
@ -260,6 +262,7 @@ class HomeActivityIntentTestRule internal constructor(
skipOnboarding: Boolean = false, skipOnboarding: Boolean = false,
tabsTrayRewriteEnabled: Boolean = false, tabsTrayRewriteEnabled: Boolean = false,
composeTopSitesEnabled: Boolean = false, composeTopSitesEnabled: Boolean = false,
translationsEnabled: Boolean = false,
) = HomeActivityIntentTestRule( ) = HomeActivityIntentTestRule(
initialTouchMode = initialTouchMode, initialTouchMode = initialTouchMode,
launchActivity = launchActivity, launchActivity = launchActivity,
@ -271,6 +274,7 @@ class HomeActivityIntentTestRule internal constructor(
isWallpaperOnboardingEnabled = false, isWallpaperOnboardingEnabled = false,
isOpenInAppBannerEnabled = false, isOpenInAppBannerEnabled = false,
composeTopSitesEnabled = composeTopSitesEnabled, composeTopSitesEnabled = composeTopSitesEnabled,
translationsEnabled = translationsEnabled,
) )
} }
} }

View File

@ -4,6 +4,7 @@
package org.mozilla.fenix.helpers package org.mozilla.fenix.helpers
import android.util.Log
import androidx.test.espresso.IdlingResourceTimeoutException import androidx.test.espresso.IdlingResourceTimeoutException
import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.NoMatchingViewException
import androidx.test.uiautomator.UiObjectNotFoundException import androidx.test.uiautomator.UiObjectNotFoundException
@ -13,7 +14,9 @@ import org.junit.rules.TestRule
import org.junit.runner.Description import org.junit.runner.Description
import org.junit.runners.model.Statement import org.junit.runners.model.Statement
import org.mozilla.fenix.components.PermissionStorage import org.mozilla.fenix.components.PermissionStorage
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AppAndSystemHelper.setNetworkEnabled import org.mozilla.fenix.helpers.AppAndSystemHelper.setNetworkEnabled
import org.mozilla.fenix.helpers.Constants.TAG
import org.mozilla.fenix.helpers.IdlingResourceHelper.unregisterAllIdlingResources import org.mozilla.fenix.helpers.IdlingResourceHelper.unregisterAllIdlingResources
import org.mozilla.fenix.helpers.TestHelper.appContext import org.mozilla.fenix.helpers.TestHelper.appContext
@ -32,68 +35,83 @@ class RetryTestRule(private val retryCount: Int = 5) : TestRule {
return statement { return statement {
for (i in 1..retryCount) { for (i in 1..retryCount) {
try { try {
Log.i(TAG, "RetryTestRule: Started try #$i.")
base.evaluate() base.evaluate()
break break
} catch (t: AssertionError) { } catch (t: AssertionError) {
setNetworkEnabled(true) setNetworkEnabled(true)
unregisterAllIdlingResources() unregisterAllIdlingResources()
runBlocking { runBlocking {
appContext.settings().alwaysOpenTheHomepageWhenOpeningTheApp = true
permissionStorage.deleteAllSitePermissions() permissionStorage.deleteAllSitePermissions()
} }
if (i == retryCount) { if (i == retryCount) {
Log.i(TAG, "RetryTestRule: Max numbers of retries reached.")
throw t throw t
} }
} catch (t: AssertionFailedError) { } catch (t: AssertionFailedError) {
unregisterAllIdlingResources() unregisterAllIdlingResources()
runBlocking { runBlocking {
appContext.settings().alwaysOpenTheHomepageWhenOpeningTheApp = true
permissionStorage.deleteAllSitePermissions() permissionStorage.deleteAllSitePermissions()
} }
if (i == retryCount) { if (i == retryCount) {
Log.i(TAG, "RetryTestRule: Max numbers of retries reached.")
throw t throw t
} }
} catch (t: UiObjectNotFoundException) { } catch (t: UiObjectNotFoundException) {
setNetworkEnabled(true) setNetworkEnabled(true)
unregisterAllIdlingResources() unregisterAllIdlingResources()
runBlocking { runBlocking {
appContext.settings().alwaysOpenTheHomepageWhenOpeningTheApp = true
permissionStorage.deleteAllSitePermissions() permissionStorage.deleteAllSitePermissions()
} }
if (i == retryCount) { if (i == retryCount) {
Log.i(TAG, "RetryTestRule: Max numbers of retries reached.")
throw t throw t
} }
} catch (t: NoMatchingViewException) { } catch (t: NoMatchingViewException) {
setNetworkEnabled(true) setNetworkEnabled(true)
unregisterAllIdlingResources() unregisterAllIdlingResources()
runBlocking { runBlocking {
appContext.settings().alwaysOpenTheHomepageWhenOpeningTheApp = true
permissionStorage.deleteAllSitePermissions() permissionStorage.deleteAllSitePermissions()
} }
if (i == retryCount) { if (i == retryCount) {
Log.i(TAG, "RetryTestRule: Max numbers of retries reached.")
throw t throw t
} }
} catch (t: IdlingResourceTimeoutException) { } catch (t: IdlingResourceTimeoutException) {
setNetworkEnabled(true) setNetworkEnabled(true)
unregisterAllIdlingResources() unregisterAllIdlingResources()
runBlocking { runBlocking {
appContext.settings().alwaysOpenTheHomepageWhenOpeningTheApp = true
permissionStorage.deleteAllSitePermissions() permissionStorage.deleteAllSitePermissions()
} }
if (i == retryCount) { if (i == retryCount) {
Log.i(TAG, "RetryTestRule: Max numbers of retries reached.")
throw t throw t
} }
} catch (t: RuntimeException) { } catch (t: RuntimeException) {
setNetworkEnabled(true) setNetworkEnabled(true)
unregisterAllIdlingResources() unregisterAllIdlingResources()
runBlocking { runBlocking {
appContext.settings().alwaysOpenTheHomepageWhenOpeningTheApp = true
permissionStorage.deleteAllSitePermissions() permissionStorage.deleteAllSitePermissions()
} }
if (i == retryCount) { if (i == retryCount) {
Log.i(TAG, "RetryTestRule: Max numbers of retries reached.")
throw t throw t
} }
} catch (t: NullPointerException) { } catch (t: NullPointerException) {
setNetworkEnabled(true) setNetworkEnabled(true)
unregisterAllIdlingResources() unregisterAllIdlingResources()
runBlocking { runBlocking {
appContext.settings().alwaysOpenTheHomepageWhenOpeningTheApp = true
permissionStorage.deleteAllSitePermissions() permissionStorage.deleteAllSitePermissions()
} }
if (i == retryCount) { if (i == retryCount) {
Log.i(TAG, "RetryTestRule: Max numbers of retries reached.")
throw t throw t
} }
} }

View File

@ -45,10 +45,12 @@ class OnboardingMapperTest {
@Test @Test
fun showNotificationTrue_showAddWidgetFalse_pagesToDisplay_returnsSortedListOfAllConvertedPages_withoutAddWidgetPage() { fun showNotificationTrue_showAddWidgetFalse_pagesToDisplay_returnsSortedListOfAllConvertedPages_withoutAddWidgetPage() {
val expected = listOf(defaultBrowserPageUiData, syncPageUiData, notificationPageUiData) val expected = listOf(defaultBrowserPageUiDataWithPrivacyCaption, syncPageUiData, notificationPageUiData)
assertEquals( assertEquals(
expected, expected,
unsortedAllKnownCardData.toPageUiData( unsortedAllKnownCardData.toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = true, showNotificationPage = true,
showAddWidgetPage = false, showAddWidgetPage = false,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -59,10 +61,12 @@ class OnboardingMapperTest {
@Test @Test
fun showNotificationFalse_showAddWidgetFalse_pagesToDisplay_returnsSortedListOfConvertedPages_withoutNotificationPage_and_addWidgetPage() { fun showNotificationFalse_showAddWidgetFalse_pagesToDisplay_returnsSortedListOfConvertedPages_withoutNotificationPage_and_addWidgetPage() {
val expected = listOf(defaultBrowserPageUiData, syncPageUiData) val expected = listOf(defaultBrowserPageUiDataWithPrivacyCaption, syncPageUiData)
assertEquals( assertEquals(
expected, expected,
unsortedAllKnownCardData.toPageUiData( unsortedAllKnownCardData.toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = false, showNotificationPage = false,
showAddWidgetPage = false, showAddWidgetPage = false,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -72,11 +76,76 @@ class OnboardingMapperTest {
} }
@Test @Test
fun showNotificationFalse_showAddWidgetTrue_pagesToDisplay_returnsSortedListOfAllConvertedPages_withoutNotificationPage() { fun pagesToDisplay_returnsSortedListOfConvertedPages_withPrivacyCaption_alwaysOnFirstPage() {
val expected = listOf(defaultBrowserPageUiData, addSearchWidgetPageUiData, syncPageUiData) var result = unsortedAllKnownCardData.toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = false,
showNotificationPage = false,
showAddWidgetPage = false,
jexlConditions = jexlConditions,
func = evalFunction,
)
assertEquals(result[0].privacyCaption, privacyCaption)
result = unsortedAllKnownCardData.toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = false,
showNotificationPage = true,
showAddWidgetPage = false,
jexlConditions = jexlConditions,
func = evalFunction,
)
assertEquals(result[0].privacyCaption, privacyCaption)
assertEquals(result[1].privacyCaption, null)
result = unsortedAllKnownCardData.toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = true,
showAddWidgetPage = false,
jexlConditions = jexlConditions,
func = evalFunction,
)
assertEquals(result[0].privacyCaption, privacyCaption)
assertEquals(result[1].privacyCaption, null)
assertEquals(result[2].privacyCaption, null)
result = unsortedAllKnownCardData.toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = false,
showNotificationPage = false,
showAddWidgetPage = true,
jexlConditions = jexlConditions,
func = evalFunction,
)
assertEquals(result[0].privacyCaption, privacyCaption)
assertEquals(result[1].privacyCaption, null)
}
@Test
fun showDefaultBrowserPageFalse_showNotificationFalse_showAddWidgetTrue_pagesToDisplay_returnsSortedListOfAllConvertedPages() {
val expected = listOf(addSearchWidgetPageUiDataWithPrivacyCaption, syncPageUiData)
assertEquals( assertEquals(
expected, expected,
unsortedAllKnownCardData.toPageUiData( unsortedAllKnownCardData.toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = false,
showNotificationPage = false,
showAddWidgetPage = true,
jexlConditions = jexlConditions,
func = evalFunction,
),
)
}
@Test
fun showNotificationFalse_showAddWidgetTrue_pagesToDisplay_returnsSortedListOfAllConvertedPages_withoutNotificationPage() {
val expected = listOf(defaultBrowserPageUiDataWithPrivacyCaption, addSearchWidgetPageUiData, syncPageUiData)
assertEquals(
expected,
unsortedAllKnownCardData.toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = false, showNotificationPage = false,
showAddWidgetPage = true, showAddWidgetPage = true,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -88,7 +157,7 @@ class OnboardingMapperTest {
@Test @Test
fun showNotificationTrue_and_showAddWidgetTrue_pagesToDisplay_returnsSortedListOfConvertedPages() { fun showNotificationTrue_and_showAddWidgetTrue_pagesToDisplay_returnsSortedListOfConvertedPages() {
val expected = listOf( val expected = listOf(
defaultBrowserPageUiData, defaultBrowserPageUiDataWithPrivacyCaption,
addSearchWidgetPageUiData, addSearchWidgetPageUiData,
syncPageUiData, syncPageUiData,
notificationPageUiData, notificationPageUiData,
@ -96,6 +165,8 @@ class OnboardingMapperTest {
assertEquals( assertEquals(
expected, expected,
unsortedAllKnownCardData.toPageUiData( unsortedAllKnownCardData.toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = true, showNotificationPage = true,
showAddWidgetPage = true, showAddWidgetPage = true,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -107,11 +178,13 @@ class OnboardingMapperTest {
@Test @Test
fun cardConditionsMatchJexlConditions_shouldDisplayCard_returnsConvertedPage() { fun cardConditionsMatchJexlConditions_shouldDisplayCard_returnsConvertedPage() {
val jexlConditions = mapOf("ALWAYS" to "true", "NEVER" to "false") val jexlConditions = mapOf("ALWAYS" to "true", "NEVER" to "false")
val expected = listOf(defaultBrowserPageUiData) val expected = listOf(defaultBrowserPageUiDataWithPrivacyCaption)
assertEquals( assertEquals(
expected, expected,
listOf(defaultBrowserCardData).toPageUiData( listOf(defaultBrowserCardData).toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = false, showNotificationPage = false,
showAddWidgetPage = false, showAddWidgetPage = false,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -128,6 +201,8 @@ class OnboardingMapperTest {
assertEquals( assertEquals(
expected, expected,
listOf(addSearchWidgetCardDataNoConditions).toPageUiData( listOf(addSearchWidgetCardDataNoConditions).toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = false, showNotificationPage = false,
showAddWidgetPage = false, showAddWidgetPage = false,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -144,6 +219,8 @@ class OnboardingMapperTest {
assertEquals( assertEquals(
expected, expected,
listOf(defaultBrowserCardData).toPageUiData( listOf(defaultBrowserCardData).toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = false, showNotificationPage = false,
showAddWidgetPage = false, showAddWidgetPage = false,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -155,11 +232,13 @@ class OnboardingMapperTest {
@Test @Test
fun prerequisitesMatchJexlConditions_shouldDisplayCard_returnsConvertedPage() { fun prerequisitesMatchJexlConditions_shouldDisplayCard_returnsConvertedPage() {
val jexlConditions = mapOf("ALWAYS" to "true") val jexlConditions = mapOf("ALWAYS" to "true")
val expected = listOf(defaultBrowserPageUiData) val expected = listOf(defaultBrowserPageUiDataWithPrivacyCaption)
assertEquals( assertEquals(
expected, expected,
listOf(defaultBrowserCardData).toPageUiData( listOf(defaultBrowserCardData).toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = false, showNotificationPage = false,
showAddWidgetPage = false, showAddWidgetPage = false,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -176,6 +255,8 @@ class OnboardingMapperTest {
assertEquals( assertEquals(
expected, expected,
listOf(defaultBrowserCardData).toPageUiData( listOf(defaultBrowserCardData).toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = false, showNotificationPage = false,
showAddWidgetPage = false, showAddWidgetPage = false,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -192,6 +273,8 @@ class OnboardingMapperTest {
assertEquals( assertEquals(
expected, expected,
listOf(addSearchWidgetCardDataNoConditions).toPageUiData( listOf(addSearchWidgetCardDataNoConditions).toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = false, showNotificationPage = false,
showAddWidgetPage = false, showAddWidgetPage = false,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -203,11 +286,13 @@ class OnboardingMapperTest {
@Test @Test
fun noDisqualifiers_shouldDisplayCard_returnsConvertedPage() { fun noDisqualifiers_shouldDisplayCard_returnsConvertedPage() {
val jexlConditions = mapOf("ALWAYS" to "true", "NEVER" to "false") val jexlConditions = mapOf("ALWAYS" to "true", "NEVER" to "false")
val expected = listOf(defaultBrowserPageUiData) val expected = listOf(defaultBrowserPageUiDataWithPrivacyCaption)
assertEquals( assertEquals(
expected, expected,
listOf(defaultBrowserCardDataNoDisqualifiers).toPageUiData( listOf(defaultBrowserCardDataNoDisqualifiers).toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = false, showNotificationPage = false,
showAddWidgetPage = false, showAddWidgetPage = false,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -219,11 +304,13 @@ class OnboardingMapperTest {
@Test @Test
fun disqualifiersMatchJexlConditions_shouldDisplayCard_returnsConvertedPage() { fun disqualifiersMatchJexlConditions_shouldDisplayCard_returnsConvertedPage() {
val jexlConditions = mapOf("NEVER" to "false") val jexlConditions = mapOf("NEVER" to "false")
val expected = listOf(syncPageUiData) val expected = listOf(syncPageUiDataWithPrivacyCaption)
assertEquals( assertEquals(
expected, expected,
listOf(syncCardData).toPageUiData( listOf(syncCardData).toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = false, showNotificationPage = false,
showAddWidgetPage = false, showAddWidgetPage = false,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -240,6 +327,8 @@ class OnboardingMapperTest {
assertEquals( assertEquals(
expected, expected,
listOf(notificationCardData).toPageUiData( listOf(notificationCardData).toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = false, showNotificationPage = false,
showAddWidgetPage = false, showAddWidgetPage = false,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -251,11 +340,13 @@ class OnboardingMapperTest {
@Test @Test
fun noPrerequisites_shouldDisplayCard_returnsConvertedPage() { fun noPrerequisites_shouldDisplayCard_returnsConvertedPage() {
val jexlConditions = mapOf("ALWAYS" to "true", "NEVER" to "false") val jexlConditions = mapOf("ALWAYS" to "true", "NEVER" to "false")
val expected = listOf(syncPageUiData) val expected = listOf(syncPageUiDataWithPrivacyCaption)
assertEquals( assertEquals(
expected, expected,
listOf(syncCardData).toPageUiData( listOf(syncCardData).toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = false, showNotificationPage = false,
showAddWidgetPage = false, showAddWidgetPage = false,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -264,24 +355,34 @@ class OnboardingMapperTest {
) )
} }
} }
val privacyCaption: Caption = mockk(relaxed = true)
private val defaultBrowserPageUiData = OnboardingPageUiData( private val defaultBrowserPageUiDataWithPrivacyCaption = OnboardingPageUiData(
type = OnboardingPageUiData.Type.DEFAULT_BROWSER, type = OnboardingPageUiData.Type.DEFAULT_BROWSER,
imageRes = R.drawable.ic_onboarding_welcome, imageRes = R.drawable.ic_onboarding_welcome,
title = "default browser title", title = "default browser title",
description = "default browser body with link text", description = "default browser body",
linkText = "link text",
primaryButtonLabel = "default browser primary button text", primaryButtonLabel = "default browser primary button text",
secondaryButtonLabel = "default browser secondary button text", secondaryButtonLabel = "default browser secondary button text",
privacyCaption = privacyCaption,
) )
private val addSearchWidgetPageUiData = OnboardingPageUiData( private val addSearchWidgetPageUiData = OnboardingPageUiData(
type = OnboardingPageUiData.Type.ADD_SEARCH_WIDGET, type = OnboardingPageUiData.Type.ADD_SEARCH_WIDGET,
imageRes = R.drawable.ic_onboarding_search_widget, imageRes = R.drawable.ic_onboarding_search_widget,
title = "add search widget title", title = "add search widget title",
description = "add search widget body with link text", description = "add search widget body",
linkText = "link text",
primaryButtonLabel = "add search widget primary button text", primaryButtonLabel = "add search widget primary button text",
secondaryButtonLabel = "add search widget secondary button text", secondaryButtonLabel = "add search widget secondary button text",
privacyCaption = null,
)
private val addSearchWidgetPageUiDataWithPrivacyCaption = OnboardingPageUiData(
type = OnboardingPageUiData.Type.ADD_SEARCH_WIDGET,
imageRes = R.drawable.ic_onboarding_search_widget,
title = "add search widget title",
description = "add search widget body",
primaryButtonLabel = "add search widget primary button text",
secondaryButtonLabel = "add search widget secondary button text",
privacyCaption = privacyCaption,
) )
private val syncPageUiData = OnboardingPageUiData( private val syncPageUiData = OnboardingPageUiData(
type = OnboardingPageUiData.Type.SYNC_SIGN_IN, type = OnboardingPageUiData.Type.SYNC_SIGN_IN,
@ -290,6 +391,16 @@ private val syncPageUiData = OnboardingPageUiData(
description = "sync body", description = "sync body",
primaryButtonLabel = "sync primary button text", primaryButtonLabel = "sync primary button text",
secondaryButtonLabel = "sync secondary button text", secondaryButtonLabel = "sync secondary button text",
privacyCaption = null,
)
private val syncPageUiDataWithPrivacyCaption = OnboardingPageUiData(
type = OnboardingPageUiData.Type.SYNC_SIGN_IN,
imageRes = R.drawable.ic_onboarding_sync,
title = "sync title",
description = "sync body",
primaryButtonLabel = "sync primary button text",
secondaryButtonLabel = "sync secondary button text",
privacyCaption = privacyCaption,
) )
private val notificationPageUiData = OnboardingPageUiData( private val notificationPageUiData = OnboardingPageUiData(
type = OnboardingPageUiData.Type.NOTIFICATION_PERMISSION, type = OnboardingPageUiData.Type.NOTIFICATION_PERMISSION,
@ -298,14 +409,14 @@ private val notificationPageUiData = OnboardingPageUiData(
description = "notification body", description = "notification body",
primaryButtonLabel = "notification primary button text", primaryButtonLabel = "notification primary button text",
secondaryButtonLabel = "notification secondary button text", secondaryButtonLabel = "notification secondary button text",
privacyCaption = null,
) )
private val defaultBrowserCardData = OnboardingCardData( private val defaultBrowserCardData = OnboardingCardData(
cardType = OnboardingCardType.DEFAULT_BROWSER, cardType = OnboardingCardType.DEFAULT_BROWSER,
imageRes = R.drawable.ic_onboarding_welcome, imageRes = R.drawable.ic_onboarding_welcome,
title = StringHolder(null, "default browser title"), title = StringHolder(null, "default browser title"),
body = StringHolder(null, "default browser body with link text"), body = StringHolder(null, "default browser body"),
linkText = StringHolder(null, "link text"),
primaryButtonLabel = StringHolder(null, "default browser primary button text"), primaryButtonLabel = StringHolder(null, "default browser primary button text"),
secondaryButtonLabel = StringHolder(null, "default browser secondary button text"), secondaryButtonLabel = StringHolder(null, "default browser secondary button text"),
ordering = 10, ordering = 10,
@ -317,8 +428,7 @@ private val defaultBrowserCardDataNoDisqualifiers = OnboardingCardData(
cardType = OnboardingCardType.DEFAULT_BROWSER, cardType = OnboardingCardType.DEFAULT_BROWSER,
imageRes = R.drawable.ic_onboarding_welcome, imageRes = R.drawable.ic_onboarding_welcome,
title = StringHolder(null, "default browser title"), title = StringHolder(null, "default browser title"),
body = StringHolder(null, "default browser body with link text"), body = StringHolder(null, "default browser body"),
linkText = StringHolder(null, "link text"),
primaryButtonLabel = StringHolder(null, "default browser primary button text"), primaryButtonLabel = StringHolder(null, "default browser primary button text"),
secondaryButtonLabel = StringHolder(null, "default browser secondary button text"), secondaryButtonLabel = StringHolder(null, "default browser secondary button text"),
ordering = 10, ordering = 10,
@ -330,8 +440,7 @@ private val addSearchWidgetCardDataNoConditions = OnboardingCardData(
cardType = OnboardingCardType.ADD_SEARCH_WIDGET, cardType = OnboardingCardType.ADD_SEARCH_WIDGET,
imageRes = R.drawable.ic_onboarding_search_widget, imageRes = R.drawable.ic_onboarding_search_widget,
title = StringHolder(null, "add search widget title"), title = StringHolder(null, "add search widget title"),
body = StringHolder(null, "add search widget body with link text"), body = StringHolder(null, "add search widget body"),
linkText = StringHolder(null, "link text"),
primaryButtonLabel = StringHolder(null, "add search widget primary button text"), primaryButtonLabel = StringHolder(null, "add search widget primary button text"),
secondaryButtonLabel = StringHolder(null, "add search widget secondary button text"), secondaryButtonLabel = StringHolder(null, "add search widget secondary button text"),
ordering = 15, ordering = 15,
@ -343,8 +452,7 @@ private val addSearchWidgetCardData = OnboardingCardData(
cardType = OnboardingCardType.ADD_SEARCH_WIDGET, cardType = OnboardingCardType.ADD_SEARCH_WIDGET,
imageRes = R.drawable.ic_onboarding_search_widget, imageRes = R.drawable.ic_onboarding_search_widget,
title = StringHolder(null, "add search widget title"), title = StringHolder(null, "add search widget title"),
body = StringHolder(null, "add search widget body with link text"), body = StringHolder(null, "add search widget body"),
linkText = StringHolder(null, "link text"),
primaryButtonLabel = StringHolder(null, "add search widget primary button text"), primaryButtonLabel = StringHolder(null, "add search widget primary button text"),
secondaryButtonLabel = StringHolder(null, "add search widget secondary button text"), secondaryButtonLabel = StringHolder(null, "add search widget secondary button text"),
ordering = 15, ordering = 15,

View File

@ -7,7 +7,6 @@ package org.mozilla.fenix.ui
import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.MockWebServer
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.customannotations.SmokeTest
@ -265,7 +264,6 @@ class AddressAutofillTest {
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1836849 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1836849
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1814032")
@Test @Test
fun verifyMultipleAddressesSelectionTest() { fun verifyMultipleAddressesSelectionTest() {
val addressFormPage = val addressFormPage =

View File

@ -14,6 +14,7 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.AppAndSystemHelper.resetSystemLocaleToEnUS
import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged
import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
@ -54,6 +55,7 @@ class ComposeNavigationToolbarTest {
@After @After
fun tearDown() { fun tearDown() {
mockWebServer.shutdown() mockWebServer.shutdown()
resetSystemLocaleToEnUS()
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/987326 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/987326

View File

@ -0,0 +1,54 @@
/* 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 org.mozilla.fenix.ui
import androidx.core.net.toUri
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithCondition
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestHelper.appContext
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
/**
* Tests for verifying the new Cookie banner blocker option and functionality.
*/
class CookieBannerBlockerTest {
@get:Rule
val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2419260
@SmokeTest
@Test
fun verifyCookieBannerBlockerSettingsOptionTest() {
runWithCondition(appContext.settings().shouldUseCookieBannerPrivateMode) {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyCookieBannerBlockerButton(enabled = true)
}
}
}
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2419273
@SmokeTest
@Test
fun verifyCFRAfterBlockingTheCookieBanner() {
runWithCondition(appContext.settings().shouldUseCookieBannerPrivateMode) {
homeScreen {
}.togglePrivateBrowsingMode()
navigationToolbar {
}.enterURLAndEnterToBrowser("voetbal24.be".toUri()) {
waitForPageToLoad()
verifyCookieBannerExists(exists = false)
verifyCookieBannerBlockerCFRExists(exists = true)
}
}
}
}

View File

@ -5,11 +5,13 @@
package org.mozilla.fenix.ui package org.mozilla.fenix.ui
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AppAndSystemHelper import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithCondition
import org.mozilla.fenix.helpers.DataGenerationHelper.getSponsoredFxSuggestPlaceHolder
import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestHelper import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.ui.robots.navigationToolbar import org.mozilla.fenix.ui.robots.navigationToolbar
@ -20,6 +22,7 @@ import org.mozilla.fenix.ui.robots.navigationToolbar
*/ */
class FirefoxSuggestTest { class FirefoxSuggestTest {
@get:Rule @get:Rule
val activityTestRule = AndroidComposeTestRule( val activityTestRule = AndroidComposeTestRule(
HomeActivityTestRule( HomeActivityTestRule(
@ -33,91 +36,156 @@ class FirefoxSuggestTest {
), ),
) { it.activity } ) { it.activity }
private val sponsoredKeyWords: Map<String, List<String>> =
mapOf(
"Amazon" to
listOf(
"Amazon.com - Official Site",
"amazon.com/?tag=admarketus-20&ref=pd_sl_924ab4435c5a5c23aa2804307ee0669ab36f88caee841ce51d1f2ecb&mfadid=adm",
),
"Nike" to
listOf(
"Nike.com - Official Site",
"nike.com/?cp=16423867261_search_318370984us128${getSponsoredFxSuggestPlaceHolder()}&mfadid=adm",
),
"Macy" to listOf(
"macys.com - Official Site",
"macys.com/?cm_mmc=Google_AdMarketPlace-_-Privacy_Instant%20Suggest-_-319101130_Broad-_-kclickid__kenshoo_clickid_&m_sc=sem&m_sb=Admarketplace&m_tp=Search&m_ac=Admarketplace&m_ag=Instant%20Suggest&m_cn=Privacy&m_pi=kclickid__kenshoo_clickid__319101130us1201${getSponsoredFxSuggestPlaceHolder()}&mfadid=adm",
),
"Spanx" to listOf(
"SPANX® - Official Site",
"spanx.com/?utm_source=admarketplace&utm_medium=cpc&utm_campaign=privacy&utm_content=319093361us1202${getSponsoredFxSuggestPlaceHolder()}&mfadid=adm",
),
"Bloom" to listOf(
"Bloomingdales.com - Official Site",
"bloomingdales.com/?cm_mmc=Admarketplace-_-Privacy-_-Privacy-_-privacy%20instant%20suggest-_-319093353us1228${getSponsoredFxSuggestPlaceHolder()}-_-kclickid__kenshoo_clickid_&mfadid=adm",
),
"Groupon" to listOf(
"groupon.com - Discover & Save!",
"groupon.com/?utm_source=google&utm_medium=cpc&utm_campaign=us_dt_sea_ggl_txt_smp_sr_cbp_ch1_nbr_k*{keyword}_m*{match-type}_d*ADMRKT_319093357us1279${getSponsoredFxSuggestPlaceHolder()}&mfadid=adm",
),
)
private val sponsoredKeyWord = sponsoredKeyWords.keys.random()
private val nonSponsoredKeyWords: Map<String, List<String>> =
mapOf(
"Marvel" to
listOf(
"Wikipedia - Marvel Cinematic Universe",
"wikipedia.org/wiki/Marvel_Cinematic_Universe",
),
"Apple" to
listOf(
"Wikipedia - Apple Inc.",
"wikipedia.org/wiki/Apple_Inc",
),
"Africa" to listOf(
"Wikipedia - African Union",
"wikipedia.org/wiki/African_Union",
),
"Ultimate" to listOf(
"Wikipedia - Ultimate Fighting Championship",
"wikipedia.org/wiki/Ultimate_Fighting_Championship",
),
"Youtube" to listOf(
"Wikipedia - YouTube",
"wikipedia.org/wiki/YouTube",
),
"Fifa" to listOf(
"Wikipedia - FIFA World Cup",
"en.m.wikipedia.org/wiki/FIFA_World_Cup",
),
)
private val nonSponsoredKeyWord = nonSponsoredKeyWords.keys.random()
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348361 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348361
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1874831")
@SmokeTest @SmokeTest
@Test @Test
fun verifyFirefoxSuggestSponsoredSearchResultsTest() { fun verifyFirefoxSuggestSponsoredSearchResultsTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) { runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar { navigationToolbar {
}.clickUrlbar { }.clickUrlbar {
typeSearch(searchTerm = "Amazon") typeSearch(searchTerm = sponsoredKeyWord)
verifySearchEngineSuggestionResults( verifySearchEngineSuggestionResults(
rule = activityTestRule, rule = activityTestRule,
searchSuggestions = arrayOf( searchSuggestions = arrayOf(
"Firefox Suggest", "Firefox Suggest",
"Amazon.com - Official Site", sponsoredKeyWords.getValue(sponsoredKeyWord)[0],
"Sponsored", "Sponsored",
), ),
searchTerm = "Amazon", searchTerm = sponsoredKeyWord,
) )
} }
} }
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348362 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348362
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1874831")
@Test @Test
fun verifyFirefoxSuggestSponsoredSearchResultsWithPartialKeywordTest() { fun verifyFirefoxSuggestSponsoredSearchResultsWithPartialKeywordTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) { runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar { navigationToolbar {
}.clickUrlbar { }.clickUrlbar {
typeSearch(searchTerm = "Amaz") typeSearch(searchTerm = sponsoredKeyWord.dropLast(1))
verifySearchEngineSuggestionResults( verifySearchEngineSuggestionResults(
rule = activityTestRule, rule = activityTestRule,
searchSuggestions = arrayOf( searchSuggestions = arrayOf(
"Firefox Suggest", "Firefox Suggest",
"Amazon.com - Official Site", sponsoredKeyWords.getValue(sponsoredKeyWord)[0],
"Sponsored", "Sponsored",
), ),
searchTerm = "Amaz", searchTerm = sponsoredKeyWord.dropLast(1),
) )
} }
} }
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348363 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348363
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1874831")
@Test @Test
fun openFirefoxSuggestSponsoredSearchResultsTest() { fun openFirefoxSuggestSponsoredSearchResultsTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) { runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar { navigationToolbar {
}.clickUrlbar { }.clickUrlbar {
typeSearch(searchTerm = "Amazon") typeSearch(searchTerm = sponsoredKeyWord)
verifySearchEngineSuggestionResults( verifySearchEngineSuggestionResults(
rule = activityTestRule, rule = activityTestRule,
searchSuggestions = arrayOf( searchSuggestions = arrayOf(
"Firefox Suggest", "Firefox Suggest",
"Amazon.com - Official Site", sponsoredKeyWords.getValue(sponsoredKeyWord)[0],
"Sponsored", "Sponsored",
), ),
searchTerm = "Amazon", searchTerm = sponsoredKeyWord,
)
}.clickSearchSuggestion("Amazon.com - Official Site") {
waitForPageToLoad()
verifyUrl(
"amazon.com/?tag=admarketus-20&ref=pd_sl_924ab4435c5a5c23aa2804307ee0669ab36f88caee841ce51d1f2ecb&mfadid=adm",
) )
}.clickSearchSuggestion(sponsoredKeyWords.getValue(sponsoredKeyWord)[0]) {
verifyUrl(sponsoredKeyWords.getValue(sponsoredKeyWord)[1])
verifyTabCounter("1") verifyTabCounter("1")
} }
} }
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348369 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348369
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1874831")
@Test @Test
fun verifyFirefoxSuggestSponsoredSearchResultsWithEditedKeywordTest() { fun verifyFirefoxSuggestSponsoredSearchResultsWithEditedKeywordTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) { runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar { navigationToolbar {
}.clickUrlbar { }.clickUrlbar {
typeSearch(searchTerm = "Amazon") typeSearch(searchTerm = sponsoredKeyWord)
deleteSearchKeywordCharacters(numberOfDeletionSteps = 3) deleteSearchKeywordCharacters(numberOfDeletionSteps = 1)
verifySearchEngineSuggestionResults( verifySearchEngineSuggestionResults(
rule = activityTestRule, rule = activityTestRule,
searchSuggestions = arrayOf( searchSuggestions = arrayOf(
"Firefox Suggest", "Firefox Suggest",
"Amazon.com - Official Site", sponsoredKeyWords.getValue(sponsoredKeyWord)[0],
"Sponsored", "Sponsored",
), ),
searchTerm = "Amazon", searchTerm = sponsoredKeyWord,
shouldEditKeyword = true, shouldEditKeyword = true,
numberOfDeletionSteps = 3, numberOfDeletionSteps = 1,
) )
} }
} }
@ -127,17 +195,17 @@ class FirefoxSuggestTest {
@SmokeTest @SmokeTest
@Test @Test
fun verifyFirefoxSuggestNonSponsoredSearchResultsTest() { fun verifyFirefoxSuggestNonSponsoredSearchResultsTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) { runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar { navigationToolbar {
}.clickUrlbar { }.clickUrlbar {
typeSearch(searchTerm = "Marvel") typeSearch(searchTerm = nonSponsoredKeyWord)
verifySearchEngineSuggestionResults( verifySearchEngineSuggestionResults(
rule = activityTestRule, rule = activityTestRule,
searchSuggestions = arrayOf( searchSuggestions = arrayOf(
"Firefox Suggest", "Firefox Suggest",
"Wikipedia - Marvel Cinematic Universe", nonSponsoredKeyWords.getValue(nonSponsoredKeyWord)[0],
), ),
searchTerm = "Marvel", searchTerm = nonSponsoredKeyWord,
) )
verifySuggestionsAreNotDisplayed( verifySuggestionsAreNotDisplayed(
rule = activityTestRule, rule = activityTestRule,
@ -152,17 +220,17 @@ class FirefoxSuggestTest {
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348375 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348375
@Test @Test
fun verifyFirefoxSuggestNonSponsoredSearchResultsWithPartialKeywordTest() { fun verifyFirefoxSuggestNonSponsoredSearchResultsWithPartialKeywordTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) { runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar { navigationToolbar {
}.clickUrlbar { }.clickUrlbar {
typeSearch(searchTerm = "Marv") typeSearch(searchTerm = nonSponsoredKeyWord.dropLast(1))
verifySearchEngineSuggestionResults( verifySearchEngineSuggestionResults(
rule = activityTestRule, rule = activityTestRule,
searchSuggestions = arrayOf( searchSuggestions = arrayOf(
"Firefox Suggest", "Firefox Suggest",
"Wikipedia - Marvel Cinematic Universe", nonSponsoredKeyWords.getValue(nonSponsoredKeyWord)[0],
), ),
searchTerm = "Marv", searchTerm = nonSponsoredKeyWord.dropLast(1),
) )
} }
} }
@ -171,23 +239,21 @@ class FirefoxSuggestTest {
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348376 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348376
@Test @Test
fun openFirefoxSuggestNonSponsoredSearchResultsTest() { fun openFirefoxSuggestNonSponsoredSearchResultsTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) { runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar { navigationToolbar {
}.clickUrlbar { }.clickUrlbar {
typeSearch(searchTerm = "Marvel") typeSearch(searchTerm = nonSponsoredKeyWord)
verifySearchEngineSuggestionResults( verifySearchEngineSuggestionResults(
rule = activityTestRule, rule = activityTestRule,
searchSuggestions = arrayOf( searchSuggestions = arrayOf(
"Firefox Suggest", "Firefox Suggest",
"Wikipedia - Marvel Cinematic Universe", nonSponsoredKeyWords.getValue(nonSponsoredKeyWord)[0],
), ),
searchTerm = "Marvel", searchTerm = nonSponsoredKeyWord,
) )
}.clickSearchSuggestion("Wikipedia - Marvel Cinematic Universe") { }.clickSearchSuggestion(nonSponsoredKeyWords.getValue(nonSponsoredKeyWord)[0]) {
waitForPageToLoad() waitForPageToLoad()
verifyUrl( verifyUrl(nonSponsoredKeyWords.getValue(nonSponsoredKeyWord)[1])
"wikipedia.org/wiki/Marvel_Cinematic_Universe",
)
} }
} }
} }

View File

@ -607,14 +607,12 @@ class LoginsTest {
revealPassword() revealPassword()
verifyPasswordSaved("firefox") verifyPasswordSaved("firefox")
}.goBackToSavedLogins { }.goBackToSavedLogins {
clickSearchLoginButton()
searchLogin("android") searchLogin("android")
viewSavedLoginDetails(originWebsite) viewSavedLoginDetails(originWebsite)
verifyLoginItemUsername("android") verifyLoginItemUsername("android")
revealPassword() revealPassword()
verifyPasswordSaved("firefox") verifyPasswordSaved("firefox")
}.goBackToSavedLogins { }.goBackToSavedLogins {
clickSearchLoginButton()
searchLogin("AnDrOiD") searchLogin("AnDrOiD")
viewSavedLoginDetails(originWebsite) viewSavedLoginDetails(originWebsite)
verifyLoginItemUsername("android") verifyLoginItemUsername("android")
@ -654,14 +652,12 @@ class LoginsTest {
revealPassword() revealPassword()
verifyPasswordSaved("firefox") verifyPasswordSaved("firefox")
}.goBackToSavedLogins { }.goBackToSavedLogins {
clickSearchLoginButton()
searchLogin("mozilla") searchLogin("mozilla")
viewSavedLoginDetails(originWebsite) viewSavedLoginDetails(originWebsite)
verifyLoginItemUsername("android") verifyLoginItemUsername("android")
revealPassword() revealPassword()
verifyPasswordSaved("firefox") verifyPasswordSaved("firefox")
}.goBackToSavedLogins { }.goBackToSavedLogins {
clickSearchLoginButton()
searchLogin("MoZiLlA") searchLogin("MoZiLlA")
viewSavedLoginDetails(originWebsite) viewSavedLoginDetails(originWebsite)
verifyLoginItemUsername("android") verifyLoginItemUsername("android")

View File

@ -37,7 +37,8 @@ class MainMenuTest {
private lateinit var mockWebServer: MockWebServer private lateinit var mockWebServer: MockWebServer
@get:Rule @get:Rule
val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides() val activityTestRule =
HomeActivityIntentTestRule.withDefaultSettingsOverrides(translationsEnabled = true)
@Before @Before
fun setUp() { fun setUp() {

View File

@ -14,6 +14,7 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.AppAndSystemHelper
import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged
import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
@ -50,6 +51,7 @@ class NavigationToolbarTest {
@After @After
fun tearDown() { fun tearDown() {
mockWebServer.shutdown() mockWebServer.shutdown()
AppAndSystemHelper.resetSystemLocaleToEnUS()
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/987326 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/987326

View File

@ -21,6 +21,7 @@ import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AppAndSystemHelper
import org.mozilla.fenix.helpers.AppAndSystemHelper.assertNativeAppOpens import org.mozilla.fenix.helpers.AppAndSystemHelper.assertNativeAppOpens
import org.mozilla.fenix.helpers.AppAndSystemHelper.denyPermission import org.mozilla.fenix.helpers.AppAndSystemHelper.denyPermission
import org.mozilla.fenix.helpers.AppAndSystemHelper.grantSystemPermission import org.mozilla.fenix.helpers.AppAndSystemHelper.grantSystemPermission
@ -92,6 +93,7 @@ class SearchTest {
@After @After
fun tearDown() { fun tearDown() {
searchMockServer.shutdown() searchMockServer.shutdown()
AppAndSystemHelper.resetSystemLocaleToEnUS()
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154189 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154189

View File

@ -327,7 +327,7 @@ class SettingsAdvancedTest {
} }
navigationToolbar { navigationToolbar {
}.enterURLAndEnterToBrowser(youTubePage) { }.enterURLAndEnterToBrowser("https://m.youtube.com/".toUri()) {
waitForPageToLoad() waitForPageToLoad()
verifyOpenLinksInAppsCFRExists(true) verifyOpenLinksInAppsCFRExists(true)
}.clickOpenLinksInAppsGoToSettingsCFRButton { }.clickOpenLinksInAppsGoToSettingsCFRButton {

View File

@ -14,6 +14,7 @@ import org.mozilla.fenix.FenixApplication
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.AppAndSystemHelper
import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources
import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged
import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
@ -51,6 +52,7 @@ class SettingsGeneralTest {
@After @After
fun tearDown() { fun tearDown() {
mockWebServer.shutdown() mockWebServer.shutdown()
AppAndSystemHelper.resetSystemLocaleToEnUS()
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2092697 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2092697

View File

@ -5,14 +5,12 @@
package org.mozilla.fenix.ui package org.mozilla.fenix.ui
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.test.espresso.Espresso.pressBack
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.TestHelper.exitMenu import org.mozilla.fenix.helpers.TestHelper.exitMenu
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.clickPageObject import org.mozilla.fenix.ui.robots.clickPageObject
import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar import org.mozilla.fenix.ui.robots.navigationToolbar
@ -182,13 +180,7 @@ class SettingsHTTPSOnlyModeTest {
waitForPageToLoad() waitForPageToLoad()
}.openNavigationToolbar { }.openNavigationToolbar {
verifyUrl(httpsPageUrl) verifyUrl(httpsPageUrl)
pressBack() }.goBackToBrowserScreen {
}
browserScreen {
}.openTabDrawer {
closeTab()
}
homeScreen {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openSettings { }.openSettings {
}.openHttpsOnlyModeMenu { }.openHttpsOnlyModeMenu {
@ -203,7 +195,6 @@ class SettingsHTTPSOnlyModeTest {
waitForPageToLoad() waitForPageToLoad()
}.openNavigationToolbar { }.openNavigationToolbar {
verifyUrl(httpPageUrl) verifyUrl(httpPageUrl)
pressBack()
} }
} }
} }

View File

@ -14,6 +14,7 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.AppAndSystemHelper.resetSystemLocaleToEnUS
import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged
import org.mozilla.fenix.helpers.AppAndSystemHelper.setSystemLocale import org.mozilla.fenix.helpers.AppAndSystemHelper.setSystemLocale
import org.mozilla.fenix.helpers.DataGenerationHelper.setTextToClipBoard import org.mozilla.fenix.helpers.DataGenerationHelper.setTextToClipBoard
@ -40,7 +41,6 @@ class SettingsSearchTest {
listOf( listOf(
"LeOSearch", "LeOSearch",
"DuckDuckGo", "DuckDuckGo",
"Google",
) )
@get:Rule @get:Rule
@ -64,6 +64,7 @@ class SettingsSearchTest {
@After @After
fun tearDown() { fun tearDown() {
mockWebServer.shutdown() mockWebServer.shutdown()
resetSystemLocaleToEnUS()
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2203333 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2203333
@ -424,6 +425,10 @@ class SettingsSearchTest {
fun verifyShowSearchSuggestionsToggleTest() { fun verifyShowSearchSuggestionsToggleTest() {
homeScreen { homeScreen {
}.openSearch { }.openSearch {
// The Google related suggestions aren't always displayed on cold run
// Bugzilla ticket: https://bugzilla.mozilla.org/show_bug.cgi?id=1813587
clickSearchSelectorButton()
selectTemporarySearchMethod("DuckDuckGo")
typeSearch("mozilla ") typeSearch("mozilla ")
verifySearchEngineSuggestionResults( verifySearchEngineSuggestionResults(
activityTestRule, activityTestRule,
@ -438,6 +443,10 @@ class SettingsSearchTest {
}.goBack { }.goBack {
}.goBack { }.goBack {
}.openSearch { }.openSearch {
// The Google related suggestions aren't always displayed on cold run
// Bugzilla ticket: https://bugzilla.mozilla.org/show_bug.cgi?id=1813587
clickSearchSelectorButton()
selectTemporarySearchMethod("DuckDuckGo")
typeSearch("mozilla") typeSearch("mozilla")
verifySuggestionsAreNotDisplayed(activityTestRule, "mozilla firefox") verifySuggestionsAreNotDisplayed(activityTestRule, "mozilla firefox")
} }

View File

@ -9,7 +9,6 @@ import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.MockWebServer
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.customannotations.SmokeTest
@ -90,7 +89,6 @@ class SponsoredShortcutsTest {
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1729335 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1729335
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/25926")
@Test @Test
fun openSponsorsAndYourPrivacyOptionTest() { fun openSponsorsAndYourPrivacyOptionTest() {
homeScreen { homeScreen {
@ -102,7 +100,6 @@ class SponsoredShortcutsTest {
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1729336 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1729336
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1807268")
@Test @Test
fun openSponsoredShortcutsSettingsOptionTest() { fun openSponsoredShortcutsSettingsOptionTest() {
homeScreen { homeScreen {

View File

@ -17,6 +17,7 @@ import androidx.compose.ui.test.performClick
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewInteraction import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.longClick
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.PickerActions import androidx.test.espresso.contrib.PickerActions
import androidx.test.espresso.matcher.RootMatchers.isDialog import androidx.test.espresso.matcher.RootMatchers.isDialog
@ -40,7 +41,6 @@ import org.junit.Assert.assertTrue
import org.junit.Assert.fail import org.junit.Assert.fail
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION
import org.mozilla.fenix.helpers.Constants.RETRY_COUNT import org.mozilla.fenix.helpers.Constants.RETRY_COUNT
import org.mozilla.fenix.helpers.Constants.TAG import org.mozilla.fenix.helpers.Constants.TAG
import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
@ -686,23 +686,28 @@ class BrowserRobot {
fun verifyCookieBannerExists(exists: Boolean) { fun verifyCookieBannerExists(exists: Boolean) {
for (i in 1..RETRY_COUNT) { for (i in 1..RETRY_COUNT) {
Log.i(TAG, "verifyCookieBannerExists: For loop: $i")
try { try {
assertUIObjectExists(cookieBanner(), exists = exists) // Wait for the blocker to kick-in and make the cookie banner disappear
itemWithResId("CybotCookiebotDialog").waitUntilGone(waitingTime)
Log.i(TAG, "verifyCookieBannerExists: Waiting for window update")
// Assert that the blocker properly dismissed the cookie banner
assertUIObjectExists(itemWithResId("CybotCookiebotDialog"), exists = exists)
break break
} catch (e: AssertionError) { } catch (e: AssertionError) {
if (i == RETRY_COUNT) { if (i == RETRY_COUNT) {
throw e throw e
} else {
browserScreen {
}.openThreeDotMenu {
}.refreshPage {
waitForPageToLoad()
} }
} }
} }
} }
assertUIObjectExists(cookieBanner(), exists = exists)
} fun verifyCookieBannerBlockerCFRExists(exists: Boolean) =
assertUIObjectExists(
itemContainingText(getStringResource(R.string.cookie_banner_cfr_message)),
exists = exists,
)
fun verifyOpenLinkInAnotherAppPrompt() { fun verifyOpenLinkInAnotherAppPrompt() {
assertUIObjectExists( assertUIObjectExists(
@ -841,7 +846,7 @@ class BrowserRobot {
button.click() button.click()
} }
fun longClickToolbar() = mDevice.findObject(By.res("$packageName:id/mozac_browser_toolbar_url_view")).click(LONG_CLICK_DURATION) fun longClickToolbar() = onView(withId(R.id.mozac_browser_toolbar_url_view)).perform(longClick())
fun verifyDownloadPromptIsDismissed() = fun verifyDownloadPromptIsDismissed() =
assertUIObjectExists( assertUIObjectExists(
@ -1149,6 +1154,7 @@ class BrowserRobot {
"$packageName:id/action", "$packageName:id/action",
getStringResource(R.string.open_in_app_cfr_positive_button_text), getStringResource(R.string.open_in_app_cfr_positive_button_text),
).clickAndWaitForNewWindow(waitingTime) ).clickAndWaitForNewWindow(waitingTime)
Log.i(TAG, "clickOpenLinksInAppsGoToSettingsCFRButton: Clicked \"Go to settings\" open links in apps CFR button")
SettingsRobot().interact() SettingsRobot().interact()
return SettingsRobot.Transition() return SettingsRobot.Transition()
@ -1302,8 +1308,6 @@ fun clearTextFieldItem(item: UiObject) {
item.clearTextField() item.clearTextField()
} }
private fun cookieBanner() = itemWithResId("startsiden-gdpr-disclaimer")
// Context menu items // Context menu items
// Link URL // Link URL
private fun contextMenuLinkUrl(linkUrl: String) = private fun contextMenuLinkUrl(linkUrl: String) =

View File

@ -26,6 +26,7 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.longClick
import androidx.test.espresso.assertion.PositionAssertions.isCompletelyAbove import androidx.test.espresso.assertion.PositionAssertions.isCompletelyAbove
import androidx.test.espresso.assertion.PositionAssertions.isPartiallyBelow import androidx.test.espresso.assertion.PositionAssertions.isPartiallyBelow
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
@ -51,7 +52,6 @@ import org.junit.Assert
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.Constants.LISTS_MAXSWIPES import org.mozilla.fenix.helpers.Constants.LISTS_MAXSWIPES
import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION
import org.mozilla.fenix.helpers.Constants.TAG import org.mozilla.fenix.helpers.Constants.TAG
import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
import org.mozilla.fenix.helpers.HomeActivityComposeTestRule import org.mozilla.fenix.helpers.HomeActivityComposeTestRule
@ -86,9 +86,9 @@ class HomeScreenRobot {
" service provider, it makes it easier to keep what you do online private from anyone" + " service provider, it makes it easier to keep what you do online private from anyone" +
" else who uses this device." " else who uses this device."
fun verifyNavigationToolbar() = assertUIObjectExists(navigationToolbar) fun verifyNavigationToolbar() = assertUIObjectExists(navigationToolbar())
fun verifyHomeScreen() = assertUIObjectExists(homeScreen) fun verifyHomeScreen() = assertUIObjectExists(homeScreen())
fun verifyPrivateBrowsingHomeScreenItems() { fun verifyPrivateBrowsingHomeScreenItems() {
verifyHomeScreenAppBarItems() verifyHomeScreenAppBarItems()
@ -97,19 +97,19 @@ class HomeScreenRobot {
} }
fun verifyHomeScreenAppBarItems() = fun verifyHomeScreenAppBarItems() =
assertUIObjectExists(homeScreen, privateBrowsingButton, homepageWordmark) assertUIObjectExists(homeScreen(), privateBrowsingButton(), homepageWordmark())
fun verifyNavigationToolbarItems(numberOfOpenTabs: String = "0") = fun verifyNavigationToolbarItems(numberOfOpenTabs: String = "0") =
assertUIObjectExists(navigationToolbar, menuButton, tabCounter(numberOfOpenTabs)) assertUIObjectExists(navigationToolbar(), menuButton, tabCounter(numberOfOpenTabs))
fun verifyHomePrivateBrowsingButton() = assertUIObjectExists(privateBrowsingButton) fun verifyHomePrivateBrowsingButton() = assertUIObjectExists(privateBrowsingButton())
fun verifyHomeMenuButton() = assertUIObjectExists(menuButton) fun verifyHomeMenuButton() = assertUIObjectExists(menuButton)
fun verifyTabButton() = assertTabButton() fun verifyTabButton() = assertTabButton()
fun verifyCollectionsHeader() = assertCollectionsHeader() fun verifyCollectionsHeader() = assertCollectionsHeader()
fun verifyNoCollectionsText() = assertNoCollectionsText() fun verifyNoCollectionsText() = assertNoCollectionsText()
fun verifyHomeWordmark() { fun verifyHomeWordmark() {
homeScreenList().scrollToBeginning(3) homeScreenList().scrollToBeginning(3)
assertUIObjectExists(homepageWordmark) assertUIObjectExists(homepageWordmark())
} }
fun verifyHomeComponent() = assertHomeComponent() fun verifyHomeComponent() = assertHomeComponent()
@ -140,7 +140,7 @@ class HomeScreenRobot {
).assertExists() ).assertExists()
it.onNodeWithText( it.onNodeWithText(
getStringResource(R.string.juno_onboarding_default_browser_description_nimbus_2), getStringResource(R.string.juno_onboarding_default_browser_description_nimbus_3),
).assertExists() ).assertExists()
it.onNodeWithText( it.onNodeWithText(
@ -292,7 +292,7 @@ class HomeScreenRobot {
mDevice.waitNotNull(findObject(By.text(expectedText)), waitingTime) mDevice.waitNotNull(findObject(By.text(expectedText)), waitingTime)
} }
fun clickFirefoxLogo() = homepageWordmark.click() fun clickFirefoxLogo() = homepageWordmark().click()
fun verifyThoughtProvokingStories(enabled: Boolean) { fun verifyThoughtProvokingStories(enabled: Boolean) {
if (enabled) { if (enabled) {
@ -481,8 +481,8 @@ class HomeScreenRobot {
} }
fun openSearch(interact: SearchRobot.() -> Unit): SearchRobot.Transition { fun openSearch(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
navigationToolbar.waitForExists(waitingTime) navigationToolbar().waitForExists(waitingTime)
navigationToolbar.click() navigationToolbar().click()
mDevice.waitForIdle() mDevice.waitForIdle()
SearchRobot().interact() SearchRobot().interact()
@ -502,14 +502,14 @@ class HomeScreenRobot {
fun togglePrivateBrowsingMode(switchPBModeOn: Boolean = true) { fun togglePrivateBrowsingMode(switchPBModeOn: Boolean = true) {
// Switch to private browsing homescreen // Switch to private browsing homescreen
if (switchPBModeOn && !isPrivateModeEnabled()) { if (switchPBModeOn && !isPrivateModeEnabled()) {
privateBrowsingButton.waitForExists(waitingTime) privateBrowsingButton().waitForExists(waitingTime)
privateBrowsingButton.click() privateBrowsingButton().click()
} }
// Switch to normal browsing homescreen // Switch to normal browsing homescreen
if (!switchPBModeOn && isPrivateModeEnabled()) { if (!switchPBModeOn && isPrivateModeEnabled()) {
privateBrowsingButton.waitForExists(waitingTime) privateBrowsingButton().waitForExists(waitingTime)
privateBrowsingButton.click() privateBrowsingButton().click()
} }
} }
@ -521,7 +521,7 @@ class HomeScreenRobot {
waitingTime, waitingTime,
) )
privateBrowsingButton.click() privateBrowsingButton().click()
} }
AddToHomeScreenRobot().interact() AddToHomeScreenRobot().interact()
@ -535,7 +535,7 @@ class HomeScreenRobot {
fun openNavigationToolbar(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition { fun openNavigationToolbar(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition {
mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar")) mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
.waitForExists(waitingTime) .waitForExists(waitingTime)
navigationToolbar.click() navigationToolbar().click()
NavigationToolbarRobot().interact() NavigationToolbarRobot().interact()
return NavigationToolbarRobot.Transition() return NavigationToolbarRobot.Transition()
@ -557,7 +557,8 @@ class HomeScreenRobot {
} }
fun openContextMenuOnSponsoredShortcut(sponsoredShortcutTitle: String, interact: HomeScreenRobot.() -> Unit): Transition { fun openContextMenuOnSponsoredShortcut(sponsoredShortcutTitle: String, interact: HomeScreenRobot.() -> Unit): Transition {
sponsoredShortcut(sponsoredShortcutTitle).click(LONG_CLICK_DURATION) sponsoredShortcut(sponsoredShortcutTitle).perform(longClick())
Log.i(TAG, "openContextMenuOnSponsoredShortcut: Long clicked to open context menu for $sponsoredShortcutTitle sponsored shortcut")
HomeScreenRobot().interact() HomeScreenRobot().interact()
return Transition() return Transition()
@ -631,8 +632,10 @@ class HomeScreenRobot {
} }
fun clickSponsoredShortcutsSettingsButton(interact: SettingsSubMenuHomepageRobot.() -> Unit): SettingsSubMenuHomepageRobot.Transition { fun clickSponsoredShortcutsSettingsButton(interact: SettingsSubMenuHomepageRobot.() -> Unit): SettingsSubMenuHomepageRobot.Transition {
Log.i(TAG, "clickSponsoredShortcutsSettingsButton: Looking for: ${sponsoredShortcutsSettingsButton.selector}")
sponsoredShortcutsSettingsButton.waitForExists(waitingTime) sponsoredShortcutsSettingsButton.waitForExists(waitingTime)
sponsoredShortcutsSettingsButton.clickAndWaitForNewWindow(waitingTime) sponsoredShortcutsSettingsButton.clickAndWaitForNewWindow(waitingTime)
Log.i(TAG, "clickSponsoredShortcutsSettingsButton: Clicked ${sponsoredShortcutsSettingsButton.selector} and waiting for $waitingTime for a new window")
SettingsSubMenuHomepageRobot().interact() SettingsSubMenuHomepageRobot().interact()
return SettingsSubMenuHomepageRobot.Transition() return SettingsSubMenuHomepageRobot.Transition()
@ -939,18 +942,19 @@ private fun saveTabsToCollectionButton() = onView(withId(R.id.add_tabs_to_collec
private fun tabsCounter() = onView(withId(R.id.tab_button)) private fun tabsCounter() = onView(withId(R.id.tab_button))
private fun sponsoredShortcut(sponsoredShortcutTitle: String) = private fun sponsoredShortcut(sponsoredShortcutTitle: String) =
mDevice.findObject( onView(
By allOf(
.res("$packageName:id/top_site_title") withId(R.id.top_site_title),
.textContains(sponsoredShortcutTitle), withText(sponsoredShortcutTitle),
),
) )
private fun storyByTopicItem(composeTestRule: ComposeTestRule, position: Int) = private fun storyByTopicItem(composeTestRule: ComposeTestRule, position: Int) =
composeTestRule.onNodeWithTag("pocket.categories").onChildAt(position - 1) composeTestRule.onNodeWithTag("pocket.categories").onChildAt(position - 1)
private val homeScreen = private fun homeScreen() =
itemWithResId("$packageName:id/homeLayout") itemWithResId("$packageName:id/homeLayout")
private val privateBrowsingButton = private fun privateBrowsingButton() =
itemWithResId("$packageName:id/privateBrowsingButton") itemWithResId("$packageName:id/privateBrowsingButton")
private fun isPrivateModeEnabled(): Boolean = private fun isPrivateModeEnabled(): Boolean =
@ -959,10 +963,10 @@ private fun isPrivateModeEnabled(): Boolean =
"Disable private browsing", "Disable private browsing",
).exists() ).exists()
private val homepageWordmark = private fun homepageWordmark() =
itemWithResId("$packageName:id/wordmark") itemWithResId("$packageName:id/wordmark")
private val navigationToolbar = private fun navigationToolbar() =
itemWithResId("$packageName:id/toolbar") itemWithResId("$packageName:id/toolbar")
private val menuButton = private val menuButton =
itemWithResId("$packageName:id/menuButton") itemWithResId("$packageName:id/menuButton")

View File

@ -15,6 +15,7 @@ import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource import androidx.test.espresso.IdlingResource
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.longClick
import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.contrib.RecyclerViewActions
@ -43,7 +44,6 @@ import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
import org.mozilla.fenix.helpers.SessionLoadedIdlingResource import org.mozilla.fenix.helpers.SessionLoadedIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
@ -149,7 +149,7 @@ class NavigationToolbarRobot {
assertTrue( assertTrue(
itemWithResId("$packageName:id/browserLayout").waitForExists(waitingTime) || itemWithResId("$packageName:id/browserLayout").waitForExists(waitingTime) ||
itemWithResId("$packageName:id/download_button").waitForExists(waitingTime) || itemWithResId("$packageName:id/download_button").waitForExists(waitingTime) ||
itemWithText(getStringResource(R.string.tcp_cfr_message)).waitForExists(waitingTime), itemWithResId("cfr.dismiss").waitForExists(waitingTime),
) )
} }
@ -272,9 +272,19 @@ class NavigationToolbarRobot {
return HomeScreenRobot.Transition() return HomeScreenRobot.Transition()
} }
fun goBackToBrowserScreen(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.pressBack()
Log.i(TAG, "goBackToBrowserScreen: Dismiss awesome bar using device back button")
mDevice.waitForWindowUpdate(packageName, waitingTimeShort)
Log.i(TAG, "goBackToBrowserScreen: Waited $waitingTimeShort for window update")
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun openTabButtonShortcutsMenu(interact: NavigationToolbarRobot.() -> Unit): Transition { fun openTabButtonShortcutsMenu(interact: NavigationToolbarRobot.() -> Unit): Transition {
mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/counter_root"))) mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/counter_root")))
tabsCounter().click(LONG_CLICK_DURATION) tabsCounter().perform(longClick())
Log.i(TAG, "Tabs counter long-click successful.") Log.i(TAG, "Tabs counter long-click successful.")
NavigationToolbarRobot().interact() NavigationToolbarRobot().interact()
@ -388,8 +398,7 @@ private fun awesomeBar() =
mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_edit_url_view")) mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_edit_url_view"))
private fun threeDotButton() = onView(withId(R.id.mozac_browser_toolbar_menu)) private fun threeDotButton() = onView(withId(R.id.mozac_browser_toolbar_menu))
private fun tabTrayButton() = onView(withId(R.id.tab_button)) private fun tabTrayButton() = onView(withId(R.id.tab_button))
private fun tabsCounter() = private fun tabsCounter() = onView(withId(R.id.mozac_browser_toolbar_browser_actions))
mDevice.findObject(By.res("$packageName:id/counter_root"))
private fun fillLinkButton() = onView(withId(R.id.fill_link_from_clipboard)) private fun fillLinkButton() = onView(withId(R.id.fill_link_from_clipboard))
private fun clearAddressBarButton() = itemWithResId("$packageName:id/mozac_browser_toolbar_clear_view") private fun clearAddressBarButton() = itemWithResId("$packageName:id/mozac_browser_toolbar_clear_view")
private fun readerViewToggle() = private fun readerViewToggle() =

View File

@ -107,6 +107,27 @@ class SettingsRobot {
fun verifyPrivacyHeading() = assertPrivacyHeading() fun verifyPrivacyHeading() = assertPrivacyHeading()
fun verifyHTTPSOnlyModeButton() = assertHTTPSOnlyModeButton() fun verifyHTTPSOnlyModeButton() = assertHTTPSOnlyModeButton()
fun verifyCookieBannerBlockerButton(enabled: Boolean) {
scrollToElementByText(getStringResource(R.string.preferences_cookie_banner_reduction_private_mode))
onView(withText(R.string.preferences_cookie_banner_reduction_private_mode))
.check(
matches(
hasCousin(
CoreMatchers.allOf(
withClassName(endsWith("Switch")),
if (enabled) {
isChecked()
} else {
isNotChecked()
},
),
),
),
)
Log.i(TAG, "verifyCookieBannerBlockerButton: Verified if cookie banner blocker toggle is enabled: $enabled")
}
fun verifyEnhancedTrackingProtectionButton() = assertEnhancedTrackingProtectionButton() fun verifyEnhancedTrackingProtectionButton() = assertEnhancedTrackingProtectionButton()
fun verifyLoginsAndPasswordsButton() = assertLoginsAndPasswordsButton() fun verifyLoginsAndPasswordsButton() = assertLoginsAndPasswordsButton()
fun verifyPrivateBrowsingButton() = assertPrivateBrowsingButton() fun verifyPrivateBrowsingButton() = assertPrivateBrowsingButton()
@ -583,6 +604,7 @@ private fun assertOpenLinksInAppsButton() {
scrollToElementByText("Open links in apps") scrollToElementByText("Open links in apps")
openLinksInAppsButton() openLinksInAppsButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
Log.i(TAG, "clickOpenLinksInAppsGoToSettingsCFRButton: Verified \"Open links in apps\" setting option")
} }
// ADVANCED SECTION // ADVANCED SECTION

View File

@ -124,7 +124,8 @@ private fun assertCurrentTimestamp() {
private fun assertWhatIsNewInFirefoxPreview() { private fun assertWhatIsNewInFirefoxPreview() {
aboutMenuList.scrollToEnd(LISTS_MAXSWIPES) aboutMenuList.scrollToEnd(LISTS_MAXSWIPES)
onView(withText("Whats new in $appName")) val firefox = TestHelper.appContext.getString(R.string.firefox)
onView(withText("Whats new in $firefox"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.perform(click()) .perform(click())
} }

View File

@ -4,6 +4,7 @@
package org.mozilla.fenix.ui.robots package org.mozilla.fenix.ui.robots
import android.util.Log
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers
@ -21,6 +22,7 @@ import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.Matchers import org.hamcrest.Matchers
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.Constants
import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
@ -61,6 +63,7 @@ class SettingsSubMenuHomepageRobot {
assertHomepageButton() assertHomepageButton()
assertLastTabButton() assertLastTabButton()
assertHomepageAfterFourHoursButton() assertHomepageAfterFourHoursButton()
Log.i(Constants.TAG, "verifyHomePageView: Verified the home page elements")
} }
fun verifySelectedOpeningScreenOption(openingScreenOption: String) = fun verifySelectedOpeningScreenOption(openingScreenOption: String) =

View File

@ -118,7 +118,7 @@ class SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot {
) )
fun searchLogin(searchTerm: String) = fun searchLogin(searchTerm: String) =
itemContainingText(getStringResource(R.string.preferences_passwords_saved_logins_search)).setText(searchTerm) itemWithResId("$packageName:id/search").setText(searchTerm)
fun verifySavedLoginsSectionUsername(username: String) = fun verifySavedLoginsSectionUsername(username: String) =
mDevice.waitNotNull(Until.findObjects(By.text(username))) mDevice.waitNotNull(Until.findObjects(By.text(username)))

View File

@ -54,9 +54,9 @@ import org.mozilla.fenix.nimbus.FxNimbus
class ThreeDotMenuMainRobot { class ThreeDotMenuMainRobot {
fun verifyShareAllTabsButton() = assertShareAllTabsButton() fun verifyShareAllTabsButton() = assertShareAllTabsButton()
fun verifySettingsButton() = assertUIObjectExists(settingsButton()) fun verifySettingsButton() = assertUIObjectExists(settingsButton())
fun verifyHistoryButton() = assertUIObjectExists(historyButton) fun verifyHistoryButton() = assertUIObjectExists(historyButton())
fun verifyThreeDotMenuExists() = threeDotMenuRecyclerViewExists() fun verifyThreeDotMenuExists() = threeDotMenuRecyclerViewExists()
fun verifyAddBookmarkButton() = assertUIObjectExists(addBookmarkButton) fun verifyAddBookmarkButton() = assertUIObjectExists(addBookmarkButton())
fun verifyEditBookmarkButton() = assertEditBookmarkButton() fun verifyEditBookmarkButton() = assertEditBookmarkButton()
fun verifyCloseAllTabsButton() = assertCloseAllTabsButton() fun verifyCloseAllTabsButton() = assertCloseAllTabsButton()
fun verifyReaderViewAppearance(visible: Boolean) = assertReaderViewAppearanceButton(visible) fun verifyReaderViewAppearance(visible: Boolean) = assertReaderViewAppearanceButton(visible)
@ -76,9 +76,9 @@ class ThreeDotMenuMainRobot {
fun verifyShareTabButton() = assertShareTabButton() fun verifyShareTabButton() = assertShareTabButton()
fun verifySelectTabs() = assertSelectTabsButton() fun verifySelectTabs() = assertSelectTabsButton()
fun verifyFindInPageButton() = assertUIObjectExists(findInPageButton) fun verifyFindInPageButton() = assertUIObjectExists(findInPageButton())
fun verifyAddToShortcutsButton(shouldExist: Boolean) = fun verifyAddToShortcutsButton(shouldExist: Boolean) =
assertUIObjectExists(addToShortcutsButton, exists = shouldExist) assertUIObjectExists(addToShortcutsButton(), exists = shouldExist)
fun verifyRemoveFromShortcutsButton() = assertRemoveFromShortcutsButton() fun verifyRemoveFromShortcutsButton() = assertRemoveFromShortcutsButton()
fun verifyShareTabsOverlay() = assertShareTabsOverlay() fun verifyShareTabsOverlay() = assertShareTabsOverlay()
@ -90,20 +90,21 @@ class ThreeDotMenuMainRobot {
fun verifyPageThreeDotMainMenuItems(isRequestDesktopSiteEnabled: Boolean) { fun verifyPageThreeDotMainMenuItems(isRequestDesktopSiteEnabled: Boolean) {
expandMenu() expandMenu()
assertUIObjectExists( assertUIObjectExists(
normalBrowsingNewTabButton, normalBrowsingNewTabButton(),
bookmarksButton, bookmarksButton(),
historyButton, historyButton(),
downloadsButton, downloadsButton(),
addOnsButton, addOnsButton(),
syncAndSaveDataButton, syncAndSaveDataButton(),
findInPageButton, findInPageButton(),
desktopSiteButton, desktopSiteButton(),
reportSiteIssueButton, reportSiteIssueButton(),
addToHomeScreenButton, addToHomeScreenButton(),
addToShortcutsButton, addToShortcutsButton(),
saveToCollectionButton, saveToCollectionButton(),
addBookmarkButton, addBookmarkButton(),
desktopSiteToggle(isRequestDesktopSiteEnabled), desktopSiteToggle(isRequestDesktopSiteEnabled),
translateButton(),
) )
// Swipe to second part of menu // Swipe to second part of menu
expandMenu() expandMenu()
@ -111,28 +112,28 @@ class ThreeDotMenuMainRobot {
settingsButton(), settingsButton(),
) )
if (FxNimbus.features.print.value().browserPrintEnabled) { if (FxNimbus.features.print.value().browserPrintEnabled) {
assertUIObjectExists(printContentButton) assertUIObjectExists(printContentButton())
} }
assertUIObjectExists( assertUIObjectExists(
backButton, backButton(),
forwardButton, forwardButton(),
shareButton, shareButton(),
refreshButton, refreshButton(),
) )
} }
fun verifyHomeThreeDotMainMenuItems(isRequestDesktopSiteEnabled: Boolean) { fun verifyHomeThreeDotMainMenuItems(isRequestDesktopSiteEnabled: Boolean) {
assertUIObjectExists( assertUIObjectExists(
bookmarksButton, bookmarksButton(),
historyButton, historyButton(),
downloadsButton, downloadsButton(),
addOnsButton, addOnsButton(),
// Disabled step due to https://github.com/mozilla-mobile/fenix/issues/26788 // Disabled step due to https://github.com/mozilla-mobile/fenix/issues/26788
// syncAndSaveDataButton, // syncAndSaveDataButton,
desktopSiteButton, desktopSiteButton(),
whatsNewButton, whatsNewButton(),
helpButton, helpButton(),
customizeHomeButton, customizeHomeButton(),
settingsButton(), settingsButton(),
desktopSiteToggle(isRequestDesktopSiteEnabled), desktopSiteToggle(isRequestDesktopSiteEnabled),
) )
@ -202,7 +203,7 @@ class ThreeDotMenuMainRobot {
fun openDownloadsManager(interact: DownloadRobot.() -> Unit): DownloadRobot.Transition { fun openDownloadsManager(interact: DownloadRobot.() -> Unit): DownloadRobot.Transition {
threeDotMenuRecyclerView().perform(swipeDown()) threeDotMenuRecyclerView().perform(swipeDown())
Log.i(TAG, "openDownloadsManager: Swiped up main menu") Log.i(TAG, "openDownloadsManager: Swiped up main menu")
downloadsButton.click() downloadsButton().click()
Log.i(TAG, "openDownloadsManager: Clicked main menu \"DOWNLOADS\" button") Log.i(TAG, "openDownloadsManager: Clicked main menu \"DOWNLOADS\" button")
DownloadRobot().interact() DownloadRobot().interact()
@ -212,7 +213,7 @@ class ThreeDotMenuMainRobot {
fun openSyncSignIn(interact: SyncSignInRobot.() -> Unit): SyncSignInRobot.Transition { fun openSyncSignIn(interact: SyncSignInRobot.() -> Unit): SyncSignInRobot.Transition {
threeDotMenuRecyclerView().perform(swipeDown()) threeDotMenuRecyclerView().perform(swipeDown())
mDevice.waitNotNull(Until.findObject(By.text("Sync and save data")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("Sync and save data")), waitingTime)
syncAndSaveDataButton.click() syncAndSaveDataButton().click()
SyncSignInRobot().interact() SyncSignInRobot().interact()
return SyncSignInRobot.Transition() return SyncSignInRobot.Transition()
@ -222,7 +223,7 @@ class ThreeDotMenuMainRobot {
threeDotMenuRecyclerView().perform(swipeDown()) threeDotMenuRecyclerView().perform(swipeDown())
mDevice.waitNotNull(Until.findObject(By.text("Bookmarks")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("Bookmarks")), waitingTime)
bookmarksButton.click() bookmarksButton().click()
assertUIObjectExists(itemWithResId("$packageName:id/bookmark_list")) assertUIObjectExists(itemWithResId("$packageName:id/bookmark_list"))
BookmarksRobot().interact() BookmarksRobot().interact()
@ -230,7 +231,7 @@ class ThreeDotMenuMainRobot {
} }
fun clickNewTabButton(interact: SearchRobot.() -> Unit): SearchRobot.Transition { fun clickNewTabButton(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
normalBrowsingNewTabButton.click() normalBrowsingNewTabButton().click()
SearchRobot().interact() SearchRobot().interact()
return SearchRobot.Transition() return SearchRobot.Transition()
@ -239,7 +240,7 @@ class ThreeDotMenuMainRobot {
fun openHistory(interact: HistoryRobot.() -> Unit): HistoryRobot.Transition { fun openHistory(interact: HistoryRobot.() -> Unit): HistoryRobot.Transition {
threeDotMenuRecyclerView().perform(swipeDown()) threeDotMenuRecyclerView().perform(swipeDown())
mDevice.waitNotNull(Until.findObject(By.text("History")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("History")), waitingTime)
historyButton.click() historyButton().click()
HistoryRobot().interact() HistoryRobot().interact()
return HistoryRobot.Transition() return HistoryRobot.Transition()
@ -247,7 +248,7 @@ class ThreeDotMenuMainRobot {
fun bookmarkPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun bookmarkPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.text("Bookmarks")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("Bookmarks")), waitingTime)
addBookmarkButton.click() addBookmarkButton().click()
BrowserRobot().interact() BrowserRobot().interact()
return BrowserRobot.Transition() return BrowserRobot.Transition()
@ -263,7 +264,7 @@ class ThreeDotMenuMainRobot {
fun openHelp(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun openHelp(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.text("Help")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("Help")), waitingTime)
helpButton.click() helpButton().click()
BrowserRobot().interact() BrowserRobot().interact()
return BrowserRobot.Transition() return BrowserRobot.Transition()
@ -278,7 +279,7 @@ class ThreeDotMenuMainRobot {
waitingTime, waitingTime,
) )
customizeHomeButton.click() customizeHomeButton().click()
mDevice.findObject( mDevice.findObject(
UiSelector().resourceId("$packageName:id/recycler_view"), UiSelector().resourceId("$packageName:id/recycler_view"),
@ -289,21 +290,21 @@ class ThreeDotMenuMainRobot {
} }
fun goForward(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun goForward(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
forwardButton.click() forwardButton().click()
BrowserRobot().interact() BrowserRobot().interact()
return BrowserRobot.Transition() return BrowserRobot.Transition()
} }
fun goToPreviousPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun goToPreviousPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
backButton.click() backButton().click()
BrowserRobot().interact() BrowserRobot().interact()
return BrowserRobot.Transition() return BrowserRobot.Transition()
} }
fun clickShareButton(interact: ShareOverlayRobot.() -> Unit): ShareOverlayRobot.Transition { fun clickShareButton(interact: ShareOverlayRobot.() -> Unit): ShareOverlayRobot.Transition {
shareButton.click() shareButton().click()
Log.i(TAG, "clickShareButton: Clicked main menu share button") Log.i(TAG, "clickShareButton: Clicked main menu share button")
mDevice.waitNotNull(Until.findObject(By.text("ALL ACTIONS")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("ALL ACTIONS")), waitingTime)
@ -320,7 +321,7 @@ class ThreeDotMenuMainRobot {
} }
fun refreshPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun refreshPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
refreshButton.also { refreshButton().also {
Log.i(TAG, "refreshPage: Looking for refresh button") Log.i(TAG, "refreshPage: Looking for refresh button")
it.waitForExists(waitingTime) it.waitForExists(waitingTime)
it.click() it.click()
@ -349,7 +350,7 @@ class ThreeDotMenuMainRobot {
fun openReportSiteIssue(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun openReportSiteIssue(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
threeDotMenuRecyclerView().perform(swipeUp()) threeDotMenuRecyclerView().perform(swipeUp())
threeDotMenuRecyclerView().perform(swipeUp()) threeDotMenuRecyclerView().perform(swipeUp())
reportSiteIssueButton.click() reportSiteIssueButton().click()
BrowserRobot().interact() BrowserRobot().interact()
return BrowserRobot.Transition() return BrowserRobot.Transition()
@ -359,7 +360,7 @@ class ThreeDotMenuMainRobot {
threeDotMenuRecyclerView().perform(swipeUp()) threeDotMenuRecyclerView().perform(swipeUp())
threeDotMenuRecyclerView().perform(swipeUp()) threeDotMenuRecyclerView().perform(swipeUp())
mDevice.waitNotNull(Until.findObject(By.text("Find in page")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("Find in page")), waitingTime)
findInPageButton.click() findInPageButton().click()
FindInPageRobot().interact() FindInPageRobot().interact()
return FindInPageRobot.Transition() return FindInPageRobot.Transition()
@ -367,7 +368,7 @@ class ThreeDotMenuMainRobot {
fun openWhatsNew(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun openWhatsNew(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.text("Whats new")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("Whats new")), waitingTime)
whatsNewButton.click() whatsNewButton().click()
BrowserRobot().interact() BrowserRobot().interact()
return BrowserRobot.Transition() return BrowserRobot.Transition()
@ -385,7 +386,7 @@ class ThreeDotMenuMainRobot {
fun addToFirefoxHome(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun addToFirefoxHome(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
for (i in 1..RETRY_COUNT) { for (i in 1..RETRY_COUNT) {
try { try {
addToShortcutsButton.also { addToShortcutsButton().also {
it.waitForExists(waitingTime) it.waitForExists(waitingTime)
it.click() it.click()
} }
@ -416,7 +417,7 @@ class ThreeDotMenuMainRobot {
} }
fun openAddToHomeScreen(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenRobot.Transition { fun openAddToHomeScreen(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenRobot.Transition {
addToHomeScreenButton.clickAndWaitForNewWindow(waitingTime) addToHomeScreenButton().clickAndWaitForNewWindow(waitingTime)
AddToHomeScreenRobot().interact() AddToHomeScreenRobot().interact()
return AddToHomeScreenRobot.Transition() return AddToHomeScreenRobot.Transition()
@ -437,7 +438,7 @@ class ThreeDotMenuMainRobot {
threeDotMenuRecyclerView().perform(swipeUp()) threeDotMenuRecyclerView().perform(swipeUp())
mDevice.waitNotNull(Until.findObject(By.text("Save to collection")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("Save to collection")), waitingTime)
saveToCollectionButton.click() saveToCollectionButton().click()
CollectionRobot().interact() CollectionRobot().interact()
return CollectionRobot.Transition() return CollectionRobot.Transition()
} }
@ -465,7 +466,7 @@ class ThreeDotMenuMainRobot {
fun switchDesktopSiteMode(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun switchDesktopSiteMode(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
threeDotMenuRecyclerView().perform(swipeUp()) threeDotMenuRecyclerView().perform(swipeUp())
threeDotMenuRecyclerView().perform(swipeUp()) threeDotMenuRecyclerView().perform(swipeUp())
desktopSiteButton.click() desktopSiteButton().click()
BrowserRobot().interact() BrowserRobot().interact()
return BrowserRobot.Transition() return BrowserRobot.Transition()
@ -481,8 +482,8 @@ class ThreeDotMenuMainRobot {
fun clickPrintButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun clickPrintButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
threeDotMenuRecyclerView().perform(swipeUp()) threeDotMenuRecyclerView().perform(swipeUp())
threeDotMenuRecyclerView().perform(swipeUp()) threeDotMenuRecyclerView().perform(swipeUp())
printButton.waitForExists(waitingTime) printButton().waitForExists(waitingTime)
printButton.click() printButton().click()
BrowserRobot().interact() BrowserRobot().interact()
return BrowserRobot.Transition() return BrowserRobot.Transition()
@ -558,7 +559,7 @@ private fun openInAppButton() =
private fun clickAddonsManagerButton() { private fun clickAddonsManagerButton() {
onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeDown()) onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeDown())
addOnsButton.click() addOnsButton().click()
} }
private fun shareAllTabsButton() = private fun shareAllTabsButton() =
@ -571,15 +572,15 @@ private fun assertShareAllTabsButton() {
) )
} }
private val bookmarksButton = private fun bookmarksButton() =
itemContainingText(getStringResource(R.string.library_bookmarks)) itemContainingText(getStringResource(R.string.library_bookmarks))
private val historyButton = private fun historyButton() =
itemContainingText(getStringResource(R.string.library_history)) itemContainingText(getStringResource(R.string.library_history))
private val downloadsButton = private fun downloadsButton() =
itemContainingText(getStringResource(R.string.library_downloads)) itemContainingText(getStringResource(R.string.library_downloads))
private val addOnsButton = private fun addOnsButton() =
itemContainingText(getStringResource(R.string.browser_menu_add_ons)) itemContainingText(getStringResource(R.string.browser_menu_add_ons))
private val desktopSiteButton = private fun desktopSiteButton() =
itemContainingText(getStringResource(R.string.browser_menu_desktop_site)) itemContainingText(getStringResource(R.string.browser_menu_desktop_site))
private fun desktopSiteToggle(state: Boolean) = private fun desktopSiteToggle(state: Boolean) =
checkedItemWithResIdAndText( checkedItemWithResIdAndText(
@ -587,31 +588,32 @@ private fun desktopSiteToggle(state: Boolean) =
getStringResource(R.string.browser_menu_desktop_site), getStringResource(R.string.browser_menu_desktop_site),
state, state,
) )
private val whatsNewButton = private fun whatsNewButton() =
itemContainingText(getStringResource(R.string.browser_menu_whats_new)) itemContainingText(getStringResource(R.string.browser_menu_whats_new))
private val helpButton = private fun helpButton() =
itemContainingText(getStringResource(R.string.browser_menu_help)) itemContainingText(getStringResource(R.string.browser_menu_help))
private val customizeHomeButton = private fun customizeHomeButton() =
itemContainingText(getStringResource(R.string.browser_menu_customize_home_1)) itemContainingText(getStringResource(R.string.browser_menu_customize_home_1))
private fun settingsButton(localizedText: String = getStringResource(R.string.browser_menu_settings)) = private fun settingsButton(localizedText: String = getStringResource(R.string.browser_menu_settings)) =
itemContainingText(localizedText) itemContainingText(localizedText)
private val syncAndSaveDataButton = private fun syncAndSaveDataButton() =
itemContainingText(getStringResource(R.string.sync_menu_sync_and_save_data)) itemContainingText(getStringResource(R.string.sync_menu_sync_and_save_data))
private val normalBrowsingNewTabButton = private fun normalBrowsingNewTabButton() =
itemContainingText(getStringResource(R.string.library_new_tab)) itemContainingText(getStringResource(R.string.library_new_tab))
private val addBookmarkButton = private fun addBookmarkButton() =
itemWithResIdAndText( itemWithResIdAndText(
"$packageName:id/checkbox", "$packageName:id/checkbox",
getStringResource(R.string.browser_menu_add), getStringResource(R.string.browser_menu_add),
) )
private val findInPageButton = itemContainingText(getStringResource(R.string.browser_menu_find_in_page)) private fun findInPageButton() = itemContainingText(getStringResource(R.string.browser_menu_find_in_page))
private val reportSiteIssueButton = itemContainingText("Report Site Issue") private fun translateButton() = itemContainingText(getStringResource(R.string.browser_menu_translations))
private val addToHomeScreenButton = itemContainingText(getStringResource(R.string.browser_menu_add_to_homescreen)) private fun reportSiteIssueButton() = itemContainingText("Report Site Issue")
private val addToShortcutsButton = itemContainingText(getStringResource(R.string.browser_menu_add_to_shortcuts)) private fun addToHomeScreenButton() = itemContainingText(getStringResource(R.string.browser_menu_add_to_homescreen))
private val saveToCollectionButton = itemContainingText(getStringResource(R.string.browser_menu_save_to_collection_2)) private fun addToShortcutsButton() = itemContainingText(getStringResource(R.string.browser_menu_add_to_shortcuts))
private val printContentButton = itemContainingText(getStringResource(R.string.menu_print)) private fun saveToCollectionButton() = itemContainingText(getStringResource(R.string.browser_menu_save_to_collection_2))
private val backButton = itemWithDescription(getStringResource(R.string.browser_menu_back)) private fun printContentButton() = itemContainingText(getStringResource(R.string.menu_print))
private val forwardButton = itemWithDescription(getStringResource(R.string.browser_menu_forward)) private fun backButton() = itemWithDescription(getStringResource(R.string.browser_menu_back))
private val shareButton = itemWithDescription(getStringResource(R.string.share_button_content_description)) private fun forwardButton() = itemWithDescription(getStringResource(R.string.browser_menu_forward))
private val refreshButton = itemWithDescription(getStringResource(R.string.browser_menu_refresh)) private fun shareButton() = itemWithDescription(getStringResource(R.string.share_button_content_description))
private val printButton = itemWithText("Print") private fun refreshButton() = itemWithDescription(getStringResource(R.string.browser_menu_refresh))
private fun printButton() = itemWithText("Print")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -4,5 +4,5 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<resources> <resources>
<!-- Name of the application --> <!-- Name of the application -->
<string name="app_name" translatable="false">LeOsium</string> <string name="app_name" translatable="false">LeOSium</string>
</resources> </resources>

View File

@ -54,7 +54,7 @@
android:name=".FenixApplication" android:name=".FenixApplication"
android:allowBackup="false" android:allowBackup="false"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="LeOSium" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/NormalTheme" android:theme="@style/NormalTheme"
@ -354,6 +354,7 @@
<service <service
android:name=".downloads.DownloadService" android:name=".downloads.DownloadService"
android:foregroundServiceType="dataSync"
android:exported="false" /> android:exported="false" />
<receiver <receiver
@ -374,8 +375,14 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<service android:name=".session.PrivateNotificationService" <service
android:exported="false" /> android:name=".session.PrivateNotificationService"
android:exported="false"
android:foregroundServiceType="specialUse">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="This foreground service allows users to easily remove private tabs from the notification" />
</service>
<service <service
android:name=".messaging.NotificationDismissedService" android:name=".messaging.NotificationDismissedService"

View File

@ -1,4 +1,4 @@
grep -RiIl 'www.google.com' | xargs sed -i 's/www.google.com/google-b-m.ddns.net/g' grep -RiIl 'www.google.com' | xargs sed -i 's/www.google.com/leosearch.ddns.net/g'

View File

@ -39,4 +39,5 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) {
FromRecentlyClosed(R.id.recentlyClosedFragment), FromRecentlyClosed(R.id.recentlyClosedFragment),
FromReviewQualityCheck(R.id.reviewQualityCheckFragment), FromReviewQualityCheck(R.id.reviewQualityCheckFragment),
FromAddonsManagementFragment(R.id.addonsManagementFragment), FromAddonsManagementFragment(R.id.addonsManagementFragment),
FromTranslationsDialogFragment(R.id.translationsDialogFragment),
} }

View File

@ -74,6 +74,11 @@ object FeatureFlags {
*/ */
const val fxSuggest = true const val fxSuggest = true
/**
* Allows users to enable SuggestStrongPassword feature.
*/
const val suggestStrongPassword = true
/** /**
* Enable Meta attribution. * Enable Meta attribution.
*/ */

View File

@ -23,7 +23,7 @@ class FenixLogSink(
priority: Log.Priority, priority: Log.Priority,
tag: String?, tag: String?,
throwable: Throwable?, throwable: Throwable?,
message: String?, message: String,
) { ) {
if (priority == Log.Priority.DEBUG && !logsDebug) { if (priority == Log.Priority.DEBUG && !logsDebug) {
return return

View File

@ -29,14 +29,12 @@ import androidx.annotation.CallSuper
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.Companion.PROTECTED
import androidx.appcompat.app.ActionBar import androidx.appcompat.app.ActionBar
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDestination import androidx.navigation.NavController
import androidx.navigation.NavDirections
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.NavigationUI import androidx.navigation.ui.NavigationUI
@ -89,10 +87,8 @@ import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.Metrics import org.mozilla.fenix.GleanMetrics.Metrics
import org.mozilla.fenix.GleanMetrics.SplashScreen import org.mozilla.fenix.GleanMetrics.SplashScreen
import org.mozilla.fenix.GleanMetrics.StartOnHome import org.mozilla.fenix.GleanMetrics.StartOnHome
import org.mozilla.fenix.addons.AddonDetailsFragmentDirections import org.mozilla.fenix.addons.ExtensionsProcessDisabledBackgroundController
import org.mozilla.fenix.addons.AddonPermissionsDetailsFragmentDirections import org.mozilla.fenix.addons.ExtensionsProcessDisabledForegroundController
import org.mozilla.fenix.addons.AddonsManagementFragmentDirections
import org.mozilla.fenix.addons.ExtensionsProcessDisabledController
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.browser.browsingmode.DefaultBrowsingModeManager import org.mozilla.fenix.browser.browsingmode.DefaultBrowsingModeManager
@ -103,18 +99,20 @@ import org.mozilla.fenix.components.metrics.fonts.FontEnumerationWorker
import org.mozilla.fenix.customtabs.ExternalAppBrowserActivity import org.mozilla.fenix.customtabs.ExternalAppBrowserActivity
import org.mozilla.fenix.databinding.ActivityHomeBinding import org.mozilla.fenix.databinding.ActivityHomeBinding
import org.mozilla.fenix.debugsettings.data.DefaultDebugSettingsRepository import org.mozilla.fenix.debugsettings.data.DefaultDebugSettingsRepository
import org.mozilla.fenix.debugsettings.ui.DebugOverlay import org.mozilla.fenix.debugsettings.ui.FenixOverlay
import org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExceptionsFragmentDirections
import org.mozilla.fenix.experiments.ResearchSurfaceDialogFragment import org.mozilla.fenix.experiments.ResearchSurfaceDialogFragment
import org.mozilla.fenix.ext.alreadyOnDestination import org.mozilla.fenix.ext.alreadyOnDestination
import org.mozilla.fenix.ext.breadcrumb import org.mozilla.fenix.ext.breadcrumb
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getBreadcrumbMessage
import org.mozilla.fenix.ext.getIntentSessionId
import org.mozilla.fenix.ext.getIntentSource
import org.mozilla.fenix.ext.getNavDirections
import org.mozilla.fenix.ext.hasTopDestination import org.mozilla.fenix.ext.hasTopDestination
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.setNavigationIcon import org.mozilla.fenix.ext.setNavigationIcon
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.extension.WebExtensionPromptFeature import org.mozilla.fenix.extension.WebExtensionPromptFeature
import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.home.intent.AssistIntentProcessor import org.mozilla.fenix.home.intent.AssistIntentProcessor
import org.mozilla.fenix.home.intent.CrashReporterIntentProcessor import org.mozilla.fenix.home.intent.CrashReporterIntentProcessor
import org.mozilla.fenix.home.intent.HomeDeepLinkIntentProcessor import org.mozilla.fenix.home.intent.HomeDeepLinkIntentProcessor
@ -124,11 +122,7 @@ import org.mozilla.fenix.home.intent.OpenSpecificTabIntentProcessor
import org.mozilla.fenix.home.intent.ReEngagementIntentProcessor import org.mozilla.fenix.home.intent.ReEngagementIntentProcessor
import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor
import org.mozilla.fenix.home.intent.StartSearchIntentProcessor import org.mozilla.fenix.home.intent.StartSearchIntentProcessor
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections
import org.mozilla.fenix.library.bookmarks.DesktopFolders import org.mozilla.fenix.library.bookmarks.DesktopFolders
import org.mozilla.fenix.library.history.HistoryFragmentDirections
import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentDirections
import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections
import org.mozilla.fenix.messaging.FenixMessageSurfaceId import org.mozilla.fenix.messaging.FenixMessageSurfaceId
import org.mozilla.fenix.messaging.FenixNimbusMessagingController import org.mozilla.fenix.messaging.FenixNimbusMessagingController
import org.mozilla.fenix.messaging.MessageNotificationWorker import org.mozilla.fenix.messaging.MessageNotificationWorker
@ -143,29 +137,12 @@ import org.mozilla.fenix.perf.ProfilerMarkers
import org.mozilla.fenix.perf.StartupPathProvider import org.mozilla.fenix.perf.StartupPathProvider
import org.mozilla.fenix.perf.StartupTimeline import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.perf.StartupTypeTelemetry import org.mozilla.fenix.perf.StartupTypeTelemetry
import org.mozilla.fenix.search.SearchDialogFragmentDirections
import org.mozilla.fenix.session.PrivateNotificationService import org.mozilla.fenix.session.PrivateNotificationService
import org.mozilla.fenix.settings.HttpsOnlyFragmentDirections
import org.mozilla.fenix.settings.SettingsFragmentDirections
import org.mozilla.fenix.settings.TrackingProtectionFragmentDirections
import org.mozilla.fenix.settings.about.AboutFragmentDirections
import org.mozilla.fenix.settings.logins.fragment.LoginDetailFragmentDirections
import org.mozilla.fenix.settings.logins.fragment.SavedLoginsAuthFragmentDirections
import org.mozilla.fenix.settings.search.SaveSearchEngineFragmentDirections
import org.mozilla.fenix.settings.search.SearchEngineFragmentDirections
import org.mozilla.fenix.settings.studies.StudiesFragmentDirections
import org.mozilla.fenix.settings.wallpaper.WallpaperSettingsFragmentDirections
import org.mozilla.fenix.share.AddNewDeviceFragmentDirections
import org.mozilla.fenix.shopping.ReviewQualityCheckFragmentDirections
import org.mozilla.fenix.shortcut.NewTabShortcutIntentProcessor.Companion.ACTION_OPEN_PRIVATE_TAB import org.mozilla.fenix.shortcut.NewTabShortcutIntentProcessor.Companion.ACTION_OPEN_PRIVATE_TAB
import org.mozilla.fenix.tabhistory.TabHistoryDialogFragment import org.mozilla.fenix.tabhistory.TabHistoryDialogFragment
import org.mozilla.fenix.tabstray.TabsTrayFragment import org.mozilla.fenix.tabstray.TabsTrayFragment
import org.mozilla.fenix.tabstray.TabsTrayFragmentDirections
import org.mozilla.fenix.theme.DefaultThemeManager import org.mozilla.fenix.theme.DefaultThemeManager
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.trackingprotection.TrackingProtectionPanelDialogFragmentDirections
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.Locale import java.util.Locale
@ -176,7 +153,7 @@ import java.util.Locale
* - home screen * - home screen
* - browser screen * - browser screen
*/ */
@SuppressWarnings("TooManyFunctions", "LargeClass", "LongParameterList", "LongMethod") @SuppressWarnings("TooManyFunctions", "LargeClass", "LongMethod")
open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
// DO NOT MOVE ANYTHING ABOVE THIS, GETTING INIT TIME IS CRITICAL // DO NOT MOVE ANYTHING ABOVE THIS, GETTING INIT TIME IS CRITICAL
// we need to store startup timestamp for warm startup. we cant directly store // we need to store startup timestamp for warm startup. we cant directly store
@ -207,8 +184,15 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
) )
} }
private val extensionsProcessDisabledPromptObserver by lazy { private val extensionsProcessDisabledForegroundController by lazy {
ExtensionsProcessDisabledController(this@HomeActivity) ExtensionsProcessDisabledForegroundController(this@HomeActivity)
}
private val extensionsProcessDisabledBackgroundController by lazy {
ExtensionsProcessDisabledBackgroundController(
browserStore = components.core.store,
appStore = components.appStore,
)
} }
private val serviceWorkerSupport by lazy { private val serviceWorkerSupport by lazy {
@ -300,9 +284,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
visibility = View.VISIBLE visibility = View.VISIBLE
setContent { setContent {
FirefoxTheme(theme = Theme.getTheme(allowPrivateTheme = false)) { FenixOverlay(
DebugOverlay() browserStore = components.core.store,
} inactiveTabsEnabled = settings().inactiveTabsAreEnabled,
)
} }
} else { } else {
setContent {} setContent {}
@ -348,7 +333,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
// Unless the activity is recreated, navigate to home first (without rendering it) // Unless the activity is recreated, navigate to home first (without rendering it)
// to add it to the back stack. // to add it to the back stack.
if (savedInstanceState == null) { if (savedInstanceState == null) {
navigateToHome() navigateToHome(navHost.navController)
} }
if (!shouldStartOnHome() && shouldNavigateToBrowserOnColdStart(savedInstanceState)) { if (!shouldStartOnHome() && shouldNavigateToBrowserOnColdStart(savedInstanceState)) {
@ -392,7 +377,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
lifecycle.addObservers( lifecycle.addObservers(
webExtensionPopupObserver, webExtensionPopupObserver,
extensionsProcessDisabledPromptObserver, extensionsProcessDisabledForegroundController,
extensionsProcessDisabledBackgroundController,
serviceWorkerSupport, serviceWorkerSupport,
webExtensionPromptFeature, webExtensionPromptFeature,
) )
@ -699,7 +685,12 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
startupPathProvider.onIntentReceived(intent) startupPathProvider.onIntentReceived(intent)
} }
open fun handleNewIntent(intent: Intent) { @VisibleForTesting
internal fun handleNewIntent(intent: Intent) {
if (this is ExternalAppBrowserActivity) {
return
}
// Diagnostic breadcrumb for "Display already aquired" crash: // Diagnostic breadcrumb for "Display already aquired" crash:
// https://github.com/mozilla-mobile/android-components/issues/7960 // https://github.com/mozilla-mobile/android-components/issues/7960
breadcrumb( breadcrumb(
@ -886,20 +877,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
super.onUserLeaveHint() super.onUserLeaveHint()
} }
protected open fun getBreadcrumbMessage(destination: NavDestination): String {
val fragmentName = resources.getResourceEntryName(destination.id)
return "Changing to fragment $fragmentName, isCustomTab: false"
}
@VisibleForTesting(otherwise = PROTECTED)
internal open fun getIntentSource(intent: SafeIntent): String? {
return when {
intent.isLauncherIntent -> APP_ICON
intent.action == Intent.ACTION_VIEW -> "LINK"
else -> null
}
}
/** /**
* External sources such as 3rd party links and shortcuts use this function to enter * External sources such as 3rd party links and shortcuts use this function to enter
* private mode directly before the content view is created. Returns the mode set by the intent * private mode directly before the content view is created. Returns the mode set by the intent
@ -984,8 +961,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
} }
} }
protected open fun getIntentSessionId(intent: SafeIntent): String? = null
/** /**
* Navigates to the browser fragment and loads a URL or performs a search (depending on the * Navigates to the browser fragment and loads a URL or performs a search (depending on the
* value of [searchTermOrURL]). * value of [searchTermOrURL]).
@ -1003,7 +978,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
* was opened from history. * was opened from history.
* @param additionalHeaders The extra headers to use when loading the URL. * @param additionalHeaders The extra headers to use when loading the URL.
*/ */
@Suppress("LongParameterList")
fun openToBrowserAndLoad( fun openToBrowserAndLoad(
searchTermOrURL: String, searchTermOrURL: String,
newTab: Boolean, newTab: Boolean,
@ -1038,65 +1012,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
} }
} }
protected open fun getNavDirections(
from: BrowserDirection,
customTabSessionId: String?,
): NavDirections? = when (from) {
BrowserDirection.FromGlobal ->
NavGraphDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromHome ->
HomeFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromWallpaper ->
WallpaperSettingsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromSearchDialog ->
SearchDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromSettings ->
SettingsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromBookmarks ->
BookmarkFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromHistory ->
HistoryFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromHistoryMetadataGroup ->
HistoryMetadataGroupFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTrackingProtectionExceptions ->
TrackingProtectionExceptionsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromHttpsOnlyMode ->
HttpsOnlyFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromAbout ->
AboutFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTrackingProtection ->
TrackingProtectionFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTrackingProtectionDialog ->
TrackingProtectionPanelDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromSavedLoginsFragment ->
SavedLoginsAuthFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromAddNewDeviceFragment ->
AddNewDeviceFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromSearchEngineFragment ->
SearchEngineFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromSaveSearchEngineFragment ->
SaveSearchEngineFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromAddonDetailsFragment ->
AddonDetailsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromAddonPermissionsDetailsFragment ->
AddonPermissionsDetailsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromLoginDetailFragment ->
LoginDetailFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTabsTray ->
TabsTrayFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromRecentlyClosed ->
RecentlyClosedFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromStudiesFragment -> StudiesFragmentDirections.actionGlobalBrowser(
customTabSessionId,
)
BrowserDirection.FromReviewQualityCheck -> ReviewQualityCheckFragmentDirections.actionGlobalBrowser(
customTabSessionId,
)
BrowserDirection.FromAddonsManagementFragment -> AddonsManagementFragmentDirections.actionGlobalBrowser(
customTabSessionId,
)
}
/** /**
* Loads a URL or performs a search (depending on the value of [searchTermOrURL]). * Loads a URL or performs a search (depending on the value of [searchTermOrURL]).
* *
@ -1194,7 +1109,12 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
settings().openNextTabInDesktopMode = false settings().openNextTabInDesktopMode = false
} }
open fun navigateToBrowserOnColdStart() { @VisibleForTesting
internal fun navigateToBrowserOnColdStart() {
if (this is ExternalAppBrowserActivity) {
return
}
// Normal tabs + cold start -> Should go back to browser if we had any tabs open when we left last // Normal tabs + cold start -> Should go back to browser if we had any tabs open when we left last
// except for PBM + Cold Start there won't be any tabs since they're evicted so we never will navigate // except for PBM + Cold Start there won't be any tabs since they're evicted so we never will navigate
if (settings().shouldReturnToBrowser && !browsingModeManager.mode.isPrivate) { if (settings().shouldReturnToBrowser && !browsingModeManager.mode.isPrivate) {
@ -1203,8 +1123,13 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
} }
} }
open fun navigateToHome() { @VisibleForTesting
navHost.navController.navigate(NavGraphDirections.actionStartupHome()) internal fun navigateToHome(navController: NavController) {
if (this is ExternalAppBrowserActivity) {
return
}
navController.navigate(NavGraphDirections.actionStartupHome())
} }
override fun attachBaseContext(base: Context) { override fun attachBaseContext(base: Context) {

View File

@ -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 org.mozilla.fenix.addons
import android.os.Handler
import android.os.Looper
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.support.webextensions.ExtensionsProcessDisabledPromptObserver
import org.mozilla.fenix.components.AppStore
import kotlin.system.exitProcess
/**
* Controller for handling extensions process spawning disabled events. This is for when the app is
* in background, the app is killed to prevent extensions from being disabled and network requests
* continuing.
*
* @param browserStore The [BrowserStore] which holds the state for showing the dialog.
* @param appStore The [AppStore] containing the application state.
* @param onExtensionsProcessDisabled Invoked when the app is in background and extensions process
* is disabled.
*/
class ExtensionsProcessDisabledBackgroundController(
browserStore: BrowserStore,
appStore: AppStore,
onExtensionsProcessDisabled: () -> Unit = { killApp() },
) : ExtensionsProcessDisabledPromptObserver(
store = browserStore,
shouldCancelOnStop = false,
onShowExtensionsProcessDisabledPrompt = {
if (!appStore.state.isForeground) {
onExtensionsProcessDisabled()
}
},
) {
companion object {
/**
* When a dialog can't be shown because the app is in the background, instead the app will
* be killed to prevent leaking network data without extensions enabled.
*/
private fun killApp() {
Handler(Looper.getMainLooper()).post {
exitProcess(0)
}
}
}
}

View File

@ -5,8 +5,6 @@
package org.mozilla.fenix.addons package org.mozilla.fenix.addons
import android.content.Context import android.content.Context
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.Button import android.widget.Button
import android.widget.TextView import android.widget.TextView
@ -20,35 +18,30 @@ import mozilla.components.support.webextensions.ExtensionsProcessDisabledPromptO
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import kotlin.system.exitProcess
/** /**
* Controller for handling extensions process spawning disabled events. When the app is in * Controller for handling extensions process spawning disabled events. When the app is in
* foreground this will call for a dialog to decide on correct action to take (retry enabling * foreground this will call for a dialog to decide on correct action to take (retry enabling
* process spawning or disable extensions). When in background, we kill the app to prevent * process spawning or disable extensions).
* extensions from being disabled and network requests continuing.
* *
* @param context to show the AlertDialog * @param context to show the AlertDialog
* @param browserStore The [BrowserStore] which holds the state for showing the dialog * @param browserStore The [BrowserStore] which holds the state for showing the dialog
* @param appStore The [AppStore] containing the application state * @param appStore The [AppStore] containing the application state
* @param builder to use for creating the dialog which can be styled as needed * @param builder to use for creating the dialog which can be styled as needed
* @param appName to be added to the message. Optional and mainly relevant for testing * @param appName to be added to the message. Optional and mainly relevant for testing
* @param onKillApp called when the app is backgrounded and extensions process is disabled
*/ */
class ExtensionsProcessDisabledController( class ExtensionsProcessDisabledForegroundController(
@UiContext context: Context, @UiContext context: Context,
browserStore: BrowserStore = context.components.core.store, browserStore: BrowserStore = context.components.core.store,
appStore: AppStore = context.components.appStore, appStore: AppStore = context.components.appStore,
builder: AlertDialog.Builder = AlertDialog.Builder(context), builder: AlertDialog.Builder = AlertDialog.Builder(context),
appName: String = context.appName, appName: String = context.appName,
onKillApp: () -> Unit = { killApp() },
) : ExtensionsProcessDisabledPromptObserver( ) : ExtensionsProcessDisabledPromptObserver(
browserStore, store = browserStore,
shouldCancelOnStop = true,
{ {
if (appStore.state.isForeground) { if (appStore.state.isForeground) {
presentDialog(context, browserStore, builder, appName) presentDialog(context, browserStore, builder, appName)
} else {
onKillApp.invoke()
} }
}, },
) { ) {
@ -61,16 +54,6 @@ class ExtensionsProcessDisabledController(
companion object { companion object {
private var shouldCreateDialog: Boolean = true private var shouldCreateDialog: Boolean = true
/**
* When a dialog can't be shown because the app is in the background, instead the app will
* be killed to prevent leaking network data without extensions enabled.
*/
private fun killApp() {
Handler(Looper.getMainLooper()).post {
exitProcess(0)
}
}
/** /**
* Present a dialog to the user notifying of extensions process spawning disabled and also asking * Present a dialog to the user notifying of extensions process spawning disabled and also asking
* whether they would like to continue trying or disable extensions. If the user chooses to retry, * whether they would like to continue trying or disable extensions. If the user chooses to retry,

View File

@ -12,6 +12,7 @@ import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.util.Log
import android.view.Gravity import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -31,9 +32,11 @@ import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -59,6 +62,7 @@ import mozilla.components.browser.thumbnails.BrowserThumbnails
import mozilla.components.concept.base.crash.Breadcrumb import mozilla.components.concept.base.crash.Breadcrumb
import mozilla.components.concept.engine.permission.SitePermissions import mozilla.components.concept.engine.permission.SitePermissions
import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.concept.storage.LoginEntry
import mozilla.components.feature.accounts.FxaCapability import mozilla.components.feature.accounts.FxaCapability
import mozilla.components.feature.accounts.FxaWebChannelFeature import mozilla.components.feature.accounts.FxaWebChannelFeature
import mozilla.components.feature.app.links.AppLinksFeature import mozilla.components.feature.app.links.AppLinksFeature
@ -79,6 +83,7 @@ import mozilla.components.feature.prompts.dialog.FullScreenNotificationDialog
import mozilla.components.feature.prompts.identitycredential.DialogColors import mozilla.components.feature.prompts.identitycredential.DialogColors
import mozilla.components.feature.prompts.identitycredential.DialogColorsProvider import mozilla.components.feature.prompts.identitycredential.DialogColorsProvider
import mozilla.components.feature.prompts.login.LoginDelegate import mozilla.components.feature.prompts.login.LoginDelegate
import mozilla.components.feature.prompts.login.SuggestStrongPasswordDelegate
import mozilla.components.feature.prompts.share.ShareDelegate import mozilla.components.feature.prompts.share.ShareDelegate
import mozilla.components.feature.readerview.ReaderViewFeature import mozilla.components.feature.readerview.ReaderViewFeature
import mozilla.components.feature.search.SearchFeature import mozilla.components.feature.search.SearchFeature
@ -95,6 +100,8 @@ import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.service.glean.private.NoExtras import mozilla.components.service.glean.private.NoExtras
import mozilla.components.service.sync.autofill.DefaultCreditCardValidationDelegate import mozilla.components.service.sync.autofill.DefaultCreditCardValidationDelegate
import mozilla.components.service.sync.logins.DefaultLoginValidationDelegate import mozilla.components.service.sync.logins.DefaultLoginValidationDelegate
import mozilla.components.service.sync.logins.LoginsApiException
import mozilla.components.service.sync.logins.SyncableLoginsStorage
import mozilla.components.support.base.feature.ActivityResultHandler import mozilla.components.support.base.feature.ActivityResultHandler
import mozilla.components.support.base.feature.PermissionsFeature import mozilla.components.support.base.feature.PermissionsFeature
import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.UserInteractionHandler
@ -108,6 +115,7 @@ import mozilla.components.support.locale.ActivityContextWrapper
import mozilla.components.ui.widgets.withCenterAlignedButtons import mozilla.components.ui.widgets.withCenterAlignedButtons
import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.MediaState import org.mozilla.fenix.GleanMetrics.MediaState
import org.mozilla.fenix.GleanMetrics.PullToRefreshInBrowser import org.mozilla.fenix.GleanMetrics.PullToRefreshInBrowser
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
@ -160,6 +168,7 @@ import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.allowUndo import org.mozilla.fenix.utils.allowUndo
import org.mozilla.fenix.wifi.SitePermissionsWifiIntegration import org.mozilla.fenix.wifi.SitePermissionsWifiIntegration
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import kotlin.coroutines.cancellation.CancellationException
import mozilla.components.feature.session.behavior.ToolbarPosition as MozacToolbarPosition import mozilla.components.feature.session.behavior.ToolbarPosition as MozacToolbarPosition
/** /**
@ -460,6 +469,7 @@ abstract class BaseBrowserFragment :
browserToolbarView.view.display.setOnSiteSecurityClickedListener { browserToolbarView.view.display.setOnSiteSecurityClickedListener {
showQuickSettingsDialog() showQuickSettingsDialog()
Events.browserToolbarSecurityIndicatorTapped.record()
} }
contextMenuFeature.set( contextMenuFeature.set(
@ -731,6 +741,18 @@ abstract class BaseBrowserFragment :
} }
} }
}, },
suggestStrongPasswordDelegate = object : SuggestStrongPasswordDelegate {
override val strongPasswordPromptViewListenerView
get() = binding.suggestStrongPasswordBar
},
isSuggestStrongPasswordEnabled = context.settings().enableSuggestStrongPassword,
onSaveLoginWithStrongPassword = { url, password ->
handleOnSaveLoginWithGeneratedStrongPassword(
passwordsStorage = context.components.core.passwordsStorage,
url = url,
password = password,
)
},
creditCardDelegate = object : CreditCardDelegate { creditCardDelegate = object : CreditCardDelegate {
override val creditCardPickerView override val creditCardPickerView
get() = binding.creditCardSelectBar get() = binding.creditCardSelectBar
@ -894,9 +916,12 @@ abstract class BaseBrowserFragment :
binding.swipeRefresh.isEnabled = shouldPullToRefreshBeEnabled(false) binding.swipeRefresh.isEnabled = shouldPullToRefreshBeEnabled(false)
if (binding.swipeRefresh.isEnabled) { if (binding.swipeRefresh.isEnabled) {
val primaryTextColor = val primaryTextColor = ThemeManager.resolveAttribute(R.attr.textPrimary, context)
ThemeManager.resolveAttribute(R.attr.textPrimary, context) val primaryBackgroundColor = ThemeManager.resolveAttribute(R.attr.layer2, context)
binding.swipeRefresh.setColorSchemeColors(primaryTextColor) binding.swipeRefresh.apply {
setColorSchemeResources(primaryTextColor)
setProgressBackgroundColorSchemeResource(primaryBackgroundColor)
}
swipeRefreshFeature.set( swipeRefreshFeature.set(
feature = SwipeRefreshFeature( feature = SwipeRefreshFeature(
requireComponents.core.store, requireComponents.core.store,
@ -1651,4 +1676,38 @@ abstract class BaseBrowserFragment :
return isValidStatus && isSameTab return isValidStatus && isSameTab
} }
private fun handleOnSaveLoginWithGeneratedStrongPassword(
passwordsStorage: SyncableLoginsStorage,
url: String,
password: String,
) {
val loginToSave = LoginEntry(
origin = url,
httpRealm = url,
username = "",
password = password,
)
var saveLoginJob: Deferred<Unit>? = null
lifecycleScope.launch(IO) {
saveLoginJob = async {
try {
passwordsStorage.add(loginToSave)
} catch (loginException: LoginsApiException) {
loginException.printStackTrace()
Log.e(
"Add new login",
"Failed to add new login with generated password.",
loginException,
)
}
saveLoginJob?.await()
}
saveLoginJob?.invokeOnCompletion {
if (it is CancellationException) {
saveLoginJob?.cancel()
}
}
}
}
} }

View File

@ -22,7 +22,6 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
* @param dismissAction Optional callback invoked when the user dismisses the banner. * @param dismissAction Optional callback invoked when the user dismisses the banner.
* @param actionToPerform The action to be performed on action button press. * @param actionToPerform The action to be performed on action button press.
*/ */
@Suppress("LongParameterList")
class DynamicInfoBanner( class DynamicInfoBanner(
private val context: Context, private val context: Context,
container: ViewGroup, container: ViewGroup,

View File

@ -26,7 +26,6 @@ import org.mozilla.fenix.ext.settings
* @property dismissAction Optional callback invoked when the user dismisses the banner. * @property dismissAction Optional callback invoked when the user dismisses the banner.
* @param actionToPerform The action to be performed on action button press. * @param actionToPerform The action to be performed on action button press.
*/ */
@SuppressWarnings("LongParameterList")
open class InfoBanner( open class InfoBanner(
private val context: Context, private val context: Context,
private val container: ViewGroup, private val container: ViewGroup,

View File

@ -53,7 +53,7 @@ class IntentProcessors(
* Provides intent processing functionality for ACTION_VIEW and ACTION_SEND intents in private tabs. * Provides intent processing functionality for ACTION_VIEW and ACTION_SEND intents in private tabs.
*/ */
val privateIntentProcessor by lazyMonitored { val privateIntentProcessor by lazyMonitored {
TabIntentProcessor(tabsUseCases, searchUseCases.newTabSearch, isPrivate = true) TabIntentProcessor(tabsUseCases, searchUseCases.newPrivateTabSearch, isPrivate = true)
} }
val customTabIntentProcessor by lazyMonitored { val customTabIntentProcessor by lazyMonitored {

View File

@ -223,7 +223,9 @@ class DefaultBrowserToolbarController(
override fun handleTranslationsButtonClick() { override fun handleTranslationsButtonClick() {
val directions = val directions =
BrowserFragmentDirections.actionBrowserFragmentToTranslationsDialogFragment() BrowserFragmentDirections.actionBrowserFragmentToTranslationsDialogFragment(
sessionId = currentSession?.id,
)
navController.navigateSafe(R.id.browserFragment, directions) navController.navigateSafe(R.id.browserFragment, directions)
} }

View File

@ -406,6 +406,14 @@ class DefaultBrowserToolbarMenuController(
.show() .show()
} }
} }
ToolbarMenu.Item.Translate -> {
val directions =
BrowserFragmentDirections.actionBrowserFragmentToTranslationsDialogFragment(
sessionId = currentSession?.id,
)
navController.navigateSafe(R.id.browserFragment, directions)
}
} }
} }
@ -420,7 +428,7 @@ class DefaultBrowserToolbarMenuController(
} }
} }
@Suppress("ComplexMethod") @Suppress("ComplexMethod", "LongMethod")
private fun trackToolbarItemInteraction(item: ToolbarMenu.Item) { private fun trackToolbarItemInteraction(item: ToolbarMenu.Item) {
when (item) { when (item) {
is ToolbarMenu.Item.OpenInFenix -> is ToolbarMenu.Item.OpenInFenix ->
@ -433,10 +441,19 @@ class DefaultBrowserToolbarMenuController(
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("open_in_app")) Events.browserMenuAction.record(Events.BrowserMenuActionExtra("open_in_app"))
is ToolbarMenu.Item.CustomizeReaderView -> is ToolbarMenu.Item.CustomizeReaderView ->
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("reader_mode_appearance")) Events.browserMenuAction.record(Events.BrowserMenuActionExtra("reader_mode_appearance"))
is ToolbarMenu.Item.Back -> is ToolbarMenu.Item.Back -> {
if (item.viewHistory) {
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("back_long_press"))
} else {
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("back")) Events.browserMenuAction.record(Events.BrowserMenuActionExtra("back"))
}
}
is ToolbarMenu.Item.Forward -> is ToolbarMenu.Item.Forward ->
if (item.viewHistory) {
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("forward_long_press"))
} else {
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("forward")) Events.browserMenuAction.record(Events.BrowserMenuActionExtra("forward"))
}
is ToolbarMenu.Item.Reload -> is ToolbarMenu.Item.Reload ->
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("reload")) Events.browserMenuAction.record(Events.BrowserMenuActionExtra("reload"))
is ToolbarMenu.Item.Stop -> is ToolbarMenu.Item.Stop ->
@ -483,6 +500,12 @@ class DefaultBrowserToolbarMenuController(
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("set_default_browser")) Events.browserMenuAction.record(Events.BrowserMenuActionExtra("set_default_browser"))
is ToolbarMenu.Item.RemoveFromTopSites -> is ToolbarMenu.Item.RemoveFromTopSites ->
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("remove_from_top_sites")) Events.browserMenuAction.record(Events.BrowserMenuActionExtra("remove_from_top_sites"))
ToolbarMenu.Item.Translate -> Events.browserMenuAction.record(
Events.BrowserMenuActionExtra(
"translate",
),
)
} }
} }

View File

@ -42,7 +42,6 @@ import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.Settings
/** /**
* Builds the toolbar object used with the 3-dot menu in the browser fragment. * Builds the toolbar object used with the 3-dot menu in the browser fragment.
@ -55,7 +54,7 @@ import org.mozilla.fenix.utils.Settings
* @param pinnedSiteStorage Used to check if the current url is a pinned site. * @param pinnedSiteStorage Used to check if the current url is a pinned site.
* @property isPinningSupported true if the launcher supports adding shortcuts. * @property isPinningSupported true if the launcher supports adding shortcuts.
*/ */
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions") @Suppress("LargeClass", "TooManyFunctions")
open class DefaultToolbarMenu( open class DefaultToolbarMenu(
private val context: Context, private val context: Context,
private val store: BrowserStore, private val store: BrowserStore,
@ -193,6 +192,14 @@ open class DefaultToolbarMenu(
fun shouldShowReaderViewCustomization(): Boolean = selectedSession?.let { fun shouldShowReaderViewCustomization(): Boolean = selectedSession?.let {
store.state.findTab(it.id)?.readerState?.active store.state.findTab(it.id)?.readerState?.active
} ?: false } ?: false
/**
* Should Translations menu item be visible?
*/
@VisibleForTesting(otherwise = PRIVATE)
fun shouldShowTranslations(): Boolean = selectedSession?.let {
context.settings().enableTranslations
} ?: false
// End of predicates // // End of predicates //
private val installToHomescreen = BrowserMenuHighlightableItem( private val installToHomescreen = BrowserMenuHighlightableItem(
@ -248,6 +255,14 @@ open class DefaultToolbarMenu(
onItemTapped.invoke(ToolbarMenu.Item.FindInPage) onItemTapped.invoke(ToolbarMenu.Item.FindInPage)
} }
private val translationsItem = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_translations),
imageResource = R.drawable.mozac_ic_translate_24,
iconTintColorResource = primaryTextColor(),
) {
onItemTapped.invoke(ToolbarMenu.Item.Translate)
}
private val desktopSiteItem = BrowserMenuImageSwitch( private val desktopSiteItem = BrowserMenuImageSwitch(
imageResource = R.drawable.ic_desktop, imageResource = R.drawable.ic_desktop,
label = context.getString(R.string.browser_menu_desktop_site), label = context.getString(R.string.browser_menu_desktop_site),
@ -405,6 +420,7 @@ open class DefaultToolbarMenu(
syncMenuItem(), syncMenuItem(),
BrowserMenuDivider(), BrowserMenuDivider(),
findInPageItem, findInPageItem,
translationsItem.apply { visible = ::shouldShowTranslations },
desktopSiteItem, desktopSiteItem,
openInRegularTabItem.apply { visible = ::shouldShowOpenInRegularTab }, openInRegularTabItem.apply { visible = ::shouldShowOpenInRegularTab },
customizeReaderView.apply { visible = ::shouldShowReaderViewCustomization }, customizeReaderView.apply { visible = ::shouldShowReaderViewCustomization },

View File

@ -76,7 +76,6 @@ abstract class ToolbarIntegration(
} }
} }
@Suppress("LongParameterList")
class DefaultToolbarIntegration( class DefaultToolbarIntegration(
context: Context, context: Context,
toolbar: BrowserToolbar, toolbar: BrowserToolbar,

View File

@ -18,6 +18,11 @@ interface ToolbarMenu {
*/ */
object OpenInRegularTab : Item() object OpenInRegularTab : Item()
object FindInPage : Item() object FindInPage : Item()
/**
* Opens the translations flow.
*/
object Translate : Item()
object Share : Item() object Share : Item()
data class Back(val viewHistory: Boolean) : Item() data class Back(val viewHistory: Boolean) : Item()
data class Forward(val viewHistory: Boolean) : Item() data class Forward(val viewHistory: Boolean) : Item()

View File

@ -41,7 +41,6 @@ import org.mozilla.fenix.theme.FirefoxTheme
* By default set to a solid color in [DefaultImagePlaceholder]. * By default set to a solid color in [DefaultImagePlaceholder].
*/ */
@Composable @Composable
@Suppress("LongParameterList")
fun Image( fun Image(
url: String, url: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,

View File

@ -7,10 +7,22 @@ package org.mozilla.fenix.compose
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.Card
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.onClick import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
@ -20,6 +32,9 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import org.mozilla.fenix.R
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
/** /**
@ -67,6 +82,12 @@ fun LinkText(
linkTextDecoration, linkTextDecoration,
) )
val showDialog = remember { mutableStateOf(false) }
val linksAvailable = stringResource(id = R.string.a11y_links_available)
if (showDialog.value) {
LinksDialog(linkTextStates) { showDialog.value = false }
}
// When using UrlAnnotation, talkback shows links in a separate dialog and // When using UrlAnnotation, talkback shows links in a separate dialog and
// opens them in the default browser. Since this component allows the caller to define the // opens them in the default browser. Since this component allows the caller to define the
// onClick behaviour - e.g. to open the link in in-app custom tab, here StringAnnotation is used // onClick behaviour - e.g. to open the link in in-app custom tab, here StringAnnotation is used
@ -76,12 +97,17 @@ fun LinkText(
style = style, style = style,
modifier = Modifier.semantics(mergeDescendants = true) { modifier = Modifier.semantics(mergeDescendants = true) {
onClick { onClick {
if (linkTextStates.size > 1) {
showDialog.value = true
} else {
linkTextStates.firstOrNull()?.let { linkTextStates.firstOrNull()?.let {
it.onClick(it.url) it.onClick(it.url)
} }
}
return@onClick true return@onClick true
} }
contentDescription = "$annotatedString $linksAvailable"
}, },
onClick = { charOffset -> onClick = { charOffset ->
onTextClick(annotatedString, charOffset, linkTextStates) onTextClick(annotatedString, charOffset, linkTextStates)
@ -89,6 +115,60 @@ fun LinkText(
) )
} }
@Composable
private fun LinksDialog(
linkTextStates: List<LinkTextState>,
onDismissRequest: () -> Unit,
) {
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(8.dp),
) {
Column(
modifier = Modifier
.background(color = FirefoxTheme.colors.layer2)
.padding(all = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResource(id = R.string.a11y_links_title),
color = FirefoxTheme.colors.textPrimary,
style = FirefoxTheme.typography.headline5,
)
linkTextStates.forEach { linkText ->
TextButton(
onClick = { linkText.onClick(linkText.url) },
modifier = Modifier
.align(Alignment.Start),
) {
Text(
text = linkText.text,
color = FirefoxTheme.colors.textAccent,
textDecoration = TextDecoration.Underline,
style = FirefoxTheme.typography.button,
)
}
}
TextButton(
onClick = { onDismissRequest() },
modifier = Modifier
.align(Alignment.End),
) {
Text(
text = stringResource(id = R.string.standard_snackbar_error_dismiss),
color = FirefoxTheme.colors.textAccent,
style = FirefoxTheme.typography.button,
)
}
}
}
}
}
@VisibleForTesting @VisibleForTesting
internal fun onTextClick( internal fun onTextClick(
annotatedString: AnnotatedString, annotatedString: AnnotatedString,
@ -231,3 +311,27 @@ private fun MultipleLinksPreview() {
} }
} }
} }
@Preview
@Composable
private fun LinksDialogPreview() {
val state1 = LinkTextState(
text = "clickable text",
url = "www.mozilla.com",
onClick = {},
)
val state2 = LinkTextState(
text = "another clickable text",
url = "www.mozilla.com",
onClick = {},
)
val linkTextStateList = listOf(state1, state2)
FirefoxTheme {
LinksDialog(
linkTextStates = linkTextStateList,
onDismissRequest = {},
)
}
}

View File

@ -35,6 +35,7 @@ private const val DISABLED_ALPHA = 0.5f
* UI for a switch with label that can be on or off. * UI for a switch with label that can be on or off.
* *
* @param label Text to be displayed next to the switch. * @param label Text to be displayed next to the switch.
* @param description An optional description text below the label.
* @param checked Whether or not the switch is checked. * @param checked Whether or not the switch is checked.
* @param onCheckedChange Invoked when Switch is being clicked, therefore the change of checked * @param onCheckedChange Invoked when Switch is being clicked, therefore the change of checked
* state is requested. * state is requested.
@ -44,6 +45,7 @@ private const val DISABLED_ALPHA = 0.5f
@Composable @Composable
fun SwitchWithLabel( fun SwitchWithLabel(
label: String, label: String,
description: String? = null,
checked: Boolean, checked: Boolean,
onCheckedChange: ((Boolean) -> Unit), onCheckedChange: ((Boolean) -> Unit),
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -59,6 +61,10 @@ fun SwitchWithLabel(
), ),
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) {
Column(
modifier = Modifier
.weight(1f),
) { ) {
Text( Text(
text = label, text = label,
@ -68,9 +74,17 @@ fun SwitchWithLabel(
FirefoxTheme.colors.textDisabled FirefoxTheme.colors.textDisabled
}, },
style = FirefoxTheme.typography.subtitle1, style = FirefoxTheme.typography.subtitle1,
modifier = Modifier.weight(1f),
) )
description?.let {
Text(
text = description,
color = FirefoxTheme.colors.textSecondary,
style = FirefoxTheme.typography.body2,
)
}
}
Switch( Switch(
modifier = Modifier.clearAndSetSemantics {}, modifier = Modifier.clearAndSetSemantics {},
checked = checked, checked = checked,
@ -139,6 +153,7 @@ private fun SwitchWithLabelPreview() {
var enabledSwitchState by remember { mutableStateOf(false) } var enabledSwitchState by remember { mutableStateOf(false) }
SwitchWithLabel( SwitchWithLabel(
label = if (enabledSwitchState) "On" else "Off", label = if (enabledSwitchState) "On" else "Off",
description = "Description text",
checked = enabledSwitchState, checked = enabledSwitchState,
onCheckedChange = { enabledSwitchState = it }, onCheckedChange = { enabledSwitchState = it },
) )

View File

@ -42,7 +42,6 @@ private const val FALLBACK_ICON_SIZE = 36
* @param alignment [Alignment] used to draw the image content. * @param alignment [Alignment] used to draw the image content.
*/ */
@Composable @Composable
@Suppress("LongParameterList")
fun TabThumbnail( fun TabThumbnail(
tab: TabSessionState, tab: TabSessionState,
storage: ThumbnailStorage, storage: ThumbnailStorage,

View File

@ -38,7 +38,6 @@ import org.mozilla.fenix.theme.FirefoxTheme
* @param fallbackContent The content to display with a thumbnail is unable to be loaded. * @param fallbackContent The content to display with a thumbnail is unable to be loaded.
*/ */
@Composable @Composable
@Suppress("LongParameterList")
fun ThumbnailImage( fun ThumbnailImage(
request: ImageLoadRequest, request: ImageLoadRequest,
storage: ThumbnailStorage, storage: ThumbnailStorage,

View File

@ -38,7 +38,6 @@ import org.mozilla.fenix.theme.FirefoxTheme
* @param onClick Optional lambda for handling header clicks. * @param onClick Optional lambda for handling header clicks.
* @param actions Optional Composable for adding UI to the end of the header. * @param actions Optional Composable for adding UI to the end of the header.
*/ */
@Suppress("LongParameterList")
@Composable @Composable
fun ExpandableListHeader( fun ExpandableListHeader(
headerText: String, headerText: String,

View File

@ -91,7 +91,7 @@ import org.mozilla.fenix.theme.FirefoxTheme
*/ */
@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class) @OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class)
@Composable @Composable
@Suppress("MagicNumber", "LongParameterList", "LongMethod") @Suppress("MagicNumber", "LongMethod")
fun TabGridItem( fun TabGridItem(
tab: TabSessionState, tab: TabSessionState,
storage: ThumbnailStorage, storage: ThumbnailStorage,

View File

@ -75,7 +75,7 @@ import org.mozilla.fenix.theme.FirefoxTheme
*/ */
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class) @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
@Composable @Composable
@Suppress("MagicNumber", "LongMethod", "LongParameterList") @Suppress("MagicNumber", "LongMethod")
fun TabListItem( fun TabListItem(
tab: TabSessionState, tab: TabSessionState,
storage: ThumbnailStorage, storage: ThumbnailStorage,
@ -209,7 +209,6 @@ private fun clickableColor() = when (isSystemInDarkTheme()) {
} }
@Composable @Composable
@Suppress("LongParameterList")
private fun Thumbnail( private fun Thumbnail(
tab: TabSessionState, tab: TabSessionState,
size: Int, size: Int,

View File

@ -5,24 +5,16 @@
package org.mozilla.fenix.customtabs package org.mozilla.fenix.customtabs
import android.app.assist.AssistContent import android.app.assist.AssistContent
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.navigation.NavDestination
import androidx.navigation.NavDirections
import mozilla.components.browser.state.selector.findCustomTab import mozilla.components.browser.state.selector.findCustomTab
import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.state.SessionState
import mozilla.components.concept.engine.manifest.WebAppManifestParser
import mozilla.components.feature.intent.ext.getSessionId
import mozilla.components.feature.pwa.ext.getWebAppManifest
import mozilla.components.support.utils.SafeIntent import mozilla.components.support.utils.SafeIntent
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import java.security.InvalidParameterException import org.mozilla.fenix.ext.getIntentSessionId
const val EXTRA_IS_SANDBOX_CUSTOM_TAB = "org.mozilla.fenix.customtabs.EXTRA_IS_SANDBOX_CUSTOM_TAB" const val EXTRA_IS_SANDBOX_CUSTOM_TAB = "org.mozilla.fenix.customtabs.EXTRA_IS_SANDBOX_CUSTOM_TAB"
@ -45,52 +37,6 @@ open class ExternalAppBrowserActivity : HomeActivity() {
} }
} }
final override fun getBreadcrumbMessage(destination: NavDestination): String {
val fragmentName = resources.getResourceEntryName(destination.id)
return "Changing to fragment $fragmentName, isCustomTab: true"
}
final override fun getIntentSource(intent: SafeIntent) = "CUSTOM_TAB"
final override fun getIntentSessionId(intent: SafeIntent) = intent.getSessionId()
override fun navigateToBrowserOnColdStart() {
// No-op for external app
}
override fun navigateToHome() {
// No-op for external app
}
override fun handleNewIntent(intent: Intent) {
// No-op for external app
}
override fun getNavDirections(
from: BrowserDirection,
customTabSessionId: String?,
): NavDirections? {
if (customTabSessionId == null) {
finishAndRemoveTask()
return null
}
val manifest = intent
.getWebAppManifest()
?.let { WebAppManifestParser().serialize(it).toString() }
return when (from) {
BrowserDirection.FromGlobal ->
NavGraphDirections.actionGlobalExternalAppBrowser(
activeSessionId = customTabSessionId,
webAppManifest = manifest,
isSandboxCustomTab = intent.getBooleanExtra(EXTRA_IS_SANDBOX_CUSTOM_TAB, false),
)
else -> throw InvalidParameterException(
"Tried to navigate to ExternalAppBrowserFragment from $from",
)
}
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()

View File

@ -26,7 +26,6 @@ import mozilla.components.feature.pwa.feature.WebAppActivityFeature
import mozilla.components.feature.pwa.feature.WebAppContentFeature import mozilla.components.feature.pwa.feature.WebAppContentFeature
import mozilla.components.feature.pwa.feature.WebAppHideToolbarFeature import mozilla.components.feature.pwa.feature.WebAppHideToolbarFeature
import mozilla.components.feature.pwa.feature.WebAppSiteControlsFeature import mozilla.components.feature.pwa.feature.WebAppSiteControlsFeature
import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.arch.lifecycle.addObservers import mozilla.components.support.ktx.android.arch.lifecycle.addObservers
import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.BuildConfig
@ -44,7 +43,7 @@ import org.mozilla.fenix.settings.quicksettings.protections.cookiebanners.getCoo
/** /**
* Fragment used for browsing the web within external apps. * Fragment used for browsing the web within external apps.
*/ */
class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler { class ExternalAppBrowserFragment : BaseBrowserFragment() {
private val args by navArgs<ExternalAppBrowserFragmentArgs>() private val args by navArgs<ExternalAppBrowserFragmentArgs>()
@ -212,9 +211,4 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
view, view,
FenixSnackbarDelegate(view), FenixSnackbarDelegate(view),
) )
companion object {
// We only care about millisecond precision for telemetry events
internal const val MS_PRECISION = 1_000_000L
}
} }

View File

@ -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 org.mozilla.fenix.debugsettings.navigation
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
/**
* A navigation destination for screens within the Debug Drawer.
*
* @property route The unique route used to navigate to the destination. This string can also contain
* optional parameters for arguments or deep linking.
* @property title The string ID of the destination's title.
* @property onClick Invoked when the destination is clicked to be navigated to.
* @property content The destination's [Composable].
*/
data class DebugDrawerDestination(
val route: String,
@StringRes val title: Int,
val onClick: () -> Unit,
val content: @Composable () -> Unit,
)

View File

@ -0,0 +1,70 @@
/* 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 org.mozilla.fenix.debugsettings.navigation
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import mozilla.components.browser.state.store.BrowserStore
import org.mozilla.fenix.R
import org.mozilla.fenix.debugsettings.store.DebugDrawerAction
import org.mozilla.fenix.debugsettings.store.DebugDrawerStore
import org.mozilla.fenix.debugsettings.tabs.TabTools as TabToolsScreen
/**
* The navigation routes for screens within the Debug Drawer.
*
* @property route The unique route used to navigate to the destination. This string can also contain
* optional parameters for arguments or deep linking.
* @property title The string ID of the destination's title.
*/
enum class DebugDrawerRoute(val route: String, @StringRes val title: Int) {
/**
* The navigation route for [TabToolsScreen].
*/
TabTools(
route = "tab_tools",
title = R.string.debug_drawer_tab_tools_title,
),
;
companion object {
/**
* Transforms the values of [DebugDrawerRoute] into a list of [DebugDrawerDestination]s.
*
* @param debugDrawerStore [DebugDrawerStore] used to dispatch navigation actions.
* @param browserStore [BrowserStore] used to add tabs in [TabToolsScreen].
* @param inactiveTabsEnabled Whether the inactive tabs feature is enabled.
*/
fun generateDebugDrawerDestinations(
debugDrawerStore: DebugDrawerStore,
browserStore: BrowserStore,
inactiveTabsEnabled: Boolean,
): List<DebugDrawerDestination> =
DebugDrawerRoute.values().map { debugDrawerRoute ->
val onClick: () -> Unit
val content: @Composable () -> Unit
when (debugDrawerRoute) {
TabTools -> {
onClick = {
debugDrawerStore.dispatch(DebugDrawerAction.NavigateTo.TabTools)
}
content = {
TabToolsScreen(
store = browserStore,
inactiveTabsEnabled = inactiveTabsEnabled,
)
}
}
}
DebugDrawerDestination(
route = debugDrawerRoute.route,
title = debugDrawerRoute.title,
onClick = onClick,
content = content,
)
}
}
}

View File

@ -0,0 +1,46 @@
/* 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 org.mozilla.fenix.debugsettings.store
import mozilla.components.lib.state.Action
import org.mozilla.fenix.debugsettings.ui.DebugDrawerHome
import org.mozilla.fenix.debugsettings.tabs.TabTools as TabToolsScreen
/**
* [Action] implementation related to [DebugDrawerStore].
*/
sealed class DebugDrawerAction : Action {
/**
* [DebugDrawerAction] fired when the user opens the drawer.
*/
object DrawerOpened : DebugDrawerAction()
/**
* [DebugDrawerAction] fired when the user closes the drawer.
*/
object DrawerClosed : DebugDrawerAction()
/**
* [DebugDrawerAction] fired when a navigation event occurs for a specific destination.
*/
sealed class NavigateTo : DebugDrawerAction() {
/**
* [NavigateTo] action fired when the debug drawer needs to navigate to [DebugDrawerHome].
*/
object Home : NavigateTo()
/**
* [NavigateTo] action fired when the debug drawer needs to navigate to [TabToolsScreen].
*/
object TabTools : NavigateTo()
}
/**
* [DebugDrawerAction] fired when a back navigation event occurs.
*/
object OnBackPressed : DebugDrawerAction()
}

View File

@ -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 org.mozilla.fenix.debugsettings.store
import androidx.navigation.NavHostController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext
import org.mozilla.fenix.debugsettings.navigation.DebugDrawerRoute
import org.mozilla.fenix.debugsettings.ui.DEBUG_DRAWER_HOME_ROUTE
/**
* Middleware that handles navigation events for the Debug Drawer feature.
*
* @param navController [NavHostController] used to execute any navigation actions on the UI.
* @param scope [CoroutineScope] used to make calls to the main thread.
*/
class DebugDrawerNavigationMiddleware(
private val navController: NavHostController,
private val scope: CoroutineScope,
) : Middleware<DebugDrawerState, DebugDrawerAction> {
override fun invoke(
context: MiddlewareContext<DebugDrawerState, DebugDrawerAction>,
next: (DebugDrawerAction) -> Unit,
action: DebugDrawerAction,
) {
next(action)
scope.launch {
when (action) {
is DebugDrawerAction.NavigateTo.Home -> navController.popBackStack(
route = DEBUG_DRAWER_HOME_ROUTE,
inclusive = false,
)
is DebugDrawerAction.NavigateTo.TabTools ->
navController.navigate(route = DebugDrawerRoute.TabTools.route)
is DebugDrawerAction.OnBackPressed -> navController.popBackStack()
is DebugDrawerAction.DrawerOpened, DebugDrawerAction.DrawerClosed -> Unit // no-op
}
}
}
}

View File

@ -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 org.mozilla.fenix.debugsettings.store
import mozilla.components.lib.state.State
/**
* UI state of the debug drawer feature.
*
* @property drawerStatus The [DrawerStatus] indicating the physical state of the drawer.
*/
data class DebugDrawerState(
val drawerStatus: DrawerStatus = DrawerStatus.Closed,
) : State

View File

@ -0,0 +1,27 @@
/* 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 org.mozilla.fenix.debugsettings.store
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.Store
/**
* A [Store] that holds the [DebugDrawerState] for the Debug Drawer and reduces [DebugDrawerAction]s
* dispatched to the store.
*/
class DebugDrawerStore(
initialState: DebugDrawerState = DebugDrawerState(),
middlewares: List<Middleware<DebugDrawerState, DebugDrawerAction>> = emptyList(),
) : Store<DebugDrawerState, DebugDrawerAction>(
initialState,
::reduce,
middlewares,
)
private fun reduce(state: DebugDrawerState, action: DebugDrawerAction): DebugDrawerState = when (action) {
is DebugDrawerAction.DrawerOpened -> state.copy(drawerStatus = DrawerStatus.Open)
is DebugDrawerAction.DrawerClosed -> state.copy(drawerStatus = DrawerStatus.Closed)
is DebugDrawerAction.NavigateTo, DebugDrawerAction.OnBackPressed -> state // handled by [DebugDrawerNavigationMiddleware]
}

View File

@ -0,0 +1,20 @@
/* 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 org.mozilla.fenix.debugsettings.store
/**
* Possible values of the debug drawer's physical state.
*/
enum class DrawerStatus {
/**
* The state of the drawer when it is closed.
*/
Closed,
/**
* The state of the drawer when it is open.
*/
Open,
}

View File

@ -0,0 +1,328 @@
/* 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 org.mozilla.fenix.debugsettings.tabs
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.material.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.core.text.isDigitsOnly
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.lib.state.ext.observeAsState
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.Divider
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.compose.button.PrimaryButton
import org.mozilla.fenix.debugsettings.ui.DebugDrawer
import org.mozilla.fenix.ext.maxActiveTime
import org.mozilla.fenix.tabstray.ext.isNormalTabInactive
import org.mozilla.fenix.theme.FirefoxTheme
/**
* Tab Tools UI for [DebugDrawer] that displays the tab counts and allows easy bulk-opening of tabs.
*
* @param store [BrowserStore] used to obtain the tab counts and fire any tab creation actions.
* @param inactiveTabsEnabled Whether the inactive tabs feature is enabled.
*/
@Composable
fun TabTools(
store: BrowserStore,
inactiveTabsEnabled: Boolean,
) {
val tabs by store.observeAsState(initialValue = emptyList()) { state -> state.tabs }
val totalTabCount = remember(tabs) { tabs.size }
val privateTabCount = remember(tabs) { tabs.filter { it.content.private }.size }
val inactiveTabCount = remember(tabs) {
if (inactiveTabsEnabled) {
tabs.filter { it.isNormalTabInactive(maxActiveTime) }.size
} else {
0
}
}
val activeTabCount = remember(tabs) { totalTabCount - privateTabCount - inactiveTabCount }
TabToolsContent(
activeTabCount = activeTabCount,
inactiveTabCount = inactiveTabCount,
privateTabCount = privateTabCount,
totalTabCount = totalTabCount,
inactiveTabsEnabled = inactiveTabsEnabled,
onCreateTabsClick = { quantity, isInactive, isPrivate ->
store.dispatch(
TabListAction.AddMultipleTabsAction(
tabs = generateTabList(
quantity = quantity,
isInactive = isInactive,
isPrivate = isPrivate,
),
),
)
},
)
}
private fun generateTabList(
quantity: Int,
isInactive: Boolean = false,
isPrivate: Boolean = false,
) = List(quantity) {
createTab(
url = "www.example.com",
private = isPrivate,
createdAt = if (isInactive) 0L else System.currentTimeMillis(),
)
}
@Composable
private fun TabToolsContent(
activeTabCount: Int,
inactiveTabCount: Int,
privateTabCount: Int,
totalTabCount: Int,
inactiveTabsEnabled: Boolean,
onCreateTabsClick: ((quantity: Int, isInactive: Boolean, isPrivate: Boolean) -> Unit),
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(all = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
TabCounter(
activeTabCount = activeTabCount,
inactiveTabCount = inactiveTabCount,
privateTabCount = privateTabCount,
totalTabCount = totalTabCount,
inactiveTabsEnabled = inactiveTabsEnabled,
)
TabCreationTool(
inactiveTabsEnabled = inactiveTabsEnabled,
onCreateTabsClick = onCreateTabsClick,
)
}
}
@Composable
private fun TabCounter(
activeTabCount: Int,
inactiveTabCount: Int,
privateTabCount: Int,
totalTabCount: Int,
inactiveTabsEnabled: Boolean,
) {
Column {
Text(
text = stringResource(R.string.debug_drawer_tab_tools_tab_count_title),
color = FirefoxTheme.colors.textPrimary,
style = FirefoxTheme.typography.headline5,
)
Spacer(modifier = Modifier.height(16.dp))
TabCountRow(
tabType = stringResource(R.string.debug_drawer_tab_tools_tab_count_normal),
count = activeTabCount.toString(),
)
if (inactiveTabsEnabled) {
TabCountRow(
tabType = stringResource(R.string.debug_drawer_tab_tools_tab_count_inactive),
count = inactiveTabCount.toString(),
)
}
TabCountRow(
tabType = stringResource(R.string.debug_drawer_tab_tools_tab_count_private),
count = privateTabCount.toString(),
)
Spacer(modifier = Modifier.height(8.dp))
Divider()
Spacer(modifier = Modifier.height(8.dp))
TabCountRow(
tabType = stringResource(R.string.debug_drawer_tab_tools_tab_count_total),
count = totalTabCount.toString(),
)
}
}
@Composable
private fun TabCountRow(
tabType: String,
count: String,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = tabType,
color = FirefoxTheme.colors.textSecondary,
style = FirefoxTheme.typography.headline6,
)
Text(
text = count,
color = FirefoxTheme.colors.textSecondary,
style = FirefoxTheme.typography.headline6,
)
}
}
private const val DEFAULT_TABS_TO_ADD = "1"
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun TabCreationTool(
inactiveTabsEnabled: Boolean,
onCreateTabsClick: ((quantity: Int, isInactive: Boolean, isPrivate: Boolean) -> Unit),
) {
var tabQuantityToCreate by rememberSaveable { mutableStateOf(DEFAULT_TABS_TO_ADD) }
var hasError by rememberSaveable { mutableStateOf(false) }
val keyboardController = LocalSoftwareKeyboardController.current
Column {
Text(
text = stringResource(R.string.debug_drawer_tab_tools_tab_creation_tool_title),
color = FirefoxTheme.colors.textPrimary,
style = FirefoxTheme.typography.headline5,
)
TextField(
value = tabQuantityToCreate,
onValueChange = {
tabQuantityToCreate = it
hasError = it.isEmpty() || !it.isDigitsOnly() || it.toInt() == 0
},
modifier = Modifier.fillMaxWidth(),
textStyle = FirefoxTheme.typography.subtitle1,
label = {
Text(
text = stringResource(R.string.debug_drawer_tab_tools_tab_creation_tool_text_field_label),
color = FirefoxTheme.colors.textPrimary,
style = FirefoxTheme.typography.caption,
)
},
isError = hasError,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
),
keyboardActions = KeyboardActions(
onDone = {
keyboardController?.hide()
},
),
singleLine = true,
colors = TextFieldDefaults.textFieldColors(
textColor = FirefoxTheme.colors.textPrimary,
backgroundColor = Color.Transparent,
cursorColor = FirefoxTheme.colors.borderFormDefault,
errorCursorColor = FirefoxTheme.colors.borderWarning,
focusedIndicatorColor = FirefoxTheme.colors.borderPrimary,
unfocusedIndicatorColor = FirefoxTheme.colors.borderPrimary,
errorIndicatorColor = FirefoxTheme.colors.borderWarning,
),
)
Spacer(modifier = Modifier.height(8.dp))
PrimaryButton(
text = stringResource(id = R.string.debug_drawer_tab_tools_tab_creation_tool_button_text_active),
enabled = !hasError,
onClick = {
onCreateTabsClick(tabQuantityToCreate.toInt(), false, false)
},
)
Spacer(modifier = Modifier.height(8.dp))
if (inactiveTabsEnabled) {
PrimaryButton(
text = stringResource(id = R.string.debug_drawer_tab_tools_tab_creation_tool_button_text_inactive),
enabled = !hasError,
onClick = {
onCreateTabsClick(tabQuantityToCreate.toInt(), true, false)
},
)
Spacer(modifier = Modifier.height(8.dp))
}
PrimaryButton(
text = stringResource(id = R.string.debug_drawer_tab_tools_tab_creation_tool_button_text_private),
enabled = !hasError,
onClick = {
onCreateTabsClick(tabQuantityToCreate.toInt(), false, true)
},
)
}
}
private data class TabToolsPreviewModel(
val inactiveTabsEnabled: Boolean = true,
)
private class TabToolsPreviewParameterProvider : PreviewParameterProvider<TabToolsPreviewModel> {
override val values: Sequence<TabToolsPreviewModel>
get() = sequenceOf(
TabToolsPreviewModel(
inactiveTabsEnabled = true,
),
TabToolsPreviewModel(
inactiveTabsEnabled = false,
),
)
}
@Composable
@LightDarkPreview
private fun TabToolsPreview(
@PreviewParameter(TabToolsPreviewParameterProvider::class) model: TabToolsPreviewModel,
) {
FirefoxTheme {
Box(
modifier = Modifier.background(color = FirefoxTheme.colors.layer1),
) {
TabTools(
store = BrowserStore(),
inactiveTabsEnabled = model.inactiveTabsEnabled,
)
}
}
}

View File

@ -0,0 +1,138 @@
/* 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 org.mozilla.fenix.debugsettings.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.debugsettings.navigation.DebugDrawerDestination
import org.mozilla.fenix.theme.FirefoxTheme
/**
* The debug drawer UI.
*
* @param navController [NavHostController] used to perform navigation actions on the [NavHost].
* @param destinations The list of [DebugDrawerDestination]s (excluding home) used to populate
* the [NavHost] with screens.
* @param onBackButtonClick Invoked when the user taps on the back button in the app bar.
*/
@Composable
fun DebugDrawer(
navController: NavHostController,
destinations: List<DebugDrawerDestination>,
onBackButtonClick: () -> Unit,
) {
var backButtonVisible by remember { mutableStateOf(false) }
var toolbarTitle by remember { mutableStateOf("") }
Column(modifier = Modifier.fillMaxSize()) {
TopAppBar(
title = {
Text(
text = toolbarTitle,
color = FirefoxTheme.colors.textPrimary,
style = FirefoxTheme.typography.headline6,
)
},
navigationIcon = if (backButtonVisible) {
topBarBackButton(onClick = onBackButtonClick)
} else {
null
},
backgroundColor = FirefoxTheme.colors.layer1,
elevation = 5.dp,
)
NavHost(
navController = navController,
startDestination = DEBUG_DRAWER_HOME_ROUTE,
modifier = Modifier.fillMaxSize(),
) {
composable(route = DEBUG_DRAWER_HOME_ROUTE) {
toolbarTitle = stringResource(id = R.string.debug_drawer_title)
backButtonVisible = false
DebugDrawerHome(destinations = destinations)
}
destinations.forEach { destination ->
composable(route = destination.route) {
toolbarTitle = stringResource(id = destination.title)
backButtonVisible = true
destination.content()
}
}
}
}
}
@Composable
private fun topBarBackButton(onClick: () -> Unit): @Composable () -> Unit = {
IconButton(
onClick = onClick,
) {
Icon(
painter = painterResource(R.drawable.mozac_ic_back_24),
contentDescription = stringResource(R.string.debug_drawer_back_button_content_description),
tint = FirefoxTheme.colors.iconPrimary,
)
}
}
@Composable
@LightDarkPreview
private fun DebugDrawerPreview() {
val navController = rememberNavController()
val destinations = remember {
List(size = 15) { index ->
DebugDrawerDestination(
route = "screen_$index",
title = R.string.debug_drawer_title,
onClick = {
navController.navigate(route = "screen_$index")
},
content = {
Text(
text = "Tool $index",
color = FirefoxTheme.colors.textPrimary,
style = FirefoxTheme.typography.headline6,
)
},
)
}
}
FirefoxTheme {
Box(modifier = Modifier.background(color = FirefoxTheme.colors.layer1)) {
DebugDrawer(
navController = navController,
destinations = destinations,
onBackButtonClick = {
navController.popBackStack()
},
)
}
}
}

View File

@ -0,0 +1,144 @@
/* 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 org.mozilla.fenix.debugsettings.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Snackbar
import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import mozilla.components.support.ktx.android.content.appName
import mozilla.components.support.ktx.android.content.appVersionName
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.Divider
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.compose.inComposePreview
import org.mozilla.fenix.compose.list.TextListItem
import org.mozilla.fenix.debugsettings.navigation.DebugDrawerDestination
import org.mozilla.fenix.theme.FirefoxTheme
/**
* The navigation route for [DebugDrawerHome].
*/
const val DEBUG_DRAWER_HOME_ROUTE = "debug_drawer_home"
/**
* The home screen of the [DebugDrawer].
*
* @param destinations The list of [DebugDrawerDestination]s to display.
*/
@Composable
fun DebugDrawerHome(
destinations: List<DebugDrawerDestination>,
) {
val lazyListState = rememberLazyListState()
val appName: String
val appVersion: String
if (inComposePreview) {
appName = "App Name Preview"
appVersion = "100.00.000"
} else {
appName = LocalContext.current.appName
appVersion = LocalContext.current.appVersionName
}
LazyColumn(
modifier = Modifier
.fillMaxSize()
.background(color = FirefoxTheme.colors.layer1),
state = lazyListState,
) {
item(key = "home_header") {
Row(
modifier = Modifier
.padding(all = 16.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = appName,
color = FirefoxTheme.colors.textPrimary,
style = FirefoxTheme.typography.headline5,
)
Text(
text = appVersion,
color = FirefoxTheme.colors.textSecondary,
style = FirefoxTheme.typography.headline5,
)
}
Divider()
}
items(
items = destinations,
key = { destination ->
destination.route
},
) { destination ->
TextListItem(
label = stringResource(id = destination.title),
onClick = destination.onClick,
)
Divider()
}
}
}
@Composable
@LightDarkPreview
private fun DebugDrawerHomePreview() {
val scope = rememberCoroutineScope()
val snackbarState = remember { SnackbarHostState() }
FirefoxTheme {
Box {
DebugDrawerHome(
destinations = List(size = 30) {
DebugDrawerDestination(
route = "screen_$it",
title = R.string.debug_drawer_title,
onClick = {
scope.launch {
snackbarState.showSnackbar("item $it clicked")
}
},
content = {},
)
},
)
SnackbarHost(
hostState = snackbarState,
modifier = Modifier.align(Alignment.BottomCenter),
) { snackbarData ->
Snackbar(
snackbarData = snackbarData,
)
}
}
}
}

View File

@ -4,33 +4,78 @@
package org.mozilla.fenix.debugsettings.ui package org.mozilla.fenix.debugsettings.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.DrawerValue
import androidx.compose.material.ModalDrawer
import androidx.compose.material.Snackbar import androidx.compose.material.Snackbar
import androidx.compose.material.SnackbarHost import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState import androidx.compose.material.SnackbarHostState
import androidx.compose.material.Text
import androidx.compose.material.rememberDrawerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.compose.annotation.LightDarkPreview import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.compose.button.FloatingActionButton import org.mozilla.fenix.compose.button.FloatingActionButton
import org.mozilla.fenix.debugsettings.navigation.DebugDrawerDestination
import org.mozilla.fenix.debugsettings.store.DrawerStatus
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
/** /**
* Overlay for presenting Fenix-wide debugging content. * Overlay for presenting app-wide debugging content.
*
* @param navController [NavHostController] used to perform navigation actions.
* @param drawerStatus The [DrawerStatus] indicating the physical state of the drawer.
* @param debugDrawerDestinations The complete list of [DebugDrawerDestination]s used to populate
* the [DebugDrawer] with sub screens.
* @param onDrawerOpen Invoked when the drawer is opened.
* @param onDrawerClose Invoked when the drawer is closed.
* @param onDrawerBackButtonClick Invoked when the user taps on the back button in the app bar.
*/ */
@Composable @Composable
fun DebugOverlay() { fun DebugOverlay(
navController: NavHostController,
drawerStatus: DrawerStatus,
debugDrawerDestinations: List<DebugDrawerDestination>,
onDrawerOpen: () -> Unit,
onDrawerClose: () -> Unit,
onDrawerBackButtonClick: () -> Unit,
) {
val snackbarState = remember { SnackbarHostState() } val snackbarState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope() val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
LaunchedEffect(drawerStatus) {
if (drawerStatus == DrawerStatus.Open) {
drawerState.open()
}
}
LaunchedEffect(drawerState) {
snapshotFlow { drawerState.currentValue }
.distinctUntilChanged()
.filter { it == DrawerValue.Closed }
.collect {
onDrawerClose()
}
}
Box( Box(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@ -41,12 +86,41 @@ fun DebugOverlay() {
.align(Alignment.CenterStart) .align(Alignment.CenterStart)
.padding(start = 16.dp), .padding(start = 16.dp),
onClick = { onClick = {
scope.launch { onDrawerOpen()
snackbarState.showSnackbar("Show debug drawer")
}
}, },
) )
// ModalDrawer utilizes a Surface, which blocks ALL clicks behind it, preventing the app
// from being interactable. This cannot be overridden in the Surface API, so we must hide
// the entire drawer when it is closed.
if (drawerStatus == DrawerStatus.Open) {
val currentLayoutDirection = LocalLayoutDirection.current
val sheetLayoutDirection = when (currentLayoutDirection) {
LayoutDirection.Rtl -> LayoutDirection.Ltr
LayoutDirection.Ltr -> LayoutDirection.Rtl
}
// Force the drawer to always open from the opposite side of the screen. We need to reset
// this below with `drawerContent` to ensure the content follows the correct direction.
CompositionLocalProvider(LocalLayoutDirection provides sheetLayoutDirection) {
ModalDrawer(
drawerContent = {
CompositionLocalProvider(LocalLayoutDirection provides currentLayoutDirection) {
DebugDrawer(
navController = navController,
destinations = debugDrawerDestinations,
onBackButtonClick = onDrawerBackButtonClick,
)
}
},
drawerBackgroundColor = FirefoxTheme.colors.layer1,
scrimColor = FirefoxTheme.colors.scrim,
drawerState = drawerState,
content = {},
)
}
}
// This must be the last element in the Box // This must be the last element in the Box
SnackbarHost( SnackbarHost(
hostState = snackbarState, hostState = snackbarState,
@ -62,13 +136,41 @@ fun DebugOverlay() {
@Composable @Composable
@LightDarkPreview @LightDarkPreview
private fun DebugOverlayPreview() { private fun DebugOverlayPreview() {
val navController = rememberNavController()
var drawerStatus by remember { mutableStateOf(DrawerStatus.Closed) }
val destinations = remember {
List(size = 15) { index ->
DebugDrawerDestination(
route = "screen_$index",
title = R.string.debug_drawer_title,
onClick = {
navController.navigate(route = "screen_$index")
},
content = {
Text(
text = "Tool $index",
color = FirefoxTheme.colors.textPrimary,
style = FirefoxTheme.typography.headline6,
)
},
)
}
}
FirefoxTheme { FirefoxTheme {
Box( DebugOverlay(
modifier = Modifier navController = navController,
.fillMaxSize() drawerStatus = drawerStatus,
.background(color = FirefoxTheme.colors.layer1), debugDrawerDestinations = destinations,
) { onDrawerOpen = {
DebugOverlay() drawerStatus = DrawerStatus.Open
} },
onDrawerClose = {
drawerStatus = DrawerStatus.Closed
},
onDrawerBackButtonClick = {
navController.popBackStack()
},
)
} }
} }

View File

@ -0,0 +1,83 @@
/* 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 org.mozilla.fenix.debugsettings.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.navigation.compose.rememberNavController
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.lib.state.ext.observeAsState
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.debugsettings.navigation.DebugDrawerRoute
import org.mozilla.fenix.debugsettings.store.DebugDrawerAction
import org.mozilla.fenix.debugsettings.store.DebugDrawerNavigationMiddleware
import org.mozilla.fenix.debugsettings.store.DebugDrawerStore
import org.mozilla.fenix.debugsettings.store.DrawerStatus
import org.mozilla.fenix.debugsettings.tabs.TabTools
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/**
* Overlay for presenting Fenix-wide debugging content.
*
* @param browserStore [BrowserStore] used to access tab data for [TabTools].
* @param inactiveTabsEnabled Whether the inactive tabs feature is enabled.
*/
@Composable
fun FenixOverlay(
browserStore: BrowserStore,
inactiveTabsEnabled: Boolean,
) {
val navController = rememberNavController()
val coroutineScope = rememberCoroutineScope()
val debugDrawerStore = remember {
DebugDrawerStore(
middlewares = listOf(
DebugDrawerNavigationMiddleware(
navController = navController,
scope = coroutineScope,
),
),
)
}
val debugDrawerDestinations = remember {
DebugDrawerRoute.generateDebugDrawerDestinations(
debugDrawerStore = debugDrawerStore,
browserStore = browserStore,
inactiveTabsEnabled = inactiveTabsEnabled,
)
}
val drawerStatus by debugDrawerStore.observeAsState(initialValue = DrawerStatus.Closed) { state ->
state.drawerStatus
}
FirefoxTheme(theme = Theme.getTheme(allowPrivateTheme = false)) {
DebugOverlay(
navController = navController,
drawerStatus = drawerStatus,
debugDrawerDestinations = debugDrawerDestinations,
onDrawerOpen = {
debugDrawerStore.dispatch(DebugDrawerAction.DrawerOpened)
},
onDrawerClose = {
debugDrawerStore.dispatch(DebugDrawerAction.DrawerClosed)
},
onDrawerBackButtonClick = {
debugDrawerStore.dispatch(DebugDrawerAction.OnBackPressed)
},
)
}
}
@LightDarkPreview
@Composable
private fun FenixOverlayPreview() {
FenixOverlay(
browserStore = BrowserStore(),
inactiveTabsEnabled = true,
)
}

View File

@ -70,7 +70,9 @@ fun createNimbus(context: Context, urlString: String?): NimbusApi {
onFetchCallback = { onFetchCallback = {
context.settings().nimbusExperimentsFetched = true context.settings().nimbusExperimentsFetched = true
} }
}.build(appInfo) }.build(appInfo).also { nimbusApi ->
nimbusApi.recordIsReady(FxNimbus.features.nimbusIsReady.value().eventCount)
}
} }
private fun Context.reportError(message: String, e: Throwable) { private fun Context.reportError(message: String, e: Throwable) {

View File

@ -15,12 +15,47 @@ import androidx.annotation.DrawableRes
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.navigation.NavDestination
import androidx.navigation.NavDirections
import mozilla.components.concept.base.crash.Breadcrumb import mozilla.components.concept.base.crash.Breadcrumb
import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.manifest.WebAppManifestParser
import mozilla.components.feature.intent.ext.getSessionId
import mozilla.components.feature.pwa.ext.getWebAppManifest
import mozilla.components.support.utils.SafeIntent
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.addons.AddonDetailsFragmentDirections
import org.mozilla.fenix.addons.AddonPermissionsDetailsFragmentDirections
import org.mozilla.fenix.addons.AddonsManagementFragmentDirections
import org.mozilla.fenix.customtabs.EXTRA_IS_SANDBOX_CUSTOM_TAB
import org.mozilla.fenix.customtabs.ExternalAppBrowserActivity
import org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExceptionsFragmentDirections
import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections
import org.mozilla.fenix.library.history.HistoryFragmentDirections
import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentDirections
import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections
import org.mozilla.fenix.search.SearchDialogFragmentDirections
import org.mozilla.fenix.settings.HttpsOnlyFragmentDirections
import org.mozilla.fenix.settings.SettingsFragmentDirections
import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.settings.TrackingProtectionFragmentDirections
import org.mozilla.fenix.settings.about.AboutFragmentDirections
import org.mozilla.fenix.settings.logins.fragment.LoginDetailFragmentDirections
import org.mozilla.fenix.settings.logins.fragment.SavedLoginsAuthFragmentDirections
import org.mozilla.fenix.settings.search.SaveSearchEngineFragmentDirections
import org.mozilla.fenix.settings.search.SearchEngineFragmentDirections
import org.mozilla.fenix.settings.studies.StudiesFragmentDirections
import org.mozilla.fenix.settings.wallpaper.WallpaperSettingsFragmentDirections
import org.mozilla.fenix.share.AddNewDeviceFragmentDirections
import org.mozilla.fenix.shopping.ReviewQualityCheckFragmentDirections
import org.mozilla.fenix.tabstray.TabsTrayFragmentDirections
import org.mozilla.fenix.trackingprotection.TrackingProtectionPanelDialogFragmentDirections
import org.mozilla.fenix.translations.TranslationsDialogFragmentDirections
import java.security.InvalidParameterException
/** /**
* Attempts to call immersive mode using the View to hide the status bar and navigation buttons. * Attempts to call immersive mode using the View to hide the status bar and navigation buttons.
@ -170,7 +205,164 @@ fun Activity.setNavigationIcon(
} }
} }
/**
* Delegate to the relevant 'get nav directions' function based on the given [Activity].
*
* @param from The [BrowserDirection] to indicate which fragment the browser is being opened from.
* @param customTabSessionId Optional custom tab session ID if navigating from a custom tab.
*
* @return the [NavDirections] for the given [Activity].
*/
fun Activity.getNavDirections(
from: BrowserDirection,
customTabSessionId: String? = null,
): NavDirections? = when (this) {
is ExternalAppBrowserActivity -> {
getExternalAppBrowserNavDirections(from, customTabSessionId)
}
else -> {
getHomeNavDirections(from)
}
}
private fun Activity.getExternalAppBrowserNavDirections(
from: BrowserDirection,
customTabSessionId: String?,
): NavDirections? {
if (customTabSessionId == null) {
finishAndRemoveTask()
return null
}
val manifest =
intent.getWebAppManifest()?.let { WebAppManifestParser().serialize(it).toString() }
return when (from) {
BrowserDirection.FromGlobal ->
NavGraphDirections.actionGlobalExternalAppBrowser(
activeSessionId = customTabSessionId,
webAppManifest = manifest,
isSandboxCustomTab = intent.getBooleanExtra(EXTRA_IS_SANDBOX_CUSTOM_TAB, false),
)
else -> throw InvalidParameterException(
"Tried to navigate to ExternalAppBrowserFragment from $from",
)
}
}
private fun getHomeNavDirections(
from: BrowserDirection,
): NavDirections = when (from) {
BrowserDirection.FromGlobal -> NavGraphDirections.actionGlobalBrowser()
BrowserDirection.FromHome -> HomeFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromWallpaper -> WallpaperSettingsFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromSearchDialog -> SearchDialogFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromSettings -> SettingsFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromBookmarks -> BookmarkFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromHistory -> HistoryFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromHistoryMetadataGroup -> HistoryMetadataGroupFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromTrackingProtectionExceptions ->
TrackingProtectionExceptionsFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromHttpsOnlyMode -> HttpsOnlyFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromAbout -> AboutFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromTrackingProtection -> TrackingProtectionFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromTrackingProtectionDialog ->
TrackingProtectionPanelDialogFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromSavedLoginsFragment -> SavedLoginsAuthFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromAddNewDeviceFragment -> AddNewDeviceFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromSearchEngineFragment -> SearchEngineFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromSaveSearchEngineFragment -> SaveSearchEngineFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromAddonDetailsFragment -> AddonDetailsFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromAddonPermissionsDetailsFragment ->
AddonPermissionsDetailsFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromLoginDetailFragment -> LoginDetailFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromTabsTray -> TabsTrayFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromRecentlyClosed -> RecentlyClosedFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromStudiesFragment -> StudiesFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromReviewQualityCheck -> ReviewQualityCheckFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromAddonsManagementFragment -> AddonsManagementFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromTranslationsDialogFragment -> TranslationsDialogFragmentDirections.actionGlobalBrowser()
}
const val REQUEST_CODE_BROWSER_ROLE = 1 const val REQUEST_CODE_BROWSER_ROLE = 1
const val SETTINGS_SELECT_OPTION_KEY = ":settings:fragment_args_key" const val SETTINGS_SELECT_OPTION_KEY = ":settings:fragment_args_key"
const val SETTINGS_SHOW_FRAGMENT_ARGS = ":settings:show_fragment_args" const val SETTINGS_SHOW_FRAGMENT_ARGS = ":settings:show_fragment_args"
const val DEFAULT_BROWSER_APP_OPTION = "default_browser" const val DEFAULT_BROWSER_APP_OPTION = "default_browser"
const val EXTERNAL_APP_BROWSER_INTENT_SOURCE = "CUSTOM_TAB"
/**
* Depending on the [Activity], maybe derive the source of the given [intent].
*
* @param intent the [SafeIntent] to derive the source from.
*/
fun Activity.getIntentSource(intent: SafeIntent): String? = when (this) {
is ExternalAppBrowserActivity -> EXTERNAL_APP_BROWSER_INTENT_SOURCE
else -> getHomeIntentSource(intent)
}
private fun getHomeIntentSource(intent: SafeIntent): String? {
return when {
intent.isLauncherIntent -> HomeActivity.APP_ICON
intent.action == Intent.ACTION_VIEW -> "LINK"
else -> null
}
}
/**
* Depending on the [Activity], maybe derive the session ID of the given [intent].
*
* @param intent the [SafeIntent] to derive the session ID from.
*/
fun Activity.getIntentSessionId(intent: SafeIntent): String? = when (this) {
is ExternalAppBrowserActivity -> getExternalAppBrowserIntentSessionId(intent)
else -> null
}
private fun getExternalAppBrowserIntentSessionId(intent: SafeIntent) = intent.getSessionId()
/**
* Get the breadcrumb message for the [Activity].
*
* @param destination the [NavDestination] required to provide the destination ID.
*/
fun Activity.getBreadcrumbMessage(destination: NavDestination): String = when (this) {
is ExternalAppBrowserActivity -> getExternalAppBrowserBreadcrumbMessage(destination.id)
else -> getHomeBreadcrumbMessage(destination.id)
}
private fun Activity.getExternalAppBrowserBreadcrumbMessage(destinationId: Int): String {
val fragmentName = resources.getResourceEntryName(destinationId)
return "Changing to fragment $fragmentName, isCustomTab: true"
}
private fun Activity.getHomeBreadcrumbMessage(destinationId: Int): String {
val fragmentName = resources.getResourceEntryName(destinationId)
return "Changing to fragment $fragmentName, isCustomTab: false"
}

View File

@ -19,7 +19,6 @@ import mozilla.components.service.sync.logins.GeckoLoginStorageDelegate
import org.mozilla.fenix.Config import org.mozilla.fenix.Config
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.geckoview.ContentBlocking import org.mozilla.geckoview.ContentBlocking
import org.mozilla.geckoview.ContentBlocking.SafeBrowsingProvider import org.mozilla.geckoview.ContentBlocking.SafeBrowsingProvider
import org.mozilla.geckoview.GeckoRuntime import org.mozilla.geckoview.GeckoRuntime
@ -134,7 +133,7 @@ object GeckoProvider {
.consoleOutput(context.components.settings.enableGeckoLogs) .consoleOutput(context.components.settings.enableGeckoLogs)
.debugLogging(Config.channel.isDebug || context.components.settings.enableGeckoLogs) .debugLogging(Config.channel.isDebug || context.components.settings.enableGeckoLogs)
.aboutConfigEnabled(true) .aboutConfigEnabled(true)
.extensionsProcessEnabled(FxNimbus.features.extensionsProcess.value().enabled) .extensionsProcessEnabled(true)
.extensionsWebAPIEnabled(true) .extensionsWebAPIEnabled(true)
.build() .build()
} }

View File

@ -49,7 +49,9 @@ import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.normalTabs import mozilla.components.browser.state.selector.normalTabs
@ -119,7 +121,6 @@ import org.mozilla.fenix.messaging.FenixNimbusMessagingController
import org.mozilla.fenix.messaging.MessagingFeature import org.mozilla.fenix.messaging.MessagingFeature
import org.mozilla.fenix.nimbus.FxNimbus import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks
import org.mozilla.fenix.perf.runBlockingIncrement
import org.mozilla.fenix.search.toolbar.DefaultSearchSelectorController import org.mozilla.fenix.search.toolbar.DefaultSearchSelectorController
import org.mozilla.fenix.search.toolbar.SearchSelectorMenu import org.mozilla.fenix.search.toolbar.SearchSelectorMenu
import org.mozilla.fenix.tabstray.TabsTrayAccessPoint import org.mozilla.fenix.tabstray.TabsTrayAccessPoint
@ -1007,17 +1008,18 @@ class HomeFragment : Fragment() {
lastAppliedWallpaperName = wallpaperName lastAppliedWallpaperName = wallpaperName
} }
else -> { else -> {
runBlockingIncrement { viewLifecycleOwner.lifecycleScope.launch {
// loadBitmap does file lookups based on name, so we don't need a fully // loadBitmap does file lookups based on name, so we don't need a fully
// qualified type to load the image // qualified type to load the image
val wallpaper = Wallpaper.Default.copy(name = wallpaperName) val wallpaper = Wallpaper.Default.copy(name = wallpaperName)
val wallpaperImage = val wallpaperImage =
requireComponents.useCases.wallpaperUseCases.loadBitmap(wallpaper) context?.let { requireComponents.useCases.wallpaperUseCases.loadBitmap(it, wallpaper) }
wallpaperImage?.let { wallpaperImage?.let {
it.scaleToBottomOfView(binding.wallpaperImageView) it.scaleToBottomOfView(binding.wallpaperImageView)
binding.wallpaperImageView.isVisible = true binding.wallpaperImageView.isVisible = true
lastAppliedWallpaperName = wallpaperName lastAppliedWallpaperName = wallpaperName
} ?: run { } ?: run {
if (!isActive) return@run
with(binding.wallpaperImageView) { with(binding.wallpaperImageView) {
isVisible = false isVisible = false
showSnackBar( showSnackBar(
@ -1051,10 +1053,14 @@ class HomeFragment : Fragment() {
} }
private fun observeWallpaperUpdates() { private fun observeWallpaperUpdates() {
consumeFrom(requireComponents.appStore) { consumeFlow(requireComponents.appStore, viewLifecycleOwner) { flow ->
val currentWallpaper = it.wallpaperState.currentWallpaper flow.filter { it.mode == BrowsingMode.Normal }
if (currentWallpaper.name != lastAppliedWallpaperName) { .map { it.wallpaperState.currentWallpaper }
applyWallpaper(wallpaperName = currentWallpaper.name, orientationChange = false) .distinctUntilChanged()
.collect {
if (it.name != lastAppliedWallpaperName) {
applyWallpaper(wallpaperName = it.name, orientationChange = false)
}
} }
} }
} }

View File

@ -47,7 +47,6 @@ import org.mozilla.fenix.GleanMetrics.HomeMenu as HomeMenuMetrics
* clicked. * clicked.
* @param fxaEntrypoint The source entry point to FxA. * @param fxaEntrypoint The source entry point to FxA.
*/ */
@Suppress("LongParameterList")
class HomeMenuView( class HomeMenuView(
private val view: View, private val view: View,
private val context: Context, private val context: Context,

View File

@ -239,7 +239,7 @@ fun PocketSponsoredStory(
* @param onDiscoverMoreClicked Callback for when the user taps an element which contains an * @param onDiscoverMoreClicked Callback for when the user taps an element which contains an
*/ */
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@Suppress("LongParameterList", "LongMethod") @Suppress("LongMethod")
@Composable @Composable
fun PocketStories( fun PocketStories(
@PreviewParameter(PocketStoryProvider::class) stories: List<PocketStory>, @PreviewParameter(PocketStoryProvider::class) stories: List<PocketStory>,
@ -367,7 +367,6 @@ private fun alignColumnToTitlePadding(screenWidth: Dp, contentPadding: Dp) =
* @param onCategoryClick Callback for when the user taps a category. * @param onCategoryClick Callback for when the user taps a category.
*/ */
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@Suppress("LongParameterList")
@Composable @Composable
fun PocketStoriesCategories( fun PocketStoriesCategories(
categories: List<PocketRecommendedStoriesCategory>, categories: List<PocketRecommendedStoriesCategory>,

View File

@ -5,7 +5,6 @@
package org.mozilla.fenix.home.recentbookmarks.view package org.mozilla.fenix.home.recentbookmarks.view
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
@ -42,10 +41,10 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import mozilla.components.browser.icons.compose.Loader import mozilla.components.browser.icons.compose.Loader
import mozilla.components.browser.icons.compose.Placeholder import mozilla.components.browser.icons.compose.Placeholder
import mozilla.components.browser.icons.compose.WithIcon
import mozilla.components.ui.colors.PhotonColors import mozilla.components.ui.colors.PhotonColors
import org.mozilla.fenix.components.components import org.mozilla.fenix.components.components
import org.mozilla.fenix.compose.ContextualMenu import org.mozilla.fenix.compose.ContextualMenu
import org.mozilla.fenix.compose.Favicon
import org.mozilla.fenix.compose.Image import org.mozilla.fenix.compose.Image
import org.mozilla.fenix.compose.MenuItem import org.mozilla.fenix.compose.MenuItem
import org.mozilla.fenix.compose.annotation.LightDarkPreview import org.mozilla.fenix.compose.annotation.LightDarkPreview
@ -172,6 +171,11 @@ private fun RecentBookmarkImage(bookmark: RecentBookmark) {
modifier = imageModifier, modifier = imageModifier,
targetSize = imageWidth, targetSize = imageWidth,
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
fallback = {
if (!bookmark.url.isNullOrEmpty()) {
FallbackBookmarkFaviconImage(url = bookmark.url)
}
},
) )
} }
!bookmark.url.isNullOrEmpty() && !inComposePreview -> { !bookmark.url.isNullOrEmpty() && !inComposePreview -> {
@ -180,23 +184,7 @@ private fun RecentBookmarkImage(bookmark: RecentBookmark) {
PlaceholderBookmarkImage() PlaceholderBookmarkImage()
} }
WithIcon { icon -> FallbackBookmarkFaviconImage(bookmark.url)
Box(
modifier = imageModifier.background(
color = FirefoxTheme.colors.layer2,
),
contentAlignment = Alignment.Center,
) {
Image(
painter = icon.painter,
contentDescription = null,
modifier = Modifier
.size(36.dp)
.clip(cardShape),
contentScale = ContentScale.Crop,
)
}
}
} }
} }
inComposePreview -> { inComposePreview -> {
@ -217,6 +205,20 @@ private fun PlaceholderBookmarkImage() {
) )
} }
@Composable
private fun FallbackBookmarkFaviconImage(
url: String,
) {
Box(
modifier = imageModifier.background(
color = FirefoxTheme.colors.layer2,
),
contentAlignment = Alignment.Center,
) {
Favicon(url = url, size = 36.dp)
}
}
@Composable @Composable
@LightDarkPreview @LightDarkPreview
private fun RecentBookmarksPreview() { private fun RecentBookmarksPreview() {

View File

@ -69,7 +69,7 @@ private const val THUMBNAIL_SIZE = 108
* @param onRemoveSyncedTab Invoked when user clicks on the "Remove" dropdown menu option. * @param onRemoveSyncedTab Invoked when user clicks on the "Remove" dropdown menu option.
*/ */
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Suppress("LongMethod", "LongParameterList") @Suppress("LongMethod")
@Composable @Composable
fun RecentSyncedTab( fun RecentSyncedTab(
tab: RecentSyncedTab?, tab: RecentSyncedTab?,

View File

@ -240,6 +240,15 @@ fun RecentTabImage(
modifier = modifier, modifier = modifier,
targetSize = THUMBNAIL_SIZE.dp, targetSize = THUMBNAIL_SIZE.dp,
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
fallback = {
TabThumbnail(
tab = tab.state,
size = LocalDensity.current.run { THUMBNAIL_SIZE.dp.toPx().toInt() },
storage = storage,
modifier = modifier,
contentScale = contentScale,
)
},
) )
} }
else -> TabThumbnail( else -> TabThumbnail(

View File

@ -190,7 +190,6 @@ class AdapterItemDiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
} }
} }
@Suppress("LongParameterList")
class SessionControlAdapter( class SessionControlAdapter(
private val interactor: SessionControlInteractor, private val interactor: SessionControlInteractor,
private val viewLifecycleOwner: LifecycleOwner, private val viewLifecycleOwner: LifecycleOwner,

View File

@ -239,7 +239,7 @@ data class TopSiteColors(
* @param onTopSiteLongClick Invoked when the user long clicks on a top site. * @param onTopSiteLongClick Invoked when the user long clicks on a top site.
* @param onTopSitesItemBound Invoked during the composition of a top site item. * @param onTopSitesItemBound Invoked during the composition of a top site item.
*/ */
@Suppress("LongParameterList", "LongMethod") @Suppress("LongMethod")
@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class) @OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
@Composable @Composable
private fun TopSiteItem( private fun TopSiteItem(
@ -401,7 +401,6 @@ private fun TopSiteFavicon(url: String, imageUrl: String? = null) {
} }
@Composable @Composable
@Suppress("LongParameterList")
private fun getMenuItems( private fun getMenuItems(
topSite: TopSite, topSite: TopSite,
onOpenInPrivateTabClicked: (topSite: TopSite) -> Unit, onOpenInPrivateTabClicked: (topSite: TopSite) -> Unit,

Some files were not shown because too many files have changed in this diff Show More