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 {
applicationId "com.leos"
minSdk = 26
minSdkVersion config.minSdkVersion
compileSdk config.compileSdkVersion
targetSdkVersion config.targetSdkVersion
versionCode 1
@ -63,7 +63,7 @@ android {
// This should be the base URL used to call the AMO API.
buildConfigField "String", "AMO_SERVER_URL", "\"https://services.addons.mozilla.org\""
def deepLinkSchemeValue = "leosium"
def deepLinkSchemeValue = "fenix-dev"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
// This allows overriding the target activity for MozillaOnline builds, which happens
@ -82,6 +82,8 @@ android {
"targetActivity": targetActivity,
"deepLinkScheme": deepLinkSchemeValue
]
buildConfigField "String[]", "SUPPORTED_LOCALE_ARRAY", getSupportedLocales()
}
def releaseTemplate = {
@ -109,14 +111,14 @@ android {
debug {
shrinkResources false
minifyEnabled false
applicationIdSuffix ".leosium.debug"
applicationIdSuffix ".fenix.debug"
resValue "bool", "IS_DEBUG", "true"
pseudoLocalesEnabled true
}
nightly releaseTemplate >> {
applicationIdSuffix ".fenix"
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
def deepLinkSchemeValue = "leosium.debug"
def deepLinkSchemeValue = "fenix-nightly"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders.putAll([
"deepLinkScheme": deepLinkSchemeValue
@ -124,7 +126,7 @@ android {
}
beta releaseTemplate >> {
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
applicationIdSuffix ".leosium.beta"
applicationIdSuffix ".firefox_beta"
def deepLinkSchemeValue = "fenix-beta"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders.putAll([
@ -141,7 +143,7 @@ android {
}
release releaseTemplate >> {
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
applicationIdSuffix ".leosium"
applicationIdSuffix ".firefox"
def deepLinkSchemeValue = "fenix"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders.putAll([
@ -239,7 +241,14 @@ android {
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 {
buildConfigField "boolean", "LEAKCANARY", "false"
}
}
// Generate Kotlin code for the Fenix Glean metrics.
@ -670,13 +678,16 @@ dependencies {
implementation ComponentsDependencies.androidx_fragment
implementation FenixDependencies.androidx_navigation_fragment
implementation FenixDependencies.androidx_navigation_ui
implementation ComponentsDependencies.androidx_compose_navigation
implementation ComponentsDependencies.androidx_recyclerview
implementation ComponentsDependencies.androidx_lifecycle_common
implementation ComponentsDependencies.androidx_lifecycle_livedata
implementation ComponentsDependencies.androidx_lifecycle_process
implementation ComponentsDependencies.androidx_lifecycle_runtime
implementation ComponentsDependencies.androidx_lifecycle_viewmodel
implementation ComponentsDependencies.androidx_lifecycle_service
implementation ComponentsDependencies.androidx_core
implementation ComponentsDependencies.androidx_core_ktx
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 {
// 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.
ext.gleanExpireByVersion = 26

File diff suppressed because it is too large Load Diff

View File

@ -135,12 +135,12 @@ events:
description: |
A string containing the name of the item the user tapped. These items
include:
add_to_homescreen, add_to_top_sites, addons_manager, back, bookmark,
bookmarks, desktop_view_off, desktop_view_on, downloads,
find_in_page, forward, history, new_tab, open_in_app, open_in_fenix,
quit, reader_mode_appearance, reload, remove_from_top_sites,
add_to_homescreen, add_to_top_sites, addons_manager, back, back_long_press,
bookmark, bookmarks, desktop_view_off, desktop_view_on, downloads,
find_in_page, forward, forward_long_press, history, new_tab, open_in_app,
open_in_fenix, quit, reader_mode_appearance, reload, remove_from_top_sites,
save_to_collection, set_default_browser, settings, share, stop,
sync_account, and print_content.
sync_account, translate and print_content.
type: string
bugs:
- https://github.com/mozilla-mobile/fenix/issues/1024
@ -475,6 +475,23 @@ events:
notification_emails:
- android-probes@mozilla.com
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:
type: event
description: |
@ -489,6 +506,22 @@ events:
notification_emails:
- android-probes@mozilla.com
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:
type: event
description: |
@ -506,6 +539,22 @@ events:
metadata:
tags:
- 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:
type: event
description: |

View File

@ -214,14 +214,6 @@ features:
type: Boolean
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:
description: A feature measuring campaign growth data
variables:
@ -341,6 +333,26 @@ features:
type: Map<String, String>
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:
description: A feature that provides Firefox Suggest search suggestions.
variables:
@ -361,6 +373,17 @@ features:
- channel: nightly
value:
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:
objects: {}

View File

@ -25,8 +25,7 @@ features:
card-type: default-browser
title: juno_onboarding_default_browser_title_nimbus_2
ordering: 10
body: juno_onboarding_default_browser_description_nimbus_2
link-text: juno_onboarding_default_browser_description_link_text
body: juno_onboarding_default_browser_description_nimbus_3
image-res: ic_onboarding_welcome
primary-button-label: juno_onboarding_default_browser_positive_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.
# This should never be defaulted.
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:
type: Image
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.
// Otherwise the code should be kept identical
@Suppress("LongParameterList")
private fun interceptFxaRequest(
engineSession: EngineSession,
uri: String,

View File

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

View File

@ -6,19 +6,15 @@ package org.mozilla.fenix.extensions
import android.content.Context
import mozilla.components.concept.engine.EngineSession
import org.json.JSONObject
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mozilla.experiments.nimbus.HardcodedNimbusFeatures
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.gecko.GeckoProvider
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 {
private lateinit var context: Context
@ -27,49 +23,12 @@ class ExtensionProcessTest {
@Before
fun setUp() {
context = TestHelper.appContext
policy =
context.components.core.trackingProtectionPolicyFactory.createTrackingProtectionPolicy()
policy = context.components.core.trackingProtectionPolicyFactory.createTrackingProtectionPolicy()
}
@Test
fun test_extension_process_can_be_enabled_by_nimbus() {
val hardcodedNimbus = HardcodedNimbusFeatures(
context,
"extensions-process" to JSONObject(
"""
{
"enabled": true
}
""".trimIndent(),
),
)
hardcodedNimbus.connectWith(FxNimbus)
fun test_extension_process_is_enabled() {
val runtime = GeckoProvider.createRuntimeSettings(context, policy)
assertTrue(FxNimbus.features.extensionsProcess.value().enabled)
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 the test in its testBlock.
* 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) {
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.
*/

View File

@ -22,6 +22,8 @@ import org.junit.Assert
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.utils.IntentUtils
import java.time.LocalDate
import java.time.LocalTime
object DataGenerationHelper {
val appContext: Context = InstrumentationRegistry.getInstrumentation().targetContext
@ -75,6 +77,28 @@ object DataGenerationHelper {
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.
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,6 @@ package org.mozilla.fenix.ui
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
@ -265,7 +264,6 @@ class AddressAutofillTest {
}
// 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
fun verifyMultipleAddressesSelectionTest() {
val addressFormPage =

View File

@ -14,6 +14,7 @@ import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
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.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
@ -54,6 +55,7 @@ class ComposeNavigationToolbarTest {
@After
fun tearDown() {
mockWebServer.shutdown()
resetSystemLocaleToEnUS()
}
// 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
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import org.junit.Ignore
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
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.TestHelper
import org.mozilla.fenix.ui.robots.navigationToolbar
@ -20,6 +22,7 @@ import org.mozilla.fenix.ui.robots.navigationToolbar
*/
class FirefoxSuggestTest {
@get:Rule
val activityTestRule = AndroidComposeTestRule(
HomeActivityTestRule(
@ -33,91 +36,156 @@ class FirefoxSuggestTest {
),
) { 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
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1874831")
@SmokeTest
@Test
fun verifyFirefoxSuggestSponsoredSearchResultsTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar {
}.clickUrlbar {
typeSearch(searchTerm = "Amazon")
typeSearch(searchTerm = sponsoredKeyWord)
verifySearchEngineSuggestionResults(
rule = activityTestRule,
searchSuggestions = arrayOf(
"Firefox Suggest",
"Amazon.com - Official Site",
sponsoredKeyWords.getValue(sponsoredKeyWord)[0],
"Sponsored",
),
searchTerm = "Amazon",
searchTerm = sponsoredKeyWord,
)
}
}
}
// 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
fun verifyFirefoxSuggestSponsoredSearchResultsWithPartialKeywordTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar {
}.clickUrlbar {
typeSearch(searchTerm = "Amaz")
typeSearch(searchTerm = sponsoredKeyWord.dropLast(1))
verifySearchEngineSuggestionResults(
rule = activityTestRule,
searchSuggestions = arrayOf(
"Firefox Suggest",
"Amazon.com - Official Site",
sponsoredKeyWords.getValue(sponsoredKeyWord)[0],
"Sponsored",
),
searchTerm = "Amaz",
searchTerm = sponsoredKeyWord.dropLast(1),
)
}
}
}
// 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
fun openFirefoxSuggestSponsoredSearchResultsTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar {
}.clickUrlbar {
typeSearch(searchTerm = "Amazon")
typeSearch(searchTerm = sponsoredKeyWord)
verifySearchEngineSuggestionResults(
rule = activityTestRule,
searchSuggestions = arrayOf(
"Firefox Suggest",
"Amazon.com - Official Site",
sponsoredKeyWords.getValue(sponsoredKeyWord)[0],
"Sponsored",
),
searchTerm = "Amazon",
)
}.clickSearchSuggestion("Amazon.com - Official Site") {
waitForPageToLoad()
verifyUrl(
"amazon.com/?tag=admarketus-20&ref=pd_sl_924ab4435c5a5c23aa2804307ee0669ab36f88caee841ce51d1f2ecb&mfadid=adm",
searchTerm = sponsoredKeyWord,
)
}.clickSearchSuggestion(sponsoredKeyWords.getValue(sponsoredKeyWord)[0]) {
verifyUrl(sponsoredKeyWords.getValue(sponsoredKeyWord)[1])
verifyTabCounter("1")
}
}
}
// 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
fun verifyFirefoxSuggestSponsoredSearchResultsWithEditedKeywordTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar {
}.clickUrlbar {
typeSearch(searchTerm = "Amazon")
deleteSearchKeywordCharacters(numberOfDeletionSteps = 3)
typeSearch(searchTerm = sponsoredKeyWord)
deleteSearchKeywordCharacters(numberOfDeletionSteps = 1)
verifySearchEngineSuggestionResults(
rule = activityTestRule,
searchSuggestions = arrayOf(
"Firefox Suggest",
"Amazon.com - Official Site",
sponsoredKeyWords.getValue(sponsoredKeyWord)[0],
"Sponsored",
),
searchTerm = "Amazon",
searchTerm = sponsoredKeyWord,
shouldEditKeyword = true,
numberOfDeletionSteps = 3,
numberOfDeletionSteps = 1,
)
}
}
@ -127,17 +195,17 @@ class FirefoxSuggestTest {
@SmokeTest
@Test
fun verifyFirefoxSuggestNonSponsoredSearchResultsTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar {
}.clickUrlbar {
typeSearch(searchTerm = "Marvel")
typeSearch(searchTerm = nonSponsoredKeyWord)
verifySearchEngineSuggestionResults(
rule = activityTestRule,
searchSuggestions = arrayOf(
"Firefox Suggest",
"Wikipedia - Marvel Cinematic Universe",
nonSponsoredKeyWords.getValue(nonSponsoredKeyWord)[0],
),
searchTerm = "Marvel",
searchTerm = nonSponsoredKeyWord,
)
verifySuggestionsAreNotDisplayed(
rule = activityTestRule,
@ -152,17 +220,17 @@ class FirefoxSuggestTest {
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348375
@Test
fun verifyFirefoxSuggestNonSponsoredSearchResultsWithPartialKeywordTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar {
}.clickUrlbar {
typeSearch(searchTerm = "Marv")
typeSearch(searchTerm = nonSponsoredKeyWord.dropLast(1))
verifySearchEngineSuggestionResults(
rule = activityTestRule,
searchSuggestions = arrayOf(
"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
@Test
fun openFirefoxSuggestNonSponsoredSearchResultsTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar {
}.clickUrlbar {
typeSearch(searchTerm = "Marvel")
typeSearch(searchTerm = nonSponsoredKeyWord)
verifySearchEngineSuggestionResults(
rule = activityTestRule,
searchSuggestions = arrayOf(
"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()
verifyUrl(
"wikipedia.org/wiki/Marvel_Cinematic_Universe",
)
verifyUrl(nonSponsoredKeyWords.getValue(nonSponsoredKeyWord)[1])
}
}
}

View File

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

View File

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

View File

@ -14,6 +14,7 @@ import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.AppAndSystemHelper
import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
@ -50,6 +51,7 @@ class NavigationToolbarTest {
@After
fun tearDown() {
mockWebServer.shutdown()
AppAndSystemHelper.resetSystemLocaleToEnUS()
}
// 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.ext.components
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.denyPermission
import org.mozilla.fenix.helpers.AppAndSystemHelper.grantSystemPermission
@ -92,6 +93,7 @@ class SearchTest {
@After
fun tearDown() {
searchMockServer.shutdown()
AppAndSystemHelper.resetSystemLocaleToEnUS()
}
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154189

View File

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

View File

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

View File

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

View File

@ -14,6 +14,7 @@ import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
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.setSystemLocale
import org.mozilla.fenix.helpers.DataGenerationHelper.setTextToClipBoard
@ -40,7 +41,6 @@ class SettingsSearchTest {
listOf(
"LeOSearch",
"DuckDuckGo",
"Google",
)
@get:Rule
@ -64,6 +64,7 @@ class SettingsSearchTest {
@After
fun tearDown() {
mockWebServer.shutdown()
resetSystemLocaleToEnUS()
}
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2203333
@ -424,6 +425,10 @@ class SettingsSearchTest {
fun verifyShowSearchSuggestionsToggleTest() {
homeScreen {
}.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 ")
verifySearchEngineSuggestionResults(
activityTestRule,
@ -438,6 +443,10 @@ class SettingsSearchTest {
}.goBack {
}.goBack {
}.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")
verifySuggestionsAreNotDisplayed(activityTestRule, "mozilla firefox")
}

View File

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

View File

@ -17,6 +17,7 @@ import androidx.compose.ui.test.performClick
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewInteraction
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.contrib.PickerActions
import androidx.test.espresso.matcher.RootMatchers.isDialog
@ -40,7 +41,6 @@ import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.mozilla.fenix.R
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.TAG
import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
@ -686,23 +686,28 @@ class BrowserRobot {
fun verifyCookieBannerExists(exists: Boolean) {
for (i in 1..RETRY_COUNT) {
Log.i(TAG, "verifyCookieBannerExists: For loop: $i")
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
} catch (e: AssertionError) {
if (i == RETRY_COUNT) {
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() {
assertUIObjectExists(
@ -841,7 +846,7 @@ class BrowserRobot {
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() =
assertUIObjectExists(
@ -1149,6 +1154,7 @@ class BrowserRobot {
"$packageName:id/action",
getStringResource(R.string.open_in_app_cfr_positive_button_text),
).clickAndWaitForNewWindow(waitingTime)
Log.i(TAG, "clickOpenLinksInAppsGoToSettingsCFRButton: Clicked \"Go to settings\" open links in apps CFR button")
SettingsRobot().interact()
return SettingsRobot.Transition()
@ -1302,8 +1308,6 @@ fun clearTextFieldItem(item: UiObject) {
item.clearTextField()
}
private fun cookieBanner() = itemWithResId("startsiden-gdpr-disclaimer")
// Context menu items
// Link URL
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.action.ViewActions
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.isPartiallyBelow
import androidx.test.espresso.assertion.ViewAssertions.matches
@ -51,7 +52,6 @@ import org.junit.Assert
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
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.DataGenerationHelper.getStringResource
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" +
" else who uses this device."
fun verifyNavigationToolbar() = assertUIObjectExists(navigationToolbar)
fun verifyNavigationToolbar() = assertUIObjectExists(navigationToolbar())
fun verifyHomeScreen() = assertUIObjectExists(homeScreen)
fun verifyHomeScreen() = assertUIObjectExists(homeScreen())
fun verifyPrivateBrowsingHomeScreenItems() {
verifyHomeScreenAppBarItems()
@ -97,19 +97,19 @@ class HomeScreenRobot {
}
fun verifyHomeScreenAppBarItems() =
assertUIObjectExists(homeScreen, privateBrowsingButton, homepageWordmark)
assertUIObjectExists(homeScreen(), privateBrowsingButton(), homepageWordmark())
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 verifyTabButton() = assertTabButton()
fun verifyCollectionsHeader() = assertCollectionsHeader()
fun verifyNoCollectionsText() = assertNoCollectionsText()
fun verifyHomeWordmark() {
homeScreenList().scrollToBeginning(3)
assertUIObjectExists(homepageWordmark)
assertUIObjectExists(homepageWordmark())
}
fun verifyHomeComponent() = assertHomeComponent()
@ -140,7 +140,7 @@ class HomeScreenRobot {
).assertExists()
it.onNodeWithText(
getStringResource(R.string.juno_onboarding_default_browser_description_nimbus_2),
getStringResource(R.string.juno_onboarding_default_browser_description_nimbus_3),
).assertExists()
it.onNodeWithText(
@ -292,7 +292,7 @@ class HomeScreenRobot {
mDevice.waitNotNull(findObject(By.text(expectedText)), waitingTime)
}
fun clickFirefoxLogo() = homepageWordmark.click()
fun clickFirefoxLogo() = homepageWordmark().click()
fun verifyThoughtProvokingStories(enabled: Boolean) {
if (enabled) {
@ -481,8 +481,8 @@ class HomeScreenRobot {
}
fun openSearch(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
navigationToolbar.waitForExists(waitingTime)
navigationToolbar.click()
navigationToolbar().waitForExists(waitingTime)
navigationToolbar().click()
mDevice.waitForIdle()
SearchRobot().interact()
@ -502,14 +502,14 @@ class HomeScreenRobot {
fun togglePrivateBrowsingMode(switchPBModeOn: Boolean = true) {
// Switch to private browsing homescreen
if (switchPBModeOn && !isPrivateModeEnabled()) {
privateBrowsingButton.waitForExists(waitingTime)
privateBrowsingButton.click()
privateBrowsingButton().waitForExists(waitingTime)
privateBrowsingButton().click()
}
// Switch to normal browsing homescreen
if (!switchPBModeOn && isPrivateModeEnabled()) {
privateBrowsingButton.waitForExists(waitingTime)
privateBrowsingButton.click()
privateBrowsingButton().waitForExists(waitingTime)
privateBrowsingButton().click()
}
}
@ -521,7 +521,7 @@ class HomeScreenRobot {
waitingTime,
)
privateBrowsingButton.click()
privateBrowsingButton().click()
}
AddToHomeScreenRobot().interact()
@ -535,7 +535,7 @@ class HomeScreenRobot {
fun openNavigationToolbar(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition {
mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
.waitForExists(waitingTime)
navigationToolbar.click()
navigationToolbar().click()
NavigationToolbarRobot().interact()
return NavigationToolbarRobot.Transition()
@ -557,7 +557,8 @@ class HomeScreenRobot {
}
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()
return Transition()
@ -631,8 +632,10 @@ class HomeScreenRobot {
}
fun clickSponsoredShortcutsSettingsButton(interact: SettingsSubMenuHomepageRobot.() -> Unit): SettingsSubMenuHomepageRobot.Transition {
Log.i(TAG, "clickSponsoredShortcutsSettingsButton: Looking for: ${sponsoredShortcutsSettingsButton.selector}")
sponsoredShortcutsSettingsButton.waitForExists(waitingTime)
sponsoredShortcutsSettingsButton.clickAndWaitForNewWindow(waitingTime)
Log.i(TAG, "clickSponsoredShortcutsSettingsButton: Clicked ${sponsoredShortcutsSettingsButton.selector} and waiting for $waitingTime for a new window")
SettingsSubMenuHomepageRobot().interact()
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 sponsoredShortcut(sponsoredShortcutTitle: String) =
mDevice.findObject(
By
.res("$packageName:id/top_site_title")
.textContains(sponsoredShortcutTitle),
onView(
allOf(
withId(R.id.top_site_title),
withText(sponsoredShortcutTitle),
),
)
private fun storyByTopicItem(composeTestRule: ComposeTestRule, position: Int) =
composeTestRule.onNodeWithTag("pocket.categories").onChildAt(position - 1)
private val homeScreen =
private fun homeScreen() =
itemWithResId("$packageName:id/homeLayout")
private val privateBrowsingButton =
private fun privateBrowsingButton() =
itemWithResId("$packageName:id/privateBrowsingButton")
private fun isPrivateModeEnabled(): Boolean =
@ -959,10 +963,10 @@ private fun isPrivateModeEnabled(): Boolean =
"Disable private browsing",
).exists()
private val homepageWordmark =
private fun homepageWordmark() =
itemWithResId("$packageName:id/wordmark")
private val navigationToolbar =
private fun navigationToolbar() =
itemWithResId("$packageName:id/toolbar")
private val 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.IdlingResource
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.matches
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.itemWithResIdAndText
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.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
@ -149,7 +149,7 @@ class NavigationToolbarRobot {
assertTrue(
itemWithResId("$packageName:id/browserLayout").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()
}
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 {
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.")
NavigationToolbarRobot().interact()
@ -388,8 +398,7 @@ private fun awesomeBar() =
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 tabTrayButton() = onView(withId(R.id.tab_button))
private fun tabsCounter() =
mDevice.findObject(By.res("$packageName:id/counter_root"))
private fun tabsCounter() = onView(withId(R.id.mozac_browser_toolbar_browser_actions))
private fun fillLinkButton() = onView(withId(R.id.fill_link_from_clipboard))
private fun clearAddressBarButton() = itemWithResId("$packageName:id/mozac_browser_toolbar_clear_view")
private fun readerViewToggle() =

View File

@ -107,6 +107,27 @@ class SettingsRobot {
fun verifyPrivacyHeading() = assertPrivacyHeading()
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 verifyLoginsAndPasswordsButton() = assertLoginsAndPasswordsButton()
fun verifyPrivateBrowsingButton() = assertPrivateBrowsingButton()
@ -583,6 +604,7 @@ private fun assertOpenLinksInAppsButton() {
scrollToElementByText("Open links in apps")
openLinksInAppsButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
Log.i(TAG, "clickOpenLinksInAppsGoToSettingsCFRButton: Verified \"Open links in apps\" setting option")
}
// ADVANCED SECTION

View File

@ -124,7 +124,8 @@ private fun assertCurrentTimestamp() {
private fun assertWhatIsNewInFirefoxPreview() {
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)))
.perform(click())
}

View File

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

View File

@ -118,7 +118,7 @@ class SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot {
)
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) =
mDevice.waitNotNull(Until.findObjects(By.text(username)))

View File

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

View File

@ -54,7 +54,7 @@
android:name=".FenixApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="LeOSium"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/NormalTheme"
@ -354,6 +354,7 @@
<service
android:name=".downloads.DownloadService"
android:foregroundServiceType="dataSync"
android:exported="false" />
<receiver
@ -374,8 +375,14 @@
</intent-filter>
</receiver>
<service android:name=".session.PrivateNotificationService"
android:exported="false" />
<service
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
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),
FromReviewQualityCheck(R.id.reviewQualityCheckFragment),
FromAddonsManagementFragment(R.id.addonsManagementFragment),
FromTranslationsDialogFragment(R.id.translationsDialogFragment),
}

View File

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

View File

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

View File

@ -29,14 +29,12 @@ import androidx.annotation.CallSuper
import androidx.annotation.IdRes
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.Companion.PROTECTED
import androidx.appcompat.app.ActionBar
import androidx.appcompat.widget.Toolbar
import androidx.core.app.NotificationManagerCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDestination
import androidx.navigation.NavDirections
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
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.SplashScreen
import org.mozilla.fenix.GleanMetrics.StartOnHome
import org.mozilla.fenix.addons.AddonDetailsFragmentDirections
import org.mozilla.fenix.addons.AddonPermissionsDetailsFragmentDirections
import org.mozilla.fenix.addons.AddonsManagementFragmentDirections
import org.mozilla.fenix.addons.ExtensionsProcessDisabledController
import org.mozilla.fenix.addons.ExtensionsProcessDisabledBackgroundController
import org.mozilla.fenix.addons.ExtensionsProcessDisabledForegroundController
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
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.databinding.ActivityHomeBinding
import org.mozilla.fenix.debugsettings.data.DefaultDebugSettingsRepository
import org.mozilla.fenix.debugsettings.ui.DebugOverlay
import org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExceptionsFragmentDirections
import org.mozilla.fenix.debugsettings.ui.FenixOverlay
import org.mozilla.fenix.experiments.ResearchSurfaceDialogFragment
import org.mozilla.fenix.ext.alreadyOnDestination
import org.mozilla.fenix.ext.breadcrumb
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.nav
import org.mozilla.fenix.ext.setNavigationIcon
import org.mozilla.fenix.ext.settings
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.CrashReporterIntentProcessor
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.SpeechProcessingIntentProcessor
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.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.FenixNimbusMessagingController
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.StartupTimeline
import org.mozilla.fenix.perf.StartupTypeTelemetry
import org.mozilla.fenix.search.SearchDialogFragmentDirections
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.tabhistory.TabHistoryDialogFragment
import org.mozilla.fenix.tabstray.TabsTrayFragment
import org.mozilla.fenix.tabstray.TabsTrayFragmentDirections
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.trackingprotection.TrackingProtectionPanelDialogFragmentDirections
import org.mozilla.fenix.utils.Settings
import java.lang.ref.WeakReference
import java.util.Locale
@ -176,7 +153,7 @@ import java.util.Locale
* - home screen
* - browser screen
*/
@SuppressWarnings("TooManyFunctions", "LargeClass", "LongParameterList", "LongMethod")
@SuppressWarnings("TooManyFunctions", "LargeClass", "LongMethod")
open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
// DO NOT MOVE ANYTHING ABOVE THIS, GETTING INIT TIME IS CRITICAL
// 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 {
ExtensionsProcessDisabledController(this@HomeActivity)
private val extensionsProcessDisabledForegroundController by lazy {
ExtensionsProcessDisabledForegroundController(this@HomeActivity)
}
private val extensionsProcessDisabledBackgroundController by lazy {
ExtensionsProcessDisabledBackgroundController(
browserStore = components.core.store,
appStore = components.appStore,
)
}
private val serviceWorkerSupport by lazy {
@ -300,9 +284,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
visibility = View.VISIBLE
setContent {
FirefoxTheme(theme = Theme.getTheme(allowPrivateTheme = false)) {
DebugOverlay()
}
FenixOverlay(
browserStore = components.core.store,
inactiveTabsEnabled = settings().inactiveTabsAreEnabled,
)
}
} else {
setContent {}
@ -348,7 +333,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
// Unless the activity is recreated, navigate to home first (without rendering it)
// to add it to the back stack.
if (savedInstanceState == null) {
navigateToHome()
navigateToHome(navHost.navController)
}
if (!shouldStartOnHome() && shouldNavigateToBrowserOnColdStart(savedInstanceState)) {
@ -392,7 +377,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
lifecycle.addObservers(
webExtensionPopupObserver,
extensionsProcessDisabledPromptObserver,
extensionsProcessDisabledForegroundController,
extensionsProcessDisabledBackgroundController,
serviceWorkerSupport,
webExtensionPromptFeature,
)
@ -699,7 +685,12 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
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:
// https://github.com/mozilla-mobile/android-components/issues/7960
breadcrumb(
@ -886,20 +877,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
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
* 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
* value of [searchTermOrURL]).
@ -1003,7 +978,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
* was opened from history.
* @param additionalHeaders The extra headers to use when loading the URL.
*/
@Suppress("LongParameterList")
fun openToBrowserAndLoad(
searchTermOrURL: String,
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]).
*
@ -1194,7 +1109,12 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
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
// 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) {
@ -1203,8 +1123,13 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
}
}
open fun navigateToHome() {
navHost.navController.navigate(NavGraphDirections.actionStartupHome())
@VisibleForTesting
internal fun navigateToHome(navController: NavController) {
if (this is ExternalAppBrowserActivity) {
return
}
navController.navigate(NavGraphDirections.actionStartupHome())
}
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
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.widget.Button
import android.widget.TextView
@ -20,35 +18,30 @@ import mozilla.components.support.webextensions.ExtensionsProcessDisabledPromptO
import org.mozilla.fenix.R
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.ext.components
import kotlin.system.exitProcess
/**
* 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
* process spawning or disable extensions). When in background, we kill the app to prevent
* extensions from being disabled and network requests continuing.
* process spawning or disable extensions).
*
* @param context to show the AlertDialog
* @param browserStore The [BrowserStore] which holds the state for showing the dialog
* @param appStore The [AppStore] containing the application state
* @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 onKillApp called when the app is backgrounded and extensions process is disabled
*/
class ExtensionsProcessDisabledController(
class ExtensionsProcessDisabledForegroundController(
@UiContext context: Context,
browserStore: BrowserStore = context.components.core.store,
appStore: AppStore = context.components.appStore,
builder: AlertDialog.Builder = AlertDialog.Builder(context),
appName: String = context.appName,
onKillApp: () -> Unit = { killApp() },
) : ExtensionsProcessDisabledPromptObserver(
browserStore,
store = browserStore,
shouldCancelOnStop = true,
{
if (appStore.state.isForeground) {
presentDialog(context, browserStore, builder, appName)
} else {
onKillApp.invoke()
}
},
) {
@ -61,16 +54,6 @@ class ExtensionsProcessDisabledController(
companion object {
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
* 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.Bundle
import android.provider.Settings
import android.util.Log
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
@ -31,9 +32,11 @@ import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.distinctUntilChangedBy
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.engine.permission.SitePermissions
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.concept.storage.LoginEntry
import mozilla.components.feature.accounts.FxaCapability
import mozilla.components.feature.accounts.FxaWebChannelFeature
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.DialogColorsProvider
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.readerview.ReaderViewFeature
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.sync.autofill.DefaultCreditCardValidationDelegate
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.PermissionsFeature
import mozilla.components.support.base.feature.UserInteractionHandler
@ -108,6 +115,7 @@ import mozilla.components.support.locale.ActivityContextWrapper
import mozilla.components.ui.widgets.withCenterAlignedButtons
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.MediaState
import org.mozilla.fenix.GleanMetrics.PullToRefreshInBrowser
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.wifi.SitePermissionsWifiIntegration
import java.lang.ref.WeakReference
import kotlin.coroutines.cancellation.CancellationException
import mozilla.components.feature.session.behavior.ToolbarPosition as MozacToolbarPosition
/**
@ -460,6 +469,7 @@ abstract class BaseBrowserFragment :
browserToolbarView.view.display.setOnSiteSecurityClickedListener {
showQuickSettingsDialog()
Events.browserToolbarSecurityIndicatorTapped.record()
}
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 {
override val creditCardPickerView
get() = binding.creditCardSelectBar
@ -894,9 +916,12 @@ abstract class BaseBrowserFragment :
binding.swipeRefresh.isEnabled = shouldPullToRefreshBeEnabled(false)
if (binding.swipeRefresh.isEnabled) {
val primaryTextColor =
ThemeManager.resolveAttribute(R.attr.textPrimary, context)
binding.swipeRefresh.setColorSchemeColors(primaryTextColor)
val primaryTextColor = ThemeManager.resolveAttribute(R.attr.textPrimary, context)
val primaryBackgroundColor = ThemeManager.resolveAttribute(R.attr.layer2, context)
binding.swipeRefresh.apply {
setColorSchemeResources(primaryTextColor)
setProgressBackgroundColorSchemeResource(primaryBackgroundColor)
}
swipeRefreshFeature.set(
feature = SwipeRefreshFeature(
requireComponents.core.store,
@ -1651,4 +1676,38 @@ abstract class BaseBrowserFragment :
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 actionToPerform The action to be performed on action button press.
*/
@Suppress("LongParameterList")
class DynamicInfoBanner(
private val context: Context,
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.
* @param actionToPerform The action to be performed on action button press.
*/
@SuppressWarnings("LongParameterList")
open class InfoBanner(
private val context: Context,
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.
*/
val privateIntentProcessor by lazyMonitored {
TabIntentProcessor(tabsUseCases, searchUseCases.newTabSearch, isPrivate = true)
TabIntentProcessor(tabsUseCases, searchUseCases.newPrivateTabSearch, isPrivate = true)
}
val customTabIntentProcessor by lazyMonitored {

View File

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

View File

@ -406,6 +406,14 @@ class DefaultBrowserToolbarMenuController(
.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) {
when (item) {
is ToolbarMenu.Item.OpenInFenix ->
@ -433,10 +441,19 @@ class DefaultBrowserToolbarMenuController(
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("open_in_app"))
is ToolbarMenu.Item.CustomizeReaderView ->
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"))
}
}
is ToolbarMenu.Item.Forward ->
if (item.viewHistory) {
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("forward_long_press"))
} else {
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("forward"))
}
is ToolbarMenu.Item.Reload ->
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("reload"))
is ToolbarMenu.Item.Stop ->
@ -483,6 +500,12 @@ class DefaultBrowserToolbarMenuController(
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("set_default_browser"))
is ToolbarMenu.Item.RemoveFromTopSites ->
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.nimbus.FxNimbus
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.
@ -55,7 +54,7 @@ import org.mozilla.fenix.utils.Settings
* @param pinnedSiteStorage Used to check if the current url is a pinned site.
* @property isPinningSupported true if the launcher supports adding shortcuts.
*/
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
@Suppress("LargeClass", "TooManyFunctions")
open class DefaultToolbarMenu(
private val context: Context,
private val store: BrowserStore,
@ -193,6 +192,14 @@ open class DefaultToolbarMenu(
fun shouldShowReaderViewCustomization(): Boolean = selectedSession?.let {
store.state.findTab(it.id)?.readerState?.active
} ?: false
/**
* Should Translations menu item be visible?
*/
@VisibleForTesting(otherwise = PRIVATE)
fun shouldShowTranslations(): Boolean = selectedSession?.let {
context.settings().enableTranslations
} ?: false
// End of predicates //
private val installToHomescreen = BrowserMenuHighlightableItem(
@ -248,6 +255,14 @@ open class DefaultToolbarMenu(
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(
imageResource = R.drawable.ic_desktop,
label = context.getString(R.string.browser_menu_desktop_site),
@ -405,6 +420,7 @@ open class DefaultToolbarMenu(
syncMenuItem(),
BrowserMenuDivider(),
findInPageItem,
translationsItem.apply { visible = ::shouldShowTranslations },
desktopSiteItem,
openInRegularTabItem.apply { visible = ::shouldShowOpenInRegularTab },
customizeReaderView.apply { visible = ::shouldShowReaderViewCustomization },

View File

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

View File

@ -18,6 +18,11 @@ interface ToolbarMenu {
*/
object OpenInRegularTab : Item()
object FindInPage : Item()
/**
* Opens the translations flow.
*/
object Translate : Item()
object Share : Item()
data class Back(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].
*/
@Composable
@Suppress("LongParameterList")
fun Image(
url: String,
modifier: Modifier = Modifier,

View File

@ -7,10 +7,22 @@ package org.mozilla.fenix.compose
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.background
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.material.Card
import androidx.compose.material.Text
import androidx.compose.material.TextButton
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.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.semantics
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.TextDecoration
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
/**
@ -67,6 +82,12 @@ fun LinkText(
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
// 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
@ -76,12 +97,17 @@ fun LinkText(
style = style,
modifier = Modifier.semantics(mergeDescendants = true) {
onClick {
if (linkTextStates.size > 1) {
showDialog.value = true
} else {
linkTextStates.firstOrNull()?.let {
it.onClick(it.url)
}
}
return@onClick true
}
contentDescription = "$annotatedString $linksAvailable"
},
onClick = { charOffset ->
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
internal fun onTextClick(
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.
*
* @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 onCheckedChange Invoked when Switch is being clicked, therefore the change of checked
* state is requested.
@ -44,6 +45,7 @@ private const val DISABLED_ALPHA = 0.5f
@Composable
fun SwitchWithLabel(
label: String,
description: String? = null,
checked: Boolean,
onCheckedChange: ((Boolean) -> Unit),
modifier: Modifier = Modifier,
@ -59,6 +61,10 @@ fun SwitchWithLabel(
),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(
modifier = Modifier
.weight(1f),
) {
Text(
text = label,
@ -68,9 +74,17 @@ fun SwitchWithLabel(
FirefoxTheme.colors.textDisabled
},
style = FirefoxTheme.typography.subtitle1,
modifier = Modifier.weight(1f),
)
description?.let {
Text(
text = description,
color = FirefoxTheme.colors.textSecondary,
style = FirefoxTheme.typography.body2,
)
}
}
Switch(
modifier = Modifier.clearAndSetSemantics {},
checked = checked,
@ -139,6 +153,7 @@ private fun SwitchWithLabelPreview() {
var enabledSwitchState by remember { mutableStateOf(false) }
SwitchWithLabel(
label = if (enabledSwitchState) "On" else "Off",
description = "Description text",
checked = enabledSwitchState,
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.
*/
@Composable
@Suppress("LongParameterList")
fun TabThumbnail(
tab: TabSessionState,
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.
*/
@Composable
@Suppress("LongParameterList")
fun ThumbnailImage(
request: ImageLoadRequest,
storage: ThumbnailStorage,

View File

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

View File

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

View File

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

View File

@ -5,24 +5,16 @@
package org.mozilla.fenix.customtabs
import android.app.assist.AssistContent
import android.content.Intent
import android.net.Uri
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import androidx.navigation.NavDestination
import androidx.navigation.NavDirections
import mozilla.components.browser.state.selector.findCustomTab
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 org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.NavGraphDirections
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"
@ -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() {
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.WebAppHideToolbarFeature
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.ktx.android.arch.lifecycle.addObservers
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.
*/
class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
class ExternalAppBrowserFragment : BaseBrowserFragment() {
private val args by navArgs<ExternalAppBrowserFragmentArgs>()
@ -212,9 +211,4 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
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
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
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.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.material.Text
import androidx.compose.material.rememberDrawerState
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.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.LayoutDirection
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.compose.annotation.LightDarkPreview
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
/**
* 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
fun DebugOverlay() {
fun DebugOverlay(
navController: NavHostController,
drawerStatus: DrawerStatus,
debugDrawerDestinations: List<DebugDrawerDestination>,
onDrawerOpen: () -> Unit,
onDrawerClose: () -> Unit,
onDrawerBackButtonClick: () -> Unit,
) {
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(
modifier = Modifier.fillMaxSize(),
@ -41,12 +86,41 @@ fun DebugOverlay() {
.align(Alignment.CenterStart)
.padding(start = 16.dp),
onClick = {
scope.launch {
snackbarState.showSnackbar("Show debug drawer")
}
onDrawerOpen()
},
)
// 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
SnackbarHost(
hostState = snackbarState,
@ -62,13 +136,41 @@ fun DebugOverlay() {
@Composable
@LightDarkPreview
private fun DebugOverlayPreview() {
FirefoxTheme {
Box(
modifier = Modifier
.fillMaxSize()
.background(color = FirefoxTheme.colors.layer1),
) {
DebugOverlay()
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 {
DebugOverlay(
navController = navController,
drawerStatus = drawerStatus,
debugDrawerDestinations = destinations,
onDrawerOpen = {
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 = {
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) {

View File

@ -15,12 +15,47 @@ import androidx.annotation.DrawableRes
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
import androidx.navigation.NavDestination
import androidx.navigation.NavDirections
import mozilla.components.concept.base.crash.Breadcrumb
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.HomeActivity
import org.mozilla.fenix.NavGraphDirections
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.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.
@ -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 SETTINGS_SELECT_OPTION_KEY = ":settings:fragment_args_key"
const val SETTINGS_SHOW_FRAGMENT_ARGS = ":settings:show_fragment_args"
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.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.geckoview.ContentBlocking
import org.mozilla.geckoview.ContentBlocking.SafeBrowsingProvider
import org.mozilla.geckoview.GeckoRuntime
@ -134,7 +133,7 @@ object GeckoProvider {
.consoleOutput(context.components.settings.enableGeckoLogs)
.debugLogging(Config.channel.isDebug || context.components.settings.enableGeckoLogs)
.aboutConfigEnabled(true)
.extensionsProcessEnabled(FxNimbus.features.extensionsProcess.value().enabled)
.extensionsProcessEnabled(true)
.extensionsWebAPIEnabled(true)
.build()
}

View File

@ -49,7 +49,9 @@ import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import mozilla.components.browser.state.selector.findTab
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.nimbus.FxNimbus
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.SearchSelectorMenu
import org.mozilla.fenix.tabstray.TabsTrayAccessPoint
@ -1007,17 +1008,18 @@ class HomeFragment : Fragment() {
lastAppliedWallpaperName = wallpaperName
}
else -> {
runBlockingIncrement {
viewLifecycleOwner.lifecycleScope.launch {
// loadBitmap does file lookups based on name, so we don't need a fully
// qualified type to load the image
val wallpaper = Wallpaper.Default.copy(name = wallpaperName)
val wallpaperImage =
requireComponents.useCases.wallpaperUseCases.loadBitmap(wallpaper)
context?.let { requireComponents.useCases.wallpaperUseCases.loadBitmap(it, wallpaper) }
wallpaperImage?.let {
it.scaleToBottomOfView(binding.wallpaperImageView)
binding.wallpaperImageView.isVisible = true
lastAppliedWallpaperName = wallpaperName
} ?: run {
if (!isActive) return@run
with(binding.wallpaperImageView) {
isVisible = false
showSnackBar(
@ -1051,10 +1053,14 @@ class HomeFragment : Fragment() {
}
private fun observeWallpaperUpdates() {
consumeFrom(requireComponents.appStore) {
val currentWallpaper = it.wallpaperState.currentWallpaper
if (currentWallpaper.name != lastAppliedWallpaperName) {
applyWallpaper(wallpaperName = currentWallpaper.name, orientationChange = false)
consumeFlow(requireComponents.appStore, viewLifecycleOwner) { flow ->
flow.filter { it.mode == BrowsingMode.Normal }
.map { it.wallpaperState.currentWallpaper }
.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.
* @param fxaEntrypoint The source entry point to FxA.
*/
@Suppress("LongParameterList")
class HomeMenuView(
private val view: View,
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
*/
@OptIn(ExperimentalComposeUiApi::class)
@Suppress("LongParameterList", "LongMethod")
@Suppress("LongMethod")
@Composable
fun PocketStories(
@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.
*/
@OptIn(ExperimentalComposeUiApi::class)
@Suppress("LongParameterList")
@Composable
fun PocketStoriesCategories(
categories: List<PocketRecommendedStoriesCategory>,

View File

@ -5,7 +5,6 @@
package org.mozilla.fenix.home.recentbookmarks.view
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.isSystemInDarkTheme
@ -42,10 +41,10 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import mozilla.components.browser.icons.compose.Loader
import mozilla.components.browser.icons.compose.Placeholder
import mozilla.components.browser.icons.compose.WithIcon
import mozilla.components.ui.colors.PhotonColors
import org.mozilla.fenix.components.components
import org.mozilla.fenix.compose.ContextualMenu
import org.mozilla.fenix.compose.Favicon
import org.mozilla.fenix.compose.Image
import org.mozilla.fenix.compose.MenuItem
import org.mozilla.fenix.compose.annotation.LightDarkPreview
@ -172,6 +171,11 @@ private fun RecentBookmarkImage(bookmark: RecentBookmark) {
modifier = imageModifier,
targetSize = imageWidth,
contentScale = ContentScale.Crop,
fallback = {
if (!bookmark.url.isNullOrEmpty()) {
FallbackBookmarkFaviconImage(url = bookmark.url)
}
},
)
}
!bookmark.url.isNullOrEmpty() && !inComposePreview -> {
@ -180,23 +184,7 @@ private fun RecentBookmarkImage(bookmark: RecentBookmark) {
PlaceholderBookmarkImage()
}
WithIcon { icon ->
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,
)
}
}
FallbackBookmarkFaviconImage(bookmark.url)
}
}
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
@LightDarkPreview
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.
*/
@OptIn(ExperimentalFoundationApi::class)
@Suppress("LongMethod", "LongParameterList")
@Suppress("LongMethod")
@Composable
fun RecentSyncedTab(
tab: RecentSyncedTab?,

View File

@ -240,6 +240,15 @@ fun RecentTabImage(
modifier = modifier,
targetSize = THUMBNAIL_SIZE.dp,
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(

View File

@ -190,7 +190,6 @@ class AdapterItemDiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
}
}
@Suppress("LongParameterList")
class SessionControlAdapter(
private val interactor: SessionControlInteractor,
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 onTopSitesItemBound Invoked during the composition of a top site item.
*/
@Suppress("LongParameterList", "LongMethod")
@Suppress("LongMethod")
@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
@Composable
private fun TopSiteItem(
@ -401,7 +401,6 @@ private fun TopSiteFavicon(url: String, imageUrl: String? = null) {
}
@Composable
@Suppress("LongParameterList")
private fun getMenuItems(
topSite: TopSite,
onOpenInPrivateTabClicked: (topSite: TopSite) -> Unit,

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