Merge tag 'v85.1.1' into upstream-sync

pull/274/head
Adam Novak 3 years ago
commit 6d4a47921c

@ -26,26 +26,24 @@ There are two releases this covers: the current sprint that is going out to Beta
## Start of Sprint X.1 [Thursday, 1st week of sprint] ## Start of Sprint X.1 [Thursday, 1st week of sprint]
- [ ] [Create an issue](https://github.com/mozilla-mobile/fenix/issues/new?template=release_checklist.md&title=Releng+for+) "Releng for v[release]" to track the current sprint. - [ ] [Create an issue](https://github.com/mozilla-mobile/fenix/issues/new?template=release_checklist.md&title=Releng+for+) "Releng for v[release]" to track the current sprint.
## Sprint X.1 End [Wednesday, 2nd week] Cutting a Beta ## Sprint X.1 End [Wednesday, 2nd week] Cutting a Beta
- [ ] Make a new Beta - [ ] Make a new Beta
- [ ] Create a branch off of master (DO NOT PUSH YET) for the *current* milestone of format `releases/v85.0.0`. After that, anything landing in master will be part of the next release. - [ ] Create a branch off of master (DO NOT PUSH YET) for the *current* milestone of format `releases_v85.0.0`. After that, anything landing in master will be part of the next release.
- ⚠️ Please **do not** use `/` in branch names anymore: Taskcluster silently ignores them and doesn't output tasks at the right index.
- [ ] Bump `version.txt` to match the new version number
- [ ] Grant [mozilla-release-automation-bot](https://github.com/mozilla-release-automation-bot) write access to this branch.
- [ ] On the new Beta branch, pin the AC version to the stable version ([example](https://github.com/mozilla-mobile/fenix/commit/e413da29f6a7a7d4a765817a9cd5687abbf27619)) with commit message "Issue #`<this releng issue>`: Pin to stable AC `<version>` for release v85" - [ ] On the new Beta branch, pin the AC version to the stable version ([example](https://github.com/mozilla-mobile/fenix/commit/e413da29f6a7a7d4a765817a9cd5687abbf27619)) with commit message "Issue #`<this releng issue>`: Pin to stable AC `<version>` for release v85"
- [ ] Update the title to include this AC version "Releng for v[release] with AC [version]" - [ ] Update the title to include this AC version "Releng for v[release] with AC [version]"
- Note: You will need code review to make changes to the release branch after this point, because it is a protected branch. - Note: You will need code review to make changes to the release branch after this point, because it is a protected branch.
- [ ] Push the branch. - [ ] Push the branch.
- [ ] Create a GitHub pre-release [Release](https://github.com/mozilla-mobile/fenix/releases) with: - [ ] Tell Release Management the first beta is ready to be shipped. They'll use https://shipit.mozilla-releng.net/new to kick off a new release. This replaces the process that involved GitHub releases. Now Github releases are automatically created by [mozilla-release-automation-bot](https://github.com/mozilla-release-automation-bot)
- [ ] Tag of the format `vX.X.X-beta.1` (v85.0.0-beta.1)
- [ ] The Target branch is the release branch (releases/v85.0.0)
- [ ] For the description of the release, look at the [Jira boards](https://jira.mozilla.com/secure/RapidBoard.jspa?rapidView=299&projectKey=FNX&view=reporting&chart=sprintRetrospective&sprint=883) for the X.1 and previous Y.2 sprints and list the major features that were added. This will help with the release notes later on.
- [ ] Click "Publish release". This will kick off a build of the branch. You can see it in the mouseover of the CI badge of the branch in the commits view. Builds are found under `signing-*` task.
- If you need to trigger a new RC build, you **MUST** draft and publish a new (pre-release) release (optionally deleting both the release and the tag). Editing an existing release and creating a new tag will **not** trigger a new build.
- [ ] Send an email to QA at mozilla-mobile-qa@softvision.com with a link to the Taskcluster build (subdirectory of the [Fenix CI](https://firefox-ci-tc.services.mozilla.com/tasks/index/mobile.v2.fenix.beta)) - [ ] Send an email to QA at mozilla-mobile-qa@softvision.com with a link to the Taskcluster build (subdirectory of the [Fenix CI](https://firefox-ci-tc.services.mozilla.com/tasks/index/mobile.v2.fenix.beta))
### Bugfix uplifts / Beta Product Integrity (Beta Release until PI green signoff) ### Bugfix uplifts / Beta Product Integrity (Beta Release until PI green signoff)
- [ ] If bugs are considered release blocker then find someone to fix them on master and the milestone branch (cherry-pick / uplift) - [ ] If bugs are considered release blocker then find someone to fix them on master and the milestone branch (cherry-pick / uplift)
- [ ] Add the uplift request to the appropriate row in the [Uplifts document](https://docs.google.com/spreadsheets/d/1qIvHpcQ3BqJtlzV5T4M1MhbWVxkNiG-ToeYnWEBW4-I/edit#gid=0). - [ ] Add the uplift request to the appropriate row in the [Uplifts document](https://docs.google.com/spreadsheets/d/1qIvHpcQ3BqJtlzV5T4M1MhbWVxkNiG-ToeYnWEBW4-I/edit#gid=0).
- [ ] If needed tag a new beta version (e.g. v1.0-beta.2) and follow the submission checklist again. - [ ] If needed, ship a new beta version (e.g. v1.0-beta.2) and follow the submission checklist again.
- [ ] Once there is GREEN QA signoff, file a [release management bugzilla for rollout](https://bugzilla.mozilla.org/show_bug.cgi?id=1664366) - [ ] Once there is GREEN QA signoff, file a [release management bugzilla for rollout](https://bugzilla.mozilla.org/show_bug.cgi?id=1664366)
- [ ] Check Sentry each day for issues on [Firefox Beta](https://sentry.prod.mozaws.net/operations/firefox-beta/) and if nothing concerning, bump release in the bugzilla (5%, 20%, 100%) - [ ] Check Sentry each day for issues on [Firefox Beta](https://sentry.prod.mozaws.net/operations/firefox-beta/) and if nothing concerning, bump release in the bugzilla (5%, 20%, 100%)
@ -59,18 +57,13 @@ There are two releases this covers: the current sprint that is going out to Beta
### Production Release Candidate [Tuesday, 3 weeks after X.1 Beta was cut] ### Production Release Candidate [Tuesday, 3 weeks after X.1 Beta was cut]
- [ ] In android-components: Create a dot release with the GeckoView Production release candidate. - [ ] In android-components: Create a dot release with the GeckoView Production release candidate.
- [ ] Open a PR against the release branch (releases/v85.0.0) with the AC version bump "Pin to stable AC `<version>` for release v85`. You will need code review. - [ ] Open a PR against the release branch (releases/v85.0.0) with the AC version bump "Pin to stable AC `<version>` for release v85`. You will need code review.
- [ ] Create a GitHub pre-release [Release](https://github.com/mozilla-mobile/fenix/releases) with: - [ ] Modify `version.txt` to now follow the pattern: `vX.X.X-rc.1` (e.g.: v85.0.0-rc.1)
- [ ] Tag of the format `vX.X.X-rc.1` (v85.0.0-rc.1) - [ ] Tell Release Management you would like to ship a new release.
- [ ] The Target branch is the release branch (releases/v85.0.0)
- [ ] For the description, copy the beta description
- [ ] Send an email to QA at mozilla-mobile-qa@softvision.com with a link to the Taskcluster build (subdirectory of the [Fenix CI](https://firefox-ci-tc.services.mozilla.com/tasks/index/mobile.v2.fenix.release)) - [ ] Send an email to QA at mozilla-mobile-qa@softvision.com with a link to the Taskcluster build (subdirectory of the [Fenix CI](https://firefox-ci-tc.services.mozilla.com/tasks/index/mobile.v2.fenix.release))
### Production Release [Release day, from [release calendar](https://docs.google.com/spreadsheets/d/1HotjliSCGOp2nTkfXrxv8qYcurNpkqLWBKbbId6ovTY/edit#gid=0)] ### Production Release [Release day, from [release calendar](https://docs.google.com/spreadsheets/d/1HotjliSCGOp2nTkfXrxv8qYcurNpkqLWBKbbId6ovTY/edit#gid=0)]
- [ ] Create a GitHub [Release](https://github.com/mozilla-mobile/fenix/releases) with: - [ ] Modify `version.txt` to now follow the pattern: `vX.1.X` (e.g.: v85.1.0)
- [ ] Tag of the format `vX.1.X` (v85.1.0) (increment the minor version for new cuts) - [ ] File Bugzilla ticket for [release management](https://bugzilla.mozilla.org/show_bug.cgi?id=1672212)
- [ ] The Target branch is the release branch (releases/v85.0.0)
- [ ] For the description, copy the beta description
- [ ] file Bugzilla ticket for [release manament](https://bugzilla.mozilla.org/show_bug.cgi?id=1672212)
- [ ] Check Sentry for new crashes. File issues and triage. - [ ] Check Sentry for new crashes. File issues and triage.
- [ ] Each day, bump the release rollout if nothing concerning (5%, 20%, 100%) - [ ] Each day, bump the release rollout if nothing concerning (5%, 20%, 100%)

4
.gitignore vendored

@ -79,12 +79,12 @@ gen-external-apklibs
# macOS # macOS
.DS_Store .DS_Store
# Token files # Secrets files, e.g. tokens
.leanplum_token .leanplum_token
.adjust_token .adjust_token
.sentry_token .sentry_token
.mls_token .mls_token
.nimbus
# Python Byte-compiled / optimized / DLL files # Python Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/

@ -0,0 +1,59 @@
pull_request_rules:
- name: Resolve conflict
conditions:
- conflict
actions:
comment:
message: This pull request has conflicts when rebasing. Could you fix it @{{author}}? 🙏
- name: MickeyMoz - Auto Merge
conditions:
- author=MickeyMoz
- status-success=pr-complete
- files~=(Gecko.kt|AndroidComponents.kt)
actions:
review:
type: APPROVE
message: MickeyMoz 💪
merge:
method: rebase
strict: smart
- name: L10N - Auto Merge
conditions:
- author=mozilla-l10n-automation-bot
- status-success=pr-complete
- files~=(strings.xml)
actions:
review:
type: APPROVE
message: LGTM 😎
merge:
method: rebase
strict: smart
- name: Release automation
conditions:
- base~=releases/.*
- author=github-actions[bot]
# Listing checks manually beause we do not have a "push complete" check yet.
- check-success=build-android-test-debug
- check-success=build-debug
- check-success=build-nightly-simulation
- check-success=lint-compare-locales
- check-success=lint-detekt
- check-success=lint-ktlint
- check-success=lint-lint
- check-success=signing-android-test-debug
- check-success=signing-debug
- check-success=signing-nightly-simulation
- check-success=test-debug
# TODO Temporarily disabled - should be renamed to -build
#- check-success=ui-test-x86-debug
- files~=(AndroidComponents.kt)
actions:
review:
type: APPROVE
message: 🚢
merge:
method: rebase
strict: smart
delete_head_branch:
force: false

@ -8,7 +8,7 @@ tasks:
- $let: - $let:
taskgraph: taskgraph:
branch: taskgraph branch: taskgraph
revision: 2b2622598df02bde211d8cedb334b7b22fb883a4 revision: a458418ef7cdd6778f1283926c6116966255bc24
trustDomain: mobile trustDomain: mobile
in: in:
$let: $let:

@ -19,6 +19,13 @@ import static org.gradle.api.tasks.testing.TestResult.ResultType
android { android {
compileSdkVersion Config.compileSdkVersion compileSdkVersion Config.compileSdkVersion
if (project.hasProperty("testBuildType")) {
// Allowing to configure the test build type via command line flag (./gradlew -PtestBuildType=beta ..)
// in order to run UI tests against other build variants than debug in automation.
testBuildType project.property("testBuildType")
}
defaultConfig { defaultConfig {
applicationId "io.github.forkmaintainers" applicationId "io.github.forkmaintainers"
minSdkVersion Config.minSdkVersion minSdkVersion Config.minSdkVersion
@ -47,8 +54,10 @@ android {
} }
def releaseTemplate = { def releaseTemplate = {
shrinkResources true // We allow disabling optimization by passing `-PdisableOptimization` to gradle. This is used
minifyEnabled true // in automation for UI testing non-debug builds.
shrinkResources !project.hasProperty("disableOptimization")
minifyEnabled !project.hasProperty("disableOptimization")
proguardFiles 'proguard-android-optimize-3.5.0-modified.txt', 'proguard-rules.pro' proguardFiles 'proguard-android-optimize-3.5.0-modified.txt', 'proguard-rules.pro'
matchingFallbacks = ['release'] // Use on the "release" build type in dependencies (AARs) matchingFallbacks = ['release'] // Use on the "release" build type in dependencies (AARs)
@ -361,6 +370,30 @@ android.applicationVariants.all { variant ->
buildConfigField 'String', 'MLS_TOKEN', '""' buildConfigField 'String', 'MLS_TOKEN', '""'
println("X_X") println("X_X")
} }
// -------------------------------------------------------------------------------------------------
// Nimbus: Read endpoint from local.properties of a local file if it exists
// -------------------------------------------------------------------------------------------------
print("Nimbus endpoint: ")
if (!isDebug) {
try {
def url = new File("${rootDir}/.nimbus").text.trim()
buildConfigField 'String', 'NIMBUS_ENDPOINT', '"' + url + '"'
println "(Added from .nimbus file)"
} catch (FileNotFoundException ignored) {
buildConfigField 'String', 'NIMBUS_ENDPOINT', 'null'
println("X_X")
}
} else if (gradle.hasProperty("localProperties.nimbus.remote-settings.url")) {
def url=gradle.getProperty("localProperties.nimbus.remote-settings.url")
buildConfigField 'String', 'NIMBUS_ENDPOINT', '"' + url + '"'
println "(Added from local.properties file)"
} else {
buildConfigField 'String', 'NIMBUS_ENDPOINT', 'null'
println("--")
}
} }
androidExtensions { androidExtensions {
@ -474,11 +507,11 @@ dependencies {
implementation Deps.mozilla_feature_webcompat_reporter implementation Deps.mozilla_feature_webcompat_reporter
implementation Deps.mozilla_service_digitalassetlinks implementation Deps.mozilla_service_digitalassetlinks
implementation Deps.mozilla_service_experiments
implementation Deps.mozilla_service_sync_logins implementation Deps.mozilla_service_sync_logins
implementation Deps.mozilla_service_firefox_accounts implementation Deps.mozilla_service_firefox_accounts
implementation Deps.mozilla_service_glean implementation Deps.mozilla_service_glean
implementation Deps.mozilla_service_location implementation Deps.mozilla_service_location
implementation Deps.mozilla_service_nimbus
implementation Deps.mozilla_support_base implementation Deps.mozilla_support_base
implementation Deps.mozilla_support_images implementation Deps.mozilla_support_images
@ -493,6 +526,7 @@ dependencies {
implementation Deps.mozilla_ui_icons implementation Deps.mozilla_ui_icons
implementation Deps.mozilla_lib_publicsuffixlist implementation Deps.mozilla_lib_publicsuffixlist
implementation Deps.mozilla_ui_widgets implementation Deps.mozilla_ui_widgets
implementation Deps.mozilla_ui_tabcounter
implementation Deps.mozilla_lib_crash implementation Deps.mozilla_lib_crash
implementation Deps.mozilla_lib_dataprotect implementation Deps.mozilla_lib_dataprotect
@ -549,6 +583,7 @@ dependencies {
androidTestImplementation Deps.androidx_work_testing androidTestImplementation Deps.androidx_work_testing
androidTestImplementation Deps.mockwebserver androidTestImplementation Deps.mockwebserver
testImplementation Deps.mozilla_support_test testImplementation Deps.mozilla_support_test
testImplementation Deps.mozilla_support_test_libstate
testImplementation Deps.androidx_junit testImplementation Deps.androidx_junit
testImplementation Deps.androidx_work_testing testImplementation Deps.androidx_work_testing
testImplementation (Deps.robolectric) { testImplementation (Deps.robolectric) {

@ -48,17 +48,6 @@
column="6"/> column="6"/>
</issue> </issue>
<issue
id="MozMultipleConstraintLayouts"
message="Flatten the view hierarchy by using one `ConstraintLayout`, if possible. If the alternative is several nested `ViewGroup`, it may not help performance and this may be worth suppressing."
errorLine1=" &lt;androidx.constraintlayout.widget.ConstraintLayout"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/component_tabstray.xml"
line="52"
column="6"/>
</issue>
<issue <issue
id="MozMultipleConstraintLayouts" id="MozMultipleConstraintLayouts"
message="Flatten the view hierarchy by using one `ConstraintLayout`, if possible. If the alternative is several nested `ViewGroup`, it may not help performance and this may be worth suppressing." message="Flatten the view hierarchy by using one `ConstraintLayout`, if possible. If the alternative is several nested `ViewGroup`, it may not help performance and this may be worth suppressing."
@ -114,17 +103,6 @@
column="14"/> column="14"/>
</issue> </issue>
<issue
id="MozMultipleConstraintLayouts"
message="Flatten the view hierarchy by using one `ConstraintLayout`, if possible. If the alternative is several nested `ViewGroup`, it may not help performance and this may be worth suppressing."
errorLine1=" &lt;androidx.constraintlayout.widget.ConstraintLayout"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/fragment_search_dialog.xml"
line="77"
column="6"/>
</issue>
<issue <issue
id="MozMultipleConstraintLayouts" id="MozMultipleConstraintLayouts"
message="Flatten the view hierarchy by using one `ConstraintLayout`, if possible. If the alternative is several nested `ViewGroup`, it may not help performance and this may be worth suppressing." message="Flatten the view hierarchy by using one `ConstraintLayout`, if possible. If the alternative is several nested `ViewGroup`, it may not help performance and this may be worth suppressing."

@ -60,5 +60,7 @@
<issue id="TooDeepLayout" severity="warning" /> <!-- depth can be customized --> <issue id="TooDeepLayout" severity="warning" /> <!-- depth can be customized -->
<issue id="TooManyViews" severity="warning" /> <!-- view count can be customized --> <issue id="TooManyViews" severity="warning" /> <!-- view count can be customized -->
<!-- Correctness: checks with increased severity -->
<issue id="PrivateResource" severity="error" />
</lint> </lint>

@ -331,6 +331,47 @@ events:
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-04-01" expires: "2021-04-01"
synced_tab_opened:
type: event
description: >
An event that indicates that a synced tab was opened.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/15369
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/16727
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-05-10"
recently_closed_tabs_opened:
type: event
description: |
An event that indicates that the user
has accessed recently closed tabs list.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/15366
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/16739
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-05-10"
copy_url_tapped:
type: event
description: |
An event that indicates that a user has selected
copy option when long pressing on url bar.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/16827
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/16915
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-05-10"
onboarding: onboarding:
fxa_auto_signin: fxa_auto_signin:
@ -702,6 +743,82 @@ metrics:
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: never expires: never
mobile_bookmarks_count:
type: counter
lifetime: application
description: |
A counter that indicates how many bookmarks a user has in the mobile
folder. This value will only be set if the user has at least *one*
bookmark. If they have 0, this ping will not get sent, resulting in
a null value. To disambiguate between a failed `mobile_bookmarks_count`
ping and 0 bookmarks, please see `has_mobile_bookmarks`.
send_in_pings:
- metrics
bugs:
- https://github.com/mozilla-mobile/fenix/issues/16941
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/16942
- https://github.com/mozilla-mobile/fenix/pull/16942#issuecomment-742794701
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"
has_mobile_bookmarks:
type: boolean
lifetime: application
description: |
A boolean that indicates if the user has bookmarks in the mobile folder
send_in_pings:
- metrics
bugs:
- https://github.com/mozilla-mobile/fenix/issues/16941
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/16942
- https://github.com/mozilla-mobile/fenix/pull/16942#issuecomment-742794701
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"
desktop_bookmarks_count:
type: counter
lifetime: application
description: |
A counter that indicates how many bookmarks a user has in the desktop
folder. This value will only be set if the user has at least *one*
bookmark. If they have 0, this ping will not get sent, resulting in a
null value. To disambiguate between a failed `desktop_bookmarks_count`
ping and 0 bookmarks, please see `has_desktop_bookmarks`.
send_in_pings:
- metrics
bugs:
- https://github.com/mozilla-mobile/fenix/issues/16941
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/16942
- https://github.com/mozilla-mobile/fenix/pull/16942#issuecomment-742794701
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"
has_desktop_bookmarks:
type: boolean
lifetime: application
description: |
A boolean that indicates if the user has bookmarks in the desktop folder
send_in_pings:
- metrics
bugs:
- https://github.com/mozilla-mobile/fenix/issues/16941
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/16942
- https://github.com/mozilla-mobile/fenix/pull/16942#issuecomment-742794701
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"
top_sites_count: top_sites_count:
type: counter type: counter
lifetime: application lifetime: application
@ -2887,6 +3004,32 @@ media_state:
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-08-01" expires: "2021-08-01"
fullscreen:
type: event
description: |
Video set to fullscreen.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/15368
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/16833
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"
picture_in_picture:
type: event
description: |
Video set to picture in picture mode.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/15368
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/16833
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"
logins: logins:
open_logins: open_logins:
@ -3109,6 +3252,58 @@ download_notification:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-04-01" expires: "2021-04-01"
downloads_misc:
download_added:
type: event
description:
A counter for how many times something is downloaded in the app.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/11578
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/16730
notification_emails:
- fenix-core@mozilla.com
expires: "2021-04-01"
downloads_management:
downloads_screen_opened:
type: event
description: >
A counter for the number of times users access the "Downloads" folder
inside the app.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/15367
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/16728
notification_emails:
- fenix-core@mozilla.com
expires: "2021-04-01"
item_opened:
type: event
description: >
A counter for how often a user tap to opens a download from inside the
"Downloads" folder.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/15367
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/16728
notification_emails:
- fenix-core@mozilla.com
expires: "2021-04-01"
item_deleted:
type: event
description: >
A counter for how often a user deletes one / more downloads at a time.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/15367
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/16728
notification_emails:
- fenix-core@mozilla.com
expires: "2021-04-01"
user_specified_search_engines: user_specified_search_engines:
custom_engine_added: custom_engine_added:
type: event type: event

@ -119,4 +119,4 @@
# Keep Android Lifecycle methods # Keep Android Lifecycle methods
# https://bugzilla.mozilla.org/show_bug.cgi?id=1596302 # https://bugzilla.mozilla.org/show_bug.cgi?id=1596302
-keep class androidx.lifecycle.** { *; } -keep class androidx.lifecycle.** { *; }

@ -5,18 +5,26 @@
package org.mozilla.fenix package org.mozilla.fenix
import android.content.Context import android.content.Context
import androidx.navigation.NavController
import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.request.RequestInterceptor import mozilla.components.concept.engine.request.RequestInterceptor
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ui.robots.appContext import org.mozilla.fenix.ui.robots.appContext
import java.lang.ref.WeakReference
/** /**
* This class overrides the application's request interceptor to * This class overrides the application's request interceptor to
* deactivate the FxA web channel * deactivate the FxA web channel
* which is not supported on the staging servers. * which is not supported on the staging servers.
*/ */
class AppRequestInterceptor(private val context: Context) : RequestInterceptor { class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
private var navController: WeakReference<NavController>? = null
fun setNavigationController(navController: NavController) {
this.navController = WeakReference(navController)
}
override fun onLoadRequest( override fun onLoadRequest(
engineSession: EngineSession, engineSession: EngineSession,
uri: String, uri: String,

@ -9,7 +9,7 @@ class RecyclerViewIdlingResource constructor(private val recycler: androidx.recy
private var callback: ResourceCallback? = null private var callback: ResourceCallback? = null
override fun isIdleNow(): Boolean { override fun isIdleNow(): Boolean {
if (recycler.adapter != null && recycler.adapter!!.itemCount > minItemCount) { if (recycler.adapter != null && recycler.adapter!!.itemCount >= minItemCount) {
if (callback != null) { if (callback != null) {
callback!!.onTransitionToIdle() callback!!.onTransitionToIdle()
} }

@ -32,6 +32,9 @@ import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.ui.robots.mDevice import org.mozilla.fenix.ui.robots.mDevice
object TestHelper { object TestHelper {
val packageName = InstrumentationRegistry.getInstrumentation().targetContext.packageName
fun scrollToElementByText(text: String): UiScrollable { fun scrollToElementByText(text: String): UiScrollable {
val appView = UiScrollable(UiSelector().scrollable(true)) val appView = UiScrollable(UiSelector().scrollable(true))
appView.scrollTextIntoView(text) appView.scrollTextIntoView(text)

@ -74,7 +74,7 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
selectFolder("Desktop Bookmarks") selectFolder("Desktop Bookmarks")
@ -112,7 +112,7 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
verifyBookmarkedURL(defaultWebPage.url.toString()) verifyBookmarkedURL(defaultWebPage.url.toString())
@ -126,7 +126,7 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
clickAddFolderButton() clickAddFolderButton()
@ -159,7 +159,7 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
}.openThreeDotMenu(defaultWebPage.url) { }.openThreeDotMenu(defaultWebPage.url) {
}.clickEdit { }.clickEdit {
@ -185,7 +185,7 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
}.openThreeDotMenu(defaultWebPage.url) { }.openThreeDotMenu(defaultWebPage.url) {
}.clickCopy { }.clickCopy {
@ -202,7 +202,7 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
}.openThreeDotMenu(defaultWebPage.url) { }.openThreeDotMenu(defaultWebPage.url) {
}.clickShare { }.clickShare {
@ -222,7 +222,7 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
}.openThreeDotMenu(defaultWebPage.url) { }.openThreeDotMenu(defaultWebPage.url) {
}.clickOpenInNewTab { }.clickOpenInNewTab {
@ -241,7 +241,7 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
}.openThreeDotMenu(defaultWebPage.url) { }.openThreeDotMenu(defaultWebPage.url) {
}.clickOpenInPrivateTab { }.clickOpenInPrivateTab {
@ -260,7 +260,7 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
}.openThreeDotMenu(defaultWebPage.url) { }.openThreeDotMenu(defaultWebPage.url) {
IdlingRegistry.getInstance().unregister(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().unregister(bookmarksListIdlingResource!!)
@ -279,13 +279,17 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
}.openThreeDotMenu(defaultWebPage.url) { }.openThreeDotMenu(defaultWebPage.url) {
IdlingRegistry.getInstance().unregister(bookmarksListIdlingResource!!)
}.clickDelete { }.clickDelete {
verifyUndoDeleteSnackBarButton() verifyUndoDeleteSnackBarButton()
clickUndoDeleteButton() clickUndoDeleteButton()
verifySnackBarHidden() verifySnackBarHidden()
bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
verifyBookmarkedURL(defaultWebPage.url.toString()) verifyBookmarkedURL(defaultWebPage.url.toString())
} }
} }
@ -299,7 +303,7 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
longTapSelectItem(defaultWebPage.url) longTapSelectItem(defaultWebPage.url)
@ -329,7 +333,7 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
longTapSelectItem(defaultWebPage.url) longTapSelectItem(defaultWebPage.url)
@ -352,7 +356,7 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
longTapSelectItem(defaultWebPage.url) longTapSelectItem(defaultWebPage.url)
@ -377,7 +381,7 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 3)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
longTapSelectItem(firstWebPage.url) longTapSelectItem(firstWebPage.url)
@ -404,7 +408,7 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
longTapSelectItem(defaultWebPage.url) longTapSelectItem(defaultWebPage.url)
@ -460,7 +464,7 @@ class BookmarksTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
}.openThreeDotMenu(defaultWebPage.url) { }.openThreeDotMenu(defaultWebPage.url) {
IdlingRegistry.getInstance().unregister(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().unregister(bookmarksListIdlingResource!!)

@ -84,7 +84,7 @@ class HistoryTest {
}.openHistory { }.openHistory {
verifyHistoryListExists() verifyHistoryListExists()
historyListIdlingResource = historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1)
IdlingRegistry.getInstance().register(historyListIdlingResource!!) IdlingRegistry.getInstance().register(historyListIdlingResource!!)
verifyHistoryMenuView() verifyHistoryMenuView()
verifyVisitedTimeTitle() verifyVisitedTimeTitle()
@ -104,7 +104,7 @@ class HistoryTest {
}.openHistory { }.openHistory {
verifyHistoryListExists() verifyHistoryListExists()
historyListIdlingResource = historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1)
IdlingRegistry.getInstance().register(historyListIdlingResource!!) IdlingRegistry.getInstance().register(historyListIdlingResource!!)
}.openThreeDotMenu { }.openThreeDotMenu {
}.clickCopy { }.clickCopy {
@ -123,7 +123,7 @@ class HistoryTest {
}.openHistory { }.openHistory {
verifyHistoryListExists() verifyHistoryListExists()
historyListIdlingResource = historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1)
IdlingRegistry.getInstance().register(historyListIdlingResource!!) IdlingRegistry.getInstance().register(historyListIdlingResource!!)
}.openThreeDotMenu { }.openThreeDotMenu {
}.clickShare { }.clickShare {
@ -145,7 +145,7 @@ class HistoryTest {
}.openHistory { }.openHistory {
verifyHistoryListExists() verifyHistoryListExists()
historyListIdlingResource = historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1)
IdlingRegistry.getInstance().register(historyListIdlingResource!!) IdlingRegistry.getInstance().register(historyListIdlingResource!!)
}.openThreeDotMenu { }.openThreeDotMenu {
}.clickOpenInNormalTab { }.clickOpenInNormalTab {
@ -166,7 +166,7 @@ class HistoryTest {
}.openHistory { }.openHistory {
verifyHistoryListExists() verifyHistoryListExists()
historyListIdlingResource = historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1)
IdlingRegistry.getInstance().register(historyListIdlingResource!!) IdlingRegistry.getInstance().register(historyListIdlingResource!!)
}.openThreeDotMenu { }.openThreeDotMenu {
}.clickOpenInPrivateTab { }.clickOpenInPrivateTab {
@ -187,7 +187,7 @@ class HistoryTest {
}.openHistory { }.openHistory {
verifyHistoryListExists() verifyHistoryListExists()
historyListIdlingResource = historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1)
IdlingRegistry.getInstance().register(historyListIdlingResource!!) IdlingRegistry.getInstance().register(historyListIdlingResource!!)
}.openThreeDotMenu { }.openThreeDotMenu {
IdlingRegistry.getInstance().unregister(historyListIdlingResource!!) IdlingRegistry.getInstance().unregister(historyListIdlingResource!!)
@ -208,7 +208,7 @@ class HistoryTest {
}.openHistory { }.openHistory {
verifyHistoryListExists() verifyHistoryListExists()
historyListIdlingResource = historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1)
IdlingRegistry.getInstance().register(historyListIdlingResource!!) IdlingRegistry.getInstance().register(historyListIdlingResource!!)
clickDeleteHistoryButton() clickDeleteHistoryButton()
IdlingRegistry.getInstance().unregister(historyListIdlingResource!!) IdlingRegistry.getInstance().unregister(historyListIdlingResource!!)
@ -230,7 +230,7 @@ class HistoryTest {
}.openHistory { }.openHistory {
verifyHistoryListExists() verifyHistoryListExists()
historyListIdlingResource = historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1)
IdlingRegistry.getInstance().register(historyListIdlingResource!!) IdlingRegistry.getInstance().register(historyListIdlingResource!!)
longTapSelectItem(firstWebPage.url) longTapSelectItem(firstWebPage.url)
} }
@ -260,7 +260,7 @@ class HistoryTest {
}.openHistory { }.openHistory {
verifyHistoryListExists() verifyHistoryListExists()
historyListIdlingResource = historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1)
IdlingRegistry.getInstance().register(historyListIdlingResource!!) IdlingRegistry.getInstance().register(historyListIdlingResource!!)
longTapSelectItem(firstWebPage.url) longTapSelectItem(firstWebPage.url)
openActionBarOverflowOrOptionsMenu(activityTestRule.activity) openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
@ -284,7 +284,7 @@ class HistoryTest {
}.openHistory { }.openHistory {
verifyHistoryListExists() verifyHistoryListExists()
historyListIdlingResource = historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1)
IdlingRegistry.getInstance().register(historyListIdlingResource!!) IdlingRegistry.getInstance().register(historyListIdlingResource!!)
longTapSelectItem(firstWebPage.url) longTapSelectItem(firstWebPage.url)
openActionBarOverflowOrOptionsMenu(activityTestRule.activity) openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
@ -311,7 +311,7 @@ class HistoryTest {
}.openHistory { }.openHistory {
verifyHistoryListExists() verifyHistoryListExists()
historyListIdlingResource = historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 2)
IdlingRegistry.getInstance().register(historyListIdlingResource!!) IdlingRegistry.getInstance().register(historyListIdlingResource!!)
longTapSelectItem(firstWebPage.url) longTapSelectItem(firstWebPage.url)
longTapSelectItem(secondWebPage.url) longTapSelectItem(secondWebPage.url)
@ -339,7 +339,7 @@ class HistoryTest {
}.openHistory { }.openHistory {
verifyHistoryListExists() verifyHistoryListExists()
historyListIdlingResource = historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1)
IdlingRegistry.getInstance().register(historyListIdlingResource!!) IdlingRegistry.getInstance().register(historyListIdlingResource!!)
longTapSelectItem(firstWebPage.url) longTapSelectItem(firstWebPage.url)
} }

@ -10,7 +10,6 @@ import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.MockWebServer
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.FenixApplication import org.mozilla.fenix.FenixApplication
@ -105,25 +104,6 @@ class SettingsBasicsTest {
} }
} }
@Ignore("This test works locally, fails on firebase. https://github.com/mozilla-mobile/fenix/issues/8174")
@Test
fun toggleSearchSuggestions() {
// Goes through the settings and changes the search suggestion toggle, then verifies it changes.
homeScreen {
}.openNavigationToolbar {
verifySearchSuggestionsAreMoreThan(1, "mozilla")
}.goBack {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
disableShowSearchSuggestions()
}.goBack {
}.goBack {
}.openNavigationToolbar {
verifySearchSuggestionsAreEqualTo(0, "mozilla")
}
}
@Test @Test
fun toggleShowVisitedSitesAndBookmarks() { fun toggleShowVisitedSitesAndBookmarks() {
// Bookmarks a few websites, toggles the history and bookmarks setting to off, then verifies if the visited and bookmarked websites do not show in the suggestions. // Bookmarks a few websites, toggles the history and bookmarks setting to off, then verifies if the visited and bookmarked websites do not show in the suggestions.

@ -174,7 +174,7 @@ class SettingsPrivacyTest {
verifyDefaultValueAutofillLogins() verifyDefaultValueAutofillLogins()
verifyDefaultValueExceptions() verifyDefaultValueExceptions()
}.openSavedLogins { }.openSavedLogins {
verifySavedLoginsView() verifySecurityPromptForLogins()
tapSetupLater() tapSetupLater()
// Verify that logins list is empty // Verify that logins list is empty
// Issue #7272 nothing is shown // Issue #7272 nothing is shown
@ -205,7 +205,7 @@ class SettingsPrivacyTest {
verifyDefaultView() verifyDefaultView()
verifyDefaultValueSyncLogins() verifyDefaultValueSyncLogins()
}.openSavedLogins { }.openSavedLogins {
verifySavedLoginsView() verifySecurityPromptForLogins()
tapSetupLater() tapSetupLater()
// Verify that the login appears correctly // Verify that the login appears correctly
verifySavedLoginFromPrompt() verifySavedLoginFromPrompt()
@ -230,7 +230,7 @@ class SettingsPrivacyTest {
verifyDefaultView() verifyDefaultView()
verifyDefaultValueSyncLogins() verifyDefaultValueSyncLogins()
}.openSavedLogins { }.openSavedLogins {
verifySavedLoginsView() verifySecurityPromptForLogins()
tapSetupLater() tapSetupLater()
// Verify that the login list is empty // Verify that the login list is empty
verifyNotSavedLoginFromPrompt() verifyNotSavedLoginFromPrompt()

@ -4,7 +4,9 @@
package org.mozilla.fenix.ui package org.mozilla.fenix.ui
import androidx.core.net.toUri import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.IdlingRegistry
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.MockWebServer
@ -12,9 +14,13 @@ import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.RecyclerViewIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource
import org.mozilla.fenix.ui.robots.clickUrlbar import org.mozilla.fenix.ui.robots.clickUrlbar
import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar import org.mozilla.fenix.ui.robots.navigationToolbar
@ -27,6 +33,17 @@ import org.mozilla.fenix.ui.robots.navigationToolbar
class SmokeTest { class SmokeTest {
private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
private lateinit var mockWebServer: MockWebServer private lateinit var mockWebServer: MockWebServer
private var awesomeBar: ViewVisibilityIdlingResource? = null
private var searchSuggestionsIdlingResource: RecyclerViewIdlingResource? = null
// This finds the dialog fragment child of the homeFragment, otherwise the awesomeBar would return null
private fun getAwesomebarView(): View? {
val homeFragment = activityTestRule.activity.supportFragmentManager.primaryNavigationFragment
val searchDialogFragment = homeFragment?.childFragmentManager?.fragments?.first {
it.javaClass.simpleName == "SearchDialogFragment"
}
return searchDialogFragment?.view?.findViewById(R.id.awesome_bar)
}
@get:Rule @get:Rule
val activityTestRule = HomeActivityTestRule() val activityTestRule = HomeActivityTestRule()
@ -94,6 +111,15 @@ class SmokeTest {
} }
} }
@Test
fun startBrowsingButtonTest() {
homeScreen {
verifyStartBrowsingButton()
}.clickStartBrowsingButton {
verifySearchView()
}
}
@Test @Test
fun verifyBasicNavigationToolbarFunctionality() { fun verifyBasicNavigationToolbarFunctionality() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -117,32 +143,71 @@ class SmokeTest {
@Test @Test
fun verifyPageMainMenuItemsTest() { fun verifyPageMainMenuItemsTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
// Add this to check openInApp and youtube is a default app available in every Android emulator/device
val youtubeUrl = "www.youtube.com"
navigationToolbar { navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) { }.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu { }.openThreeDotMenu {
verifyThreeDotMainMenuItems() verifyThreeDotMainMenuItems()
}
}
// Could be removed when more smoke tests from the History category are added
@Test
fun openMainMenuHistoryItemTest() {
homeScreen {
}.openThreeDotMenu {
}.openHistory { }.openHistory {
verifyHistoryMenuView() verifyHistoryMenuView()
}.goBackToBrowser { }
}
// Could be removed when more smoke tests from the Bookmarks category are added
@Test
fun openMainMenuBookmarksItemTest() {
homeScreen {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
verifyBookmarksMenuView() verifyBookmarksMenuView()
}.goBackToBrowser { }
}
@Test
fun openMainMenuSyncedTabsItemTest() {
homeScreen {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openSyncedTabs { }.openSyncedTabs {
verifySyncedTabsMenuHeader() verifySyncedTabsMenuHeader()
}.goBack { }
}
// Could be removed when more smoke tests from the Settings category are added
@Test
fun openMainMenuSettingsItemTest() {
homeScreen {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openSettings { }.openSettings {
verifySettingsView() verifySettingsView()
}.goBackToBrowser { }
}
@Test
fun openMainMenuFindInPageTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openFindInPage { }.openFindInPage {
verifyFindInPageSearchBarItems() verifyFindInPageSearchBarItems()
}.closeFindInPage { }
}
@Test
fun openMainMenuAddTopSiteTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu { }.openThreeDotMenu {
}.addToFirefoxHome { }.addToFirefoxHome {
verifySnackBarText("Added to top sites!") verifySnackBarText("Added to top sites!")
@ -150,32 +215,73 @@ class SmokeTest {
}.openNewTab { }.openNewTab {
}.dismissSearchBar { }.dismissSearchBar {
verifyExistingTopSitesTabs(defaultWebPage.title) verifyExistingTopSitesTabs(defaultWebPage.title)
}.openTabDrawer { }
}.openTab(defaultWebPage.title) { }
@Test
fun mainMenuAddToHomeScreenTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openAddToHomeScreen { }.openAddToHomeScreen {
verifyShortcutNameField(defaultWebPage.title) verifyShortcutNameField(defaultWebPage.title)
clickAddShortcutButton() clickAddShortcutButton()
clickAddAutomaticallyButton() clickAddAutomaticallyButton()
}.openHomeScreenShortcut(defaultWebPage.title) { }.openHomeScreenShortcut(defaultWebPage.title) {
verifyPageContent(defaultWebPage.content)
}
}
@Test
fun openMainMenuAddToCollectionTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openSaveToCollection { }.openSaveToCollection {
verifyCollectionNameTextField() verifyCollectionNameTextField()
}.exitSaveCollection { }
}
@Test
fun mainMenuBookmarkButtonTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu { }.openThreeDotMenu {
}.bookmarkPage { }.bookmarkPage {
verifySnackBarText("Bookmark saved!") verifySnackBarText("Bookmark saved!")
}
}
@Test
fun mainMenuShareButtonTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu { }.openThreeDotMenu {
}.sharePage { }.sharePage {
verifyShareAppsLayout() verifyShareAppsLayout()
}.closeShareDialogReturnToPage { }
}
@Test
fun mainMenuRefreshButtonTest() {
val refreshWebPage = TestAssetHelper.getRefreshAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(refreshWebPage.url) {
mDevice.waitForIdle()
}.openThreeDotMenu { }.openThreeDotMenu {
verifyThreeDotMenuExists()
verifyRefreshButton()
}.refreshPage { }.refreshPage {
verifyUrl(defaultWebPage.url.toString()) verifyPageContent("REFRESHED")
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(youtubeUrl.toUri()) {
}.openThreeDotMenu {
verifyOpenInAppButton()
} }
} }
@ -201,7 +307,6 @@ class SmokeTest {
}.goBackToBrowser { }.goBackToBrowser {
clickEnhancedTrackingProtectionPanel() clickEnhancedTrackingProtectionPanel()
verifyEnhancedTrackingProtectionSwitch() verifyEnhancedTrackingProtectionSwitch()
// Turning off TP Switch results in adding the WebPage to exception list
clickEnhancedTrackingProtectionSwitchOffOn() clickEnhancedTrackingProtectionSwitchOffOn()
} }
} }
@ -214,7 +319,7 @@ class SmokeTest {
homeScreen { homeScreen {
}.openSearch { }.openSearch {
verifyKeyboardVisibility() verifyKeyboardVisibility()
clickSearchEngineButton() clickSearchEngineShortcutButton()
verifySearchEngineList() verifySearchEngineList()
changeDefaultSearchEngine("Amazon.com") changeDefaultSearchEngine("Amazon.com")
verifySearchEngineIcon("Amazon.com") verifySearchEngineIcon("Amazon.com")
@ -222,7 +327,7 @@ class SmokeTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) { }.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openTabDrawer { }.openTabDrawer {
}.openNewTab { }.openNewTab {
clickSearchEngineButton() clickSearchEngineShortcutButton()
mDevice.waitForIdle() mDevice.waitForIdle()
changeDefaultSearchEngine("Bing") changeDefaultSearchEngine("Bing")
verifySearchEngineIcon("Bing") verifySearchEngineIcon("Bing")
@ -230,7 +335,7 @@ class SmokeTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) { }.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openTabDrawer { }.openTabDrawer {
}.openNewTab { }.openNewTab {
clickSearchEngineButton() clickSearchEngineShortcutButton()
mDevice.waitForIdle() mDevice.waitForIdle()
changeDefaultSearchEngine("DuckDuckGo") changeDefaultSearchEngine("DuckDuckGo")
verifySearchEngineIcon("DuckDuckGo") verifySearchEngineIcon("DuckDuckGo")
@ -238,7 +343,7 @@ class SmokeTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) { }.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openTabDrawer { }.openTabDrawer {
}.openNewTab { }.openNewTab {
clickSearchEngineButton() clickSearchEngineShortcutButton()
changeDefaultSearchEngine("Wikipedia") changeDefaultSearchEngine("Wikipedia")
verifySearchEngineIcon("Wikipedia") verifySearchEngineIcon("Wikipedia")
}.goToSearchEngine { }.goToSearchEngine {
@ -254,4 +359,135 @@ class SmokeTest {
} }
} }
} }
@Test
fun addPredefinedSearchEngineTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
openAddSearchEngineMenu()
verifyAddSearchEngineList()
addNewSearchEngine("YouTube")
verifyEngineListContains("YouTube")
}.goBack {
}.goBack {
}.openSearch {
verifyKeyboardVisibility()
clickSearchEngineShortcutButton()
verifyEnginesListShortcutContains("YouTube")
}
}
@Test
fun toggleSearchSuggestions() {
// Goes through the settings and changes the search suggestion toggle, then verifies it changes.
homeScreen {
}.openNavigationToolbar {
typeSearchTerm("mozilla")
val awesomeBarView = getAwesomebarView()
awesomeBarView?.let {
awesomeBar = ViewVisibilityIdlingResource(it, View.VISIBLE)
}
IdlingRegistry.getInstance().register(awesomeBar!!)
searchSuggestionsIdlingResource =
RecyclerViewIdlingResource(awesomeBarView as RecyclerView, 1)
IdlingRegistry.getInstance().register(searchSuggestionsIdlingResource!!)
verifySearchSuggestionsAreMoreThan(0)
IdlingRegistry.getInstance().unregister(searchSuggestionsIdlingResource!!)
}.goBack {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
disableShowSearchSuggestions()
}.goBack {
}.goBack {
}.openNavigationToolbar {
typeSearchTerm("mozilla")
searchSuggestionsIdlingResource =
RecyclerViewIdlingResource(getAwesomebarView() as RecyclerView)
IdlingRegistry.getInstance().register(searchSuggestionsIdlingResource!!)
verifySearchSuggestionsAreEqualTo(0)
IdlingRegistry.getInstance().unregister(searchSuggestionsIdlingResource!!)
}
}
@Test
fun swipeToSwitchTabTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
}.openTabDrawer {
}.openNewTab {
}.submitQuery(secondWebPage.url.toString()) {
swipeNavBarRight(secondWebPage.url.toString())
verifyPageContent(firstWebPage.content)
swipeNavBarLeft(firstWebPage.url.toString())
verifyPageContent(secondWebPage.content)
}
}
@Test
fun updateSavedLoginTest() {
val saveLoginTest =
TestAssetHelper.getSaveLoginAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(saveLoginTest.url) {
verifySaveLoginPromptIsShown()
// Click Save to save the login
saveLoginFromPrompt("Save")
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(saveLoginTest.url) {
enterPassword("test")
verifyUpdateLoginPromptIsShown()
// Click Update to change the saved password
saveLoginFromPrompt("Update")
}.openThreeDotMenu {
}.openSettings {
TestHelper.scrollToElementByText("Logins and passwords")
}.openLoginsAndPasswordSubMenu {
}.openSavedLogins {
verifySecurityPromptForLogins()
tapSetupLater()
// Verify that the login appears correctly
verifySavedLoginFromPrompt()
viewSavedLoginDetails()
revealPassword()
verifyPasswordSaved("test")
}
}
@Test
fun redirectToAppPermissionsSystemSettingsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuSitePermissions {
}.openCamera {
verifyBlockedByAndroid()
}.goBack {
}.openLocation {
verifyBlockedByAndroid()
}.goBack {
}.openMicrophone {
verifyBlockedByAndroid()
clickGoToSettingsButton()
openAppSystemPermissionsSettings()
switchAppPermissionSystemSetting("Camera")
switchAppPermissionSystemSetting("Location")
switchAppPermissionSystemSetting("Microphone")
mDevice.pressBack()
mDevice.pressBack()
verifyUnblockedByAndroid()
}.goBack {
}.openLocation {
verifyUnblockedByAndroid()
}.goBack {
}.openCamera {
verifyUnblockedByAndroid()
}
}
} }

@ -35,6 +35,7 @@ import org.junit.Assert.assertEquals
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.helpers.ext.waitNotNull
@ -216,7 +217,7 @@ class BookmarksRobot {
} }
fun openThreeDotMenu(bookmarkTitle: String, interact: ThreeDotMenuBookmarksRobot.() -> Unit): ThreeDotMenuBookmarksRobot.Transition { fun openThreeDotMenu(bookmarkTitle: String, interact: ThreeDotMenuBookmarksRobot.() -> Unit): ThreeDotMenuBookmarksRobot.Transition {
mDevice.waitNotNull(Until.findObject(res("org.mozilla.fenix.debug:id/overflow_menu"))) mDevice.waitNotNull(Until.findObject(res("$packageName:id/overflow_menu")))
threeDotMenu(bookmarkTitle).click() threeDotMenu(bookmarkTitle).click()
ThreeDotMenuBookmarksRobot().interact() ThreeDotMenuBookmarksRobot().interact()

@ -9,6 +9,7 @@ package org.mozilla.fenix.ui.robots
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.widget.EditText
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.pressBack import androidx.test.espresso.Espresso.pressBack
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
@ -38,12 +39,15 @@ import org.junit.Assert.assertTrue
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.SessionLoadedIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.helpers.ext.waitNotNull
class BrowserRobot { class BrowserRobot {
private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource
fun verifyCurrentPrivateSession(context: Context) { fun verifyCurrentPrivateSession(context: Context) {
val session = context.components.core.sessionManager.selectedSession val session = context.components.core.sessionManager.selectedSession
assertTrue("Current session is private", session?.private!!) assertTrue("Current session is private", session?.private!!)
@ -51,13 +55,17 @@ class BrowserRobot {
fun verifyUrl(url: String) { fun verifyUrl(url: String) {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
sessionLoadedIdlingResource = SessionLoadedIdlingResource()
mDevice.waitNotNull( mDevice.waitNotNull(
Until.findObject(By.res("org.mozilla.fenix.debug:id/mozac_browser_toolbar_url_view")), Until.findObject(By.res("$packageName:id/mozac_browser_toolbar_url_view")),
waitingTime waitingTime
) )
TestAssetHelper.waitingTime
onView(withId(R.id.mozac_browser_toolbar_url_view)) runWithIdleRes(sessionLoadedIdlingResource) {
.check(matches(withText(containsString(url.replace("http://", ""))))) onView(withId(R.id.mozac_browser_toolbar_url_view))
.check(matches(withText(containsString(url.replace("http://", "")))))
}
} }
fun verifyHelpUrl() { fun verifyHelpUrl() {
@ -78,11 +86,16 @@ class BrowserRobot {
*/ */
fun verifyPageContent(expectedText: String) { fun verifyPageContent(expectedText: String) {
sessionLoadedIdlingResource = SessionLoadedIdlingResource()
mDevice.waitNotNull( mDevice.waitNotNull(
Until.findObject(By.res("org.mozilla.fenix.debug:id/engineView")), Until.findObject(By.res("$packageName:id/engineView")),
waitingTime waitingTime
) )
assertTrue(mDevice.findObject(UiSelector().text(expectedText)).waitForExists(waitingTime))
runWithIdleRes(sessionLoadedIdlingResource) {
assertTrue(mDevice.findObject(UiSelector().textContains(expectedText)).waitForExists(waitingTime))
}
} }
fun verifyTabCounter(expectedText: String) { fun verifyTabCounter(expectedText: String) {
@ -312,17 +325,36 @@ class BrowserRobot {
} }
fun verifySaveLoginPromptIsShown() { fun verifySaveLoginPromptIsShown() {
mDevice.waitNotNull(Until.findObjects(text("test@example.com")), waitingTime) mDevice.findObject(UiSelector().text("test@example.com")).waitForExists(waitingTime)
val submitButton = mDevice.findObject(By.res("submit")) val submitButton = mDevice.findObject(By.res("submit"))
submitButton.clickAndWait(Until.newWindow(), waitingTime) submitButton.clickAndWait(Until.newWindow(), waitingTime)
// Click save to save the login // Click save to save the login
mDevice.waitNotNull(Until.findObjects(text("Save"))) mDevice.waitNotNull(Until.findObjects(text("Save")))
} }
fun verifyUpdateLoginPromptIsShown() {
val submitButton = mDevice.findObject(By.res("submit"))
submitButton.clickAndWait(Until.newWindow(), waitingTime)
mDevice.waitNotNull(Until.findObjects(text("Update")))
}
fun saveLoginFromPrompt(optionToSaveLogin: String) { fun saveLoginFromPrompt(optionToSaveLogin: String) {
mDevice.findObject(text(optionToSaveLogin)).click() mDevice.findObject(text(optionToSaveLogin)).click()
} }
fun enterPassword(password: String) {
val passwordField = mDevice.findObject(
UiSelector()
.resourceId("password")
.className(EditText::class.java)
)
passwordField.waitForExists(waitingTime)
passwordField.setText(password)
// wait until the password is hidden
assertTrue(mDevice.findObject(UiSelector().text(password)).waitUntilGone(waitingTime))
}
fun clickMediaPlayerPlayButton() { fun clickMediaPlayerPlayButton() {
mediaPlayerPlayButton().waitForExists(waitingTime) mediaPlayerPlayButton().waitForExists(waitingTime)
mediaPlayerPlayButton().click() mediaPlayerPlayButton().click()
@ -338,6 +370,28 @@ class BrowserRobot {
assertTrue(pausedStateMessage.waitForExists(waitingTime)) assertTrue(pausedStateMessage.waitForExists(waitingTime))
} }
fun swipeNavBarRight(tabUrl: String) {
// failing to swipe on Firebase sometimes, so it tries again
try {
navURLBar().perform(ViewActions.swipeRight())
assertTrue(mDevice.findObject(UiSelector().text(tabUrl)).waitUntilGone(waitingTime))
} catch (e: AssertionError) {
navURLBar().perform(ViewActions.swipeRight())
assertTrue(mDevice.findObject(UiSelector().text(tabUrl)).waitUntilGone(waitingTime))
}
}
fun swipeNavBarLeft(tabUrl: String) {
// failing to swipe on Firebase sometimes, so it tries again
try {
navURLBar().perform(ViewActions.swipeLeft())
assertTrue(mDevice.findObject(UiSelector().text(tabUrl)).waitUntilGone(waitingTime))
} catch (e: AssertionError) {
navURLBar().perform(ViewActions.swipeLeft())
assertTrue(mDevice.findObject(UiSelector().text(tabUrl)).waitUntilGone(waitingTime))
}
}
class Transition { class Transition {
private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
private fun threeDotButton() = onView( private fun threeDotButton() = onView(
@ -367,11 +421,8 @@ class BrowserRobot {
fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition { fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
mDevice.waitForIdle(waitingTime) mDevice.waitForIdle(waitingTime)
tabsCounter().click() tabsCounter().click()
mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/tab_layout")),
mDevice.waitNotNull( waitingTime)
Until.findObject(By.res("org.mozilla.fenix.debug:id/tab_layout")),
waitingTime
)
TabDrawerRobot().interact() TabDrawerRobot().interact()
return TabDrawerRobot.Transition() return TabDrawerRobot.Transition()

@ -26,6 +26,8 @@ import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_APPS_PHOTOS import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_APPS_PHOTOS
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.packageName
/** /**
* Implementation of Robot Pattern for download UI handling. * Implementation of Robot Pattern for download UI handling.
@ -40,7 +42,6 @@ class DownloadRobot {
fun verifyPhotosAppOpens() = assertPhotosOpens() fun verifyPhotosAppOpens() = assertPhotosOpens()
class Transition { class Transition {
fun clickDownload(interact: DownloadRobot.() -> Unit): Transition { fun clickDownload(interact: DownloadRobot.() -> Unit): Transition {
clickDownloadButton().click() clickDownloadButton().click()
@ -93,7 +94,7 @@ fun downloadRobot(interact: DownloadRobot.() -> Unit): DownloadRobot.Transition
} }
private fun assertDownloadPrompt() { private fun assertDownloadPrompt() {
mDevice.waitNotNull(Until.findObjects(By.res("org.mozilla.fenix.debug:id/download_button"))) mDevice.waitNotNull(Until.findObjects(By.res("$packageName:id/download_button")))
} }
private fun assertDownloadNotificationPopup() { private fun assertDownloadNotificationPopup() {

@ -15,6 +15,7 @@ import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.helpers.isChecked import org.mozilla.fenix.helpers.isChecked
@ -89,7 +90,7 @@ fun enhancedTrackingProtection(interact: EnhancedTrackingProtectionRobot.() -> U
private fun assertEnhancedTrackingProtectionNotice() { private fun assertEnhancedTrackingProtectionNotice() {
mDevice.waitNotNull( mDevice.waitNotNull(
Until.findObject(By.res("org.mozilla.fenix.debug:id/onboarding_message")), Until.findObject(By.res("$packageName:id/onboarding_message")),
TestAssetHelper.waitingTime TestAssetHelper.waitingTime
) )
} }

@ -11,6 +11,7 @@ import android.widget.EditText
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.longClick import androidx.test.espresso.action.ViewActions.longClick
@ -38,6 +39,7 @@ import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import androidx.test.uiautomator.Until.findObject import androidx.test.uiautomator.Until.findObject
import mozilla.components.support.ktx.android.content.appName import mozilla.components.support.ktx.android.content.appName
import mozilla.components.browser.state.state.searchEngines
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.containsString import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.CoreMatchers.instanceOf import org.hamcrest.CoreMatchers.instanceOf
@ -45,9 +47,10 @@ import org.hamcrest.CoreMatchers.not
import org.hamcrest.Matchers import org.hamcrest.Matchers
import org.junit.Assert import org.junit.Assert
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.Search import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.helpers.ext.waitNotNull
@ -367,7 +370,7 @@ class HomeScreenRobot {
tabsCounter().click() tabsCounter().click()
mDevice.waitNotNull( mDevice.waitNotNull(
Until.findObject(By.res("org.mozilla.fenix.debug:id/tab_layout")), Until.findObject(By.res("$packageName:id/tab_layout")),
waitingTime waitingTime
) )
@ -393,6 +396,14 @@ class HomeScreenRobot {
openThreeDotMenu { }.openSettings { }.goBack { } openThreeDotMenu { }.openSettings { }.goBack { }
} }
fun clickStartBrowsingButton(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
scrollToElementByText("Start browsing")
startBrowsingButton().click()
SearchRobot().interact()
return SearchRobot.Transition()
}
fun togglePrivateBrowsingMode() { fun togglePrivateBrowsingMode() {
onView(ViewMatchers.withResourceName("privateBrowsingButton")) onView(ViewMatchers.withResourceName("privateBrowsingButton"))
.perform(click()) .perform(click())
@ -579,10 +590,11 @@ private fun verifySearchEngineIcon(searchEngineIcon: Bitmap, searchEngineName: S
} }
private fun getSearchEngine(searchEngineName: String) = private fun getSearchEngine(searchEngineName: String) =
Search(appContext).searchEngineManager.getDefaultSearchEngine(appContext, searchEngineName) appContext.components.core.store.state.search.searchEngines.find { it.name == searchEngineName }
private fun verifySearchEngineIcon(searchEngineName: String) { private fun verifySearchEngineIcon(searchEngineName: String) {
val ddgSearchEngine = getSearchEngine(searchEngineName) val ddgSearchEngine = getSearchEngine(searchEngineName)
?: throw AssertionError("No search engine with name $searchEngineName")
verifySearchEngineIcon(ddgSearchEngine.icon, ddgSearchEngine.name) verifySearchEngineIcon(ddgSearchEngine.icon, ddgSearchEngine.name)
} }
@ -806,3 +818,8 @@ private fun tab(title: String) =
withText(title) withText(title)
) )
) )
private fun startBrowsingButton(): ViewInteraction {
scrollToElementByText("Start browsing")
return onView(allOf(withText("Start browsing")))
}

@ -23,6 +23,7 @@ import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.helpers.ext.waitNotNull
import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.allOf
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.packageName
/* /*
* Implementation of Robot Pattern for the multiple selection toolbar of History and Bookmarks menus. * Implementation of Robot Pattern for the multiple selection toolbar of History and Bookmarks menus.
@ -99,7 +100,7 @@ class LibrarySubMenusMultipleSelectionToolbarRobot {
fun clickOpenNewTab(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition { fun clickOpenNewTab(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
openInNewTabButton().click() openInNewTabButton().click()
mDevice.waitNotNull( mDevice.waitNotNull(
Until.findObject(By.res("org.mozilla.fenix.debug:id/tab_layout")), Until.findObject(By.res("$packageName:id/tab_layout")),
waitingTime waitingTime
) )
@ -110,7 +111,7 @@ class LibrarySubMenusMultipleSelectionToolbarRobot {
fun clickOpenPrivateTab(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition { fun clickOpenPrivateTab(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
openInPrivateTabButton().click() openInPrivateTabButton().click()
mDevice.waitNotNull( mDevice.waitNotNull(
Until.findObject(By.res("org.mozilla.fenix.debug:id/tab_layout")), Until.findObject(By.res("$packageName:id/tab_layout")),
waitingTime waitingTime
) )

@ -26,13 +26,13 @@ import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import kotlinx.android.synthetic.main.fragment_search_dialog.view.*
import org.hamcrest.CoreMatchers.anyOf import org.hamcrest.CoreMatchers.anyOf
import org.hamcrest.CoreMatchers.containsString import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.CoreMatchers.not import org.hamcrest.CoreMatchers.not
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.SessionLoadedIdlingResource import org.mozilla.fenix.helpers.SessionLoadedIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.assertions.AwesomeBarAssertion.Companion.suggestionsAreEqualTo import org.mozilla.fenix.helpers.assertions.AwesomeBarAssertion.Companion.suggestionsAreEqualTo
import org.mozilla.fenix.helpers.assertions.AwesomeBarAssertion.Companion.suggestionsAreGreaterThan import org.mozilla.fenix.helpers.assertions.AwesomeBarAssertion.Companion.suggestionsAreGreaterThan
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
@ -43,16 +43,18 @@ import org.mozilla.fenix.helpers.ext.waitNotNull
*/ */
class NavigationToolbarRobot { class NavigationToolbarRobot {
fun verifySearchSuggestionsAreMoreThan(suggestionSize: Int, searchTerm: String) = fun verifySearchSuggestionsAreMoreThan(suggestionSize: Int) =
assertSuggestionsAreMoreThan(suggestionSize, searchTerm) assertSuggestionsAreMoreThan(suggestionSize)
fun verifySearchSuggestionsAreEqualTo(suggestionSize: Int, searchTerm: String) = fun verifySearchSuggestionsAreEqualTo(suggestionSize: Int) =
assertSuggestionsAreEqualTo(suggestionSize, searchTerm) assertSuggestionsAreEqualTo(suggestionSize)
fun verifyNoHistoryBookmarks() = assertNoHistoryBookmarks() fun verifyNoHistoryBookmarks() = assertNoHistoryBookmarks()
fun verifyTabButtonShortcutMenuItems() = assertTabButtonShortcutMenuItems() fun verifyTabButtonShortcutMenuItems() = assertTabButtonShortcutMenuItems()
fun typeSearchTerm(searchTerm: String) = awesomeBar().perform(typeText(searchTerm))
class Transition { class Transition {
private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource
@ -60,12 +62,12 @@ class NavigationToolbarRobot {
fun goBackToWebsite(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun goBackToWebsite(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitNotNull( mDevice.waitNotNull(
Until.findObject(By.res("org.mozilla.fenix.debug:id/toolbar")), Until.findObject(By.res("$packageName:id/toolbar")),
waitingTime waitingTime
) )
urlBar().click() urlBar().click()
mDevice.waitNotNull( mDevice.waitNotNull(
Until.findObject(By.res("org.mozilla.fenix.debug:id/mozac_browser_toolbar_edit_url_view")), Until.findObject(By.res("$packageName:id/mozac_browser_toolbar_edit_url_view")),
waitingTime waitingTime
) )
clearAddressBar().click() clearAddressBar().click()
@ -82,13 +84,12 @@ class NavigationToolbarRobot {
): BrowserRobot.Transition { ): BrowserRobot.Transition {
sessionLoadedIdlingResource = SessionLoadedIdlingResource() sessionLoadedIdlingResource = SessionLoadedIdlingResource()
mDevice.waitNotNull( mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/toolbar")),
Until.findObject(By.res("org.mozilla.fenix.debug:id/toolbar")), waitingTime
waitingTime
) )
urlBar().click() urlBar().click()
mDevice.waitNotNull( mDevice.waitNotNull(
Until.findObject(By.res("org.mozilla.fenix.debug:id/mozac_browser_toolbar_edit_url_view")), Until.findObject(By.res("$packageName:id/mozac_browser_toolbar_edit_url_view")),
waitingTime waitingTime
) )
@ -109,10 +110,7 @@ class NavigationToolbarRobot {
} }
fun openThreeDotMenu(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition { fun openThreeDotMenu(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition {
mDevice.waitNotNull( mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/mozac_browser_toolbar_menu")), waitingTime)
Until.findObject(By.res("org.mozilla.fenix.debug:id/mozac_browser_toolbar_menu")),
waitingTime
)
threeDotButton().click() threeDotButton().click()
ThreeDotMenuMainRobot().interact() ThreeDotMenuMainRobot().interact()
@ -134,11 +132,7 @@ class NavigationToolbarRobot {
interact: BrowserRobot.() -> Unit interact: BrowserRobot.() -> Unit
): BrowserRobot.Transition { ): BrowserRobot.Transition {
sessionLoadedIdlingResource = SessionLoadedIdlingResource() sessionLoadedIdlingResource = SessionLoadedIdlingResource()
mDevice.waitNotNull( mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/toolbar")), waitingTime)
Until.findObject(By.res("org.mozilla.fenix.debug:id/toolbar")),
waitingTime
)
urlBar().click() urlBar().click()
awesomeBar().perform(replaceText(url.toString()), pressImeActionButton()) awesomeBar().perform(replaceText(url.toString()), pressImeActionButton())
@ -246,18 +240,12 @@ fun clickUrlbar(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
return SearchRobot.Transition() return SearchRobot.Transition()
} }
private fun assertSuggestionsAreEqualTo(suggestionSize: Int, searchTerm: String) { private fun assertSuggestionsAreEqualTo(suggestionSize: Int) {
mDevice.waitForIdle()
awesomeBar().perform(typeText(searchTerm))
mDevice.waitForIdle() mDevice.waitForIdle()
onView(withId(R.id.awesome_bar)).check(suggestionsAreEqualTo(suggestionSize)) onView(withId(R.id.awesome_bar)).check(suggestionsAreEqualTo(suggestionSize))
} }
private fun assertSuggestionsAreMoreThan(suggestionSize: Int, searchTerm: String) { private fun assertSuggestionsAreMoreThan(suggestionSize: Int) {
mDevice.waitForIdle()
awesomeBar().perform(typeText(searchTerm))
mDevice.waitForIdle() mDevice.waitForIdle()
onView(withId(R.id.awesome_bar)).check(suggestionsAreGreaterThan(suggestionSize)) onView(withId(R.id.awesome_bar)).check(suggestionsAreGreaterThan(suggestionSize))
} }

@ -6,18 +6,19 @@
package org.mozilla.fenix.ui.robots package org.mozilla.fenix.ui.robots
import android.widget.ToggleButton
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewInteraction import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.swipeDown
import androidx.test.espresso.action.ViewActions.typeText import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.Visibility import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
@ -37,6 +38,7 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.SessionLoadedIdlingResource import org.mozilla.fenix.helpers.SessionLoadedIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.helpers.ext.waitNotNull
@ -62,15 +64,16 @@ class SearchRobot {
} }
fun verifyDefaultSearchEngine(expectedText: String) = assertDefaultSearchEngine(expectedText) fun verifyDefaultSearchEngine(expectedText: String) = assertDefaultSearchEngine(expectedText)
fun verifyEnginesListShortcutContains(searchEngineName: String) = assertEngineListShortcutContains(searchEngineName)
fun changeDefaultSearchEngine(searchEngineName: String) = fun changeDefaultSearchEngine(searchEngineName: String) =
selectDefaultSearchEngine(searchEngineName) selectDefaultSearchEngine(searchEngineName)
fun clickSearchEngineButton() { fun clickSearchEngineShortcutButton() {
val searchEngineButton = mDevice.findObject(UiSelector() val searchEnginesShortcutButton = mDevice.findObject(UiSelector()
.instance(1) .resourceId("$packageName:id/search_engines_shortcut_button"))
.className(ToggleButton::class.java)) searchEnginesShortcutButton.waitForExists(waitingTime)
searchEngineButton.waitForExists(waitingTime) searchEnginesShortcutButton.click()
searchEngineButton.click()
} }
fun clickScanButton() { fun clickScanButton() {
@ -266,6 +269,12 @@ private fun assertSearchEngineList() {
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
} }
private fun assertEngineListShortcutContains(searchEngineName: String) {
onView(withId(R.id.awesome_bar))
.perform(swipeDown())
.check(matches(hasDescendant(withText(searchEngineName))))
}
private fun selectDefaultSearchEngine(searchEngine: String) { private fun selectDefaultSearchEngine(searchEngine: String) {
onView(withId(R.id.mozac_browser_toolbar_edit_icon)).click() onView(withId(R.id.mozac_browser_toolbar_edit_icon)).click()
onView(withText(searchEngine)) onView(withText(searchEngine))

@ -32,10 +32,13 @@ class SettingsSubMenuDataCollectionRobot {
fun verifyMarketingDataSwitchDefault() = assertMarketingDataValueSwitchDefault() fun verifyMarketingDataSwitchDefault() = assertMarketingDataValueSwitchDefault()
fun verifyExperimentsSwitchDefault() = assertExperimentsSwitchDefault()
fun verifyDataCollectionSubMenuItems() { fun verifyDataCollectionSubMenuItems() {
verifyDataCollectionOptions() verifyDataCollectionOptions()
verifyUsageAndTechnicalDataSwitchDefault() verifyUsageAndTechnicalDataSwitchDefault()
verifyMarketingDataSwitchDefault() verifyMarketingDataSwitchDefault()
verifyExperimentsSwitchDefault()
} }
class Transition { class Transition {
@ -76,6 +79,9 @@ private fun assertDataCollectionOptions() {
onView(withText(marketingDataText)) onView(withText(marketingDataText))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
onView(withText(R.string.preference_experiments_2)).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
onView(withText(R.string.preference_experiments_summary_2)).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
} }
private fun usageAndTechnicalDataButton() = onView(withText(R.string.preference_usage_data)) private fun usageAndTechnicalDataButton() = onView(withText(R.string.preference_usage_data))
@ -87,3 +93,8 @@ private fun marketingDataButton() = onView(withText(R.string.preferences_marketi
private fun assertMarketingDataValueSwitchDefault() = marketingDataButton() private fun assertMarketingDataValueSwitchDefault() = marketingDataButton()
.assertIsEnabled(isEnabled = true) .assertIsEnabled(isEnabled = true)
private fun experimentsButton() = onView(withText(R.string.preference_experiments_2))
private fun assertExperimentsSwitchDefault() = experimentsButton()
.assertIsEnabled(isEnabled = true)

@ -10,11 +10,15 @@ import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility 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.By
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.containsString import org.hamcrest.CoreMatchers.containsString
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.helpers.ext.waitNotNull
/** /**
@ -22,7 +26,7 @@ import org.mozilla.fenix.helpers.ext.waitNotNull
*/ */
class SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot { class SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot {
fun verifySavedLoginsView() = assertSavedLoginsView() fun verifySecurityPromptForLogins() = assertSavedLoginsView()
fun verifySavedLoginsAfterSync() { fun verifySavedLoginsAfterSync() {
mDevice.waitNotNull( mDevice.waitNotNull(
@ -43,6 +47,13 @@ class SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot {
fun verifyLocalhostExceptionAdded() = onView(ViewMatchers.withText(containsString("localhost"))) fun verifyLocalhostExceptionAdded() = onView(ViewMatchers.withText(containsString("localhost")))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
fun viewSavedLoginDetails() = onView(ViewMatchers.withText("test@example.com")).click()
fun revealPassword() = onView(withId(R.id.revealPasswordButton)).click()
fun verifyPasswordSaved(password: String) =
onView(withId(R.id.passwordText)).check(matches(withText(password)))
class Transition { class Transition {
fun goBack(interact: SettingsSubMenuLoginsAndPasswordRobot.() -> Unit): SettingsSubMenuLoginsAndPasswordRobot.Transition { fun goBack(interact: SettingsSubMenuLoginsAndPasswordRobot.() -> Unit): SettingsSubMenuLoginsAndPasswordRobot.Transition {
goBackButton().perform(ViewActions.click()) goBackButton().perform(ViewActions.click())

@ -125,9 +125,6 @@ private fun assertOpenLinksInPrivateTabOff() {
} }
private fun assertPrivateBrowsingShortcutIcon() { private fun assertPrivateBrowsingShortcutIcon() {
mDevice.wait( mDevice.wait(Until.findObject(text("Private Firefox Preview")), waitingTime)
Until.findObject(text("Private Firefox Preview")),
waitingTime
)
assertTrue(mDevice.hasObject(text("Private Firefox Preview"))) assertTrue(mDevice.hasObject(text("Private Firefox Preview")))
} }

@ -13,6 +13,7 @@ import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers.Visibility import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
@ -20,6 +21,8 @@ import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import org.hamcrest.CoreMatchers import org.hamcrest.CoreMatchers
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.click
/** /**
* Implementation of Robot Pattern for the settings search sub menu. * Implementation of Robot Pattern for the settings search sub menu.
@ -32,12 +35,26 @@ class SettingsSubMenuSearchRobot {
fun verifyShowClipboardSuggestions() = assertShowClipboardSuggestions() fun verifyShowClipboardSuggestions() = assertShowClipboardSuggestions()
fun verifySearchBrowsingHistory() = assertSearchBrowsingHistory() fun verifySearchBrowsingHistory() = assertSearchBrowsingHistory()
fun verifySearchBookmarks() = assertSearchBookmarks() fun verifySearchBookmarks() = assertSearchBookmarks()
fun changeDefaultSearchEngine(searchEngineName: String) = fun changeDefaultSearchEngine(searchEngineName: String) =
selectDefaultSearchEngine(searchEngineName) selectSearchEngine(searchEngineName)
fun disableShowSearchSuggestions() = toggleShowSearchSuggestions() fun disableShowSearchSuggestions() = toggleShowSearchSuggestions()
fun enableShowSearchShortcuts() = toggleShowSearchShortcuts() fun enableShowSearchShortcuts() = toggleShowSearchShortcuts()
fun openAddSearchEngineMenu() = addSearchEngineButton().click()
fun verifyAddSearchEngineList() = assertAddSearchEngineList()
fun verifyEngineListContains(searchEngineName: String) = assertEngineListContains(searchEngineName)
fun saveNewSearchEngine() = addSearchEngineSaveButton().click()
fun addNewSearchEngine(searchEngineName: String) {
selectSearchEngine(searchEngineName)
saveNewSearchEngine()
}
class Transition { class Transition {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@ -120,16 +137,12 @@ private fun assertSearchBookmarks() {
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
} }
private fun selectDefaultSearchEngine(searchEngine: String) { private fun selectSearchEngine(searchEngine: String) {
onView(withText(searchEngine)) onView(withText(searchEngine))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.perform(click()) .perform(click())
} }
private fun selectDuckDuckGoAsSearchEngine() {
selectDefaultSearchEngine("DuckDuckGo")
}
private fun toggleShowSearchSuggestions() { private fun toggleShowSearchSuggestions() {
onView(withId(androidx.preference.R.id.recycler_view)).perform( onView(withId(androidx.preference.R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>( RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
@ -154,3 +167,17 @@ private fun toggleShowSearchShortcuts() {
private fun goBackButton() = private fun goBackButton() =
onView(CoreMatchers.allOf(withContentDescription("Navigate up"))) onView(CoreMatchers.allOf(withContentDescription("Navigate up")))
private fun addSearchEngineButton() = onView(withText("Add search engine"))
private fun assertAddSearchEngineList() {
onView(withText("Reddit")).check(matches(isDisplayed()))
onView(withText("YouTube")).check(matches(isDisplayed()))
onView(withText("Other")).check(matches(isDisplayed()))
}
private fun addSearchEngineSaveButton() = onView(withId(R.id.add_search_engine))
private fun assertEngineListContains(searchEngineName: String) {
onView(withId(R.id.search_engine_group)).check(matches(hasDescendant(withText(searchEngineName))))
}

@ -7,14 +7,18 @@ package org.mozilla.fenix.ui.robots
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.Visibility import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.Matchers.not
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.assertIsChecked import org.mozilla.fenix.helpers.assertIsChecked
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
@ -41,6 +45,8 @@ class SettingsSubMenuSitePermissionsCommonRobot {
fun verifyBlockedByAndroid() = assertBlockedByAndroid() fun verifyBlockedByAndroid() = assertBlockedByAndroid()
fun verifyUnblockedByAndroid() = assertUnblockedByAndroid()
fun verifyToAllowIt() = assertToAllowIt() fun verifyToAllowIt() = assertToAllowIt()
fun verifyGotoAndroidSettings() = assertGotoAndroidSettings() fun verifyGotoAndroidSettings() = assertGotoAndroidSettings()
@ -81,6 +87,22 @@ class SettingsSubMenuSitePermissionsCommonRobot {
verifyCheckCommonRadioButtonDefault() verifyCheckCommonRadioButtonDefault()
} }
fun clickGoToSettingsButton() {
goToSettingsButton().click()
mDevice.findObject(UiSelector().resourceId("com.android.settings:id/list"))
.waitForExists(waitingTime)
}
fun openAppSystemPermissionsSettings() {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mDevice.findObject(UiSelector().textContains("Permissions")).click()
}
fun switchAppPermissionSystemSetting(permissionCategory: String) {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mDevice.findObject(UiSelector().textContains(permissionCategory)).click()
}
class Transition { class Transition {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())!! val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())!!
@ -131,6 +153,9 @@ private fun assertCheckCommonRadioButtonDefault() {
private fun assertBlockedByAndroid() = onView(withText(R.string.phone_feature_blocked_by_android)) private fun assertBlockedByAndroid() = onView(withText(R.string.phone_feature_blocked_by_android))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertUnblockedByAndroid() = onView(withText(R.string.phone_feature_blocked_by_android))
.check(matches(not(isDisplayed())))
private fun assertToAllowIt() = onView(withText(R.string.phone_feature_blocked_intro)) private fun assertToAllowIt() = onView(withText(R.string.phone_feature_blocked_intro))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
@ -143,8 +168,10 @@ private fun assertTapPermissions() = onView(withText("2. Tap Permissions"))
private fun assertToggleNameToON(name: String) = onView(withText(name)) private fun assertToggleNameToON(name: String) = onView(withText(name))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertGoToSettingsButton() = onView(withId(R.id.settings_button)) private fun assertGoToSettingsButton() =
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) goToSettingsButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun goBackButton() = private fun goBackButton() =
onView(allOf(withContentDescription("Navigate up"))) onView(allOf(withContentDescription("Navigate up")))
private fun goToSettingsButton() = onView(withId(R.id.settings_button))

@ -115,7 +115,6 @@ class ThreeDotMenuMainRobot {
fun verifyAddFirefoxHome() = assertAddToFirefoxHome() fun verifyAddFirefoxHome() = assertAddToFirefoxHome()
fun verifyAddToMobileHome() = assertAddToMobileHome() fun verifyAddToMobileHome() = assertAddToMobileHome()
fun verifyDesktopSite() = assertDesktopSite() fun verifyDesktopSite() = assertDesktopSite()
fun verifyOpenInAppButton() = assertOpenInAppButton()
fun verifyDownloadsButton() = assertDownloadsButton() fun verifyDownloadsButton() = assertDownloadsButton()
fun verifyThreeDotMainMenuItems() { fun verifyThreeDotMainMenuItems() {
@ -519,17 +518,6 @@ private fun assertDesktopSite() {
desktopSiteButton().check(matches(isDisplayed())) desktopSiteButton().check(matches(isDisplayed()))
} }
private fun openInAppButton() =
onView(allOf(withText(R.string.browser_menu_open_app_link)))
private fun assertOpenInAppButton() {
onView(withId(R.id.mozac_browser_menu_recyclerView))
.perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText(R.string.browser_menu_open_app_link))
)
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun downloadsButton() = onView(withText(R.string.library_downloads)) private fun downloadsButton() = onView(withText(R.string.library_downloads))
private fun assertDownloadsButton() { private fun assertDownloadsButton() {
onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeDown()) onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeDown())

@ -232,6 +232,9 @@
<service android:name=".media.MediaService" <service android:name=".media.MediaService"
android:exported="false" /> android:exported="false" />
<service android:name=".media.MediaSessionService"
android:exported="false" />
<service <service
android:name=".customtabs.CustomTabsService" android:name=".customtabs.CustomTabsService"
android:exported="true" android:exported="true"

@ -0,0 +1,42 @@
/* 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/. */
/**
* Handles the parsing of the ErrorPages URI and then passes them to injectValues
*/
function parseQuery(queryString) {
if (queryString[0] === '?') {
queryString = queryString.substr(1);
}
const query = Object.fromEntries(new URLSearchParams(queryString).entries());
injectValues(query);
};
/**
* Updates the HTML elements based on the queryMap
*/
function injectValues(queryMap) {
// Go through each element and inject the values
document.title = queryMap.title;
document.getElementById('errorTitleText').innerHTML = queryMap.title;
document.getElementById('errorShortDesc').innerHTML = queryMap.description;
// If no image is passed in, remove the element so as not to leave an empty iframe
const errorImage = document.getElementById('errorImage');
if (!queryMap.image) {
errorImage.remove();
} else {
errorImage.src = "resource://android/assets/" + queryMap.image;
}
}
document.addEventListener('DOMContentLoaded', function () {
if (window.history.length == 1) {
document.getElementById('backButton').style.display = 'none';
} else {
document.getElementById('backButton').addEventListener('click', () => window.history.back() );
}
});
parseQuery(document.documentURI);

@ -8,6 +8,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width; user-scalable=false;" /> <meta name="viewport" content="width=device-width; user-scalable=false;" />
<meta http-equiv="Content-Security-Policy" content="default-src resource:; object-src 'none'" />
<link rel="stylesheet" type="text/css" href="shared_error_style.css" /> <link rel="stylesheet" type="text/css" href="shared_error_style.css" />
<link rel="stylesheet" type="text/css" href="high_risk_error_style.css" /> <link rel="stylesheet" type="text/css" href="high_risk_error_style.css" />
</head> </head>
@ -29,14 +30,9 @@
</div> </div>
<!-- Back Button --> <!-- Back Button -->
<button id="backButton" onclick="window.history.back()">Go back</button> <button id="backButton">Go back</button>
</div> </div>
</body> </body>
<script src="./errorPageScripts.js"></script> <script src="./highRiskErrorPages.js"></script>
<script type="text/javascript">
if (window.history.length == 1) {
document.getElementById('backButton').style.display = 'none';
}
</script>
</html> </html>

@ -0,0 +1,116 @@
/* 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/. */
/**
* Handles the parsing of the ErrorPages URI and then passes them to injectValues
*/
function parseQuery(queryString) {
if (queryString[0] === '?') {
queryString = queryString.substr(1);
}
const query = Object.fromEntries(new URLSearchParams(queryString).entries());
injectValues(query);
updateShowSSL(query);
};
/**
* Updates the HTML elements based on the queryMap
*/
function injectValues(queryMap) {
// Go through each element and inject the values
document.title = queryMap.title;
document.getElementById('errorTitleText').innerHTML = queryMap.title;
document.getElementById('errorShortDesc').innerHTML = queryMap.description;
document.getElementById('errorTryAgain').innerHTML = queryMap.button;
document.getElementById('advancedButton').innerHTML = queryMap.badCertAdvanced;
document.getElementById('badCertTechnicalInfo').innerHTML = queryMap.badCertTechInfo;
document.getElementById('advancedPanelBackButton').innerHTML = queryMap.badCertGoBack;
document.getElementById('advancedPanelAcceptButton').innerHTML = queryMap.badCertAcceptTemporary;
// If no image is passed in, remove the element so as not to leave an empty iframe
const errorImage = document.getElementById('errorImage');
if (!queryMap.image) {
errorImage.remove();
} else {
errorImage.src = "resource://android/assets/" + queryMap.image;
}
};
let advancedVisible = false;
/**
* Used to show or hide the "advanced" button based on the validity of the SSL certificate
*/
function updateShowSSL(queryMap) {
/** @type {'true' | 'false'} */
const showSSL = queryMap.showSSL;
if (typeof document.addCertException === 'undefined') {
document.getElementById('advancedButton').style.display='none';
} else {
if (showSSL === 'true') {
document.getElementById('advancedButton').style.display='block';
} else {
document.getElementById('advancedButton').style.display='none';
}
}
};
/**
* Used to display information about the SSL certificate in `error_pages.html`
*/
function toggleAdvancedAndScroll() {
const advancedPanel = document.getElementById('badCertAdvancedPanel');
if (advancedVisible) {
advancedPanel.style.display='none';
} else {
advancedPanel.style.display='block';
}
advancedVisible = !advancedVisible;
const horizontalLine = document.getElementById("horizontalLine");
const advancedPanelAcceptButton = document.getElementById(
"advancedPanelAcceptButton"
);
const badCertAdvancedPanel = document.getElementById(
"badCertAdvancedPanel"
);
// We know that the button is being displayed
if (badCertAdvancedPanel.style.display === "block") {
horizontalLine.hidden = false;
advancedPanelAcceptButton.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "nearest",
});
} else {
horizontalLine.hidden = true;
}
};
/**
* Used to bypass an SSL pages in `error_pages.html`
*/
async function acceptAndContinue(temporary) {
try {
await document.addCertException(temporary);
location.reload();
} catch (error) {
console.error("Unexpected error: " + error);
}
};
document.addEventListener('DOMContentLoaded', function () {
if (window.history.length == 1) {
document.getElementById('advancedPanelBackButton').style.display = 'none';
} else {
document.getElementById('advancedPanelBackButton').addEventListener('click', () => window.history.back());
}
document.getElementById('errorTryAgain').addEventListener('click', () => window.location.reload());
document.getElementById('advancedButton').addEventListener('click', toggleAdvancedAndScroll);
document.getElementById('advancedPanelAcceptButton').addEventListener('click', () => acceptAndContinue(true));
});
parseQuery(document.documentURI);

@ -8,6 +8,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width; user-scalable=false;" /> <meta name="viewport" content="width=device-width; user-scalable=false;" />
<meta http-equiv="Content-Security-Policy" content="default-src resource:; object-src 'none'" />
<link rel="stylesheet" type="text/css" href="shared_error_style.css" /> <link rel="stylesheet" type="text/css" href="shared_error_style.css" />
<link <link
rel="stylesheet" rel="stylesheet"
@ -33,13 +34,12 @@
</div> </div>
<!-- Retry Button --> <!-- Retry Button -->
<button id="errorTryAgain" onclick="window.location.reload()"></button> <button id="errorTryAgain"></button>
<!-- Advanced Button --> <!-- Advanced Button -->
<button <button
id="advancedButton" id="advancedButton"
class="buttonSecondary hidden" class="buttonSecondary hidden"
onclick="toggleAdvancedAndScroll()"
></button> ></button>
<hr id="horizontalLine" hidden /> <hr id="horizontalLine" hidden />
@ -52,7 +52,6 @@
> >
<button <button
id="advancedPanelBackButton" id="advancedPanelBackButton"
onClick="window.history.back()"
></button> ></button>
</div> </div>
<div <div
@ -62,7 +61,6 @@
<button <button
id="advancedPanelAcceptButton" id="advancedPanelAcceptButton"
class="buttonSecondary" class="buttonSecondary"
onClick="acceptAndContinue(true)"
></button> ></button>
</div> </div>
</div> </div>
@ -100,5 +98,5 @@
} }
</script> </script>
<script src="./errorPageScripts.js"></script> <script src="./lowMediumErrorPages.js"></script>
</html> </html>

@ -7,6 +7,7 @@ package org.mozilla.fenix
import android.content.Context import android.content.Context
import android.net.ConnectivityManager import android.net.ConnectivityManager
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.navigation.NavController
import mozilla.components.browser.errorpages.ErrorPages import mozilla.components.browser.errorpages.ErrorPages
import mozilla.components.browser.errorpages.ErrorType import mozilla.components.browser.errorpages.ErrorType
import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineSession
@ -14,8 +15,18 @@ import mozilla.components.concept.engine.request.RequestInterceptor
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.isOnline import org.mozilla.fenix.ext.isOnline
import java.lang.ref.WeakReference
class AppRequestInterceptor(
private val context: Context
) : RequestInterceptor {
private var navController: WeakReference<NavController>? = null
fun setNavigationController(navController: NavController) {
this.navController = WeakReference(navController)
}
class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
override fun onLoadRequest( override fun onLoadRequest(
engineSession: EngineSession, engineSession: EngineSession,
uri: String, uri: String,
@ -26,6 +37,11 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
isDirectNavigation: Boolean, isDirectNavigation: Boolean,
isSubframeRequest: Boolean isSubframeRequest: Boolean
): RequestInterceptor.InterceptionResponse? { ): RequestInterceptor.InterceptionResponse? {
interceptAmoRequest(uri, isSameDomain, hasUserGesture)?.let { response ->
return response
}
return context.components.services.appLinksInterceptor return context.components.services.appLinksInterceptor
.onLoadRequest( .onLoadRequest(
engineSession, engineSession,
@ -56,7 +72,43 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
htmlResource = riskLevel.htmlRes htmlResource = riskLevel.htmlRes
) )
return RequestInterceptor.ErrorResponse.Uri(errorPageUri) return RequestInterceptor.ErrorResponse(errorPageUri)
}
/**
* Checks if the provided [uri] is a request to install an add-on from addons.mozilla.org and
* redirects to Add-ons Manager to trigger installation if needed.
*
* @return [RequestInterceptor.InterceptionResponse.Deny] when installation was triggered and
* the original request can be skipped, otherwise null to continue loading the page.
*/
private fun interceptAmoRequest(
uri: String,
isSameDomain: Boolean,
hasUserGesture: Boolean
): RequestInterceptor.InterceptionResponse? {
// First we execute a quick check to see if this is a request we're interested in i.e. a
// request triggered by the user and coming from AMO.
if (hasUserGesture && isSameDomain && uri.startsWith(AMO_BASE_URL)) {
// Check if this is a request to install an add-on.
val matchResult = AMO_INSTALL_URL_REGEX.toRegex().matchEntire(uri)
if (matchResult != null) {
// Navigate and trigger add-on installation.
matchResult.groupValues.getOrNull(1)?.let { addonId ->
navController?.get()?.navigate(
NavGraphDirections.actionGlobalAddonsManagementFragment(addonId)
)
// We've redirected to the add-ons management fragment, skip original request.
return RequestInterceptor.InterceptionResponse.Deny
}
}
}
// In all other case we let the original request proceed.
return null
} }
/** /**
@ -116,5 +168,7 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
companion object { companion object {
internal const val LOW_AND_MEDIUM_RISK_ERROR_PAGES = "low_and_medium_risk_error_pages.html" internal const val LOW_AND_MEDIUM_RISK_ERROR_PAGES = "low_and_medium_risk_error_pages.html"
internal const val HIGH_RISK_ERROR_PAGES = "high_risk_error_pages.html" internal const val HIGH_RISK_ERROR_PAGES = "high_risk_error_pages.html"
internal const val AMO_BASE_URL = "https://addons.mozilla.org"
internal const val AMO_INSTALL_URL_REGEX = "$AMO_BASE_URL/android/downloads/file/([^\\s]+)/([^\\s]+\\.xpi)"
} }
} }

@ -27,12 +27,23 @@ object FeatureFlags {
const val externalDownloadManager = true const val externalDownloadManager = true
/** /**
* Enables swipe to delete in bookmarks * Enables ETP cookie purging
*/ */
val bookmarkSwipeToDelete = Config.channel.isNightlyOrDebug val etpCookiePurging = Config.channel.isNightlyOrDebug
/** /**
* Enables ETP cookie purging * Enables the Nimbus experiments library, especially the settings toggle to opt-out of
* all experiments.
*/ */
val etpCookiePurging = Config.channel.isNightlyOrDebug val nimbusExperiments = Config.channel.isNightlyOrDebug
/**
* Enables the new MediaSession API.
*/
val newMediaSessionApi = Config.channel.isNightlyOrDebug
/**
* Enabled showing site permission indicators in the toolbars.
*/
val permissionIndicatorsToolbar = Config.channel.isNightlyOrDebug
} }

@ -22,10 +22,10 @@ import kotlinx.coroutines.launch
import mozilla.appservices.Megazord import mozilla.appservices.Megazord
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.browser.state.action.SystemAction import mozilla.components.browser.state.action.SystemAction
import mozilla.components.concept.base.crash.Breadcrumb
import mozilla.components.concept.push.PushProcessor import mozilla.components.concept.push.PushProcessor
import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider
import mozilla.components.lib.crash.CrashReporter import mozilla.components.lib.crash.CrashReporter
import mozilla.components.service.experiments.Experiments
import mozilla.components.service.glean.Glean import mozilla.components.service.glean.Glean
import mozilla.components.service.glean.config.Configuration import mozilla.components.service.glean.config.Configuration
import mozilla.components.service.glean.net.ConceptFetchHttpUploader import mozilla.components.service.glean.net.ConceptFetchHttpUploader
@ -170,25 +170,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
registerActivityLifecycleCallbacks(PerformanceActivityLifecycleCallbacks(queue)) registerActivityLifecycleCallbacks(PerformanceActivityLifecycleCallbacks(queue))
} }
fun queueInitExperiments() {
@Suppress("ControlFlowWithEmptyBody")
if (settings().isExperimentationEnabled) {
queue.runIfReadyOrQueue {
Experiments.initialize(
applicationContext = applicationContext,
onExperimentsUpdated = null,
configuration = mozilla.components.service.experiments.Configuration(
httpClient = components.core.client,
kintoEndpoint = KINTO_ENDPOINT_PROD
)
)
}
} else {
// We should make a better way to opt out for when we have more experiments
// See https://github.com/mozilla-mobile/fenix/issues/6278
}
}
fun queueInitStorageAndServices() { fun queueInitStorageAndServices() {
components.performance.visualCompletenessQueue.queue.runIfReadyOrQueue { components.performance.visualCompletenessQueue.queue.runIfReadyOrQueue {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
@ -229,7 +210,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
// We init these items in the visual completeness queue to avoid them initing in the critical // We init these items in the visual completeness queue to avoid them initing in the critical
// startup path, before the UI finishes drawing (i.e. visual completeness). // startup path, before the UI finishes drawing (i.e. visual completeness).
queueInitExperiments()
queueInitStorageAndServices() queueInitStorageAndServices()
queueMetrics() queueMetrics()
queueReviewPrompt() queueReviewPrompt()
@ -317,6 +297,21 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
override fun onTrimMemory(level: Int) { override fun onTrimMemory(level: Int) {
super.onTrimMemory(level) super.onTrimMemory(level)
// Additional logging and breadcrumb to debug memory issues:
// https://github.com/mozilla-mobile/fenix/issues/12731
logger.info("onTrimMemory(), level=$level, main=${isMainProcess()}")
components.analytics.crashReporter.recordCrashBreadcrumb(Breadcrumb(
category = "Memory",
message = "onTrimMemory()",
data = mapOf(
"level" to level.toString(),
"main" to isMainProcess().toString()
),
level = Breadcrumb.Level.INFO
))
runOnlyInMainProcess { runOnlyInMainProcess {
components.core.icons.onTrimMemory(level) components.core.icons.onTrimMemory(level)
@ -458,9 +453,5 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
} }
} }
companion object {
private const val KINTO_ENDPOINT_PROD = "https://firefox.settings.services.mozilla.com/v1"
}
override fun getWorkManagerConfiguration() = Builder().setMinimumLoggingLevel(INFO).build() override fun getWorkManagerConfiguration() = Builder().setMinimumLoggingLevel(INFO).build()
} }

@ -37,15 +37,19 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.browser.search.SearchEngine import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.WebExtensionState import mozilla.components.browser.state.state.WebExtensionState
import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.EngineView import mozilla.components.concept.engine.EngineView
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import mozilla.components.feature.contextmenu.DefaultSelectionActionDelegate import mozilla.components.feature.contextmenu.DefaultSelectionActionDelegate
import mozilla.components.feature.privatemode.notification.PrivateNotificationFeature import mozilla.components.feature.privatemode.notification.PrivateNotificationFeature
import mozilla.components.feature.search.BrowserStoreSearchAdapter import mozilla.components.feature.search.BrowserStoreSearchAdapter
import mozilla.components.feature.search.ext.legacy
import mozilla.components.service.fxa.sync.SyncReason import mozilla.components.service.fxa.sync.SyncReason
import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.ktx.android.arch.lifecycle.addObservers import mozilla.components.support.ktx.android.arch.lifecycle.addObservers
@ -81,6 +85,7 @@ import org.mozilla.fenix.home.intent.OpenSpecificTabIntentProcessor
import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor
import org.mozilla.fenix.home.intent.StartSearchIntentProcessor import org.mozilla.fenix.home.intent.StartSearchIntentProcessor
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections
import org.mozilla.fenix.library.bookmarks.DesktopFolders
import org.mozilla.fenix.library.history.HistoryFragmentDirections import org.mozilla.fenix.library.history.HistoryFragmentDirections
import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections
import org.mozilla.fenix.perf.Performance import org.mozilla.fenix.perf.Performance
@ -139,7 +144,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
private val externalSourceIntentProcessors by lazy { private val externalSourceIntentProcessors by lazy {
listOf( listOf(
SpeechProcessingIntentProcessor(this, components.analytics.metrics), SpeechProcessingIntentProcessor(this, components.core.store, components.analytics.metrics),
StartSearchIntentProcessor(components.analytics.metrics), StartSearchIntentProcessor(components.analytics.metrics),
DeepLinkIntentProcessor(this, components.analytics.leanplumMetricsService), DeepLinkIntentProcessor(this, components.analytics.leanplumMetricsService),
OpenBrowserIntentProcessor(this, ::getIntentSessionId), OpenBrowserIntentProcessor(this, ::getIntentSessionId),
@ -237,6 +242,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
startupTelemetryOnCreateCalled(intent.toSafeIntent(), savedInstanceState != null) startupTelemetryOnCreateCalled(intent.toSafeIntent(), savedInstanceState != null)
components.core.requestInterceptor.setNavigationController(navHost.navController)
StartupTimeline.onActivityCreateEndHome(this) // DO NOT MOVE ANYTHING BELOW HERE. StartupTimeline.onActivityCreateEndHome(this) // DO NOT MOVE ANYTHING BELOW HERE.
} }
@ -337,6 +344,20 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
// https://github.com/mozilla-mobile/android-components/issues/8679 // https://github.com/mozilla-mobile/android-components/issues/8679
settings().topSitesSize = components.core.topSitesStorage.cachedTopSites.size settings().topSitesSize = components.core.topSitesStorage.cachedTopSites.size
lifecycleScope.launch(IO) {
components.core.bookmarksStorage.getTree(BookmarkRoot.Root.id, true)?.let {
val desktopRootNode = DesktopFolders(
applicationContext,
showMobileRoot = false
).withOptionalDesktopFolders(it)
settings().desktopBookmarksSize = getBookmarkCount(desktopRootNode)
}
components.core.bookmarksStorage.getTree(BookmarkRoot.Mobile.id, true)?.let {
settings().mobileBookmarksSize = getBookmarkCount(it)
}
}
super.onPause() super.onPause()
// Diagnostic breadcrumb for "Display already aquired" crash: // Diagnostic breadcrumb for "Display already aquired" crash:
@ -356,6 +377,25 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
BrowsersCache.resetAll() BrowsersCache.resetAll()
} }
private fun getBookmarkCount(node: BookmarkNode): Int {
val children = node.children
return if (children == null) {
0
} else {
var count = 0
for (child in children) {
if (child.type == BookmarkNodeType.FOLDER) {
count += getBookmarkCount(child)
} else if (child.type == BookmarkNodeType.ITEM) {
count++
}
}
count
}
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
@ -737,23 +777,24 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
} }
} else components.useCases.sessionUseCases.loadUrl } else components.useCases.sessionUseCases.loadUrl
val searchUseCase: (String) -> Unit = { searchTerms -> // In situations where we want to perform a search but have no search engine (e.g. the user
// has removed all of them, or we couldn't load any) we will pass searchTermOrURL to Gecko
// and let it try to load whatever was entered.
if ((!forceSearch && searchTermOrURL.isUrl()) || engine == null) {
loadUrlUseCase.invoke(searchTermOrURL.toNormalizedUrl(), flags)
} else {
if (newTab) { if (newTab) {
components.useCases.searchUseCases.newTabSearch components.useCases.searchUseCases.newTabSearch
.invoke( .invoke(
searchTerms, searchTermOrURL,
SessionState.Source.USER_ENTERED, SessionState.Source.USER_ENTERED,
true, true,
mode.isPrivate, mode.isPrivate,
searchEngine = engine searchEngine = engine.legacy()
) )
} else components.useCases.searchUseCases.defaultSearch.invoke(searchTerms, engine) } else {
} components.useCases.searchUseCases.defaultSearch.invoke(searchTermOrURL, engine.legacy())
}
if (!forceSearch && searchTermOrURL.isUrl()) {
loadUrlUseCase.invoke(searchTermOrURL.toNormalizedUrl(), flags)
} else {
searchUseCase.invoke(searchTermOrURL)
} }
if (components.core.engine.profiler?.isProfilerActive() == true) { if (components.core.engine.profiler?.isProfilerActive() == true) {

@ -7,6 +7,7 @@ package org.mozilla.fenix
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import mozilla.components.browser.state.action.BrowserAction import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.ContentAction import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.DownloadAction
import mozilla.components.browser.state.action.TabListAction import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.normalTabs import mozilla.components.browser.state.selector.normalTabs
@ -48,7 +49,7 @@ class TelemetryMiddleware(
} }
} }
@Suppress("TooGenericExceptionCaught") @Suppress("TooGenericExceptionCaught", "ComplexMethod")
override fun invoke( override fun invoke(
context: MiddlewareContext<BrowserState, BrowserAction>, context: MiddlewareContext<BrowserState, BrowserAction>,
next: (BrowserAction) -> Unit, next: (BrowserAction) -> Unit,
@ -86,6 +87,9 @@ class TelemetryMiddleware(
} }
} }
} }
is DownloadAction.AddDownloadAction -> {
metrics.track(Event.DownloadAdded)
}
} }
next(action) next(action)

@ -15,11 +15,13 @@ import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityEvent
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.annotation.VisibleForTesting
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.fragment_add_ons_management.addonProgressOverlay import kotlinx.android.synthetic.main.fragment_add_ons_management.addonProgressOverlay
import kotlinx.android.synthetic.main.fragment_add_ons_management.view.add_ons_empty_message import kotlinx.android.synthetic.main.fragment_add_ons_management.view.add_ons_empty_message
@ -37,6 +39,7 @@ import mozilla.components.feature.addons.ui.translateName
import io.github.forkmaintainers.iceraven.components.PagedAddonInstallationDialogFragment import io.github.forkmaintainers.iceraven.components.PagedAddonInstallationDialogFragment
import io.github.forkmaintainers.iceraven.components.PagedAddonsManagerAdapter import io.github.forkmaintainers.iceraven.components.PagedAddonsManagerAdapter
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getRootView import org.mozilla.fenix.ext.getRootView
@ -54,6 +57,8 @@ import java.util.concurrent.CancellationException
@Suppress("TooManyFunctions", "LargeClass") @Suppress("TooManyFunctions", "LargeClass")
class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management) { class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management) {
private val args by navArgs<AddonsManagementFragmentArgs>()
/** /**
* Whether or not an add-on installation is in progress. * Whether or not an add-on installation is in progress.
*/ */
@ -70,6 +75,15 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
return super.onCreateView(inflater, container, savedInstanceState) return super.onCreateView(inflater, container, savedInstanceState)
} }
private var installExternalAddonComplete: Boolean
set(value) {
arguments?.putBoolean(BUNDLE_KEY_INSTALL_EXTERNAL_ADDON_COMPLETE, value)
}
get() {
return arguments?.getBoolean(BUNDLE_KEY_INSTALL_EXTERNAL_ADDON_COMPLETE, false) ?: false
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
bindRecyclerView(view) bindRecyclerView(view)
@ -156,9 +170,13 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
val recyclerView = view.add_ons_list val recyclerView = view.add_ons_list
recyclerView.layoutManager = LinearLayoutManager(requireContext()) recyclerView.layoutManager = LinearLayoutManager(requireContext())
val shouldRefresh = adapter != null val shouldRefresh = adapter != null
// If the fragment was launched to install an "external" add-on from AMO, we deactivate
// the cache to get the most up-to-date list of add-ons to match against.
val allowCache = args.installAddonId == null || installExternalAddonComplete
lifecycleScope.launch(IO) { lifecycleScope.launch(IO) {
try { try {
addons = requireContext().components.addonManager.getAddons() addons = requireContext().components.addonManager.getAddons(allowCache = allowCache)
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
runIfFragmentIsAttached { runIfFragmentIsAttached {
if (!shouldRefresh) { if (!shouldRefresh) {
@ -177,6 +195,12 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
if (shouldRefresh) { if (shouldRefresh) {
adapter?.updateAddons(addons!!) adapter?.updateAddons(addons!!)
} }
args.installAddonId?.let { addonIn ->
if (!installExternalAddonComplete) {
installExternalAddon(addons, addonIn)
}
}
} }
} }
} catch (e: AddonManagerException) { } catch (e: AddonManagerException) {
@ -195,8 +219,32 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
} }
} }
@VisibleForTesting
internal fun installExternalAddon(supportedAddons: List<Addon>, installAddonId: String) {
val addonToInstall = supportedAddons.find { it.downloadId == installAddonId }
if (addonToInstall == null) {
showErrorSnackBar(getString(R.string.addon_not_supported_error))
} else {
if (addonToInstall.isInstalled()) {
showErrorSnackBar(getString(R.string.addon_already_installed))
} else {
showPermissionDialog(addonToInstall)
}
}
installExternalAddonComplete = true
}
@VisibleForTesting
internal fun showErrorSnackBar(text: String) {
runIfFragmentIsAttached {
view?.let {
showSnackBar(it, text, FenixSnackbar.LENGTH_LONG)
}
}
}
private fun createAddonStyle(context: Context): PagedAddonsManagerAdapter.Style { private fun createAddonStyle(context: Context): PagedAddonsManagerAdapter.Style {
return PagedAddonsManagerAdapter.Style( return AddonsManagerAdapter.Style(
sectionsTextColor = ThemeManager.resolveAttribute(R.attr.primaryText, context), sectionsTextColor = ThemeManager.resolveAttribute(R.attr.primaryText, context),
addonNameTextColor = ThemeManager.resolveAttribute(R.attr.primaryText, context), addonNameTextColor = ThemeManager.resolveAttribute(R.attr.primaryText, context),
addonSummaryTextColor = ThemeManager.resolveAttribute(R.attr.secondaryText, context), addonSummaryTextColor = ThemeManager.resolveAttribute(R.attr.secondaryText, context),
@ -218,7 +266,8 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
as? PagedAddonInstallationDialogFragment != null as? PagedAddonInstallationDialogFragment != null
} }
private fun showPermissionDialog(addon: Addon) { @VisibleForTesting
internal fun showPermissionDialog(addon: Addon) {
if (!isInstallationInProgress && !hasExistingPermissionDialogFragment()) { if (!isInstallationInProgress && !hasExistingPermissionDialogFragment()) {
val dialog = PermissionsDialogFragment.newInstance( val dialog = PermissionsDialogFragment.newInstance(
addon = addon, addon = addon,
@ -352,5 +401,6 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
companion object { companion object {
private const val PERMISSIONS_DIALOG_FRAGMENT_TAG = "ADDONS_PERMISSIONS_DIALOG_FRAGMENT" private const val PERMISSIONS_DIALOG_FRAGMENT_TAG = "ADDONS_PERMISSIONS_DIALOG_FRAGMENT"
private const val INSTALLATION_DIALOG_FRAGMENT_TAG = "ADDONS_INSTALLATION_DIALOG_FRAGMENT" private const val INSTALLATION_DIALOG_FRAGMENT_TAG = "ADDONS_INSTALLATION_DIALOG_FRAGMENT"
private const val BUNDLE_KEY_INSTALL_EXTERNAL_ADDON_COMPLETE = "INSTALL_EXTERNAL_ADDON_COMPLETE"
} }
} }

@ -14,10 +14,10 @@ import org.mozilla.fenix.components.FenixSnackbar
* @param view A [View] used to determine a parent for the [FenixSnackbar]. * @param view A [View] used to determine a parent for the [FenixSnackbar].
* @param text The text to display in the [FenixSnackbar]. * @param text The text to display in the [FenixSnackbar].
*/ */
internal fun showSnackBar(view: View, text: String) { internal fun showSnackBar(view: View, text: String, duration: Int = FenixSnackbar.LENGTH_SHORT) {
FenixSnackbar.make( FenixSnackbar.make(
view = view, view = view,
duration = FenixSnackbar.LENGTH_SHORT, duration = duration,
isDisplayedWithBrowserToolbar = true isDisplayedWithBrowserToolbar = true
) )
.setText(text) .setText(text)

@ -58,7 +58,7 @@ import mozilla.components.feature.contextmenu.ContextMenuFeature
import mozilla.components.feature.downloads.DownloadsFeature import mozilla.components.feature.downloads.DownloadsFeature
import mozilla.components.feature.downloads.manager.FetchDownloadManager import mozilla.components.feature.downloads.manager.FetchDownloadManager
import mozilla.components.feature.intent.ext.EXTRA_SESSION_ID import mozilla.components.feature.intent.ext.EXTRA_SESSION_ID
import mozilla.components.feature.media.fullscreen.MediaFullscreenOrientationFeature import mozilla.components.feature.media.fullscreen.MediaSessionFullscreenFeature
import mozilla.components.feature.privatemode.feature.SecureWindowFeature import mozilla.components.feature.privatemode.feature.SecureWindowFeature
import mozilla.components.feature.prompts.PromptFeature import mozilla.components.feature.prompts.PromptFeature
import mozilla.components.feature.prompts.share.ShareDelegate import mozilla.components.feature.prompts.share.ShareDelegate
@ -86,6 +86,7 @@ import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.IntentReceiverActivity import org.mozilla.fenix.IntentReceiverActivity
import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.OnBackLongPressedListener import org.mozilla.fenix.OnBackLongPressedListener
import org.mozilla.fenix.addons.runIfFragmentIsAttached
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.readermode.DefaultReaderModeController import org.mozilla.fenix.browser.readermode.DefaultReaderModeController
@ -118,10 +119,16 @@ import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.HomeScreenViewModel import org.mozilla.fenix.home.HomeScreenViewModel
import org.mozilla.fenix.home.SharedViewModel import org.mozilla.fenix.home.SharedViewModel
import org.mozilla.fenix.onboarding.FenixOnboarding
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.allowUndo import org.mozilla.fenix.utils.allowUndo
import org.mozilla.fenix.wifi.SitePermissionsWifiIntegration import org.mozilla.fenix.wifi.SitePermissionsWifiIntegration
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import mozilla.components.feature.media.fullscreen.MediaFullscreenOrientationFeature
import org.mozilla.fenix.FeatureFlags.newMediaSessionApi
import org.mozilla.fenix.settings.PhoneFeature
import org.mozilla.fenix.settings.quicksettings.QuickSettingsSheetDialogFragmentDirections
/** /**
* Base fragment extended by [BrowserFragment]. * Base fragment extended by [BrowserFragment].
@ -140,7 +147,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
protected val browserInteractor: BrowserToolbarViewInteractor protected val browserInteractor: BrowserToolbarViewInteractor
get() = _browserInteractor!! get() = _browserInteractor!!
private var _browserToolbarView: BrowserToolbarView? = null @VisibleForTesting
@Suppress("VariableNaming")
internal var _browserToolbarView: BrowserToolbarView? = null
@VisibleForTesting @VisibleForTesting
internal val browserToolbarView: BrowserToolbarView internal val browserToolbarView: BrowserToolbarView
get() = _browserToolbarView!! get() = _browserToolbarView!!
@ -164,6 +173,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
private val secureWindowFeature = ViewBoundFeatureWrapper<SecureWindowFeature>() private val secureWindowFeature = ViewBoundFeatureWrapper<SecureWindowFeature>()
private var fullScreenMediaFeature = private var fullScreenMediaFeature =
ViewBoundFeatureWrapper<MediaFullscreenOrientationFeature>() ViewBoundFeatureWrapper<MediaFullscreenOrientationFeature>()
private var fullScreenMediaSessionFeature =
ViewBoundFeatureWrapper<MediaSessionFullscreenFeature>()
private val searchFeature = ViewBoundFeatureWrapper<SearchFeature>() private val searchFeature = ViewBoundFeatureWrapper<SearchFeature>()
private var pipFeature: PictureInPictureFeature? = null private var pipFeature: PictureInPictureFeature? = null
@ -176,6 +187,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
private val sharedViewModel: SharedViewModel by activityViewModels() private val sharedViewModel: SharedViewModel by activityViewModels()
@VisibleForTesting
internal val onboarding by lazy { FenixOnboarding(requireContext()) }
@CallSuper @CallSuper
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -218,6 +232,11 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
} }
observeTabSelection(requireComponents.core.store) observeTabSelection(requireComponents.core.store)
if (!onboarding.userHasBeenOnboarded()) {
observeTabSource(requireComponents.core.store)
}
requireContext().accessibilityManager.addAccessibilityStateChangeListener(this) requireContext().accessibilityManager.addAccessibilityStateChangeListener(this)
} }
@ -255,6 +274,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
isPrivate = activity.browsingModeManager.mode.isPrivate isPrivate = activity.browsingModeManager.mode.isPrivate
) )
val browserToolbarController = DefaultBrowserToolbarController( val browserToolbarController = DefaultBrowserToolbarController(
store = store,
activity = activity, activity = activity,
navController = findNavController(), navController = findNavController(),
metrics = requireComponents.analytics.metrics, metrics = requireComponents.analytics.metrics,
@ -350,6 +370,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
showQuickSettingsDialog() showQuickSettingsDialog()
} }
browserToolbarView.view.display.setOnPermissionIndicatorClickedListener {
navigateToAutoplaySetting()
}
browserToolbarView.view.display.setOnTrackingProtectionClickedListener { browserToolbarView.view.display.setOnTrackingProtectionClickedListener {
context.metrics.track(Event.TrackingProtectionIconPressed) context.metrics.track(Event.TrackingProtectionIconPressed)
showTrackingProtectionPanel() showTrackingProtectionPanel()
@ -380,14 +404,25 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
view = view view = view
) )
fullScreenMediaFeature.set( if (newMediaSessionApi) {
feature = MediaFullscreenOrientationFeature( fullScreenMediaSessionFeature.set(
requireActivity(), feature = MediaSessionFullscreenFeature(
context.components.core.store requireActivity(),
), context.components.core.store
owner = this, ),
view = view owner = this,
) view = view
)
} else {
fullScreenMediaFeature.set(
feature = MediaFullscreenOrientationFeature(
requireActivity(),
context.components.core.store
),
owner = this,
view = view
)
}
val downloadFeature = DownloadsFeature( val downloadFeature = DownloadsFeature(
context.applicationContext, context.applicationContext,
@ -438,6 +473,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
val dynamicDownloadDialog = DynamicDownloadDialog( val dynamicDownloadDialog = DynamicDownloadDialog(
container = view.browserLayout, container = view.browserLayout,
downloadState = downloadState, downloadState = downloadState,
metrics = requireComponents.analytics.metrics,
didFail = downloadJobStatus == DownloadState.Status.FAILED, didFail = downloadJobStatus == DownloadState.Status.FAILED,
tryAgain = downloadFeature::tryAgain, tryAgain = downloadFeature::tryAgain,
onCannotOpenFile = { onCannotOpenFile = {
@ -561,7 +597,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
useCase.invoke(request.query) useCase.invoke(request.query)
requireActivity().startActivity(openInFenixIntent) requireActivity().startActivity(openInFenixIntent)
} else { } else {
useCase.invoke(request.query, parentSession = parentSession) useCase.invoke(request.query, parentSessionId = parentSession?.id)
} }
}, },
owner = this, owner = this,
@ -609,7 +645,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
context.settings().setSitePermissionSettingListener(viewLifecycleOwner) { context.settings().setSitePermissionSettingListener(viewLifecycleOwner) {
// If the user connects to WIFI while on the BrowserFragment, this will update the // If the user connects to WIFI while on the BrowserFragment, this will update the
// SitePermissionsRules (specifically autoplay) accordingly // SitePermissionsRules (specifically autoplay) accordingly
assignSitePermissionsRules() runIfFragmentIsAttached {
assignSitePermissionsRules()
}
} }
assignSitePermissionsRules() assignSitePermissionsRules()
@ -633,8 +671,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
.collect { tab -> pipModeChanged(tab) } .collect { tab -> pipModeChanged(tab) }
} }
view.swipeRefresh.isEnabled = view.swipeRefresh.isEnabled = shouldPullToRefreshBeEnabled()
FeatureFlags.pullToRefreshEnabled && context.settings().isPullToRefreshEnabledInBrowser
if (view.swipeRefresh.isEnabled) { if (view.swipeRefresh.isEnabled) {
val primaryTextColor = val primaryTextColor =
ThemeManager.resolveAttribute(R.attr.primaryText, context) ThemeManager.resolveAttribute(R.attr.primaryText, context)
@ -679,6 +717,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
tab -> arrayOf(tab.content.url, tab.content.loadRequest) tab -> arrayOf(tab.content.url, tab.content.loadRequest)
} }
.collect { .collect {
findInPageIntegration.onBackPressed()
browserToolbarView.expand() browserToolbarView.expand()
} }
} }
@ -749,6 +788,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
DynamicDownloadDialog( DynamicDownloadDialog(
container = view.browserLayout, container = view.browserLayout,
downloadState = savedDownloadState.first, downloadState = savedDownloadState.first,
metrics = requireComponents.analytics.metrics,
didFail = savedDownloadState.second, didFail = savedDownloadState.second,
tryAgain = onTryAgain, tryAgain = onTryAgain,
onCannotOpenFile = onCannotOpenFile, onCannotOpenFile = onCannotOpenFile,
@ -760,6 +800,13 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
browserToolbarView.expand() browserToolbarView.expand()
} }
@VisibleForTesting
internal fun shouldPullToRefreshBeEnabled(): Boolean {
return FeatureFlags.pullToRefreshEnabled &&
requireContext().settings().isPullToRefreshEnabledInBrowser &&
!(requireActivity() as HomeActivity).isImmersive
}
private fun initializeEngineView(toolbarHeight: Int) { private fun initializeEngineView(toolbarHeight: Int) {
val context = requireContext() val context = requireContext()
@ -842,6 +889,25 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
} }
} }
@VisibleForTesting
@Suppress("ComplexCondition")
internal fun observeTabSource(store: BrowserStore) {
consumeFlow(store) { flow ->
flow.mapNotNull { state ->
state.selectedTab
}
.collect {
if (!onboarding.userHasBeenOnboarded() &&
it.content.loadRequest?.triggeredByRedirect != true &&
it.source !in intentSourcesList &&
it.content.url !in onboardingLinksList
) {
onboarding.finish()
}
}
}
}
private fun handleTabSelected(selectedTab: TabSessionState) { private fun handleTabSelected(selectedTab: TabSessionState) {
if (!this.isRemoving) { if (!this.isRemoving) {
updateThemeForSession(selectedTab) updateThemeForSession(selectedTab)
@ -1120,6 +1186,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
} }
final override fun onPictureInPictureModeChanged(enabled: Boolean) { final override fun onPictureInPictureModeChanged(enabled: Boolean) {
if (enabled) requireComponents.analytics.metrics.track(Event.MediaPictureInPictureState)
pipFeature?.onPictureInPictureModeChanged(enabled) pipFeature?.onPictureInPictureModeChanged(enabled)
} }
@ -1152,6 +1219,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
browserToolbarView.expand() browserToolbarView.expand()
// Without this, fullscreen has a margin at the top. // Without this, fullscreen has a margin at the top.
engineView.setVerticalClipping(0) engineView.setVerticalClipping(0)
requireComponents.analytics.metrics.track(Event.MediaFullscreenState)
} else { } else {
activity?.exitImmersiveModeIfNeeded() activity?.exitImmersiveModeIfNeeded()
(activity as? HomeActivity)?.let { activity -> (activity as? HomeActivity)?.let { activity ->
@ -1207,6 +1276,17 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
private const val REQUEST_CODE_DOWNLOAD_PERMISSIONS = 1 private const val REQUEST_CODE_DOWNLOAD_PERMISSIONS = 1
private const val REQUEST_CODE_PROMPT_PERMISSIONS = 2 private const val REQUEST_CODE_PROMPT_PERMISSIONS = 2
private const val REQUEST_CODE_APP_PERMISSIONS = 3 private const val REQUEST_CODE_APP_PERMISSIONS = 3
val onboardingLinksList: List<String> = listOf(
SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.PRIVATE_NOTICE),
SupportUtils.getFirefoxAccountSumoUrl()
)
val intentSourcesList: List<SessionState.Source> = listOf(
SessionState.Source.ACTION_SEARCH,
SessionState.Source.ACTION_SEND,
SessionState.Source.ACTION_VIEW
)
} }
override fun onAccessibilityStateChanged(enabled: Boolean) { override fun onAccessibilityStateChanged(enabled: Boolean) {
@ -1214,4 +1294,23 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
browserToolbarView.setScrollFlags(enabled) browserToolbarView.setScrollFlags(enabled)
} }
} }
private fun navigateToAutoplaySetting() {
val directions = QuickSettingsSheetDialogFragmentDirections
.actionGlobalSitePermissionsManagePhoneFeature(PhoneFeature.AUTOPLAY_AUDIBLE)
findNavController().navigate(directions)
}
// This method is called in response to native web extension messages from
// content scripts (e.g the reader view extension). By the time these
// messages are processed the fragment/view may no longer be attached.
internal fun safeInvalidateBrowserToolbarView() {
runIfFragmentIsAttached {
val toolbarView = _browserToolbarView
if (toolbarView != null) {
toolbarView.view.invalidateActions()
toolbarView.toolbarIntegration.invalidateMenu()
}
}
}
} }

@ -5,9 +5,7 @@
package org.mozilla.fenix.browser package org.mozilla.fenix.browser
import android.content.Context import android.content.Context
import android.os.Bundle
import android.os.StrictMode import android.os.StrictMode
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
@ -30,7 +28,6 @@ import mozilla.components.feature.tabs.WindowFeature
import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.addons.runIfFragmentIsAttached
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
@ -53,17 +50,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
private var readerModeAvailable = false private var readerModeAvailable = false
private var openInAppOnboardingObserver: OpenInAppOnboardingObserver? = null private var openInAppOnboardingObserver: OpenInAppOnboardingObserver? = null
private var pwaOnboardingObserver: PwaOnboardingObserver? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view = super.onCreateView(inflater, container, savedInstanceState)
startPostponedEnterTransition()
return view
}
@Suppress("LongMethod") @Suppress("LongMethod")
override fun initializeUI(view: View): Session? { override fun initializeUI(view: View): Session? {
@ -121,11 +108,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
readerModeAvailable = available readerModeAvailable = available
readerModeAction.setSelected(active) readerModeAction.setSelected(active)
safeInvalidateBrowserToolbarView()
runIfFragmentIsAttached {
browserToolbarView.view.invalidateActions()
browserToolbarView.toolbarIntegration.invalidateMenu()
}
} }
}, },
owner = this, owner = this,
@ -156,6 +139,9 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
) { ) {
browserToolbarView.view browserToolbarView.view
} }
@Suppress("DEPRECATION")
// TODO Use browser store instead of session observer: https://github.com/mozilla-mobile/fenix/issues/16945
session?.register(toolbarSessionObserver, viewLifecycleOwner, autoPause = true) session?.register(toolbarSessionObserver, viewLifecycleOwner, autoPause = true)
if (settings.shouldShowOpenInAppCfr && session != null) { if (settings.shouldShowOpenInAppCfr && session != null) {
@ -166,6 +152,8 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
appLinksUseCases = context.components.useCases.appLinksUseCases, appLinksUseCases = context.components.useCases.appLinksUseCases,
container = browserLayout as ViewGroup container = browserLayout as ViewGroup
) )
@Suppress("DEPRECATION")
// TODO Use browser store instead of session observer: https://github.com/mozilla-mobile/fenix/issues/16949
session.register( session.register(
openInAppOnboardingObserver!!, openInAppOnboardingObserver!!,
owner = this, owner = this,
@ -174,15 +162,15 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
} }
if (!settings.userKnowsAboutPwas) { if (!settings.userKnowsAboutPwas) {
session?.register( pwaOnboardingObserver = PwaOnboardingObserver(
PwaOnboardingObserver( store = context.components.core.store,
navController = findNavController(), lifecycleOwner = this,
settings = settings, navController = findNavController(),
webAppUseCases = context.components.useCases.webAppUseCases settings = settings,
), webAppUseCases = context.components.useCases.webAppUseCases
owner = this, ).also {
autoPause = true it.start()
) }
} }
subscribeToTabCollections() subscribeToTabCollections()
@ -193,9 +181,13 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
// This observer initialized in onStart has a reference to fragment's view. // This observer initialized in onStart has a reference to fragment's view.
// Prevent it leaking the view after the latter onDestroyView. // Prevent it leaking the view after the latter onDestroyView.
if (openInAppOnboardingObserver != null) { if (openInAppOnboardingObserver != null) {
@Suppress("DEPRECATION")
// TODO Use browser store instead of session observer: https://github.com/mozilla-mobile/fenix/issues/16949
getSessionById()?.unregister(openInAppOnboardingObserver!!) getSessionById()?.unregister(openInAppOnboardingObserver!!)
openInAppOnboardingObserver = null openInAppOnboardingObserver = null
} }
pwaOnboardingObserver?.stop()
} }
private fun subscribeToTabCollections() { private fun subscribeToTabCollections() {
@ -247,7 +239,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
} }
private val collectionStorageObserver = object : TabCollectionStorage.Observer { private val collectionStorageObserver = object : TabCollectionStorage.Observer {
override fun onCollectionCreated(title: String, sessions: List<Session>) { override fun onCollectionCreated(title: String, sessions: List<Session>, id: Long?) {
showTabSavedToCollectionSnackbar(sessions.size, true) showTabSavedToCollectionSnackbar(sessions.size, true)
} }

@ -6,6 +6,7 @@ package org.mozilla.fenix.browser
import android.content.Context import android.content.Context
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.navigation.NavController import androidx.navigation.NavController
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.feature.app.links.AppLinksUseCases import mozilla.components.feature.app.links.AppLinksUseCases
@ -25,8 +26,10 @@ class OpenInAppOnboardingObserver(
private val container: ViewGroup private val container: ViewGroup
) : Session.Observer { ) : Session.Observer {
private var sessionDomainForDisplayedBanner: String? = null @VisibleForTesting
private var infoBanner: InfoBanner? = null internal var sessionDomainForDisplayedBanner: String? = null
@VisibleForTesting
internal var infoBanner: InfoBanner? = null
override fun onUrlChanged(session: Session, url: String) { override fun onUrlChanged(session: Session, url: String) {
sessionDomainForDisplayedBanner?.let { sessionDomainForDisplayedBanner?.let {
@ -36,15 +39,14 @@ class OpenInAppOnboardingObserver(
} }
} }
@Suppress("ComplexCondition")
override fun onLoadingStateChanged(session: Session, loading: Boolean) { override fun onLoadingStateChanged(session: Session, loading: Boolean) {
if (loading || settings.openLinksInExternalApp || !settings.shouldShowOpenInAppCfr) {
return
}
val appLink = appLinksUseCases.appLinkRedirect val appLink = appLinksUseCases.appLinkRedirect
if (!loading && if (appLink(session.url).hasExternalApp()) {
!settings.openLinksInExternalApp &&
settings.shouldShowOpenInAppCfr &&
appLink(session.url).hasExternalApp()
) {
infoBanner = InfoBanner( infoBanner = InfoBanner(
context = context, context = context,
message = context.getString(R.string.open_in_app_cfr_info_message), message = context.getString(R.string.open_in_app_cfr_info_message),

@ -13,8 +13,8 @@ import android.widget.FrameLayout
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.doOnNextLayout import androidx.core.view.doOnNextLayout
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import kotlinx.android.synthetic.main.mozac_ui_tabcounter_layout.view.*
import kotlinx.android.synthetic.main.tab_preview.view.* import kotlinx.android.synthetic.main.tab_preview.view.*
import kotlinx.android.synthetic.main.tabs_tray_tab_counter.view.*
import mozilla.components.browser.thumbnails.loader.ThumbnailLoader import mozilla.components.browser.thumbnails.loader.ThumbnailLoader
import mozilla.components.concept.base.images.ImageLoadRequest import mozilla.components.concept.base.images.ImageLoadRequest
import org.mozilla.fenix.R import org.mozilla.fenix.R

@ -8,13 +8,20 @@ import android.app.Application
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.StrictMode
import mozilla.components.lib.crash.CrashReporter import mozilla.components.lib.crash.CrashReporter
import mozilla.components.lib.crash.service.CrashReporterService import mozilla.components.lib.crash.service.CrashReporterService
import mozilla.components.lib.crash.service.GleanCrashReporterService import mozilla.components.lib.crash.service.GleanCrashReporterService
import mozilla.components.lib.crash.service.MozillaSocorroService import mozilla.components.lib.crash.service.MozillaSocorroService
import mozilla.components.lib.crash.service.SentryService import mozilla.components.lib.crash.service.SentryService
import mozilla.components.service.nimbus.NimbusApi
import mozilla.components.service.nimbus.Nimbus as NimbusEnabled
import mozilla.components.service.nimbus.NimbusDisabled
import mozilla.components.service.nimbus.NimbusServerSettings
import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.Config import org.mozilla.fenix.Config
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ReleaseChannel import org.mozilla.fenix.ReleaseChannel
@ -22,6 +29,7 @@ import org.mozilla.fenix.components.metrics.AdjustMetricsService
import org.mozilla.fenix.components.metrics.GleanMetricsService import org.mozilla.fenix.components.metrics.GleanMetricsService
import org.mozilla.fenix.components.metrics.LeanplumMetricsService import org.mozilla.fenix.components.metrics.LeanplumMetricsService
import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.lazyMonitored import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.utils.Mockable import org.mozilla.fenix.utils.Mockable
@ -90,7 +98,7 @@ class Analytics(
val metrics: MetricController by lazyMonitored { val metrics: MetricController by lazyMonitored {
MetricController.create( MetricController.create(
listOf( listOf(
GleanMetricsService(context), GleanMetricsService(context, lazy { context.components.core.store }),
leanplumMetricsService, leanplumMetricsService,
AdjustMetricsService(context as Application) AdjustMetricsService(context as Application)
), ),
@ -98,6 +106,38 @@ class Analytics(
isMarketingDataTelemetryEnabled = { context.settings().isMarketingTelemetryEnabled } isMarketingDataTelemetryEnabled = { context.settings().isMarketingTelemetryEnabled }
) )
} }
val experiments: NimbusApi by lazyMonitored {
if (FeatureFlags.nimbusExperiments) {
// Eventually we'll want to use `NimbusDisabled` when we have no NIMBUS_ENDPOINT.
// but we keep this here to not mix feature flags and how we configure Nimbus.
val url: String? = BuildConfig.NIMBUS_ENDPOINT
val serverSettings = if (!url.isNullOrBlank()) {
NimbusServerSettings(url = Uri.parse(url))
} else {
null
}
NimbusEnabled(context, serverSettings).apply {
// Global opt out state is stored in Nimbus, and shouldn't be toggled to `true`
// from the app unless the user does so from a UI control.
// However, the user may have opt-ed out of mako experiments already, so
// we should respect that setting here.
val enabled =
context.components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
context.settings().isExperimentationEnabled
}
if (!enabled) {
globalUserParticipation = enabled
}
// Nimbus should look after downloading experiment definitions from remote settings
// on another thread, and making sure we don't hit the server each time we start.
updateExperiments()
}
} else {
NimbusDisabled()
}
}
} }
fun isSentryEnabled() = !BuildConfig.SENTRY_TOKEN.isNullOrEmpty() fun isSentryEnabled() = !BuildConfig.SENTRY_TOKEN.isNullOrEmpty()

@ -53,14 +53,12 @@ class Components(private val context: Context) {
} }
val services by lazyMonitored { Services(context, backgroundServices.accountManager) } val services by lazyMonitored { Services(context, backgroundServices.accountManager) }
val core by lazyMonitored { Core(context, analytics.crashReporter, strictMode) } val core by lazyMonitored { Core(context, analytics.crashReporter, strictMode) }
val search by lazyMonitored { Search(context) }
val useCases by lazyMonitored { val useCases by lazyMonitored {
UseCases( UseCases(
context, context,
core.engine, core.engine,
core.sessionManager, core.sessionManager,
core.store, core.store,
search.searchEngineManager,
core.webAppShortcutManager, core.webAppShortcutManager,
core.topSitesStorage core.topSitesStorage
) )
@ -69,7 +67,9 @@ class Components(private val context: Context) {
IntentProcessors( IntentProcessors(
context, context,
core.sessionManager, core.sessionManager,
core.store,
useCases.sessionUseCases, useCases.sessionUseCases,
useCases.tabsUseCases,
useCases.searchUseCases, useCases.searchUseCases,
core.relationChecker, core.relationChecker,
core.customTabsStore, core.customTabsStore,

@ -23,7 +23,6 @@ import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.session.engine.EngineMiddleware import mozilla.components.browser.session.engine.EngineMiddleware
import mozilla.components.browser.session.storage.SessionStorage import mozilla.components.browser.session.storage.SessionStorage
import mozilla.components.browser.session.undo.UndoMiddleware import mozilla.components.browser.session.undo.UndoMiddleware
import mozilla.components.browser.state.action.RecentlyClosedAction
import mozilla.components.browser.state.action.RestoreCompleteAction import mozilla.components.browser.state.action.RestoreCompleteAction
import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
@ -40,12 +39,13 @@ import mozilla.components.concept.fetch.Client
import mozilla.components.feature.customtabs.store.CustomTabsServiceStore import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
import mozilla.components.feature.downloads.DownloadMiddleware import mozilla.components.feature.downloads.DownloadMiddleware
import mozilla.components.feature.logins.exceptions.LoginExceptionStorage import mozilla.components.feature.logins.exceptions.LoginExceptionStorage
import mozilla.components.feature.media.RecordingDevicesNotificationFeature import mozilla.components.feature.media.middleware.RecordingDevicesMiddleware
import mozilla.components.feature.media.middleware.MediaMiddleware
import mozilla.components.feature.pwa.ManifestStorage import mozilla.components.feature.pwa.ManifestStorage
import mozilla.components.feature.pwa.WebAppShortcutManager import mozilla.components.feature.pwa.WebAppShortcutManager
import mozilla.components.feature.readerview.ReaderViewMiddleware import mozilla.components.feature.readerview.ReaderViewMiddleware
import mozilla.components.feature.recentlyclosed.RecentlyClosedMiddleware import mozilla.components.feature.recentlyclosed.RecentlyClosedMiddleware
import mozilla.components.feature.search.middleware.SearchMiddleware
import mozilla.components.feature.search.region.RegionMiddleware
import mozilla.components.feature.session.HistoryDelegate import mozilla.components.feature.session.HistoryDelegate
import mozilla.components.feature.top.sites.DefaultTopSitesStorage import mozilla.components.feature.top.sites.DefaultTopSitesStorage
import mozilla.components.feature.top.sites.PinnedSiteStorage import mozilla.components.feature.top.sites.PinnedSiteStorage
@ -57,19 +57,22 @@ import mozilla.components.lib.dataprotect.generateEncryptionKey
import mozilla.components.service.digitalassetlinks.RelationChecker import mozilla.components.service.digitalassetlinks.RelationChecker
import mozilla.components.service.digitalassetlinks.local.StatementApi import mozilla.components.service.digitalassetlinks.local.StatementApi
import mozilla.components.service.digitalassetlinks.local.StatementRelationChecker import mozilla.components.service.digitalassetlinks.local.StatementRelationChecker
import mozilla.components.service.location.LocationService
import mozilla.components.service.location.MozillaLocationService
import mozilla.components.service.sync.logins.SyncableLoginsStorage import mozilla.components.service.sync.logins.SyncableLoginsStorage
import mozilla.components.support.locale.LocaleManager import mozilla.components.support.locale.LocaleManager
import org.mozilla.fenix.AppRequestInterceptor import org.mozilla.fenix.AppRequestInterceptor
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.Config import org.mozilla.fenix.Config
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.perf.StrictModeManager import org.mozilla.fenix.perf.StrictModeManager
import org.mozilla.fenix.TelemetryMiddleware import org.mozilla.fenix.TelemetryMiddleware
import org.mozilla.fenix.components.search.SearchMigration
import org.mozilla.fenix.downloads.DownloadService import org.mozilla.fenix.downloads.DownloadService
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.lazyMonitored import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.media.MediaService
import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry
import org.mozilla.fenix.search.telemetry.incontent.InContentTelemetry import org.mozilla.fenix.search.telemetry.incontent.InContentTelemetry
import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.settings.SupportUtils
@ -77,6 +80,11 @@ import org.mozilla.fenix.settings.advanced.getSelectedLocale
import org.mozilla.fenix.utils.Mockable import org.mozilla.fenix.utils.Mockable
import org.mozilla.fenix.utils.getUndoDelay import org.mozilla.fenix.utils.getUndoDelay
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import mozilla.components.feature.media.MediaSessionFeature
import mozilla.components.feature.media.middleware.MediaMiddleware
import org.mozilla.fenix.FeatureFlags.newMediaSessionApi
import org.mozilla.fenix.media.MediaService
import org.mozilla.fenix.media.MediaSessionService
/** /**
* Component group for all core browser functionality. * Component group for all core browser functionality.
@ -94,7 +102,7 @@ class Core(
*/ */
val engine: Engine by lazyMonitored { val engine: Engine by lazyMonitored {
val defaultSettings = DefaultSettings( val defaultSettings = DefaultSettings(
requestInterceptor = AppRequestInterceptor(context), requestInterceptor = requestInterceptor,
remoteDebuggingEnabled = context.settings().isRemoteDebuggingEnabled && remoteDebuggingEnabled = context.settings().isRemoteDebuggingEnabled &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M, Build.VERSION.SDK_INT >= Build.VERSION_CODES.M,
testingModeEnabled = false, testingModeEnabled = false,
@ -135,6 +143,15 @@ class Core(
} }
} }
/**
* Passed to [engine] to intercept requests for app links,
* and various features triggered by page load requests.
*
* NB: This does not need to be lazy as it is initialized
* with the engine on startup.
*/
val requestInterceptor = AppRequestInterceptor(context)
/** /**
* [Client] implementation to be used for code depending on `concept-fetch`` * [Client] implementation to be used for code depending on `concept-fetch``
*/ */
@ -153,14 +170,21 @@ class Core(
SessionStorage(context, engine = engine) SessionStorage(context, engine = engine)
} }
private val locationService: LocationService by lazyMonitored {
if (Config.channel.isDebug || BuildConfig.MLS_TOKEN.isEmpty()) {
LocationService.default()
} else {
MozillaLocationService(context, client, BuildConfig.MLS_TOKEN)
}
}
/** /**
* The [BrowserStore] holds the global [BrowserState]. * The [BrowserStore] holds the global [BrowserState].
*/ */
val store by lazyMonitored { val store by lazyMonitored {
BrowserStore( val middlewareList =
middleware = listOf( mutableListOf(
RecentlyClosedMiddleware(context, RECENTLY_CLOSED_MAX, engine), RecentlyClosedMiddleware(context, RECENTLY_CLOSED_MAX, engine),
MediaMiddleware(context, MediaService::class.java),
DownloadMiddleware(context, DownloadService::class.java), DownloadMiddleware(context, DownloadService::class.java),
ReaderViewMiddleware(), ReaderViewMiddleware(),
TelemetryMiddleware( TelemetryMiddleware(
@ -169,11 +193,23 @@ class Core(
metrics metrics
), ),
ThumbnailsMiddleware(thumbnailStorage), ThumbnailsMiddleware(thumbnailStorage),
UndoMiddleware(::lookupSessionManager, context.getUndoDelay()) UndoMiddleware(::lookupSessionManager, context.getUndoDelay()),
) + EngineMiddleware.create(engine, ::findSessionById) RegionMiddleware(context, locationService),
).also { SearchMiddleware(
it.dispatch(RecentlyClosedAction.InitializeRecentlyClosedState) context,
additionalBundledSearchEngineIds = listOf("reddit", "youtube"),
migration = SearchMigration(context)
),
RecordingDevicesMiddleware(context)
)
if (!newMediaSessionApi) {
middlewareList.add(MediaMiddleware(context, MediaService::class.java))
} }
BrowserStore(
middleware = middlewareList + EngineMiddleware.create(engine, ::findSessionById)
)
} }
private fun lookupSessionManager(): SessionManager { private fun lookupSessionManager(): SessionManager {
@ -213,10 +249,6 @@ class Core(
// Install the "cookies" WebExtension and tracks user interaction with SERPs. // Install the "cookies" WebExtension and tracks user interaction with SERPs.
searchTelemetry.install(engine, store) searchTelemetry.install(engine, store)
// Show an ongoing notification when recording devices (camera, microphone) are used by web content
RecordingDevicesNotificationFeature(context, sessionManager)
.enable()
// Restore the previous state. // Restore the previous state.
GlobalScope.launch(Dispatchers.Main) { GlobalScope.launch(Dispatchers.Main) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@ -255,6 +287,10 @@ class Core(
context, engine, icons, R.drawable.ic_status_logo, context, engine, icons, R.drawable.ic_status_logo,
permissionStorage.permissionsStorage, HomeActivity::class.java permissionStorage.permissionsStorage, HomeActivity::class.java
) )
if (newMediaSessionApi) {
MediaSessionFeature(context, MediaSessionService::class.java, store).start()
}
} }
} }

@ -6,6 +6,7 @@ package org.mozilla.fenix.components
import android.content.Context import android.content.Context
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.customtabs.CustomTabIntentProcessor import mozilla.components.feature.customtabs.CustomTabIntentProcessor
import mozilla.components.feature.customtabs.store.CustomTabsServiceStore import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
import mozilla.components.feature.intent.processing.TabIntentProcessor import mozilla.components.feature.intent.processing.TabIntentProcessor
@ -14,6 +15,7 @@ import mozilla.components.feature.pwa.intent.TrustedWebActivityIntentProcessor
import mozilla.components.feature.pwa.intent.WebAppIntentProcessor import mozilla.components.feature.pwa.intent.WebAppIntentProcessor
import mozilla.components.feature.search.SearchUseCases import mozilla.components.feature.search.SearchUseCases
import mozilla.components.feature.session.SessionUseCases import mozilla.components.feature.session.SessionUseCases
import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.service.digitalassetlinks.RelationChecker import mozilla.components.service.digitalassetlinks.RelationChecker
import mozilla.components.support.migration.MigrationIntentProcessor import mozilla.components.support.migration.MigrationIntentProcessor
import mozilla.components.support.migration.state.MigrationStore import mozilla.components.support.migration.state.MigrationStore
@ -30,7 +32,9 @@ import org.mozilla.fenix.utils.Mockable
class IntentProcessors( class IntentProcessors(
private val context: Context, private val context: Context,
private val sessionManager: SessionManager, private val sessionManager: SessionManager,
private val store: BrowserStore,
private val sessionUseCases: SessionUseCases, private val sessionUseCases: SessionUseCases,
private val tabsUseCases: TabsUseCases,
private val searchUseCases: SearchUseCases, private val searchUseCases: SearchUseCases,
private val relationChecker: RelationChecker, private val relationChecker: RelationChecker,
private val customTabsStore: CustomTabsServiceStore, private val customTabsStore: CustomTabsServiceStore,
@ -62,13 +66,12 @@ class IntentProcessors(
val externalAppIntentProcessors by lazyMonitored { val externalAppIntentProcessors by lazyMonitored {
listOf( listOf(
TrustedWebActivityIntentProcessor( TrustedWebActivityIntentProcessor(
sessionManager = sessionManager, addNewTabUseCase = tabsUseCases.addTab,
loadUrlUseCase = sessionUseCases.loadUrl,
packageManager = context.packageManager, packageManager = context.packageManager,
relationChecker = relationChecker, relationChecker = relationChecker,
store = customTabsStore store = customTabsStore
), ),
WebAppIntentProcessor(sessionManager, sessionUseCases.loadUrl, manifestStorage), WebAppIntentProcessor(store, tabsUseCases.addTab, sessionUseCases.loadUrl, manifestStorage),
FennecWebAppIntentProcessor(context, sessionManager, sessionUseCases.loadUrl, manifestStorage) FennecWebAppIntentProcessor(context, sessionManager, sessionUseCases.loadUrl, manifestStorage)
) )
} }

@ -1,37 +0,0 @@
/* 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.components
import android.content.Context
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import mozilla.components.browser.search.SearchEngineManager
import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.utils.Mockable
/**
* Component group for all search engine integration related functionality.
*/
@Mockable
class Search(private val context: Context) {
val provider = FenixSearchEngineProvider(context)
/**
* This component provides access to a centralized registry of search engines.
*/
val searchEngineManager by lazyMonitored {
SearchEngineManager(
coroutineContext = IO,
providers = listOf(provider)
).apply {
registerForLocaleUpdates(context)
GlobalScope.launch {
defaultSearchEngine = provider.getDefaultEngine(context)
}
}
}
}

@ -40,7 +40,7 @@ class TabCollectionStorage(
/** /**
* A collection has been created * A collection has been created
*/ */
fun onCollectionCreated(title: String, sessions: List<Session>) = Unit fun onCollectionCreated(title: String, sessions: List<Session>, id: Long?) = Unit
/** /**
* Tab(s) have been added to collection * Tab(s) have been added to collection
@ -63,8 +63,8 @@ class TabCollectionStorage(
} }
suspend fun createCollection(title: String, sessions: List<Session>) = ioScope.launch { suspend fun createCollection(title: String, sessions: List<Session>) = ioScope.launch {
collectionStorage.createCollection(title, sessions) val id = collectionStorage.createCollection(title, sessions)
notifyObservers { onCollectionCreated(title, sessions) } notifyObservers { onCollectionCreated(title, sessions, id) }
}.join() }.join()
suspend fun addTabsToCollection(tabCollection: TabCollection, sessions: List<Session>) = ioScope.launch { suspend fun addTabsToCollection(tabCollection: TabCollection, sessions: List<Session>) = ioScope.launch {

@ -5,7 +5,6 @@
package org.mozilla.fenix.components package org.mozilla.fenix.components
import android.content.Context import android.content.Context
import mozilla.components.browser.search.SearchEngineManager
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine import mozilla.components.concept.engine.Engine
@ -15,7 +14,7 @@ import mozilla.components.feature.downloads.DownloadsUseCases
import mozilla.components.feature.pwa.WebAppShortcutManager import mozilla.components.feature.pwa.WebAppShortcutManager
import mozilla.components.feature.pwa.WebAppUseCases import mozilla.components.feature.pwa.WebAppUseCases
import mozilla.components.feature.search.SearchUseCases import mozilla.components.feature.search.SearchUseCases
import mozilla.components.browser.search.ext.toDefaultSearchEngineProvider import mozilla.components.feature.search.ext.toDefaultSearchEngineProvider
import mozilla.components.feature.session.SessionUseCases import mozilla.components.feature.session.SessionUseCases
import mozilla.components.feature.session.SettingsUseCases import mozilla.components.feature.session.SettingsUseCases
import mozilla.components.feature.session.TrackingProtectionUseCases import mozilla.components.feature.session.TrackingProtectionUseCases
@ -36,7 +35,6 @@ class UseCases(
private val engine: Engine, private val engine: Engine,
private val sessionManager: SessionManager, private val sessionManager: SessionManager,
private val store: BrowserStore, private val store: BrowserStore,
private val searchEngineManager: SearchEngineManager,
private val shortcutManager: WebAppShortcutManager, private val shortcutManager: WebAppShortcutManager,
private val topSitesStorage: TopSitesStorage private val topSitesStorage: TopSitesStorage
) { ) {
@ -56,8 +54,8 @@ class UseCases(
val searchUseCases by lazyMonitored { val searchUseCases by lazyMonitored {
SearchUseCases( SearchUseCases(
store, store,
searchEngineManager.toDefaultSearchEngineProvider(context), store.toDefaultSearchEngineProvider(),
sessionManager tabsUseCases
) )
} }
@ -69,7 +67,7 @@ class UseCases(
val appLinksUseCases by lazyMonitored { AppLinksUseCases(context.applicationContext) } val appLinksUseCases by lazyMonitored { AppLinksUseCases(context.applicationContext) }
val webAppUseCases by lazyMonitored { val webAppUseCases by lazyMonitored {
WebAppUseCases(context, sessionManager, shortcutManager) WebAppUseCases(context, store, shortcutManager)
} }
val downloadUseCases by lazyMonitored { DownloadsUseCases(store) } val downloadUseCases by lazyMonitored { DownloadsUseCases(store) }

@ -6,7 +6,7 @@ package org.mozilla.fenix.components.metrics
import android.content.Context import android.content.Context
import mozilla.components.browser.errorpages.ErrorType import mozilla.components.browser.errorpages.ErrorType
import mozilla.components.browser.search.SearchEngine import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.feature.top.sites.TopSite import mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.GleanMetrics.Addons import org.mozilla.fenix.GleanMetrics.Addons
import org.mozilla.fenix.GleanMetrics.AppTheme import org.mozilla.fenix.GleanMetrics.AppTheme
@ -106,6 +106,8 @@ sealed class Event {
object MediaPlayState : Event() object MediaPlayState : Event()
object MediaPauseState : Event() object MediaPauseState : Event()
object MediaStopState : Event() object MediaStopState : Event()
object MediaFullscreenState : Event()
object MediaPictureInPictureState : Event()
object InAppNotificationDownloadOpen : Event() object InAppNotificationDownloadOpen : Event()
object InAppNotificationDownloadTryAgain : Event() object InAppNotificationDownloadTryAgain : Event()
object NotificationDownloadCancel : Event() object NotificationDownloadCancel : Event()
@ -113,6 +115,10 @@ sealed class Event {
object NotificationDownloadPause : Event() object NotificationDownloadPause : Event()
object NotificationDownloadResume : Event() object NotificationDownloadResume : Event()
object NotificationDownloadTryAgain : Event() object NotificationDownloadTryAgain : Event()
object DownloadAdded : Event()
object DownloadsScreenOpened : Event()
object DownloadsItemOpened : Event()
object DownloadsItemDeleted : Event()
object NotificationMediaPlay : Event() object NotificationMediaPlay : Event()
object NotificationMediaPause : Event() object NotificationMediaPause : Event()
object TopSiteOpenDefault : Event() object TopSiteOpenDefault : Event()
@ -183,6 +189,12 @@ sealed class Event {
object TabSettingsOpened : Event() object TabSettingsOpened : Event()
object CopyUrlUsed : Event()
object SyncedTabOpened : Event()
object RecentlyClosedTabsOpened : Event()
// Interaction events with extras // Interaction events with extras
data class TopSiteSwipeCarousel(val page: Int) : Event() { data class TopSiteSwipeCarousel(val page: Int) : Event() {
@ -404,7 +416,7 @@ sealed class Event {
// https://github.com/mozilla-mobile/fenix/issues/1607 // https://github.com/mozilla-mobile/fenix/issues/1607
// Sanitize identifiers for custom search engines. // Sanitize identifiers for custom search engines.
val identifier: String val identifier: String
get() = if (isCustom) "custom" else engine.identifier get() = if (isCustom) "custom" else engine.id
val searchEngine: SearchEngine val searchEngine: SearchEngine
get() = when (this) { get() = when (this) {

@ -5,6 +5,9 @@
package org.mozilla.fenix.components.metrics package org.mozilla.fenix.components.metrics
import android.content.Context import android.content.Context
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.search.ext.legacy
import mozilla.components.feature.search.ext.waitForSelectedOrDefaultSearchEngine
import mozilla.components.service.fxa.manager.SyncEnginesStorage import mozilla.components.service.fxa.manager.SyncEnginesStorage
import mozilla.components.service.glean.Glean import mozilla.components.service.glean.Glean
import mozilla.components.service.glean.private.NoExtraKeys import mozilla.components.service.glean.private.NoExtraKeys
@ -22,6 +25,8 @@ import org.mozilla.fenix.GleanMetrics.ContextualHintTrackingProtection
import org.mozilla.fenix.GleanMetrics.CrashReporter import org.mozilla.fenix.GleanMetrics.CrashReporter
import org.mozilla.fenix.GleanMetrics.CustomTab import org.mozilla.fenix.GleanMetrics.CustomTab
import org.mozilla.fenix.GleanMetrics.DownloadNotification import org.mozilla.fenix.GleanMetrics.DownloadNotification
import org.mozilla.fenix.GleanMetrics.DownloadsMisc
import org.mozilla.fenix.GleanMetrics.DownloadsManagement
import org.mozilla.fenix.GleanMetrics.ErrorPage import org.mozilla.fenix.GleanMetrics.ErrorPage
import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.FindInPage import org.mozilla.fenix.GleanMetrics.FindInPage
@ -399,6 +404,12 @@ private val Event.wrapper: EventWrapper<*>?
is Event.MediaStopState -> EventWrapper<NoExtraKeys>( is Event.MediaStopState -> EventWrapper<NoExtraKeys>(
{ MediaState.stop.record(it) } { MediaState.stop.record(it) }
) )
is Event.MediaFullscreenState -> EventWrapper<NoExtraKeys>(
{ MediaState.fullscreen.record(it) }
)
is Event.MediaPictureInPictureState -> EventWrapper<NoExtraKeys>(
{ MediaState.pictureInPicture.record(it) }
)
is Event.InAppNotificationDownloadOpen -> EventWrapper<NoExtraKeys>( is Event.InAppNotificationDownloadOpen -> EventWrapper<NoExtraKeys>(
{ DownloadNotification.inAppOpen.record(it) } { DownloadNotification.inAppOpen.record(it) }
) )
@ -420,6 +431,18 @@ private val Event.wrapper: EventWrapper<*>?
is Event.NotificationDownloadTryAgain -> EventWrapper<NoExtraKeys>( is Event.NotificationDownloadTryAgain -> EventWrapper<NoExtraKeys>(
{ DownloadNotification.tryAgain.record(it) } { DownloadNotification.tryAgain.record(it) }
) )
is Event.DownloadAdded -> EventWrapper<NoExtraKeys>(
{ DownloadsMisc.downloadAdded.record(it) }
)
is Event.DownloadsScreenOpened -> EventWrapper<NoExtraKeys>(
{ DownloadsManagement.downloadsScreenOpened.record(it) }
)
is Event.DownloadsItemOpened -> EventWrapper<NoExtraKeys>(
{ DownloadsManagement.itemOpened.record(it) }
)
is Event.DownloadsItemDeleted -> EventWrapper<NoExtraKeys>(
{ DownloadsManagement.itemDeleted.record(it) }
)
is Event.NotificationMediaPlay -> EventWrapper<NoExtraKeys>( is Event.NotificationMediaPlay -> EventWrapper<NoExtraKeys>(
{ MediaNotification.play.record(it) } { MediaNotification.play.record(it) }
) )
@ -656,6 +679,17 @@ private val Event.wrapper: EventWrapper<*>?
{ ProgressiveWebApp.background.record(it) }, { ProgressiveWebApp.background.record(it) },
{ ProgressiveWebApp.backgroundKeys.valueOf(it) } { ProgressiveWebApp.backgroundKeys.valueOf(it) }
) )
is Event.CopyUrlUsed -> EventWrapper<NoExtraKeys>(
{ Events.copyUrlTapped.record(it) }
)
is Event.SyncedTabOpened -> EventWrapper<NoExtraKeys>(
{ Events.syncedTabOpened.record(it) }
)
is Event.RecentlyClosedTabsOpened -> EventWrapper<NoExtraKeys>(
{ Events.recentlyClosedTabsOpened.record(it) }
)
Event.MasterPasswordMigrationDisplayed -> EventWrapper<NoExtraKeys>( Event.MasterPasswordMigrationDisplayed -> EventWrapper<NoExtraKeys>(
{ MasterPassword.displayed.record(it) } { MasterPassword.displayed.record(it) }
@ -683,6 +717,7 @@ private val Event.wrapper: EventWrapper<*>?
class GleanMetricsService( class GleanMetricsService(
private val context: Context, private val context: Context,
private val store: Lazy<BrowserStore>,
private val browsersCache: BrowsersCache = BrowsersCache, private val browsersCache: BrowsersCache = BrowsersCache,
private val mozillaProductDetector: MozillaProductDetector = MozillaProductDetector private val mozillaProductDetector: MozillaProductDetector = MozillaProductDetector
) : MetricsService { ) : MetricsService {
@ -754,6 +789,18 @@ class GleanMetricsService(
topSitesCount.add(topSitesSize) topSitesCount.add(topSitesSize)
} }
val desktopBookmarksSize = context.settings().desktopBookmarksSize
hasDesktopBookmarks.set(desktopBookmarksSize > 0)
if (desktopBookmarksSize > 0) {
desktopBookmarksCount.add(desktopBookmarksSize)
}
val mobileBookmarksSize = context.settings().mobileBookmarksSize
hasMobileBookmarks.set(mobileBookmarksSize > 0)
if (mobileBookmarksSize > 0) {
mobileBookmarksCount.add(mobileBookmarksSize)
}
toolbarPosition.set( toolbarPosition.set(
when (context.settings().toolbarPosition) { when (context.settings().toolbarPosition) {
ToolbarPosition.BOTTOM -> Event.ToolbarPositionChanged.Position.BOTTOM.name ToolbarPosition.BOTTOM -> Event.ToolbarPositionChanged.Position.BOTTOM.name
@ -765,20 +812,18 @@ class GleanMetricsService(
closeTabSetting.set(context.settings().getTabTimeoutPingString()) closeTabSetting.set(context.settings().getTabTimeoutPingString())
} }
SearchDefaultEngine.apply { store.value.waitForSelectedOrDefaultSearchEngine { searchEngine ->
val defaultEngine = context if (searchEngine != null) {
.components SearchDefaultEngine.apply {
.search code.set(searchEngine.id)
.searchEngineManager name.set(searchEngine.name)
.defaultSearchEngine ?: return@apply submissionUrl.set(searchEngine.legacy().buildSearchUrl(""))
}
}
code.set(defaultEngine.identifier) activationPing.checkAndSend()
name.set(defaultEngine.name) installationPing.checkAndSend()
submissionUrl.set(defaultEngine.buildSearchUrl(""))
} }
activationPing.checkAndSend()
installationPing.checkAndSend()
} }
private fun setPreferenceMetrics() { private fun setPreferenceMetrics() {

@ -12,11 +12,12 @@ import com.google.android.gms.common.GooglePlayServicesNotAvailableException
import com.google.android.gms.common.GooglePlayServicesRepairableException import com.google.android.gms.common.GooglePlayServicesRepairableException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import mozilla.components.browser.search.SearchEngine import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.support.base.log.logger.Logger import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.components.metrics.Event.PerformedSearch.SearchAccessPoint import org.mozilla.fenix.components.metrics.Event.PerformedSearch.SearchAccessPoint
import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.searchEngineManager
import java.io.IOException import java.io.IOException
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
import java.security.spec.InvalidKeySpecException import java.security.spec.InvalidKeySpecException
@ -26,11 +27,11 @@ import javax.crypto.spec.PBEKeySpec
object MetricsUtils { object MetricsUtils {
fun createSearchEvent( fun createSearchEvent(
engine: SearchEngine, engine: SearchEngine,
context: Context, store: BrowserStore,
searchAccessPoint: SearchAccessPoint searchAccessPoint: SearchAccessPoint
): Event.PerformedSearch? { ): Event.PerformedSearch? {
val isShortcut = engine != context.searchEngineManager.defaultSearchEngine val isShortcut = engine != store.state.search.selectedOrDefaultSearchEngine
val isCustom = CustomSearchEngineStore.isCustomSearchEngine(context, engine.identifier) val isCustom = engine.type == SearchEngine.Type.CUSTOM
val engineSource = val engineSource =
if (isShortcut) Event.PerformedSearch.EngineSource.Shortcut(engine, isCustom) if (isShortcut) Event.PerformedSearch.EngineSource.Shortcut(engine, isCustom)

@ -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.components.search
import android.content.Context
import android.content.SharedPreferences
import mozilla.components.browser.search.SearchEngineParser
import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.feature.search.ext.migrate
import mozilla.components.feature.search.middleware.SearchMiddleware
import org.mozilla.fenix.ext.components
import org.xmlpull.v1.XmlPullParserException
import java.io.BufferedInputStream
import java.io.IOException
private const val PREF_FILE_SEARCH_ENGINES = "custom-search-engines"
private const val PREF_KEY_CUSTOM_SEARCH_ENGINES = "pref_custom_search_engines"
private const val PREF_KEY_MIGRATED = "pref_search_migrated"
/**
* Helper class to migrate the search related data in Fenix to the "Android Components" implementation.
*/
internal class SearchMigration(
private val context: Context
) : SearchMiddleware.Migration {
override fun getValuesToMigrate(): SearchMiddleware.Migration.MigrationValues? {
val preferences = context.getSharedPreferences(PREF_FILE_SEARCH_ENGINES, Context.MODE_PRIVATE)
if (preferences.getBoolean(PREF_KEY_MIGRATED, false)) {
return null
}
val values = SearchMiddleware.Migration.MigrationValues(
customSearchEngines = loadCustomSearchEngines(preferences),
defaultSearchEngineName = context.components.settings.defaultSearchEngineName
)
preferences.edit()
.putBoolean(PREF_KEY_MIGRATED, true)
.apply()
return values
}
private fun loadCustomSearchEngines(
preferences: SharedPreferences
): List<SearchEngine> {
val ids = preferences.getStringSet(PREF_KEY_CUSTOM_SEARCH_ENGINES, emptySet()) ?: emptySet()
val parser = SearchEngineParser()
return ids.mapNotNull { id ->
val xml = preferences.getString(id, null)
parser.loadSafely(id, xml?.byteInputStream()?.buffered())
}
}
}
private fun SearchEngineParser.loadSafely(id: String, stream: BufferedInputStream?): SearchEngine? {
return try {
stream?.let { load(id, it).migrate() }
} catch (e: IOException) {
null
} catch (e: XmlPullParserException) {
null
}
}

@ -1,129 +0,0 @@
/* 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.components.searchengine
import android.content.Context
import android.content.SharedPreferences
import mozilla.components.browser.icons.IconRequest
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.search.SearchEngineParser
import mozilla.components.browser.search.provider.SearchEngineList
import mozilla.components.browser.search.provider.SearchEngineProvider
import mozilla.components.support.ktx.android.content.PreferencesHolder
import mozilla.components.support.ktx.android.content.stringSetPreference
import org.mozilla.fenix.ext.components
/**
* SearchEngineProvider implementation to load user entered custom search engines.
*/
class CustomSearchEngineProvider : SearchEngineProvider {
override suspend fun loadSearchEngines(context: Context): SearchEngineList {
return SearchEngineList(CustomSearchEngineStore.loadCustomSearchEngines(context), null)
}
}
/**
* Object to handle storing custom search engines
*/
object CustomSearchEngineStore {
class EngineNameAlreadyExists : Exception()
/**
* Add a search engine to the store.
* @param context [Context] used for various Android interactions.
* @param engineName The name of the search engine
* @param searchQuery The templated search string for the search engine
* @throws EngineNameAlreadyExists if you try to add a search engine that already exists
*/
suspend fun addSearchEngine(context: Context, engineName: String, searchQuery: String) {
val storage = engineStorage(context)
if (storage.customSearchEngineIds.contains(engineName)) { throw EngineNameAlreadyExists() }
val icon = context.components.core.icons.loadIcon(IconRequest(searchQuery)).await()
val searchEngineXml = SearchEngineWriter.buildSearchEngineXML(engineName, searchQuery, icon.bitmap)
val engines = storage.customSearchEngineIds.toMutableSet()
engines.add(engineName)
storage.customSearchEngineIds = engines
storage[engineName] = searchEngineXml
}
/**
* Updates an existing search engine.
* To prevent duplicate search engines we want to remove the old engine before adding the new one
* @param context [Context] used for various Android interactions.
* @param oldEngineName the name of the engine you want to replace
* @param newEngineName the name of the engine you want to save
* @param searchQuery The templated search string for the search engine
*/
suspend fun updateSearchEngine(
context: Context,
oldEngineName: String,
newEngineName: String,
searchQuery: String
) {
removeSearchEngine(context, oldEngineName)
addSearchEngine(context, newEngineName, searchQuery)
}
/**
* Removes a search engine from the store
* @param context [Context] used for various Android interactions.
* @param engineId the id of the engine you want to remove
*/
fun removeSearchEngine(context: Context, engineId: String) {
val storage = engineStorage(context)
val customEngines = storage.customSearchEngineIds
storage.customSearchEngineIds = customEngines.filterNot { it == engineId }.toSet()
storage[engineId] = null
}
/**
* Checks the store to see if it contains a search engine
* @param context [Context] used for various Android interactions.
* @param engineId The name of the engine to check
*/
fun isCustomSearchEngine(context: Context, engineId: String): Boolean {
val storage = engineStorage(context)
return storage.customSearchEngineIds.contains(engineId)
}
/**
* Creates a list of [SearchEngine] from the store
* @param context [Context] used for various Android interactions.
*/
fun loadCustomSearchEngines(context: Context): List<SearchEngine> {
val storage = engineStorage(context)
val parser = SearchEngineParser()
val engines = storage.customSearchEngineIds
return engines.mapNotNull {
val engineXml = storage[it] ?: return@mapNotNull null
val engineInputStream = engineXml.byteInputStream().buffered()
parser.load(it, engineInputStream)
}
}
/**
* Creates a helper object to help interact with [SharedPreferences]
* @param context [Context] used for various Android interactions.
*/
private fun engineStorage(context: Context) = object : PreferencesHolder {
override val preferences: SharedPreferences
get() = context.getSharedPreferences(PREF_FILE_SEARCH_ENGINES, Context.MODE_PRIVATE)
var customSearchEngineIds by stringSetPreference(PREF_KEY_CUSTOM_SEARCH_ENGINES, emptySet())
operator fun get(engineId: String): String? {
return preferences.getString(engineId, null)
}
operator fun set(engineId: String, value: String?) {
preferences.edit().putString(engineId, value).apply()
}
}
private const val PREF_KEY_CUSTOM_SEARCH_ENGINES = "pref_custom_search_engines"
const val PREF_FILE_SEARCH_ENGINES = "custom-search-engines"
}

@ -1,84 +0,0 @@
/* 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.components.searchengine
import android.graphics.Bitmap
import android.util.Base64
import android.util.Log
import org.w3c.dom.Document
import java.io.ByteArrayOutputStream
import java.io.StringWriter
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.parsers.ParserConfigurationException
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerConfigurationException
import javax.xml.transform.TransformerException
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
private const val BITMAP_COMPRESS_QUALITY = 100
private fun Bitmap.toBase64(): String {
val stream = ByteArrayOutputStream()
compress(Bitmap.CompressFormat.PNG, BITMAP_COMPRESS_QUALITY, stream)
val encodedImage = Base64.encodeToString(stream.toByteArray(), Base64.DEFAULT)
return "data:image/png;base64,$encodedImage"
}
class SearchEngineWriter {
companion object {
private const val LOG_TAG = "SearchEngineWriter"
fun buildSearchEngineXML(engineName: String, searchQuery: String, iconBitmap: Bitmap): String? {
try {
val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument()
val rootElement = document!!.createElement("OpenSearchDescription")
rootElement.setAttribute("xmlns", "http://a9.com/-/spec/opensearch/1.1/")
document.appendChild(rootElement)
val shortNameElement = document.createElement("ShortName")
shortNameElement.textContent = engineName
rootElement.appendChild(shortNameElement)
val imageElement = document.createElement("Image")
imageElement.setAttribute("width", "16")
imageElement.setAttribute("height", "16")
imageElement.textContent = iconBitmap.toBase64()
rootElement.appendChild(imageElement)
val descriptionElement = document.createElement("Description")
descriptionElement.textContent = engineName
rootElement.appendChild(descriptionElement)
val urlElement = document.createElement("Url")
urlElement.setAttribute("type", "text/html")
val templateSearchString = searchQuery.replace("%s", "{searchTerms}")
urlElement.setAttribute("template", templateSearchString)
rootElement.appendChild(urlElement)
return xmlToString(document)
} catch (e: ParserConfigurationException) {
Log.e(LOG_TAG, "Couldn't create new Document for building search engine XML", e)
return null
}
}
private fun xmlToString(doc: Document): String? {
val writer = StringWriter()
try {
val tf = TransformerFactory.newInstance().newTransformer()
tf.setOutputProperty(OutputKeys.ENCODING, "UTF-8")
tf.transform(DOMSource(doc), StreamResult(writer))
} catch (e: TransformerConfigurationException) {
return null
} catch (e: TransformerException) {
return null
}
return writer.toString()
}
}
}

@ -4,6 +4,8 @@
package org.mozilla.fenix.components.toolbar package org.mozilla.fenix.components.toolbar
import mozilla.components.ui.tabcounter.TabCounterMenu
open class BrowserInteractor( open class BrowserInteractor(
private val browserToolbarController: BrowserToolbarController, private val browserToolbarController: BrowserToolbarController,
private val menuController: BrowserToolbarMenuController private val menuController: BrowserToolbarMenuController

@ -7,8 +7,11 @@ package org.mozilla.fenix.components.toolbar
import androidx.navigation.NavController import androidx.navigation.NavController
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.EngineView import mozilla.components.concept.engine.EngineView
import mozilla.components.support.ktx.kotlin.isUrl import mozilla.components.support.ktx.kotlin.isUrl
import mozilla.components.ui.tabcounter.TabCounterMenu
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserAnimator.Companion.getToolbarNavOptions import org.mozilla.fenix.browser.BrowserAnimator.Companion.getToolbarNavOptions
@ -37,6 +40,7 @@ interface BrowserToolbarController {
} }
class DefaultBrowserToolbarController( class DefaultBrowserToolbarController(
private val store: BrowserStore,
private val activity: HomeActivity, private val activity: HomeActivity,
private val navController: NavController, private val navController: NavController,
private val metrics: MetricController, private val metrics: MetricController,
@ -65,15 +69,15 @@ class DefaultBrowserToolbarController(
override fun handleToolbarPasteAndGo(text: String) { override fun handleToolbarPasteAndGo(text: String) {
if (text.isUrl()) { if (text.isUrl()) {
sessionManager.selectedSession?.searchTerms = "" store.updateSearchTermsOfSelectedSession("")
activity.components.useCases.sessionUseCases.loadUrl.invoke(text) activity.components.useCases.sessionUseCases.loadUrl.invoke(text)
return return
} }
sessionManager.selectedSession?.searchTerms = text store.updateSearchTermsOfSelectedSession(text)
activity.components.useCases.searchUseCases.defaultSearch.invoke( activity.components.useCases.searchUseCases.defaultSearch.invoke(
text, text,
session = sessionManager.selectedSession sessionId = sessionManager.selectedSession?.id
) )
} }
@ -105,11 +109,12 @@ class DefaultBrowserToolbarController(
override fun handleTabCounterItemInteraction(item: TabCounterMenu.Item) { override fun handleTabCounterItemInteraction(item: TabCounterMenu.Item) {
when (item) { when (item) {
is TabCounterMenu.Item.CloseTab -> { is TabCounterMenu.Item.CloseTab -> {
metrics.track(
Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.CLOSE_TAB)
)
sessionManager.selectedSession?.let { sessionManager.selectedSession?.let {
// When closing the last tab we must show the undo snackbar in the home fragment // When closing the last tab we must show the undo snackbar in the home fragment
if (sessionManager.sessionsOfType(it.private).count() == 1) { if (sessionManager.sessionsOfType(it.private).count() == 1) {
// The tab tray always returns to normal mode so do that here too
activity.browsingModeManager.mode = BrowsingMode.Normal
homeViewModel.sessionToDelete = it.id homeViewModel.sessionToDelete = it.id
navController.navigate( navController.navigate(
BrowserFragmentDirections.actionGlobalHome() BrowserFragmentDirections.actionGlobalHome()
@ -123,8 +128,24 @@ class DefaultBrowserToolbarController(
} }
} }
is TabCounterMenu.Item.NewTab -> { is TabCounterMenu.Item.NewTab -> {
activity.browsingModeManager.mode = item.mode metrics.track(
navController.navigate(BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true)) Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.NEW_TAB)
)
activity.browsingModeManager.mode = BrowsingMode.Normal
navController.navigate(
BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true)
)
}
is TabCounterMenu.Item.NewPrivateTab -> {
metrics.track(
Event.TabCounterMenuItemTapped(
Event.TabCounterMenuItemTapped.Item.NEW_PRIVATE_TAB
)
)
activity.browsingModeManager.mode = BrowsingMode.Private
navController.navigate(
BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true)
)
} }
} }
} }
@ -139,3 +160,14 @@ class DefaultBrowserToolbarController(
internal const val TELEMETRY_BROWSER_IDENTIFIER = "browserMenu" internal const val TELEMETRY_BROWSER_IDENTIFIER = "browserMenu"
} }
} }
private fun BrowserStore.updateSearchTermsOfSelectedSession(
searchTerms: String
) {
val selectedTabId = state.selectedTabId ?: return
dispatch(ContentAction.UpdateSearchTermsAction(
selectedTabId,
searchTerms
))
}

@ -236,10 +236,16 @@ class DefaultBrowserToolbarMenuController(
sessionManager.select(customTabSession) sessionManager.select(customTabSession)
// Switch to the actual browser which should now display our new selected session // Switch to the actual browser which should now display our new selected session
activity.startActivity(openInFenixIntent) activity.startActivity(openInFenixIntent.apply {
// We never want to launch the browser in the same task as the external app
// activity. So we force a new task here. IntentReceiverActivity will do the
// right thing and take care of routing to an already existing browser and avoid
// cloning a new one.
flags = flags or Intent.FLAG_ACTIVITY_NEW_TASK
})
// Close this activity since it is no longer displaying any session // Close this activity (and the task) since it is no longer displaying any session
activity.finish() activity.finishAndRemoveTask()
} }
ToolbarMenu.Item.Quit -> { ToolbarMenu.Item.Quit -> {
// We need to show the snackbar while the browsing data is deleting (if "Delete // We need to show the snackbar while the browsing data is deleting (if "Delete

@ -29,6 +29,7 @@ import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.browser.toolbar.behavior.BrowserToolbarBottomBehavior import mozilla.components.browser.toolbar.behavior.BrowserToolbarBottomBehavior
import mozilla.components.browser.toolbar.display.DisplayToolbar import mozilla.components.browser.toolbar.display.DisplayToolbar
import mozilla.components.support.utils.URLStringUtils import mozilla.components.support.utils.URLStringUtils
import mozilla.components.ui.tabcounter.TabCounterMenu
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.customtabs.CustomTabToolbarIntegration import org.mozilla.fenix.customtabs.CustomTabToolbarIntegration
import org.mozilla.fenix.customtabs.CustomTabToolbarMenu import org.mozilla.fenix.customtabs.CustomTabToolbarMenu
@ -155,7 +156,8 @@ class BrowserToolbarView(
menu = primaryTextColor, menu = primaryTextColor,
hint = secondaryTextColor, hint = secondaryTextColor,
separator = separatorColor, separator = separatorColor,
trackingProtection = primaryTextColor trackingProtection = primaryTextColor,
permissionHighlights = primaryTextColor
) )
display.hint = context.getString(R.string.search_hint) display.hint = context.getString(R.string.search_hint)
@ -164,9 +166,9 @@ class BrowserToolbarView(
val menuToolbar: ToolbarMenu val menuToolbar: ToolbarMenu
if (isCustomTabSession) { if (isCustomTabSession) {
menuToolbar = CustomTabToolbarMenu( menuToolbar = CustomTabToolbarMenu(
this, context = this,
sessionManager, store = components.core.store,
customTabSession?.id, sessionId = customTabSession?.id,
shouldReverseItems = toolbarPosition == ToolbarPosition.TOP, shouldReverseItems = toolbarPosition == ToolbarPosition.TOP,
onItemTapped = { onItemTapped = {
it.performHapticIfNeeded(view) it.performHapticIfNeeded(view)
@ -183,7 +185,6 @@ class BrowserToolbarView(
interactor.onBrowserToolbarMenuItemTapped(it) interactor.onBrowserToolbarMenuItemTapped(it)
}, },
lifecycleOwner = lifecycleOwner, lifecycleOwner = lifecycleOwner,
sessionManager = sessionManager,
store = components.core.store, store = components.core.store,
bookmarksStorage = bookmarkStorage, bookmarksStorage = bookmarkStorage,
isPinningSupported = isPinningSupported isPinningSupported = isPinningSupported
@ -272,10 +273,6 @@ class BrowserToolbarView(
} }
} }
companion object {
private const val TOOLBAR_ELEVATION = 16
}
@Suppress("ComplexCondition") @Suppress("ComplexCondition")
private fun ToolbarMenu.Item.performHapticIfNeeded(view: View) { private fun ToolbarMenu.Item.performHapticIfNeeded(view: View) {
if (this is ToolbarMenu.Item.Reload && this.bypassCache || if (this is ToolbarMenu.Item.Reload && this.bypassCache ||

@ -6,10 +6,14 @@ package org.mozilla.fenix.components.toolbar
import android.content.Context import android.content.Context
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.annotation.VisibleForTesting
import androidx.core.content.ContextCompat.getColor import androidx.core.content.ContextCompat.getColor
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.browser.menu.BrowserMenuHighlight import mozilla.components.browser.menu.BrowserMenuHighlight
import mozilla.components.browser.menu.WebExtensionBrowserMenuBuilder import mozilla.components.browser.menu.WebExtensionBrowserMenuBuilder
@ -19,13 +23,15 @@ import mozilla.components.browser.menu.item.BrowserMenuImageSwitch
import mozilla.components.browser.menu.item.BrowserMenuImageText import mozilla.components.browser.menu.item.BrowserMenuImageText
import mozilla.components.browser.menu.item.BrowserMenuItemToolbar import mozilla.components.browser.menu.item.BrowserMenuItemToolbar
import mozilla.components.browser.menu.item.WebExtensionPlaceholderMenuItem import mozilla.components.browser.menu.item.WebExtensionPlaceholderMenuItem
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.storage.BookmarksStorage import mozilla.components.concept.storage.BookmarksStorage
import mozilla.components.feature.webcompat.reporter.WebCompatReporterFeature import mozilla.components.feature.webcompat.reporter.WebCompatReporterFeature
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.ktx.android.content.getColorFromAttr import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
@ -36,7 +42,7 @@ import org.mozilla.fenix.theme.ThemeManager
/** /**
* Builds the toolbar object used with the 3-dot menu in the browser fragment. * Builds the toolbar object used with the 3-dot menu in the browser fragment.
* @param sessionManager Reference to the session manager that contains all tabs. * @param store reference to the application's [BrowserStore].
* @param hasAccountProblem If true, there was a problem signing into the Firefox account. * @param hasAccountProblem If true, there was a problem signing into the Firefox account.
* @param shouldReverseItems If true, reverse the menu items. * @param shouldReverseItems If true, reverse the menu items.
* @param onItemTapped Called when a menu item is tapped. * @param onItemTapped Called when a menu item is tapped.
@ -44,9 +50,9 @@ import org.mozilla.fenix.theme.ThemeManager
* @param bookmarksStorage Used to check if a page is bookmarked. * @param bookmarksStorage Used to check if a page is bookmarked.
*/ */
@Suppress("LargeClass", "LongParameterList") @Suppress("LargeClass", "LongParameterList")
@ExperimentalCoroutinesApi
class DefaultToolbarMenu( class DefaultToolbarMenu(
private val context: Context, private val context: Context,
private val sessionManager: SessionManager,
private val store: BrowserStore, private val store: BrowserStore,
hasAccountProblem: Boolean = false, hasAccountProblem: Boolean = false,
shouldReverseItems: Boolean, shouldReverseItems: Boolean,
@ -59,8 +65,7 @@ class DefaultToolbarMenu(
private var currentUrlIsBookmarked = false private var currentUrlIsBookmarked = false
private var isBookmarkedJob: Job? = null private var isBookmarkedJob: Job? = null
/** Gets the current browser session */ private val selectedSession: TabSessionState? get() = store.state.selectedTab
private val session: Session? get() = sessionManager.selectedSession
override val menuBuilder by lazy { override val menuBuilder by lazy {
WebExtensionBrowserMenuBuilder( WebExtensionBrowserMenuBuilder(
@ -81,7 +86,7 @@ class DefaultToolbarMenu(
primaryContentDescription = context.getString(R.string.browser_menu_back), primaryContentDescription = context.getString(R.string.browser_menu_back),
primaryImageTintResource = primaryTextColor(), primaryImageTintResource = primaryTextColor(),
isInPrimaryState = { isInPrimaryState = {
session?.canGoBack ?: true selectedSession?.content?.canGoBack ?: true
}, },
secondaryImageTintResource = ThemeManager.resolveAttribute(R.attr.disabled, context), secondaryImageTintResource = ThemeManager.resolveAttribute(R.attr.disabled, context),
disableInSecondaryState = true, disableInSecondaryState = true,
@ -95,7 +100,7 @@ class DefaultToolbarMenu(
primaryContentDescription = context.getString(R.string.browser_menu_forward), primaryContentDescription = context.getString(R.string.browser_menu_forward),
primaryImageTintResource = primaryTextColor(), primaryImageTintResource = primaryTextColor(),
isInPrimaryState = { isInPrimaryState = {
session?.canGoForward ?: true selectedSession?.content?.canGoForward ?: true
}, },
secondaryImageTintResource = ThemeManager.resolveAttribute(R.attr.disabled, context), secondaryImageTintResource = ThemeManager.resolveAttribute(R.attr.disabled, context),
disableInSecondaryState = true, disableInSecondaryState = true,
@ -109,7 +114,7 @@ class DefaultToolbarMenu(
primaryContentDescription = context.getString(R.string.browser_menu_refresh), primaryContentDescription = context.getString(R.string.browser_menu_refresh),
primaryImageTintResource = primaryTextColor(), primaryImageTintResource = primaryTextColor(),
isInPrimaryState = { isInPrimaryState = {
session?.loading == false selectedSession?.content?.loading == false
}, },
secondaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_stop, secondaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_stop,
secondaryContentDescription = context.getString(R.string.browser_menu_stop), secondaryContentDescription = context.getString(R.string.browser_menu_stop),
@ -117,7 +122,7 @@ class DefaultToolbarMenu(
disableInSecondaryState = false, disableInSecondaryState = false,
longClickListener = { onItemTapped.invoke(ToolbarMenu.Item.Reload(bypassCache = true)) } longClickListener = { onItemTapped.invoke(ToolbarMenu.Item.Reload(bypassCache = true)) }
) { ) {
if (session?.loading == true) { if (selectedSession?.content?.loading == true) {
onItemTapped.invoke(ToolbarMenu.Item.Stop) onItemTapped.invoke(ToolbarMenu.Item.Stop)
} else { } else {
onItemTapped.invoke(ToolbarMenu.Item.Reload(bypassCache = false)) onItemTapped.invoke(ToolbarMenu.Item.Reload(bypassCache = false))
@ -157,19 +162,19 @@ class DefaultToolbarMenu(
// Predicates that need to be repeatedly called as the session changes // Predicates that need to be repeatedly called as the session changes
private fun canAddToHomescreen(): Boolean = private fun canAddToHomescreen(): Boolean =
session != null && isPinningSupported && selectedSession != null && isPinningSupported &&
!context.components.useCases.webAppUseCases.isInstallable() !context.components.useCases.webAppUseCases.isInstallable()
private fun canInstall(): Boolean = private fun canInstall(): Boolean =
session != null && isPinningSupported && selectedSession != null && isPinningSupported &&
context.components.useCases.webAppUseCases.isInstallable() context.components.useCases.webAppUseCases.isInstallable()
private fun shouldShowOpenInApp(): Boolean = session?.let { session -> private fun shouldShowOpenInApp(): Boolean = selectedSession?.let { session ->
val appLink = context.components.useCases.appLinksUseCases.appLinkRedirect val appLink = context.components.useCases.appLinksUseCases.appLinkRedirect
appLink(session.url).hasExternalApp() appLink(session.content.url).hasExternalApp()
} ?: false } ?: false
private fun shouldShowReaderAppearance(): Boolean = session?.let { private fun shouldShowReaderAppearance(): Boolean = selectedSession?.let {
store.state.findTab(it.id)?.readerState?.active store.state.findTab(it.id)?.readerState?.active
} ?: false } ?: false
// End of predicates // // End of predicates //
@ -234,7 +239,7 @@ class DefaultToolbarMenu(
imageResource = R.drawable.ic_desktop, imageResource = R.drawable.ic_desktop,
label = context.getString(R.string.browser_menu_desktop_site), label = context.getString(R.string.browser_menu_desktop_site),
initialState = { initialState = {
session?.desktopMode ?: false selectedSession?.content?.desktopMode ?: false
} }
) { checked -> ) { checked ->
onItemTapped.invoke(ToolbarMenu.Item.RequestDesktop(checked)) onItemTapped.invoke(ToolbarMenu.Item.RequestDesktop(checked))
@ -353,44 +358,28 @@ class DefaultToolbarMenu(
} }
@ColorRes @ColorRes
private fun primaryTextColor() = ThemeManager.resolveAttribute(R.attr.primaryText, context) @VisibleForTesting
internal fun primaryTextColor() = ThemeManager.resolveAttribute(R.attr.primaryText, context)
private var currentSessionObserver: Pair<Session, Session.Observer>? = null
@VisibleForTesting
private fun registerForIsBookmarkedUpdates() { internal fun registerForIsBookmarkedUpdates() {
session?.let { store.flowScoped(lifecycleOwner) { flow ->
registerForUrlChanges(it) flow.mapNotNull { state -> state.selectedTab }
} .ifAnyChanged { tab ->
arrayOf(
val sessionManagerObserver = object : SessionManager.Observer { tab.id,
override fun onSessionSelected(session: Session) { tab.content.url
// Unregister any old session observer before registering a new session observer )
currentSessionObserver?.let { }
it.first.unregister(it.second) .collect {
currentUrlIsBookmarked = false
updateCurrentUrlIsBookmarked(it.content.url)
} }
currentUrlIsBookmarked = false
updateCurrentUrlIsBookmarked(session.url)
registerForUrlChanges(session)
}
}
sessionManager.register(sessionManagerObserver, lifecycleOwner)
}
private fun registerForUrlChanges(session: Session) {
val sessionObserver = object : Session.Observer {
override fun onUrlChanged(session: Session, url: String) {
currentUrlIsBookmarked = false
updateCurrentUrlIsBookmarked(url)
}
} }
currentSessionObserver = Pair(session, sessionObserver)
updateCurrentUrlIsBookmarked(session.url)
session.register(sessionObserver, lifecycleOwner)
} }
private fun updateCurrentUrlIsBookmarked(newUrl: String) { @VisibleForTesting
internal fun updateCurrentUrlIsBookmarked(newUrl: String) {
isBookmarkedJob?.cancel() isBookmarkedJob?.cancel()
isBookmarkedJob = lifecycleOwner.lifecycleScope.launch { isBookmarkedJob = lifecycleOwner.lifecycleScope.launch {
currentUrlIsBookmarked = bookmarksStorage currentUrlIsBookmarked = bookmarksStorage

@ -0,0 +1,60 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.components.toolbar
import android.content.Context
import androidx.annotation.VisibleForTesting
import mozilla.components.concept.menu.candidate.DividerMenuCandidate
import mozilla.components.concept.menu.candidate.MenuCandidate
import mozilla.components.ui.tabcounter.TabCounterMenu
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
class FenixTabCounterMenu(
context: Context,
onItemTapped: (Item) -> Unit,
iconColor: Int? = null
) : TabCounterMenu(context, onItemTapped, iconColor) {
@VisibleForTesting
internal fun menuItems(showOnly: BrowsingMode): List<MenuCandidate> {
return when (showOnly) {
BrowsingMode.Normal -> listOf(newTabItem)
BrowsingMode.Private -> listOf(newPrivateTabItem)
}
}
@VisibleForTesting
internal fun menuItems(toolbarPosition: ToolbarPosition): List<MenuCandidate> {
val items = listOf(
newTabItem,
newPrivateTabItem,
DividerMenuCandidate(),
closeTabItem
)
return when (toolbarPosition) {
ToolbarPosition.BOTTOM -> items.reversed()
ToolbarPosition.TOP -> items
}
}
/**
* Update the displayed menu items.
* @param showOnly Show only the new tab item corresponding to the given [BrowsingMode].
*/
fun updateMenu(showOnly: BrowsingMode) {
val items = menuItems(showOnly)
menuController.submitList(items)
}
/**
* Update the displayed menu items.
* @param toolbarPosition Return a list that is ordered based on the given [ToolbarPosition].
*/
fun updateMenu(toolbarPosition: ToolbarPosition) {
menuController.submitList(menuItems(toolbarPosition))
}
}

@ -1,271 +0,0 @@
/* 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.components.toolbar
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.Typeface
import android.util.AttributeSet
import android.util.TypedValue
import android.view.LayoutInflater
import android.widget.RelativeLayout
import androidx.core.view.updatePadding
import kotlinx.android.synthetic.main.mozac_ui_tabcounter_layout.view.*
import org.mozilla.fenix.R
import java.text.NumberFormat
class TabCounter @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : RelativeLayout(context, attrs, defStyle) {
private val animationSet: AnimatorSet
init {
val inflater = LayoutInflater.from(context)
inflater.inflate(R.layout.mozac_ui_tabcounter_layout, this)
// This is needed because without this counter box will be empty.
setCount(INTERNAL_COUNT)
animationSet = createAnimatorSet()
}
private fun updateContentDescription(count: Int) {
counter_root.contentDescription = if (count == 1) {
context?.getString(R.string.open_tab_tray_single)
} else {
String.format(context.getString(R.string.open_tab_tray_plural), count.toString())
}
}
fun setCountWithAnimation(count: Int) {
setCount(count)
// No need to animate on these cases.
when {
INTERNAL_COUNT == 0 -> return // Initial state.
INTERNAL_COUNT == count -> return // There isn't any tab added or removed.
INTERNAL_COUNT > MAX_VISIBLE_TABS -> return // There are still over MAX_VISIBLE_TABS tabs open.
}
// Cancel previous animations if necessary.
if (animationSet.isRunning) {
animationSet.cancel()
}
// Trigger animations.
animationSet.start()
}
fun setCount(count: Int) {
updateContentDescription(count)
adjustTextSize(count)
counter_text.text = formatForDisplay(count)
INTERNAL_COUNT = count
}
private fun createAnimatorSet(): AnimatorSet {
val animatorSet = AnimatorSet()
createBoxAnimatorSet(animatorSet)
createTextAnimatorSet(animatorSet)
return animatorSet
}
private fun createBoxAnimatorSet(animatorSet: AnimatorSet) {
// The first animator, fadeout in 33 ms (49~51, 2 frames).
val fadeOut = ObjectAnimator.ofFloat(
counter_box, "alpha",
ANIM_BOX_FADEOUT_FROM, ANIM_BOX_FADEOUT_TO
).setDuration(ANIM_BOX_FADEOUT_DURATION)
// Move up on y-axis, from 0.0 to -5.3 in 50ms, with fadeOut (49~52, 3 frames).
val moveUp1 = ObjectAnimator.ofFloat(
counter_box, "translationY",
ANIM_BOX_MOVEUP1_TO, ANIM_BOX_MOVEUP1_FROM
).setDuration(ANIM_BOX_MOVEUP1_DURATION)
// Move down on y-axis, from -5.3 to -1.0 in 116ms, after moveUp1 (52~59, 7 frames).
val moveDown2 = ObjectAnimator.ofFloat(
counter_box, "translationY",
ANIM_BOX_MOVEDOWN2_FROM, ANIM_BOX_MOVEDOWN2_TO
).setDuration(ANIM_BOX_MOVEDOWN2_DURATION)
// FadeIn in 66ms, with moveDown2 (52~56, 4 frames).
val fadeIn = ObjectAnimator.ofFloat(
counter_box, "alpha",
ANIM_BOX_FADEIN_FROM, ANIM_BOX_FADEIN_TO
).setDuration(ANIM_BOX_FADEIN_DURATION)
// Move down on y-axis, from -1.0 to 2.7 in 116ms, after moveDown2 (59~66, 7 frames).
val moveDown3 = ObjectAnimator.ofFloat(
counter_box, "translationY",
ANIM_BOX_MOVEDOWN3_FROM, ANIM_BOX_MOVEDOWN3_TO
).setDuration(ANIM_BOX_MOVEDOWN3_DURATION)
// Move up on y-axis, from 2.7 to 0 in 133ms, after moveDown3 (66~74, 8 frames).
val moveUp4 = ObjectAnimator.ofFloat(
counter_box, "translationY",
ANIM_BOX_MOVEDOWN4_FROM, ANIM_BOX_MOVEDOWN4_TO
).setDuration(ANIM_BOX_MOVEDOWN4_DURATION)
// Scale up height from 2% to 105% in 100ms, after moveUp1 and delay 16ms (53~59, 6 frames).
val scaleUp1 = ObjectAnimator.ofFloat(
counter_box, "scaleY",
ANIM_BOX_SCALEUP1_FROM, ANIM_BOX_SCALEUP1_TO
).setDuration(ANIM_BOX_SCALEUP1_DURATION)
scaleUp1.startDelay = ANIM_BOX_SCALEUP1_DELAY // delay 1 frame after moveUp1
// Scale down height from 105% to 99% in 116ms, after scaleUp1 (59~66, 7 frames).
val scaleDown2 = ObjectAnimator.ofFloat(
counter_box, "scaleY",
ANIM_BOX_SCALEDOWN2_FROM, ANIM_BOX_SCALEDOWN2_TO
).setDuration(ANIM_BOX_SCALEDOWN2_DURATION)
// Scale up height from 99% to 100% in 133ms, after scaleDown2 (66~74, 8 frames).
val scaleUp3 = ObjectAnimator.ofFloat(
counter_box, "scaleY",
ANIM_BOX_SCALEUP3_FROM, ANIM_BOX_SCALEUP3_TO
).setDuration(ANIM_BOX_SCALEUP3_DURATION)
animatorSet.play(fadeOut).with(moveUp1)
animatorSet.play(moveUp1).before(moveDown2)
animatorSet.play(moveDown2).with(fadeIn)
animatorSet.play(moveDown2).before(moveDown3)
animatorSet.play(moveDown3).before(moveUp4)
animatorSet.play(moveUp1).before(scaleUp1)
animatorSet.play(scaleUp1).before(scaleDown2)
animatorSet.play(scaleDown2).before(scaleUp3)
}
private fun createTextAnimatorSet(animatorSet: AnimatorSet) {
val firstAnimator = animatorSet.childAnimations[0]
// Fadeout in 100ms, with firstAnimator (49~51, 2 frames).
val fadeOut = ObjectAnimator.ofFloat(
counter_text, "alpha",
ANIM_TEXT_FADEOUT_FROM, ANIM_TEXT_FADEOUT_TO
).setDuration(ANIM_TEXT_FADEOUT_DURATION)
// FadeIn in 66 ms, after fadeOut with delay 96ms (57~61, 4 frames).
val fadeIn = ObjectAnimator.ofFloat(
counter_text, "alpha",
ANIM_TEXT_FADEIN_FROM, ANIM_TEXT_FADEIN_TO
).setDuration(ANIM_TEXT_FADEIN_DURATION)
fadeIn.startDelay = (ANIM_TEXT_FADEIN_DELAY) // delay 6 frames after fadeOut
// Move down on y-axis, from 0 to 4.4 in 66ms, with fadeIn (57~61, 4 frames).
val moveDown = ObjectAnimator.ofFloat(
counter_text, "translationY",
ANIM_TEXT_MOVEDOWN_FROM, ANIM_TEXT_MOVEDOWN_TO
).setDuration(ANIM_TEXT_MOVEDOWN_DURATION)
moveDown.startDelay = (ANIM_TEXT_MOVEDOWN_DELAY) // delay 6 frames after fadeOut
// Move up on y-axis, from 0 to 4.4 in 66ms, after moveDown (61~69, 8 frames).
val moveUp = ObjectAnimator.ofFloat(
counter_text, "translationY",
ANIM_TEXT_MOVEUP_FROM, ANIM_TEXT_MOVEUP_TO
).setDuration(ANIM_TEXT_MOVEUP_DURATION)
animatorSet.play(firstAnimator).with(fadeOut)
animatorSet.play(fadeOut).before(fadeIn)
animatorSet.play(fadeIn).with(moveDown)
animatorSet.play(moveDown).before(moveUp)
}
private fun formatForDisplay(count: Int): String {
return if (count > MAX_VISIBLE_TABS) {
counter_text.updatePadding(bottom = INFINITE_CHAR_PADDING_BOTTOM)
SO_MANY_TABS_OPEN
} else NumberFormat.getInstance().format(count.toLong())
}
private fun adjustTextSize(newCount: Int) {
val newRatio = if (newCount in TWO_DIGITS_TAB_COUNT_THRESHOLD..MAX_VISIBLE_TABS) {
TWO_DIGITS_SIZE_RATIO
} else {
ONE_DIGIT_SIZE_RATIO
}
val counterBoxWidth =
context.resources.getDimensionPixelSize(R.dimen.tab_counter_box_width_height)
val textSize = newRatio * counterBoxWidth
counter_text.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
counter_text.setTypeface(null, Typeface.BOLD)
counter_text.setPadding(0, 0, 0, 0)
}
companion object {
internal var INTERNAL_COUNT = 0
internal const val MAX_VISIBLE_TABS = 99
internal const val SO_MANY_TABS_OPEN = ""
internal const val INFINITE_CHAR_PADDING_BOTTOM = 6
internal const val ONE_DIGIT_SIZE_RATIO = 0.5f
internal const val TWO_DIGITS_SIZE_RATIO = 0.4f
internal const val TWO_DIGITS_TAB_COUNT_THRESHOLD = 10
// createBoxAnimatorSet
private const val ANIM_BOX_FADEOUT_FROM = 1.0f
private const val ANIM_BOX_FADEOUT_TO = 0.0f
private const val ANIM_BOX_FADEOUT_DURATION = 33L
private const val ANIM_BOX_MOVEUP1_FROM = 0.0f
private const val ANIM_BOX_MOVEUP1_TO = -5.3f
private const val ANIM_BOX_MOVEUP1_DURATION = 50L
private const val ANIM_BOX_MOVEDOWN2_FROM = -5.3f
private const val ANIM_BOX_MOVEDOWN2_TO = -1.0f
private const val ANIM_BOX_MOVEDOWN2_DURATION = 167L
private const val ANIM_BOX_FADEIN_FROM = 0.01f
private const val ANIM_BOX_FADEIN_TO = 1.0f
private const val ANIM_BOX_FADEIN_DURATION = 66L
private const val ANIM_BOX_MOVEDOWN3_FROM = -1.0f
private const val ANIM_BOX_MOVEDOWN3_TO = 2.7f
private const val ANIM_BOX_MOVEDOWN3_DURATION = 116L
private const val ANIM_BOX_MOVEDOWN4_FROM = 2.7f
private const val ANIM_BOX_MOVEDOWN4_TO = 0.0f
private const val ANIM_BOX_MOVEDOWN4_DURATION = 133L
private const val ANIM_BOX_SCALEUP1_FROM = 0.02f
private const val ANIM_BOX_SCALEUP1_TO = 1.05f
private const val ANIM_BOX_SCALEUP1_DURATION = 100L
private const val ANIM_BOX_SCALEUP1_DELAY = 16L
private const val ANIM_BOX_SCALEDOWN2_FROM = 1.05f
private const val ANIM_BOX_SCALEDOWN2_TO = 0.99f
private const val ANIM_BOX_SCALEDOWN2_DURATION = 116L
private const val ANIM_BOX_SCALEUP3_FROM = 0.99f
private const val ANIM_BOX_SCALEUP3_TO = 1.00f
private const val ANIM_BOX_SCALEUP3_DURATION = 133L
// createTextAnimatorSet
private const val ANIM_TEXT_FADEOUT_FROM = 1.0f
private const val ANIM_TEXT_FADEOUT_TO = 0.0f
private const val ANIM_TEXT_FADEOUT_DURATION = 33L
private const val ANIM_TEXT_FADEIN_FROM = 0.01f
private const val ANIM_TEXT_FADEIN_TO = 1.0f
private const val ANIM_TEXT_FADEIN_DURATION = 66L
private const val ANIM_TEXT_FADEIN_DELAY = 16L * 6
private const val ANIM_TEXT_MOVEDOWN_FROM = 0.0f
private const val ANIM_TEXT_MOVEDOWN_TO = 4.4f
private const val ANIM_TEXT_MOVEDOWN_DURATION = 66L
private const val ANIM_TEXT_MOVEDOWN_DELAY = 16L * 6
private const val ANIM_TEXT_MOVEUP_FROM = 4.4f
private const val ANIM_TEXT_MOVEUP_TO = 0.0f
private const val ANIM_TEXT_MOVEUP_DURATION = 66L
}
}

@ -1,121 +0,0 @@
/* 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.components.toolbar
import android.content.Context
import androidx.annotation.VisibleForTesting
import mozilla.components.browser.menu2.BrowserMenuController
import mozilla.components.concept.menu.MenuController
import mozilla.components.concept.menu.candidate.DividerMenuCandidate
import mozilla.components.concept.menu.candidate.DrawableMenuIcon
import mozilla.components.concept.menu.candidate.MenuCandidate
import mozilla.components.concept.menu.candidate.TextMenuCandidate
import mozilla.components.concept.menu.candidate.TextStyle
import mozilla.components.support.ktx.android.content.getColorFromAttr
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
class TabCounterMenu(
context: Context,
private val metrics: MetricController,
private val onItemTapped: (Item) -> Unit
) {
sealed class Item {
object CloseTab : Item()
data class NewTab(val mode: BrowsingMode) : Item()
}
val menuController: MenuController by lazy { BrowserMenuController() }
private val newTabItem: TextMenuCandidate
private val newPrivateTabItem: TextMenuCandidate
private val closeTabItem: TextMenuCandidate
init {
val primaryTextColor = context.getColorFromAttr(R.attr.primaryText)
val textStyle = TextStyle(color = primaryTextColor)
newTabItem = TextMenuCandidate(
text = context.getString(R.string.browser_menu_new_tab),
start = DrawableMenuIcon(
context,
R.drawable.ic_new,
tint = primaryTextColor
),
textStyle = textStyle
) {
metrics.track(Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.NEW_TAB))
onItemTapped(Item.NewTab(BrowsingMode.Normal))
}
newPrivateTabItem = TextMenuCandidate(
text = context.getString(R.string.home_screen_shortcut_open_new_private_tab_2),
start = DrawableMenuIcon(
context,
R.drawable.ic_private_browsing,
tint = primaryTextColor
),
textStyle = textStyle
) {
metrics.track(Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.NEW_PRIVATE_TAB))
onItemTapped(Item.NewTab(BrowsingMode.Private))
}
closeTabItem = TextMenuCandidate(
text = context.getString(R.string.close_tab),
start = DrawableMenuIcon(
context,
R.drawable.ic_close,
tint = primaryTextColor
),
textStyle = textStyle
) {
metrics.track(Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.CLOSE_TAB))
onItemTapped(Item.CloseTab)
}
}
@VisibleForTesting
internal fun menuItems(showOnly: BrowsingMode): List<MenuCandidate> {
return when (showOnly) {
BrowsingMode.Normal -> listOf(newTabItem)
BrowsingMode.Private -> listOf(newPrivateTabItem)
}
}
@VisibleForTesting
internal fun menuItems(toolbarPosition: ToolbarPosition): List<MenuCandidate> {
val items = listOf(
newTabItem,
newPrivateTabItem,
DividerMenuCandidate(),
closeTabItem
)
return when (toolbarPosition) {
ToolbarPosition.BOTTOM -> items.reversed()
ToolbarPosition.TOP -> items
}
}
/**
* Update the displayed menu items.
* @param showOnly Show only the new tab item corresponding to the given [BrowsingMode].
*/
fun updateMenu(showOnly: BrowsingMode) {
menuController.submitList(menuItems(showOnly))
}
/**
* Update the displayed menu items.
* @param toolbarPosition Return a list that is ordered based on the given [ToolbarPosition].
*/
fun updateMenu(toolbarPosition: ToolbarPosition) {
menuController.submitList(menuItems(toolbarPosition))
}
}

@ -1,85 +0,0 @@
/* 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.components.toolbar
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.toolbar.Toolbar
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.ktx.android.content.res.resolveAttribute
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.ext.components
import java.lang.ref.WeakReference
/**
* A [Toolbar.Action] implementation that shows a [TabCounter].
*/
@OptIn(ExperimentalCoroutinesApi::class)
class TabCounterToolbarButton(
private val lifecycleOwner: LifecycleOwner,
private val onItemTapped: (TabCounterMenu.Item) -> Unit = {},
private val showTabs: () -> Unit
) : Toolbar.Action {
private var reference: WeakReference<TabCounter> = WeakReference<TabCounter>(null)
override fun createView(parent: ViewGroup): View {
val store = parent.context.components.core.store
val metrics = parent.context.components.analytics.metrics
val settings = parent.context.components.settings
store.flowScoped(lifecycleOwner) { flow ->
flow.map { state -> state.getNormalOrPrivateTabs(isPrivate(store)).size }
.ifChanged()
.collect { tabs -> updateCount(tabs) }
}
val menu = TabCounterMenu(parent.context, metrics, onItemTapped)
menu.updateMenu(settings.toolbarPosition)
val view = TabCounter(parent.context).apply {
reference = WeakReference(this)
setOnClickListener {
showTabs.invoke()
}
setOnLongClickListener {
menu.menuController.show(anchor = it)
true
}
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?) {
setCount(store.state.getNormalOrPrivateTabs(isPrivate(store)).size)
}
override fun onViewDetachedFromWindow(v: View?) { /* no-op */ }
})
}
// Set selectableItemBackgroundBorderless
view.setBackgroundResource(parent.context.theme.resolveAttribute(
android.R.attr.selectableItemBackgroundBorderless
))
return view
}
override fun bind(view: View) = Unit
private fun updateCount(count: Int) {
reference.get()?.setCountWithAnimation(count)
}
private fun isPrivate(store: BrowserStore): Boolean {
return store.state.selectedTab?.content?.private ?: false
}
}

@ -7,19 +7,24 @@ package org.mozilla.fenix.components.toolbar
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.domains.autocomplete.DomainAutocompleteProvider import mozilla.components.browser.domains.autocomplete.DomainAutocompleteProvider
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs
import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.browser.toolbar.display.DisplayToolbar import mozilla.components.browser.toolbar.display.DisplayToolbar
import mozilla.components.concept.engine.Engine import mozilla.components.concept.engine.Engine
import mozilla.components.concept.storage.HistoryStorage import mozilla.components.concept.storage.HistoryStorage
import mozilla.components.feature.tabs.toolbar.TabCounterToolbarButton
import mozilla.components.feature.toolbar.ToolbarAutocompleteFeature import mozilla.components.feature.toolbar.ToolbarAutocompleteFeature
import mozilla.components.feature.toolbar.ToolbarFeature import mozilla.components.feature.toolbar.ToolbarFeature
import mozilla.components.feature.toolbar.ToolbarPresenter import mozilla.components.feature.toolbar.ToolbarPresenter
import mozilla.components.lib.publicsuffixlist.PublicSuffixList import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.support.base.feature.LifecycleAwareFeature import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.components.support.ktx.android.view.hideKeyboard
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
@ -35,9 +40,10 @@ abstract class ToolbarIntegration(
renderStyle: ToolbarFeature.RenderStyle renderStyle: ToolbarFeature.RenderStyle
) : LifecycleAwareFeature { ) : LifecycleAwareFeature {
val store = context.components.core.store
private val toolbarPresenter: ToolbarPresenter = ToolbarPresenter( private val toolbarPresenter: ToolbarPresenter = ToolbarPresenter(
toolbar, toolbar,
context.components.core.store, store,
sessionId, sessionId,
ToolbarFeature.UrlRenderConfiguration( ToolbarFeature.UrlRenderConfiguration(
PublicSuffixList(context), PublicSuffixList(context),
@ -106,7 +112,7 @@ class DefaultToolbarIntegration(
Configuration.UI_MODE_NIGHT_YES -> { Configuration.UI_MODE_NIGHT_YES -> {
AppCompatResources.getDrawable(context, R.drawable.shield_dark) AppCompatResources.getDrawable(context, R.drawable.shield_dark)
} }
else -> null else -> AppCompatResources.getDrawable(context, R.drawable.shield_light)
} }
toolbar.display.indicators = toolbar.display.indicators =
@ -123,6 +129,10 @@ class DefaultToolbarIntegration(
) )
} }
if (FeatureFlags.permissionIndicatorsToolbar) {
toolbar.display.indicators += DisplayToolbar.Indicators.PERMISSION_HIGHLIGHTS
}
toolbar.display.displayIndicatorSeparator = toolbar.display.displayIndicatorSeparator =
context.settings().shouldUseTrackingProtection context.settings().shouldUseTrackingProtection
@ -139,16 +149,40 @@ class DefaultToolbarIntegration(
)!! )!!
) )
val tabsAction = TabCounterToolbarButton( val tabCounterMenu = FenixTabCounterMenu(
lifecycleOwner, context = context,
onItemTapped = { onItemTapped = {
interactor.onTabCounterMenuItemTapped(it) interactor.onTabCounterMenuItemTapped(it)
}, },
iconColor =
if (isPrivate) {
ContextCompat.getColor(context, R.color.primary_text_private_theme)
} else {
null
}
).also {
it.updateMenu(context.settings().toolbarPosition)
}
val tabsAction = TabCounterToolbarButton(
lifecycleOwner = lifecycleOwner,
showTabs = { showTabs = {
toolbar.hideKeyboard() toolbar.hideKeyboard()
interactor.onTabCounterClicked() interactor.onTabCounterClicked()
} },
store = store,
menu = tabCounterMenu,
privateColor = ContextCompat.getColor(context, R.color.primary_text_private_theme)
) )
val tabCount = if (isPrivate) {
store.state.privateTabs.size
} else {
store.state.normalTabs.size
}
tabsAction.updateCount(tabCount)
toolbar.addBrowserAction(tabsAction) toolbar.addBrowserAction(tabsAction)
val engineForSpeculativeConnects = if (!isPrivate) engine else null val engineForSpeculativeConnects = if (!isPrivate) engine else null

@ -17,8 +17,9 @@ import mozilla.components.browser.menu.item.BrowserMenuImageSwitch
import mozilla.components.browser.menu.item.BrowserMenuImageText import mozilla.components.browser.menu.item.BrowserMenuImageText
import mozilla.components.browser.menu.item.BrowserMenuItemToolbar import mozilla.components.browser.menu.item.BrowserMenuItemToolbar
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import mozilla.components.browser.session.Session import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.toolbar.ToolbarMenu import org.mozilla.fenix.components.toolbar.ToolbarMenu
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
@ -29,14 +30,14 @@ import java.util.Locale
/** /**
* Builds the toolbar object used with the 3-dot menu in the custom tab browser fragment. * Builds the toolbar object used with the 3-dot menu in the custom tab browser fragment.
* @param sessionManager Reference to the session manager that contains all tabs. * @param store reference to the application's [BrowserStore].
* @param sessionId ID of the open custom tab session. * @param sessionId ID of the open custom tab session.
* @param shouldReverseItems If true, reverse the menu items. * @param shouldReverseItems If true, reverse the menu items.
* @param onItemTapped Called when a menu item is tapped. * @param onItemTapped Called when a menu item is tapped.
*/ */
class CustomTabToolbarMenu( class CustomTabToolbarMenu(
private val context: Context, private val context: Context,
private val sessionManager: SessionManager, private val store: BrowserStore,
private val sessionId: String?, private val sessionId: String?,
private val shouldReverseItems: Boolean, private val shouldReverseItems: Boolean,
private val onItemTapped: (ToolbarMenu.Item) -> Unit = {} private val onItemTapped: (ToolbarMenu.Item) -> Unit = {}
@ -45,7 +46,7 @@ class CustomTabToolbarMenu(
override val menuBuilder by lazy { BrowserMenuBuilder(menuItems) } override val menuBuilder by lazy { BrowserMenuBuilder(menuItems) }
/** Gets the current custom tab session */ /** Gets the current custom tab session */
private val session: Session? get() = sessionId?.let { sessionManager.findSessionById(it) } private val session: TabSessionState? get() = sessionId?.let { store.state.findTab(it) }
private val appName = context.getString(R.string.app_name) private val appName = context.getString(R.string.app_name)
override val menuToolbar by lazy { override val menuToolbar by lazy {
@ -54,7 +55,7 @@ class CustomTabToolbarMenu(
primaryContentDescription = context.getString(R.string.browser_menu_back), primaryContentDescription = context.getString(R.string.browser_menu_back),
primaryImageTintResource = primaryTextColor(), primaryImageTintResource = primaryTextColor(),
isInPrimaryState = { isInPrimaryState = {
session?.canGoBack ?: true session?.content?.canGoBack ?: true
}, },
secondaryImageTintResource = ThemeManager.resolveAttribute( secondaryImageTintResource = ThemeManager.resolveAttribute(
R.attr.disabled, R.attr.disabled,
@ -71,7 +72,7 @@ class CustomTabToolbarMenu(
primaryContentDescription = context.getString(R.string.browser_menu_forward), primaryContentDescription = context.getString(R.string.browser_menu_forward),
primaryImageTintResource = primaryTextColor(), primaryImageTintResource = primaryTextColor(),
isInPrimaryState = { isInPrimaryState = {
session?.canGoForward ?: true session?.content?.canGoForward ?: true
}, },
secondaryImageTintResource = ThemeManager.resolveAttribute( secondaryImageTintResource = ThemeManager.resolveAttribute(
R.attr.disabled, R.attr.disabled,
@ -88,7 +89,7 @@ class CustomTabToolbarMenu(
primaryContentDescription = context.getString(R.string.browser_menu_refresh), primaryContentDescription = context.getString(R.string.browser_menu_refresh),
primaryImageTintResource = primaryTextColor(), primaryImageTintResource = primaryTextColor(),
isInPrimaryState = { isInPrimaryState = {
session?.loading == false session?.content?.loading == false
}, },
secondaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_stop, secondaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_stop,
secondaryContentDescription = context.getString(R.string.browser_menu_stop), secondaryContentDescription = context.getString(R.string.browser_menu_stop),
@ -96,7 +97,7 @@ class CustomTabToolbarMenu(
disableInSecondaryState = false, disableInSecondaryState = false,
longClickListener = { onItemTapped.invoke(ToolbarMenu.Item.Reload(bypassCache = true)) } longClickListener = { onItemTapped.invoke(ToolbarMenu.Item.Reload(bypassCache = true)) }
) { ) {
if (session?.loading == true) { if (session?.content?.loading == true) {
onItemTapped.invoke(ToolbarMenu.Item.Stop) onItemTapped.invoke(ToolbarMenu.Item.Stop)
} else { } else {
onItemTapped.invoke(ToolbarMenu.Item.Reload(bypassCache = false)) onItemTapped.invoke(ToolbarMenu.Item.Reload(bypassCache = false))
@ -108,7 +109,7 @@ class CustomTabToolbarMenu(
private fun shouldShowOpenInApp(): Boolean = session?.let { session -> private fun shouldShowOpenInApp(): Boolean = session?.let { session ->
val appLink = context.components.useCases.appLinksUseCases.appLinkRedirect val appLink = context.components.useCases.appLinksUseCases.appLinkRedirect
appLink(session.url).hasExternalApp() appLink(session.content.url).hasExternalApp()
} ?: false } ?: false
private val menuItems by lazy { private val menuItems by lazy {
@ -132,7 +133,7 @@ class CustomTabToolbarMenu(
private val desktopMode = BrowserMenuImageSwitch( private val desktopMode = BrowserMenuImageSwitch(
imageResource = R.drawable.ic_desktop, imageResource = R.drawable.ic_desktop,
label = context.getString(R.string.browser_menu_desktop_site), label = context.getString(R.string.browser_menu_desktop_site),
initialState = { session?.desktopMode ?: false } initialState = { session?.content?.desktopMode ?: false }
) { checked -> ) { checked ->
onItemTapped.invoke(ToolbarMenu.Item.RequestDesktop(checked)) onItemTapped.invoke(ToolbarMenu.Item.RequestDesktop(checked))
} }

@ -7,6 +7,7 @@ package org.mozilla.fenix.customtabs
import android.app.Activity import android.app.Activity
import androidx.appcompat.content.res.AppCompatResources.getDrawable import androidx.appcompat.content.res.AppCompatResources.getDrawable
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.browser.toolbar.display.DisplayToolbar import mozilla.components.browser.toolbar.display.DisplayToolbar
import mozilla.components.feature.customtabs.CustomTabsToolbarFeature import mozilla.components.feature.customtabs.CustomTabsToolbarFeature
@ -18,6 +19,7 @@ import org.mozilla.fenix.ext.settings
class CustomTabsIntegration( class CustomTabsIntegration(
sessionManager: SessionManager, sessionManager: SessionManager,
store: BrowserStore,
toolbar: BrowserToolbar, toolbar: BrowserToolbar,
sessionId: String, sessionId: String,
activity: Activity, activity: Activity,
@ -74,7 +76,7 @@ class CustomTabsIntegration(
private val customTabToolbarMenu by lazy { private val customTabToolbarMenu by lazy {
CustomTabToolbarMenu( CustomTabToolbarMenu(
activity, activity,
sessionManager, store,
sessionId, sessionId,
shouldReverseItems, shouldReverseItems,
onItemTapped = onItemTapped onItemTapped = onItemTapped

@ -5,10 +5,13 @@
package org.mozilla.fenix.customtabs package org.mozilla.fenix.customtabs
import android.content.Intent import android.content.Intent
import androidx.annotation.VisibleForTesting
import androidx.navigation.NavDestination import androidx.navigation.NavDestination
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
import kotlinx.android.synthetic.main.activity_home.* import kotlinx.android.synthetic.main.activity_home.*
import mozilla.components.browser.session.runWithSession import mozilla.components.browser.session.runWithSession
import mozilla.components.browser.state.selector.findCustomTab
import mozilla.components.browser.state.state.SessionState
import mozilla.components.concept.engine.manifest.WebAppManifestParser import mozilla.components.concept.engine.manifest.WebAppManifestParser
import mozilla.components.feature.intent.ext.getSessionId import mozilla.components.feature.intent.ext.getSessionId
import mozilla.components.feature.pwa.ext.getWebAppManifest import mozilla.components.feature.pwa.ext.getWebAppManifest
@ -24,7 +27,20 @@ import java.security.InvalidParameterException
* Activity that holds the [ExternalAppBrowserFragment] that is launched within an external app, * Activity that holds the [ExternalAppBrowserFragment] that is launched within an external app,
* such as custom tabs and progressive web apps. * such as custom tabs and progressive web apps.
*/ */
@Suppress("TooManyFunctions")
open class ExternalAppBrowserActivity : HomeActivity() { open class ExternalAppBrowserActivity : HomeActivity() {
override fun onResume() {
super.onResume()
if (!hasExternalTab()) {
// An ExternalAppBrowserActivity is always bound to a specific tab. If this tab doesn't
// exist anymore on resume then this activity has nothing to display anymore. Let's just
// finish it AND remove this task to avoid it hanging around in the recent apps screen.
// Without this the parent HomeActivity class may decide to show the browser UI and we
// end up with multiple browsers (causing "display already acquired" crashes).
finishAndRemoveTask()
}
}
final override fun getBreadcrumbMessage(destination: NavDestination): String { final override fun getBreadcrumbMessage(destination: NavDestination): String {
val fragmentName = resources.getResourceEntryName(destination.id) val fragmentName = resources.getResourceEntryName(destination.id)
@ -86,8 +102,7 @@ open class ExternalAppBrowserActivity : HomeActivity() {
// When this activity finishes, the process is staying around and the session still // When this activity finishes, the process is staying around and the session still
// exists then remove it now to free all its resources. Once this activity is finished // exists then remove it now to free all its resources. Once this activity is finished
// then there's no way to get back to it other than relaunching it. // then there's no way to get back to it other than relaunching it.
val sessionId = getIntentSessionId(SafeIntent(intent)) components.core.sessionManager.runWithSession(getExternalTabId()) { session ->
components.core.sessionManager.runWithSession(sessionId) { session ->
// If the custom tag config has been removed we are opening this in normal browsing // If the custom tag config has been removed we are opening this in normal browsing
if (session.customTabConfig != null) { if (session.customTabConfig != null) {
remove(session) remove(session)
@ -96,4 +111,20 @@ open class ExternalAppBrowserActivity : HomeActivity() {
} }
} }
} }
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun hasExternalTab(): Boolean {
return getExternalTab() != null
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun getExternalTab(): SessionState? {
val id = getExternalTabId() ?: return null
return components.core.store.state.findCustomTab(id)
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun getExternalTabId(): String? {
return getIntentSessionId(SafeIntent(intent))
}
} }

@ -65,6 +65,7 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
customTabsIntegration.set( customTabsIntegration.set(
feature = CustomTabsIntegration( feature = CustomTabsIntegration(
sessionManager = requireComponents.core.sessionManager, sessionManager = requireComponents.core.sessionManager,
store = requireComponents.core.store,
toolbar = toolbar, toolbar = toolbar,
sessionId = customTabSessionId, sessionId = customTabSessionId,
activity = activity, activity = activity,
@ -125,7 +126,7 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
), ),
ManifestUpdateFeature( ManifestUpdateFeature(
activity.applicationContext, activity.applicationContext,
requireComponents.core.sessionManager, requireComponents.core.store,
requireComponents.core.webAppShortcutManager, requireComponents.core.webAppShortcutManager,
requireComponents.core.webAppManifestStorage, requireComponents.core.webAppManifestStorage,
customTabSessionId, customTabSessionId,
@ -135,7 +136,7 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
viewLifecycleOwner.lifecycle.addObserver( viewLifecycleOwner.lifecycle.addObserver(
WebAppSiteControlsFeature( WebAppSiteControlsFeature(
activity.applicationContext, activity.applicationContext,
requireComponents.core.sessionManager, requireComponents.core.store,
requireComponents.useCases.sessionUseCases.reload, requireComponents.useCases.sessionUseCases.reload,
customTabSessionId, customTabSessionId,
manifest, manifest,

@ -69,7 +69,7 @@ class FennecWebAppIntentProcessor(
webAppManifest?.toCustomTabConfig() ?: createFallbackCustomTabConfig() webAppManifest?.toCustomTabConfig() ?: createFallbackCustomTabConfig()
sessionManager.add(session) sessionManager.add(session)
loadUrlUseCase(url, session, EngineSession.LoadUrlFlags.external()) loadUrlUseCase(url, session.id, EngineSession.LoadUrlFlags.external())
intent.putSessionId(session.id) intent.putSessionId(session.id)

@ -7,8 +7,8 @@ package org.mozilla.fenix.customtabs
import android.app.Notification import android.app.Notification
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.state.CustomTabSessionState
import mozilla.components.concept.engine.manifest.WebAppManifest import mozilla.components.concept.engine.manifest.WebAppManifest
import mozilla.components.feature.pwa.feature.SiteControlsBuilder import mozilla.components.feature.pwa.feature.SiteControlsBuilder
import mozilla.components.feature.session.SessionUseCases import mozilla.components.feature.session.SessionUseCases
@ -36,6 +36,7 @@ class WebAppSiteControlsBuilder(
override fun getFilter() = inner.getFilter() override fun getFilter() = inner.getFilter()
override fun onReceiveBroadcast(context: Context, session: Session, intent: Intent) = override fun onReceiveBroadcast(context: Context, tab: CustomTabSessionState, intent: Intent) {
inner.onReceiveBroadcast(context, session, intent) inner.onReceiveBroadcast(context, tab, intent)
}
} }

@ -14,6 +14,7 @@ import mozilla.components.feature.downloads.AbstractFetchDownloadService
import mozilla.components.feature.downloads.toMegabyteOrKilobyteString import mozilla.components.feature.downloads.toMegabyteOrKilobyteString
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
@ -26,6 +27,7 @@ import org.mozilla.fenix.ext.settings
class DynamicDownloadDialog( class DynamicDownloadDialog(
private val container: ViewGroup, private val container: ViewGroup,
private val downloadState: DownloadState?, private val downloadState: DownloadState?,
private val metrics: MetricController,
private val didFail: Boolean, private val didFail: Boolean,
private val tryAgain: (String) -> Unit, private val tryAgain: (String) -> Unit,
private val onCannotOpenFile: () -> Unit, private val onCannotOpenFile: () -> Unit,
@ -99,6 +101,8 @@ class DynamicDownloadDialog(
mozilla.components.feature.downloads.R.string.mozac_feature_downloads_button_open mozilla.components.feature.downloads.R.string.mozac_feature_downloads_button_open
) )
setOnClickListener { setOnClickListener {
metrics.track(Event.DownloadsItemOpened)
val fileWasOpened = AbstractFetchDownloadService.openFile( val fileWasOpened = AbstractFetchDownloadService.openFile(
context = context, context = context,
contentType = downloadState.contentType, contentType = downloadState.contentType,

@ -0,0 +1,21 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.experiments
class Experiments {
companion object {
const val A_A_NIMBUS_VALIDATION = "fenix-nimbus-validation"
const val BOOKMARK_ICON = "fenix-bookmark-list-icon"
}
}
class ExperimentBranch {
companion object {
const val TREATMENT = "treatment"
const val CONTROL = "control"
const val A1 = "A1"
const val A2 = "A2"
}
}

@ -14,7 +14,6 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.accessibility.AccessibilityManager import android.view.accessibility.AccessibilityManager
import androidx.annotation.StringRes import androidx.annotation.StringRes
import mozilla.components.browser.search.SearchEngineManager
import mozilla.components.support.locale.LocaleManager import mozilla.components.support.locale.LocaleManager
import org.mozilla.fenix.FenixApplication import org.mozilla.fenix.FenixApplication
import org.mozilla.fenix.components.Components import org.mozilla.fenix.components.Components
@ -41,12 +40,6 @@ val Context.components: Components
val Context.metrics: MetricController val Context.metrics: MetricController
get() = this.components.analytics.metrics get() = this.components.analytics.metrics
/**
* Helper function to get the SearchEngineManager off of context.
*/
val Context.searchEngineManager: SearchEngineManager
get() = this.components.search.searchEngineManager
fun Context.asActivity() = (this as? ContextThemeWrapper)?.baseContext as? Activity fun Context.asActivity() = (this as? ContextThemeWrapper)?.baseContext as? Activity
?: this as? Activity ?: this as? Activity

@ -0,0 +1,42 @@
/* 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.ext
import mozilla.components.service.nimbus.NimbusApi
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.FeatureFlags
/**
* Gets the branch of the given `experimentId` and transforms it with given closure.
*
* If we're enrolled in the experiment, the transform is passed the branch id/slug as a `String`.
*
* If we're not enrolled in the experiment, or the experiment is not valid then the transform
* is passed a `null`.
*/
@Suppress("TooGenericExceptionCaught")
fun <T> NimbusApi.withExperiment(experimentId: String, transform: (String?) -> T): T {
val branch = if (FeatureFlags.nimbusExperiments) {
try {
getExperimentBranch(experimentId)
} catch (e: Throwable) {
Logger.error("Failed to getExperimentBranch($experimentId)", e)
null
}
} else {
null
}
return transform(branch)
}
/**
* The degenerate case of `withExperiment(String, (String?) -> T))`, with an identity transform.
*
* Short-hand for `mozilla.components.service.nimbus.NimbusApi.getExperimentBranch`.
*/
fun NimbusApi.withExperiment(experimentId: String) =
this.withExperiment(experimentId, ::identity)
private fun <T> identity(value: T) = value

@ -15,7 +15,9 @@ import android.view.Display.FLAG_SECURE
import android.view.Gravity import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.View.AccessibilityDelegate
import android.view.ViewGroup import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import android.widget.Button import android.widget.Button
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.PopupWindow import android.widget.PopupWindow
@ -39,25 +41,34 @@ import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_home.* import kotlinx.android.synthetic.main.fragment_home.*
import kotlinx.android.synthetic.main.fragment_home.view.* import kotlinx.android.synthetic.main.fragment_home.view.bottomBarShadow
import kotlinx.android.synthetic.main.no_collections_message.view.* import kotlinx.android.synthetic.main.fragment_home.view.bottom_bar
import kotlinx.android.synthetic.main.fragment_home.view.homeAppBar
import kotlinx.android.synthetic.main.fragment_home.view.menuButton
import kotlinx.android.synthetic.main.fragment_home.view.sessionControlRecyclerView
import kotlinx.android.synthetic.main.fragment_home.view.tab_button
import kotlinx.android.synthetic.main.fragment_home.view.toolbar
import kotlinx.android.synthetic.main.fragment_home.view.toolbarLayout
import kotlinx.android.synthetic.main.fragment_home.view.toolbar_wrapper
import kotlinx.android.synthetic.main.no_collections_message.view.add_tabs_to_collections_button
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.appservices.places.BookmarkRoot import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.menu.view.MenuButton import mozilla.components.browser.menu.view.MenuButton
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.selector.normalTabs import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs import mozilla.components.browser.state.selector.privateTabs
import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.AuthType
@ -65,9 +76,12 @@ import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSitesConfig import mozilla.components.feature.top.sites.TopSitesConfig
import mozilla.components.feature.top.sites.TopSitesFeature import mozilla.components.feature.top.sites.TopSitesFeature
import mozilla.components.lib.state.ext.consumeFlow
import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.content.res.resolveAttribute import mozilla.components.support.ktx.android.content.res.resolveAttribute
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import mozilla.components.ui.tabcounter.TabCounterMenu
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
@ -81,7 +95,7 @@ import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.tips.FenixTipManager import org.mozilla.fenix.components.tips.FenixTipManager
import org.mozilla.fenix.components.tips.Tip import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.components.tips.providers.MasterPasswordTipProvider import org.mozilla.fenix.components.tips.providers.MasterPasswordTipProvider
import org.mozilla.fenix.components.toolbar.TabCounterMenu import org.mozilla.fenix.components.toolbar.FenixTabCounterMenu
import org.mozilla.fenix.components.toolbar.ToolbarPosition import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.hideToolbar import org.mozilla.fenix.ext.hideToolbar
@ -99,7 +113,6 @@ import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.settings.SupportUtils.SumoTopic.HELP import org.mozilla.fenix.settings.SupportUtils.SumoTopic.HELP
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.FragmentPreDrawManager
import org.mozilla.fenix.utils.ToolbarPopupWindow import org.mozilla.fenix.utils.ToolbarPopupWindow
import org.mozilla.fenix.utils.allowUndo import org.mozilla.fenix.utils.allowUndo
import org.mozilla.fenix.whatsnew.WhatsNew import org.mozilla.fenix.whatsnew.WhatsNew
@ -123,14 +136,6 @@ class HomeFragment : Fragment() {
private val browsingModeManager get() = (activity as HomeActivity).browsingModeManager private val browsingModeManager get() = (activity as HomeActivity).browsingModeManager
private val collectionStorageObserver = object : TabCollectionStorage.Observer { private val collectionStorageObserver = object : TabCollectionStorage.Observer {
override fun onCollectionCreated(title: String, sessions: List<Session>) {
scrollAndAnimateCollection()
}
override fun onTabsAdded(tabCollection: TabCollection, sessions: List<Session>) {
scrollAndAnimateCollection(tabCollection)
}
override fun onCollectionRenamed(tabCollection: TabCollection, title: String) { override fun onCollectionRenamed(tabCollection: TabCollection, title: String) {
lifecycleScope.launch(Main) { lifecycleScope.launch(Main) {
view?.sessionControlRecyclerView?.adapter?.notifyDataSetChanged() view?.sessionControlRecyclerView?.adapter?.notifyDataSetChanged()
@ -156,13 +161,14 @@ class HomeFragment : Fragment() {
get() = _sessionControlInteractor!! get() = _sessionControlInteractor!!
private var sessionControlView: SessionControlView? = null private var sessionControlView: SessionControlView? = null
private var appBarLayout: AppBarLayout? = null
private lateinit var currentMode: CurrentMode private lateinit var currentMode: CurrentMode
private val topSitesFeature = ViewBoundFeatureWrapper<TopSitesFeature>() private val topSitesFeature = ViewBoundFeatureWrapper<TopSitesFeature>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
postponeEnterTransition()
bundleArgs = args.toBundle() bundleArgs = args.toBundle()
lifecycleScope.launch(IO) { lifecycleScope.launch(IO) {
if (!onboarding.userHasBeenOnboarded()) { if (!onboarding.userHasBeenOnboarded()) {
@ -227,9 +233,11 @@ class HomeFragment : Fragment() {
settings = components.settings, settings = components.settings,
engine = components.core.engine, engine = components.core.engine,
metrics = components.analytics.metrics, metrics = components.analytics.metrics,
store = store,
sessionManager = sessionManager, sessionManager = sessionManager,
tabCollectionStorage = components.core.tabCollectionStorage, tabCollectionStorage = components.core.tabCollectionStorage,
addTabUseCase = components.useCases.tabsUseCases.addTab, addTabUseCase = components.useCases.tabsUseCases.addTab,
reloadUrlUseCase = components.useCases.sessionUseCases.reload,
fragmentStore = homeFragmentStore, fragmentStore = homeFragmentStore,
navController = findNavController(), navController = findNavController(),
viewLifecycleScope = viewLifecycleOwner.lifecycleScope, viewLifecycleScope = viewLifecycleOwner.lifecycleScope,
@ -251,6 +259,8 @@ class HomeFragment : Fragment() {
updateSessionControlView(view) updateSessionControlView(view)
appBarLayout = view.homeAppBar
activity.themeManager.applyStatusBarTheme(activity) activity.themeManager.applyStatusBarTheme(activity)
return view return view
} }
@ -327,53 +337,9 @@ class HomeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
FragmentPreDrawManager(this).execute { observeSearchEngineChanges()
homeViewModel.layoutManagerState?.also { parcelable ->
sessionControlView!!.view.layoutManager?.onRestoreInstanceState(parcelable)
}
homeViewModel.layoutManagerState = null
// We have to delay so that the keyboard collapses and the view is resized before the
// animation from SearchFragment happens
delay(ANIMATION_DELAY)
}
viewLifecycleOwner.lifecycleScope.launch(IO) {
// This is necessary due to a bug in viewLifecycleOwner. See:
// https://github.com/mozilla-mobile/android-components/blob/master/components/lib/state/src/main/java/mozilla/components/lib/state/ext/Fragment.kt#L32-L56
// TODO remove when viewLifecycleOwner is fixed
val context = context ?: return@launch
val iconSize =
context.resources.getDimensionPixelSize(R.dimen.preference_icon_drawable_size)
val searchEngine = context.components.search.provider.getDefaultEngine(context)
val searchIcon = BitmapDrawable(context.resources, searchEngine.icon)
searchIcon.setBounds(0, 0, iconSize, iconSize)
withContext(Main) {
search_engine_icon?.setImageDrawable(searchIcon)
}
}
createHomeMenu(requireContext(), WeakReference(view.menuButton)) createHomeMenu(requireContext(), WeakReference(view.menuButton))
val tabCounterMenu = TabCounterMenu( createTabCounterMenu(view)
view.context,
metrics = view.context.components.analytics.metrics
) {
if (it is TabCounterMenu.Item.NewTab) {
(activity as HomeActivity).browsingModeManager.mode = it.mode
}
}
val inverseBrowsingMode = when ((activity as HomeActivity).browsingModeManager.mode) {
BrowsingMode.Normal -> BrowsingMode.Private
BrowsingMode.Private -> BrowsingMode.Normal
}
tabCounterMenu.updateMenu(showOnly = inverseBrowsingMode)
view.tab_button.setOnLongClickListener {
tabCounterMenu.menuController.show(anchor = it)
true
}
view.menuButton.setColorFilter( view.menuButton.setColorFilter(
ContextCompat.getColor( ContextCompat.getColor(
@ -385,7 +351,6 @@ class HomeFragment : Fragment() {
view.toolbar.compoundDrawablePadding = view.toolbar.compoundDrawablePadding =
view.resources.getDimensionPixelSize(R.dimen.search_bar_search_engine_icon_padding) view.resources.getDimensionPixelSize(R.dimen.search_bar_search_engine_icon_padding)
view.toolbar_wrapper.setOnClickListener { view.toolbar_wrapper.setOnClickListener {
hideOnboardingIfNeeded()
navigateToSearch() navigateToSearch()
requireComponents.analytics.metrics.track(Event.SearchBarTapped(Event.SearchBarTapped.Source.HOME)) requireComponents.analytics.metrics.track(Event.SearchBarTapped(Event.SearchBarTapped.Source.HOME))
} }
@ -443,6 +408,64 @@ class HomeFragment : Fragment() {
if (bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR)) { if (bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR)) {
navigateToSearch() navigateToSearch()
} else if (bundleArgs.getLong(FOCUS_ON_COLLECTION, -1) >= 0) {
// No need to scroll to async'd loaded TopSites if we want to scroll to collections.
homeViewModel.shouldScrollToTopSites = false
/* Triggered when the user has added a tab to a collection and has tapped
* the View action on the [TabsTrayDialogFragment] snackbar.*/
scrollAndAnimateCollection(bundleArgs.getLong(FOCUS_ON_COLLECTION, -1))
}
}
private fun observeSearchEngineChanges() {
consumeFlow(store) { flow ->
flow.map { state -> state.search.selectedOrDefaultSearchEngine }
.ifChanged()
.collect { searchEngine ->
if (searchEngine != null) {
val iconSize =
requireContext().resources.getDimensionPixelSize(R.dimen.preference_icon_drawable_size)
val searchIcon = BitmapDrawable(requireContext().resources, searchEngine.icon)
searchIcon.setBounds(0, 0, iconSize, iconSize)
search_engine_icon?.setImageDrawable(searchIcon)
} else {
search_engine_icon.setImageDrawable(null)
}
}
}
}
private fun createTabCounterMenu(view: View) {
val browsingModeManager = (activity as HomeActivity).browsingModeManager
val mode = browsingModeManager.mode
val onItemTapped: (TabCounterMenu.Item) -> Unit = {
if (it is TabCounterMenu.Item.NewTab) {
browsingModeManager.mode = BrowsingMode.Normal
} else if (it is TabCounterMenu.Item.NewPrivateTab) {
browsingModeManager.mode = BrowsingMode.Private
}
}
val tabCounterMenu = FenixTabCounterMenu(
view.context,
onItemTapped,
iconColor = if (mode == BrowsingMode.Private) {
ContextCompat.getColor(requireContext(), R.color.primary_text_private_theme)
} else {
null
}
)
val inverseBrowsingMode = when (mode) {
BrowsingMode.Normal -> BrowsingMode.Private
BrowsingMode.Private -> BrowsingMode.Normal
}
tabCounterMenu.updateMenu(showOnly = inverseBrowsingMode)
view.tab_button.setOnLongClickListener {
tabCounterMenu.menuController.show(anchor = it)
true
} }
} }
@ -499,14 +522,17 @@ class HomeFragment : Fragment() {
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
_sessionControlInteractor = null _sessionControlInteractor = null
sessionControlView = null sessionControlView = null
appBarLayout = null
bundleArgs.clear() bundleArgs.clear()
requireActivity().window.clearFlags(FLAG_SECURE) requireActivity().window.clearFlags(FLAG_SECURE)
} }
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
subscribeToTabCollections() subscribeToTabCollections()
val context = requireContext() val context = requireContext()
@ -614,12 +640,6 @@ class HomeFragment : Fragment() {
}.show() }.show()
} }
override fun onStop() {
super.onStop()
homeViewModel.layoutManagerState =
sessionControlView!!.view.layoutManager?.onSaveInstanceState()
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (browsingModeManager.mode == BrowsingMode.Private) { if (browsingModeManager.mode == BrowsingMode.Private) {
@ -698,6 +718,20 @@ class HomeFragment : Fragment() {
} }
private fun navigateToSearch() { private fun navigateToSearch() {
// Dismisses the search dialog when the home content is scrolled
val recyclerView = sessionControlView!!.view
val listener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == RecyclerView.SCROLL_STATE_DRAGGING || newState == RecyclerView.SCROLL_STATE_SETTLING) {
findNavController().navigateUp()
recyclerView.removeOnScrollListener(this)
}
}
}
recyclerView.addOnScrollListener(listener)
val directions = val directions =
HomeFragmentDirections.actionGlobalSearchDialog( HomeFragmentDirections.actionGlobalSearchDialog(
sessionId = null sessionId = null
@ -815,31 +849,23 @@ class HomeFragment : Fragment() {
requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this) requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this)
} }
/**
* This method will find and scroll to the row of the specified collection Id.
* */
private fun scrollAndAnimateCollection( private fun scrollAndAnimateCollection(
changedCollection: TabCollection? = null collectionIdToSelect: Long = -1
) { ) {
if (view != null) { if (view != null && collectionIdToSelect >= 0) {
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
val recyclerView = sessionControlView!!.view val recyclerView = sessionControlView!!.view
delay(ANIM_SCROLL_DELAY) delay(ANIM_SCROLL_DELAY)
val tabsSize = store.state val indexOfCollection =
.getNormalOrPrivateTabs(browsingModeManager.mode.isPrivate) NON_COLLECTION_ITEM_NUM + findIndexOfSpecificCollection(collectionIdToSelect)
.size
var indexOfCollection = tabsSize + NON_TAB_ITEM_NUM
changedCollection?.let { changedCollection ->
requireComponents.core.tabCollectionStorage.cachedTabCollections
.filterIndexed { index, tabCollection ->
if (tabCollection.id == changedCollection.id) {
indexOfCollection = tabsSize + NON_TAB_ITEM_NUM + index
return@filterIndexed true
}
false
}
}
val lastVisiblePosition = val lastVisiblePosition =
(recyclerView.layoutManager as? LinearLayoutManager)?.findLastCompletelyVisibleItemPosition() (recyclerView.layoutManager as? LinearLayoutManager)?.findLastCompletelyVisibleItemPosition()
?: 0 ?: 0
if (lastVisiblePosition < indexOfCollection) { if (lastVisiblePosition < indexOfCollection) {
val onScrollListener = object : RecyclerView.OnScrollListener() { val onScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged( override fun onScrollStateChanged(
@ -848,6 +874,7 @@ class HomeFragment : Fragment() {
) { ) {
super.onScrollStateChanged(recyclerView, newState) super.onScrollStateChanged(recyclerView, newState)
if (newState == SCROLL_STATE_IDLE) { if (newState == SCROLL_STATE_IDLE) {
appBarLayout?.setExpanded(false)
animateCollection(indexOfCollection) animateCollection(indexOfCollection)
recyclerView.removeOnScrollListener(this) recyclerView.removeOnScrollListener(this)
} }
@ -856,12 +883,34 @@ class HomeFragment : Fragment() {
recyclerView.addOnScrollListener(onScrollListener) recyclerView.addOnScrollListener(onScrollListener)
recyclerView.smoothScrollToPosition(indexOfCollection) recyclerView.smoothScrollToPosition(indexOfCollection)
} else { } else {
appBarLayout?.setExpanded(false)
animateCollection(indexOfCollection) animateCollection(indexOfCollection)
} }
} }
} }
} }
/**
* Returns index of the collection with the specified id.
* */
private fun findIndexOfSpecificCollection(
changedCollectionId: Long
): Int {
var result = 0
requireComponents.core.tabCollectionStorage.cachedTabCollections
.filterIndexed { index, tabCollection ->
if (tabCollection.id == changedCollectionId) {
result = index
return@filterIndexed true
}
false
}
return result
}
/**
* Will highlight the border of the collection with the specified index.
* */
private fun animateCollection(indexOfCollection: Int) { private fun animateCollection(indexOfCollection: Int) {
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
val viewHolder = val viewHolder =
@ -889,22 +938,41 @@ class HomeFragment : Fragment() {
?.setDuration(FADE_ANIM_DURATION) ?.setDuration(FADE_ANIM_DURATION)
?.setListener(listener)?.start() ?.setListener(listener)?.start()
}.invokeOnCompletion { }.invokeOnCompletion {
showSavedSnackbar() val a11yEnabled = context?.settings()?.accessibilityServicesEnabled ?: false
if (a11yEnabled) {
focusCollectionForTalkBack(indexOfCollection)
}
} }
} }
private fun showSavedSnackbar() { /**
* Will focus the collection with [indexOfCollection] for accessibility services.
* */
private fun focusCollectionForTalkBack(indexOfCollection: Int) {
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
delay(ANIM_SNACKBAR_DELAY) var focusedForAccessibility = false
view?.let { view -> view?.let { mainView ->
FenixSnackbar.make( mainView.accessibilityDelegate = object : AccessibilityDelegate() {
view = view, override fun onRequestSendAccessibilityEvent(
duration = Snackbar.LENGTH_LONG, host: ViewGroup,
isDisplayedWithBrowserToolbar = false child: View,
) event: AccessibilityEvent
.setText(view.context.getString(R.string.create_collection_tabs_saved_new_collection)) ): Boolean {
.setAnchorView(snackbarAnchorView) if (!focusedForAccessibility &&
.show() event.eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED
) {
sessionControlView?.view?.findViewHolderForAdapterPosition(
indexOfCollection
)?.itemView?.let { viewToFocus ->
focusedForAccessibility = true
viewToFocus.requestFocus()
viewToFocus.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
return false
}
}
return super.onRequestSendAccessibilityEvent(host, child, event)
}
}
} }
} }
} }
@ -930,8 +998,11 @@ class HomeFragment : Fragment() {
) )
} }
// TODO use [FenixTabCounterToolbarButton] instead of [TabCounter]:
// https://github.com/mozilla-mobile/fenix/issues/16792
private fun updateTabCounter(browserState: BrowserState) { private fun updateTabCounter(browserState: BrowserState) {
val tabCount = if (browsingModeManager.mode.isPrivate) { val tabCount = if (browsingModeManager.mode.isPrivate) {
view?.tab_button?.setColor(ContextCompat.getColor(requireContext(), R.color.primary_text_private_theme))
browserState.privateTabs.size browserState.privateTabs.size
} else { } else {
browserState.normalTabs.size browserState.normalTabs.size
@ -950,13 +1021,18 @@ class HomeFragment : Fragment() {
const val ALL_PRIVATE_TABS = "all_private" const val ALL_PRIVATE_TABS = "all_private"
private const val FOCUS_ON_ADDRESS_BAR = "focusOnAddressBar" private const val FOCUS_ON_ADDRESS_BAR = "focusOnAddressBar"
private const val FOCUS_ON_COLLECTION = "focusOnCollection"
private const val ANIMATION_DELAY = 100L private const val ANIMATION_DELAY = 100L
private const val NON_TAB_ITEM_NUM = 3 /**
* Represents the number of items in [sessionControlView] that are NOT part of
* the list of collections. At the moment these are topSites pager, collections header.
* */
private const val NON_COLLECTION_ITEM_NUM = 2
private const val ANIM_SCROLL_DELAY = 100L private const val ANIM_SCROLL_DELAY = 100L
private const val ANIM_ON_SCREEN_DELAY = 200L private const val ANIM_ON_SCREEN_DELAY = 200L
private const val FADE_ANIM_DURATION = 150L private const val FADE_ANIM_DURATION = 150L
private const val ANIM_SNACKBAR_DELAY = 100L
private const val CFR_WIDTH_DIVIDER = 1.7 private const val CFR_WIDTH_DIVIDER = 1.7
private const val CFR_Y_OFFSET = -20 private const val CFR_Y_OFFSET = -20
} }

@ -22,8 +22,11 @@ import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.support.ktx.android.content.getColorFromAttr import mozilla.components.support.ktx.android.content.getColorFromAttr
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.experiments.ExperimentBranch
import org.mozilla.fenix.experiments.Experiments
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.withExperiment
import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.whatsnew.WhatsNew import org.mozilla.fenix.whatsnew.WhatsNew
@ -96,17 +99,38 @@ class HomeMenu(
onItemTapped.invoke(Item.WhatsNew) onItemTapped.invoke(Item.WhatsNew)
} }
val experiments = context.components.analytics.experiments
val bookmarksIcon = experiments.withExperiment(Experiments.BOOKMARK_ICON) {
when (it) {
ExperimentBranch.TREATMENT -> R.drawable.ic_bookmark_list
else -> R.drawable.ic_bookmark_filled
}
}
val bookmarksItem = BrowserMenuImageText( val bookmarksItem = BrowserMenuImageText(
context.getString(R.string.library_bookmarks), context.getString(R.string.library_bookmarks),
R.drawable.ic_bookmark_filled, bookmarksIcon,
primaryTextColor primaryTextColor
) { ) {
onItemTapped.invoke(Item.Bookmarks) onItemTapped.invoke(Item.Bookmarks)
} }
// We want to validate that the Nimbus experiments library is working, from the android UI
// all the way back to the data science backend. We're not testing the user's preference
// or response, we're end-to-end testing the experiments platform.
// So here, we're running multiple identical branches with the same treatment, and if the
// user isn't targeted, then we get still get the same treatment.
// The `let` block is degenerate here, but left here so as to document the form of how experiments
// are implemented here.
val historyIcon = experiments.withExperiment(Experiments.A_A_NIMBUS_VALIDATION) {
when (it) {
ExperimentBranch.A1 -> R.drawable.ic_history
ExperimentBranch.A2 -> R.drawable.ic_history
else -> R.drawable.ic_history
}
}
val historyItem = BrowserMenuImageText( val historyItem = BrowserMenuImageText(
context.getString(R.string.library_history), context.getString(R.string.library_history),
R.drawable.ic_history, historyIcon,
primaryTextColor primaryTextColor
) { ) {
onItemTapped.invoke(Item.History) onItemTapped.invoke(Item.History)

@ -4,7 +4,6 @@
package org.mozilla.fenix.home package org.mozilla.fenix.home
import android.os.Parcelable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
class HomeScreenViewModel : ViewModel() { class HomeScreenViewModel : ViewModel() {
@ -13,8 +12,6 @@ class HomeScreenViewModel : ViewModel() {
*/ */
var sessionToDelete: String? = null var sessionToDelete: String? = null
var layoutManagerState: Parcelable? = null
/** /**
* Used to remember if we need to scroll to top of the homeFragment's recycleView (top sites) see #8561 * Used to remember if we need to scroll to top of the homeFragment's recycleView (top sites) see #8561
* */ * */

@ -44,7 +44,7 @@ class FennecBookmarkShortcutsIntentProcessor(
val session = Session(url, private = false, source = SessionState.Source.HOME_SCREEN) val session = Session(url, private = false, source = SessionState.Source.HOME_SCREEN)
sessionManager.add(session, selected = true) sessionManager.add(session, selected = true)
loadUrlUseCase(url, session, EngineSession.LoadUrlFlags.external()) loadUrlUseCase(url, session.id, EngineSession.LoadUrlFlags.external())
intent.action = ACTION_VIEW intent.action = ACTION_VIEW
intent.putSessionId(session.id) intent.putSessionId(session.id)

@ -7,24 +7,26 @@ package org.mozilla.fenix.home.intent
import android.content.Intent import android.content.Intent
import androidx.navigation.NavController import androidx.navigation.NavController
import mozilla.components.feature.media.service.AbstractMediaService import mozilla.components.feature.media.service.AbstractMediaService
import mozilla.components.feature.media.service.AbstractMediaSessionService
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.FeatureFlags.newMediaSessionApi
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
/** /**
* When the media notification is clicked we need to switch to the tab where the audio/video is * When the media notification is clicked we need to switch to the tab where the audio/video is
* playing. This intent has the following informations: * playing. This intent has the following informations:
* action - [AbstractMediaService.Companion.ACTION_SWITCH_TAB] * action - [AbstractMediaSessionService.Companion.ACTION_SWITCH_TAB]
* extra string for the tab id - [AbstractMediaService.Companion.EXTRA_TAB_ID] * extra string for the tab id - [AbstractMediaSessionService.Companion.EXTRA_TAB_ID]
*/ */
class OpenSpecificTabIntentProcessor( class OpenSpecificTabIntentProcessor(
private val activity: HomeActivity private val activity: HomeActivity
) : HomeIntentProcessor { ) : HomeIntentProcessor {
override fun process(intent: Intent, navController: NavController, out: Intent): Boolean { override fun process(intent: Intent, navController: NavController, out: Intent): Boolean {
if (intent.action == AbstractMediaService.Companion.ACTION_SWITCH_TAB) { if (intent.action == getAction()) {
val sessionManager = activity.components.core.sessionManager val sessionManager = activity.components.core.sessionManager
val sessionId = intent.extras?.getString(AbstractMediaService.Companion.EXTRA_TAB_ID) val sessionId = intent.extras?.getString(getTabId())
val session = sessionId?.let { sessionManager.findSessionById(it) } val session = sessionId?.let { sessionManager.findSessionById(it) }
if (session != null) { if (session != null) {
sessionManager.select(session) sessionManager.select(session)
@ -36,3 +38,19 @@ class OpenSpecificTabIntentProcessor(
return false return false
} }
} }
private fun getAction(): String {
return if (newMediaSessionApi) {
AbstractMediaSessionService.Companion.ACTION_SWITCH_TAB
} else {
AbstractMediaService.Companion.ACTION_SWITCH_TAB
}
}
private fun getTabId(): String {
return if (newMediaSessionApi) {
AbstractMediaSessionService.Companion.EXTRA_TAB_ID
} else {
AbstractMediaService.Companion.EXTRA_TAB_ID
}
}

@ -7,6 +7,9 @@ package org.mozilla.fenix.home.intent
import android.content.Intent import android.content.Intent
import android.os.StrictMode import android.os.StrictMode
import androidx.navigation.NavController import androidx.navigation.NavController
import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.search.ext.waitForSelectedOrDefaultSearchEngine
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
@ -21,30 +24,48 @@ import org.mozilla.fenix.widget.VoiceSearchActivity.Companion.SPEECH_PROCESSING
*/ */
class SpeechProcessingIntentProcessor( class SpeechProcessingIntentProcessor(
private val activity: HomeActivity, private val activity: HomeActivity,
private val store: BrowserStore,
private val metrics: MetricController private val metrics: MetricController
) : HomeIntentProcessor { ) : HomeIntentProcessor {
override fun process(intent: Intent, navController: NavController, out: Intent): Boolean { override fun process(intent: Intent, navController: NavController, out: Intent): Boolean {
return if (intent.extras?.getBoolean(HomeActivity.OPEN_TO_BROWSER_AND_LOAD) == true) { if (
out.putExtra(HomeActivity.OPEN_TO_BROWSER_AND_LOAD, false) !intent.hasExtra(SPEECH_PROCESSING) ||
activity.components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) { intent.extras?.getBoolean(HomeActivity.OPEN_TO_BROWSER_AND_LOAD) != true
val searchEvent = MetricsUtils.createSearchEvent( ) {
activity.components.search.provider.getDefaultEngine(activity), return false
activity, }
Event.PerformedSearch.SearchAccessPoint.WIDGET
out.putExtra(HomeActivity.OPEN_TO_BROWSER_AND_LOAD, false)
store.waitForSelectedOrDefaultSearchEngine { searchEngine ->
if (searchEngine != null) {
launchToBrowser(
searchEngine,
intent.getStringExtra(SPEECH_PROCESSING).orEmpty()
) )
searchEvent?.let { metrics.track(it) }
} }
}
activity.openToBrowserAndLoad( return true
searchTermOrURL = intent.getStringExtra(SPEECH_PROCESSING).orEmpty(), }
newTab = true,
from = BrowserDirection.FromGlobal, private fun launchToBrowser(searchEngine: SearchEngine, text: String) {
forceSearch = true activity.components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
val searchEvent = MetricsUtils.createSearchEvent(
searchEngine,
store,
Event.PerformedSearch.SearchAccessPoint.WIDGET
) )
true searchEvent?.let { metrics.track(it) }
} else {
false
} }
activity.openToBrowserAndLoad(
searchTermOrURL = text,
newTab = true,
from = BrowserDirection.FromGlobal,
engine = searchEngine,
forceSearch = true
)
} }
} }

@ -6,8 +6,10 @@ package org.mozilla.fenix.home.intent
import android.content.Intent import android.content.Intent
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.navOptions
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
@ -22,25 +24,25 @@ class StartSearchIntentProcessor(
override fun process(intent: Intent, navController: NavController, out: Intent): Boolean { override fun process(intent: Intent, navController: NavController, out: Intent): Boolean {
val event = intent.extras?.getString(HomeActivity.OPEN_TO_SEARCH) val event = intent.extras?.getString(HomeActivity.OPEN_TO_SEARCH)
var source: Event.PerformedSearch.SearchAccessPoint? = null
return if (event != null) { return if (event != null) {
when (event) { val source = when (event) {
SEARCH_WIDGET -> { SEARCH_WIDGET -> {
metrics.track(Event.SearchWidgetNewTabPressed) metrics.track(Event.SearchWidgetNewTabPressed)
source = Event.PerformedSearch.SearchAccessPoint.WIDGET Event.PerformedSearch.SearchAccessPoint.WIDGET
} }
STATIC_SHORTCUT_NEW_TAB -> { STATIC_SHORTCUT_NEW_TAB -> {
metrics.track(Event.PrivateBrowsingStaticShortcutTab) metrics.track(Event.PrivateBrowsingStaticShortcutTab)
source = Event.PerformedSearch.SearchAccessPoint.SHORTCUT Event.PerformedSearch.SearchAccessPoint.SHORTCUT
} }
STATIC_SHORTCUT_NEW_PRIVATE_TAB -> { STATIC_SHORTCUT_NEW_PRIVATE_TAB -> {
metrics.track(Event.PrivateBrowsingStaticShortcutPrivateTab) metrics.track(Event.PrivateBrowsingStaticShortcutPrivateTab)
source = Event.PerformedSearch.SearchAccessPoint.SHORTCUT Event.PerformedSearch.SearchAccessPoint.SHORTCUT
} }
PRIVATE_BROWSING_PINNED_SHORTCUT -> { PRIVATE_BROWSING_PINNED_SHORTCUT -> {
metrics.track(Event.PrivateBrowsingPinnedShortcutPrivateTab) metrics.track(Event.PrivateBrowsingPinnedShortcutPrivateTab)
source = Event.PerformedSearch.SearchAccessPoint.SHORTCUT Event.PerformedSearch.SearchAccessPoint.SHORTCUT
} }
else -> null
} }
out.removeExtra(HomeActivity.OPEN_TO_SEARCH) out.removeExtra(HomeActivity.OPEN_TO_SEARCH)
@ -51,7 +53,12 @@ class StartSearchIntentProcessor(
searchAccessPoint = it searchAccessPoint = it
) )
} }
directions?.let { navController.nav(null, it) } directions?.let {
val options = navOptions {
popUpTo = R.id.homeFragment
}
navController.nav(null, it, options)
}
true true
} else { } else {
false false

@ -12,8 +12,11 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.feature.session.SessionUseCases
import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tab.collections.ext.restore import mozilla.components.feature.tab.collections.ext.restore
import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.feature.tabs.TabsUseCases
@ -34,7 +37,6 @@ import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.sessionsOfType import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.HomeFragment import org.mozilla.fenix.home.HomeFragment
import org.mozilla.fenix.home.HomeFragmentAction import org.mozilla.fenix.home.HomeFragmentAction
import org.mozilla.fenix.home.HomeFragmentDirections import org.mozilla.fenix.home.HomeFragmentDirections
@ -158,6 +160,11 @@ interface SessionControlController {
* @see [CollectionInteractor.onRemoveCollectionsPlaceholder] * @see [CollectionInteractor.onRemoveCollectionsPlaceholder]
*/ */
fun handleRemoveCollectionsPlaceholder() fun handleRemoveCollectionsPlaceholder()
/**
* @see [CollectionInteractor.onCollectionMenuOpened] and [TopSiteInteractor.onTopSiteMenuOpened]
*/
fun handleMenuOpened()
} }
@Suppress("TooManyFunctions", "LargeClass") @Suppress("TooManyFunctions", "LargeClass")
@ -167,8 +174,10 @@ class DefaultSessionControlController(
private val engine: Engine, private val engine: Engine,
private val metrics: MetricController, private val metrics: MetricController,
private val sessionManager: SessionManager, private val sessionManager: SessionManager,
private val store: BrowserStore,
private val tabCollectionStorage: TabCollectionStorage, private val tabCollectionStorage: TabCollectionStorage,
private val addTabUseCase: TabsUseCases.AddNewTabUseCase, private val addTabUseCase: TabsUseCases.AddNewTabUseCase,
private val reloadUrlUseCase: SessionUseCases.ReloadUrlUseCase,
private val fragmentStore: HomeFragmentStore, private val fragmentStore: HomeFragmentStore,
private val navController: NavController, private val navController: NavController,
private val viewLifecycleScope: CoroutineScope, private val viewLifecycleScope: CoroutineScope,
@ -193,13 +202,19 @@ class DefaultSessionControlController(
) )
} }
override fun handleMenuOpened() {
dismissSearchDialogIfDisplayed()
}
override fun handleCollectionOpenTabClicked(tab: ComponentTab) { override fun handleCollectionOpenTabClicked(tab: ComponentTab) {
dismissSearchDialogIfDisplayed()
sessionManager.restore( sessionManager.restore(
activity, activity,
engine, engine,
tab, tab,
onTabRestored = { onTabRestored = {
activity.openToBrowser(BrowserDirection.FromHome) activity.openToBrowser(BrowserDirection.FromHome)
reloadUrlUseCase.invoke(sessionManager.selectedSession)
}, },
onFailure = { onFailure = {
activity.openToBrowserAndLoad( activity.openToBrowserAndLoad(
@ -256,6 +271,7 @@ class DefaultSessionControlController(
} }
override fun handleCollectionShareTabsClicked(collection: TabCollection) { override fun handleCollectionShareTabsClicked(collection: TabCollection) {
dismissSearchDialogIfDisplayed()
showShareFragment( showShareFragment(
collection.title, collection.title,
collection.tabs.map { ShareData(url = it.url, title = it.title) } collection.tabs.map { ShareData(url = it.url, title = it.title) }
@ -282,6 +298,7 @@ class DefaultSessionControlController(
} }
override fun handlePrivateBrowsingLearnMoreClicked() { override fun handlePrivateBrowsingLearnMoreClicked() {
dismissSearchDialogIfDisplayed()
activity.openToBrowserAndLoad( activity.openToBrowserAndLoad(
searchTermOrURL = SupportUtils.getGenericSumoURLForTopic searchTermOrURL = SupportUtils.getGenericSumoURLForTopic
(SupportUtils.SumoTopic.PRIVATE_BROWSING_MYTHS), (SupportUtils.SumoTopic.PRIVATE_BROWSING_MYTHS),
@ -293,9 +310,9 @@ class DefaultSessionControlController(
override fun handleRenameTopSiteClicked(topSite: TopSite) { override fun handleRenameTopSiteClicked(topSite: TopSite) {
activity.let { activity.let {
val customLayout = val customLayout =
LayoutInflater.from(it).inflate(R.layout.top_sites_rename_dialog, null) LayoutInflater.from(it).inflate(R.layout.top_sites_rename_dialog, null)
val topSiteLabelEditText: EditText = val topSiteLabelEditText: EditText =
customLayout.findViewById(R.id.top_site_title) customLayout.findViewById(R.id.top_site_title)
topSiteLabelEditText.setText(topSite.title) topSiteLabelEditText.setText(topSite.title)
AlertDialog.Builder(it).apply { AlertDialog.Builder(it).apply {
@ -344,6 +361,7 @@ class DefaultSessionControlController(
} }
override fun handleSelectTopSite(url: String, type: TopSite.Type) { override fun handleSelectTopSite(url: String, type: TopSite.Type) {
dismissSearchDialogIfDisplayed()
metrics.track(Event.TopSiteOpenInNewTab) metrics.track(Event.TopSiteOpenInNewTab)
when (type) { when (type) {
TopSite.Type.DEFAULT -> metrics.track(Event.TopSiteOpenDefault) TopSite.Type.DEFAULT -> metrics.track(Event.TopSiteOpenDefault)
@ -362,6 +380,12 @@ class DefaultSessionControlController(
activity.openToBrowser(BrowserDirection.FromHome) activity.openToBrowser(BrowserDirection.FromHome)
} }
private fun dismissSearchDialogIfDisplayed() {
if (navController.currentDestination?.id == R.id.searchDialogFragment) {
navController.navigateUp()
}
}
override fun handleStartBrowsingClicked() { override fun handleStartBrowsingClicked() {
hideOnboarding() hideOnboarding()
} }
@ -444,21 +468,23 @@ class DefaultSessionControlController(
} }
override fun handlePasteAndGo(clipboardText: String) { override fun handlePasteAndGo(clipboardText: String) {
val searchEngine = store.state.search.selectedOrDefaultSearchEngine
activity.openToBrowserAndLoad( activity.openToBrowserAndLoad(
searchTermOrURL = clipboardText, searchTermOrURL = clipboardText,
newTab = true, newTab = true,
from = BrowserDirection.FromHome, from = BrowserDirection.FromHome,
engine = activity.components.search.provider.getDefaultEngine(activity) engine = searchEngine
) )
val event = if (clipboardText.isUrl()) { val event = if (clipboardText.isUrl() || searchEngine == null) {
Event.EnteredUrl(false) Event.EnteredUrl(false)
} else { } else {
val searchAccessPoint = Event.PerformedSearch.SearchAccessPoint.ACTION val searchAccessPoint = Event.PerformedSearch.SearchAccessPoint.ACTION
searchAccessPoint.let { sap -> searchAccessPoint.let { sap ->
MetricsUtils.createSearchEvent( MetricsUtils.createSearchEvent(
activity.components.search.provider.getDefaultEngine(activity), searchEngine,
activity, store,
sap sap
) )
} }

@ -23,6 +23,7 @@ interface TabSessionInteractor {
/** /**
* Interface for collection related actions in the [SessionControlInteractor]. * Interface for collection related actions in the [SessionControlInteractor].
*/ */
@SuppressWarnings("TooManyFunctions")
interface CollectionInteractor { interface CollectionInteractor {
/** /**
* Shows the Collection Creation fragment for selecting the tabs to add to the given tab * Shows the Collection Creation fragment for selecting the tabs to add to the given tab
@ -98,6 +99,11 @@ interface CollectionInteractor {
* User has removed the collections placeholder from home. * User has removed the collections placeholder from home.
*/ */
fun onRemoveCollectionsPlaceholder() fun onRemoveCollectionsPlaceholder()
/**
* User has opened collection 3 dot menu.
*/
fun onCollectionMenuOpened()
} }
interface ToolbarInteractor { interface ToolbarInteractor {
@ -177,6 +183,11 @@ interface TopSiteInteractor {
* @param type The type of the top site. * @param type The type of the top site.
*/ */
fun onSelectTopSite(url: String, type: TopSite.Type) fun onSelectTopSite(url: String, type: TopSite.Type)
/**
* Called when top site menu is opened.
*/
fun onTopSiteMenuOpened()
} }
/** /**
@ -276,4 +287,12 @@ class SessionControlInteractor(
override fun onRemoveCollectionsPlaceholder() { override fun onRemoveCollectionsPlaceholder() {
controller.handleRemoveCollectionsPlaceholder() controller.handleRemoveCollectionsPlaceholder()
} }
override fun onCollectionMenuOpened() {
controller.handleMenuOpened()
}
override fun onTopSiteMenuOpened() {
controller.handleMenuOpened()
}
} }

@ -47,6 +47,7 @@ class CollectionViewHolder(
} }
collection_overflow_button.setOnClickListener { collection_overflow_button.setOnClickListener {
interactor.onCollectionMenuOpened()
collectionMenu.menuBuilder collectionMenu.menuBuilder
.build(view.context) .build(view.context)
.show(anchor = it) .show(anchor = it)

@ -36,6 +36,7 @@ class TopSiteItemViewHolder(
} }
top_site_item.setOnLongClickListener { top_site_item.setOnLongClickListener {
interactor.onTopSiteMenuOpened()
it.context.components.analytics.metrics.track(Event.TopSiteLongPress(topSite.type)) it.context.components.analytics.metrics.track(Event.TopSiteLongPress(topSite.type))
val topSiteMenu = TopSiteItemMenu(view.context, topSite.type != FRECENT) { item -> val topSiteMenu = TopSiteItemMenu(view.context, topSite.type != FRECENT) { item ->

@ -12,7 +12,6 @@ import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.setToolbarColors import org.mozilla.fenix.ext.setToolbarColors
abstract class LibraryPageFragment<T> : Fragment() { abstract class LibraryPageFragment<T> : Fragment() {
@ -36,7 +35,6 @@ abstract class LibraryPageFragment<T> : Fragment() {
} }
(activity as HomeActivity).browsingModeManager.mode = BrowsingMode.fromBoolean(private) (activity as HomeActivity).browsingModeManager.mode = BrowsingMode.fromBoolean(private)
hideToolbar()
} }
override fun onDetach() { override fun onDetach() {

@ -45,8 +45,6 @@ interface BookmarkController {
fun handleBookmarkFolderDeletion(nodes: Set<BookmarkNode>) fun handleBookmarkFolderDeletion(nodes: Set<BookmarkNode>)
fun handleRequestSync() fun handleRequestSync()
fun handleBackPressed() fun handleBackPressed()
fun handleStartSwipingItem()
fun handleStopSwipingItem()
} }
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
@ -171,14 +169,6 @@ class DefaultBookmarkController(
} }
} }
override fun handleStartSwipingItem() {
store.dispatch(BookmarkFragmentAction.SwipeRefreshAvailabilityChanged(false))
}
override fun handleStopSwipingItem() {
store.dispatch(BookmarkFragmentAction.SwipeRefreshAvailabilityChanged(true))
}
private fun openInNewTab( private fun openInNewTab(
searchTermOrURL: String, searchTermOrURL: String,
newTab: Boolean, newTab: Boolean,

@ -120,12 +120,4 @@ class BookmarkFragmentInteractor(
override fun onRequestSync() { override fun onRequestSync() {
bookmarksController.handleRequestSync() bookmarksController.handleRequestSync()
} }
override fun onStartSwipingItem() {
bookmarksController.handleStartSwipingItem()
}
override fun onStopSwipingItem() {
bookmarksController.handleStopSwipingItem()
}
} }

@ -24,14 +24,12 @@ class BookmarkFragmentStore(
* @property guidBackstack A set of guids for bookmark nodes we have visited. Used to traverse back * @property guidBackstack A set of guids for bookmark nodes we have visited. Used to traverse back
* up the tree after a sync. * up the tree after a sync.
* @property isLoading true if bookmarks are still being loaded from disk * @property isLoading true if bookmarks are still being loaded from disk
* @property isSwipeToRefreshEnabled true if swipe to refresh should be enabled
*/ */
data class BookmarkFragmentState( data class BookmarkFragmentState(
val tree: BookmarkNode?, val tree: BookmarkNode?,
val mode: Mode = Mode.Normal(), val mode: Mode = Mode.Normal(),
val guidBackstack: List<String> = emptyList(), val guidBackstack: List<String> = emptyList(),
val isLoading: Boolean = true, val isLoading: Boolean = true
val isSwipeToRefreshEnabled: Boolean = true
) : State { ) : State {
sealed class Mode : SelectionHolder<BookmarkNode> { sealed class Mode : SelectionHolder<BookmarkNode> {
override val selectedItems = emptySet<BookmarkNode>() override val selectedItems = emptySet<BookmarkNode>()
@ -52,7 +50,6 @@ sealed class BookmarkFragmentAction : Action {
object DeselectAll : BookmarkFragmentAction() object DeselectAll : BookmarkFragmentAction()
object StartSync : BookmarkFragmentAction() object StartSync : BookmarkFragmentAction()
object FinishSync : BookmarkFragmentAction() object FinishSync : BookmarkFragmentAction()
data class SwipeRefreshAvailabilityChanged(val enabled: Boolean) : BookmarkFragmentAction()
} }
/** /**
@ -88,13 +85,11 @@ private fun bookmarkFragmentStateReducer(
tree = action.tree, tree = action.tree,
mode = mode, mode = mode,
guidBackstack = backstack, guidBackstack = backstack,
isLoading = false, isLoading = false
isSwipeToRefreshEnabled = mode !is BookmarkFragmentState.Mode.Selecting
) )
} }
is BookmarkFragmentAction.Select -> state.copy( is BookmarkFragmentAction.Select -> state.copy(
mode = BookmarkFragmentState.Mode.Selecting(state.mode.selectedItems + action.item), mode = BookmarkFragmentState.Mode.Selecting(state.mode.selectedItems + action.item)
isSwipeToRefreshEnabled = false
) )
is BookmarkFragmentAction.Deselect -> { is BookmarkFragmentAction.Deselect -> {
val items = state.mode.selectedItems - action.item val items = state.mode.selectedItems - action.item
@ -104,8 +99,7 @@ private fun bookmarkFragmentStateReducer(
BookmarkFragmentState.Mode.Selecting(items) BookmarkFragmentState.Mode.Selecting(items)
} }
state.copy( state.copy(
mode = mode, mode = mode
isSwipeToRefreshEnabled = mode !is BookmarkFragmentState.Mode.Selecting
) )
} }
is BookmarkFragmentAction.DeselectAll -> is BookmarkFragmentAction.DeselectAll ->
@ -114,21 +108,15 @@ private fun bookmarkFragmentStateReducer(
BookmarkFragmentState.Mode.Syncing BookmarkFragmentState.Mode.Syncing
} else { } else {
BookmarkFragmentState.Mode.Normal() BookmarkFragmentState.Mode.Normal()
}, }
isSwipeToRefreshEnabled = true
) )
is BookmarkFragmentAction.StartSync -> state.copy( is BookmarkFragmentAction.StartSync -> state.copy(
mode = BookmarkFragmentState.Mode.Syncing, mode = BookmarkFragmentState.Mode.Syncing
isSwipeToRefreshEnabled = true
) )
is BookmarkFragmentAction.FinishSync -> state.copy( is BookmarkFragmentAction.FinishSync -> state.copy(
mode = BookmarkFragmentState.Mode.Normal( mode = BookmarkFragmentState.Mode.Normal(
showMenu = shouldShowMenu(state.tree?.guid) showMenu = shouldShowMenu(state.tree?.guid)
), )
isSwipeToRefreshEnabled = true
)
is BookmarkFragmentAction.SwipeRefreshAvailabilityChanged -> state.copy(
isSwipeToRefreshEnabled = action.enabled && state.mode !is BookmarkFragmentState.Mode.Selecting
) )
} }
} }

@ -1,146 +0,0 @@
/* 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.library.bookmarks
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import androidx.appcompat.content.res.AppCompatResources
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.concept.storage.BookmarkNodeType
import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.android.content.getDrawableWithTint
import mozilla.components.support.ktx.android.util.dpToPx
import org.mozilla.fenix.R
import org.mozilla.fenix.home.sessioncontrol.SwipeToDeleteCallback
import org.mozilla.fenix.library.bookmarks.viewholders.BookmarkNodeViewHolder
import org.mozilla.fenix.library.bookmarks.viewholders.BookmarkSeparatorViewHolder
class BookmarkTouchHelper(interactor: BookmarkViewInteractor) :
ItemTouchHelper(BookmarkTouchCallback(interactor))
class BookmarkTouchCallback(
private val interactor: BookmarkViewInteractor
) : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
override fun getSwipeDirs(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
// Swiping separators is currently not supported.
if (viewHolder is BookmarkSeparatorViewHolder) {
return 0
}
val item = (viewHolder as BookmarkNodeViewHolder).item
return if (item?.inRoots() == true) {
0
} else {
super.getSwipeDirs(recyclerView, viewHolder)
}
}
/**
* Delete the bookmark when swiped.
*/
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val item = (viewHolder as BookmarkNodeViewHolder).item
item?.let {
interactor.onDelete(setOf(it))
// We need to notify the adapter of a change if we swipe a folder to prevent
// visual bugs when cancelling deletion of a folder
if (item.type == BookmarkNodeType.FOLDER) {
viewHolder.bindingAdapter?.notifyItemChanged(viewHolder.bindingAdapterPosition)
}
}
}
override fun onChildDraw(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
val icon = recyclerView.context.getDrawableWithTint(
R.drawable.ic_delete,
recyclerView.context.getColorFromAttr(R.attr.destructive)
)!!
val background = AppCompatResources.getDrawable(
recyclerView.context,
R.drawable.swipe_delete_background
)!!
val margin =
SwipeToDeleteCallback.MARGIN.dpToPx(recyclerView.resources.displayMetrics)
val cellHeight = viewHolder.itemView.bottom - viewHolder.itemView.top
val iconTop = viewHolder.itemView.top + (cellHeight - icon.intrinsicHeight) / 2
val iconBottom = iconTop + icon.intrinsicHeight
when {
dX > 0 -> { // Swiping to the right
val backgroundBounds = Rect(
viewHolder.itemView.left, viewHolder.itemView.top,
(viewHolder.itemView.left + dX).toInt() + SwipeToDeleteCallback.BACKGROUND_CORNER_OFFSET,
viewHolder.itemView.bottom
)
val iconLeft = viewHolder.itemView.left + margin
val iconRight = viewHolder.itemView.left + margin + icon.intrinsicWidth
val iconBounds = Rect(iconLeft, iconTop, iconRight, iconBottom)
setBounds(background, backgroundBounds, icon, iconBounds)
draw(background, icon, c)
}
dX < 0 -> { // Swiping to the left
val backgroundBounds = Rect(
(viewHolder.itemView.right + dX).toInt() - SwipeToDeleteCallback.BACKGROUND_CORNER_OFFSET,
viewHolder.itemView.top, viewHolder.itemView.right, viewHolder.itemView.bottom
)
val iconLeft = viewHolder.itemView.right - margin - icon.intrinsicWidth
val iconRight = viewHolder.itemView.right - margin
val iconBounds = Rect(iconLeft, iconTop, iconRight, iconBottom)
setBounds(background, backgroundBounds, icon, iconBounds)
draw(background, icon, c)
}
else -> { // View not swiped
val bounds = Rect(0, 0, 0, 0)
setBounds(background, bounds, icon, bounds)
}
}
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean = false
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
interactor.onStartSwipingItem()
} else {
interactor.onStopSwipingItem()
}
}
private fun setBounds(
background: Drawable,
backgroundBounds: Rect,
icon: Drawable,
iconBounds: Rect
) {
background.bounds = backgroundBounds
icon.bounds = iconBounds
}
private fun draw(background: Drawable, icon: Drawable, c: Canvas) {
background.draw(c)
icon.draw(c)
}
}

@ -13,7 +13,6 @@ import kotlinx.android.synthetic.main.component_bookmark.view.*
import mozilla.appservices.places.BookmarkRoot import mozilla.appservices.places.BookmarkRoot
import mozilla.components.concept.storage.BookmarkNode import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.UserInteractionHandler
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.library.LibraryPageView import org.mozilla.fenix.library.LibraryPageView
@ -99,16 +98,6 @@ interface BookmarkViewInteractor : SelectionInteractor<BookmarkNode> {
* *
*/ */
fun onRequestSync() fun onRequestSync()
/**
* Handles the start of a swipe on a bookmark.
*/
fun onStartSwipingItem()
/**
* Handles the end of a swipe on a bookmark.
*/
fun onStopSwipingItem()
} }
class BookmarkView( class BookmarkView(
@ -135,10 +124,6 @@ class BookmarkView(
view.swipe_refresh.setOnRefreshListener { view.swipe_refresh.setOnRefreshListener {
interactor.onRequestSync() interactor.onRequestSync()
} }
if (FeatureFlags.bookmarkSwipeToDelete) {
BookmarkTouchHelper(interactor).attachToRecyclerView(view.bookmark_list)
}
} }
fun update(state: BookmarkFragmentState) { fun update(state: BookmarkFragmentState) {
@ -166,7 +151,8 @@ class BookmarkView(
} }
} }
view.bookmarks_progress_bar.isVisible = state.isLoading view.bookmarks_progress_bar.isVisible = state.isLoading
view.swipe_refresh.isEnabled = state.isSwipeToRefreshEnabled view.swipe_refresh.isEnabled =
state.mode is BookmarkFragmentState.Mode.Normal || state.mode is BookmarkFragmentState.Mode.Syncing
view.swipe_refresh.isRefreshing = state.mode is BookmarkFragmentState.Mode.Syncing view.swipe_refresh.isRefreshing = state.mode is BookmarkFragmentState.Mode.Syncing
} }

@ -36,7 +36,7 @@ class DownloadAdapter(
override fun onBindViewHolder(holder: DownloadsListItemViewHolder, position: Int) { override fun onBindViewHolder(holder: DownloadsListItemViewHolder, position: Int) {
val current = downloads[position] val current = downloads[position]
val isPendingDeletion = pendingDeletionIds.contains(current.id) val isPendingDeletion = pendingDeletionIds.contains(current.id)
holder.bind(downloads[position], position == 0, mode, isPendingDeletion) holder.bind(downloads[position], mode, isPendingDeletion)
} }
fun updateDownloads(downloads: List<DownloadItem>) { fun updateDownloads(downloads: List<DownloadItem>) {

@ -32,6 +32,8 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.addons.showSnackBar import org.mozilla.fenix.addons.showSnackBar
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.filterNotExistsOnDisk import org.mozilla.fenix.ext.filterNotExistsOnDisk
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
@ -45,6 +47,7 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
private lateinit var downloadStore: DownloadFragmentStore private lateinit var downloadStore: DownloadFragmentStore
private lateinit var downloadView: DownloadView private lateinit var downloadView: DownloadView
private lateinit var downloadInteractor: DownloadInteractor private lateinit var downloadInteractor: DownloadInteractor
private lateinit var metrics: MetricController
private var undoScope: CoroutineScope? = null private var undoScope: CoroutineScope? = null
private var pendingDownloadDeletionJob: (suspend () -> Unit)? = null private var pendingDownloadDeletionJob: (suspend () -> Unit)? = null
@ -109,9 +112,13 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setHasOptionsMenu(true) setHasOptionsMenu(true)
metrics = requireComponents.analytics.metrics
metrics.track(Event.DownloadsScreenOpened)
} }
private fun displayDeleteAll() { private fun displayDeleteAll() {
metrics.track(Event.DownloadsItemDeleted)
activity?.let { activity -> activity?.let { activity ->
AlertDialog.Builder(activity).apply { AlertDialog.Builder(activity).apply {
setMessage(R.string.download_delete_all_dialog) setMessage(R.string.download_delete_all_dialog)
@ -128,7 +135,7 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
showSnackBar( showSnackBar(
requireView(), requireView(),
getString(R.string.download_delete_multiple_items_snackbar) getString(R.string.download_delete_multiple_items_snackbar_1)
) )
} }
} }
@ -140,6 +147,8 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
} }
private fun deleteDownloadItems(items: Set<DownloadItem>) { private fun deleteDownloadItems(items: Set<DownloadItem>) {
metrics.track(Event.DownloadsItemDeleted)
updatePendingDownloadToDelete(items) updatePendingDownloadToDelete(items)
undoScope = CoroutineScope(IO) undoScope = CoroutineScope(IO)
undoScope?.allowUndo( undoScope?.allowUndo(
@ -175,7 +184,7 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
inflater.inflate(menuRes, menu) inflater.inflate(menuRes, menu)
menu.findItem(R.id.delete_downloads_multi_select)?.title = menu.findItem(R.id.delete_downloads_multi_select)?.title =
SpannableString(getString(R.string.bookmark_menu_delete_button)).apply { SpannableString(getString(R.string.download_delete_item_1)).apply {
setTextColor(requireContext(), R.attr.destructive) setTextColor(requireContext(), R.attr.destructive)
} }
} }
@ -191,16 +200,23 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
downloadStore.dispatch(DownloadFragmentAction.ExitEditMode) downloadStore.dispatch(DownloadFragmentAction.ExitEditMode)
true true
} }
R.id.select_all_downloads_multi_select -> {
for (items in downloadStore.state.items) {
downloadInteractor.select(items)
}
true
}
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
private fun getMultiSelectSnackBarMessage(downloadItems: Set<DownloadItem>): String { private fun getMultiSelectSnackBarMessage(downloadItems: Set<DownloadItem>): String {
return if (downloadItems.size > 1) { return if (downloadItems.size > 1) {
getString(R.string.download_delete_multiple_items_snackbar) getString(R.string.download_delete_multiple_items_snackbar_1)
} else { } else {
String.format( String.format(
requireContext().getString( requireContext().getString(
R.string.history_delete_single_item_snackbar R.string.download_delete_single_item_snackbar
), downloadItems.first().fileName ), downloadItems.first().fileName
) )
} }
@ -226,6 +242,8 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
filePath = item.filePath filePath = item.filePath
) )
} }
metrics.track(Event.DownloadsItemOpened)
} }
private fun getDeleteDownloadItemsOperation(items: Set<DownloadItem>): (suspend () -> Unit) { private fun getDeleteDownloadItemsOperation(items: Set<DownloadItem>): (suspend () -> Unit) {

@ -114,7 +114,7 @@ class DownloadView(
download_list.isVisible = userHasDownloads download_list.isVisible = userHasDownloads
download_empty_view.isVisible = !userHasDownloads download_empty_view.isVisible = !userHasDownloads
if (!userHasDownloads) { if (!userHasDownloads) {
download_empty_view.announceForAccessibility(context.getString(R.string.download_empty_message)) download_empty_view.announceForAccessibility(context.getString(R.string.download_empty_message_1))
} }
} }

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

Loading…
Cancel
Save