Compare commits
3 Commits
Author | SHA1 | Date |
---|---|---|
harvey186 | ff6bef97c7 | |
harvey186 | 7a6de5f3e6 | |
harvey186 | c64e459308 |
|
@ -0,0 +1,78 @@
|
||||||
|
name: "🐞 Bug report"
|
||||||
|
description: Create a report to help us improve.
|
||||||
|
title: "[Bug]: "
|
||||||
|
labels: ["🐞 bug", "needs:triage"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
- Please do your best to search for duplicate issues before filing a new issue so we can keep our issue board clean.
|
||||||
|
- Have a look at ["I want to file an issue!"][info] for more information.
|
||||||
|
|
||||||
|
[info]: https://github.com/fork-maintainers/iceraven-browser#i-want-to-file-an-issue
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce
|
||||||
|
description: Steps to reproduce the behaviour.
|
||||||
|
placeholder: |
|
||||||
|
1. Have a tab open..
|
||||||
|
2. Go to..
|
||||||
|
3. Click on..
|
||||||
|
4. Observe..
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Expected behaviour
|
||||||
|
placeholder: A menu should open..
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Actual behaviour
|
||||||
|
placeholder: The app closes unexpectedly..
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
# Device information
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Device name
|
||||||
|
description: The name of the device model and manufacturer.
|
||||||
|
placeholder: Google Pixel 2
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Android version
|
||||||
|
description: You can find the Android version information in the About section of your device's system settings.
|
||||||
|
placeholder: Android 10
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Iceraven version
|
||||||
|
description: You can find this information in Settings -> About Iceraven.
|
||||||
|
placeholder: 2.22.0
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Device logs
|
||||||
|
description: |
|
||||||
|
Device logs or crash information can greatly aid in debugging. You can find some details here on how to [retrieve device logs or crash IDs][log].
|
||||||
|
|
||||||
|
[log]: https://github.com/fork-maintainers/iceraven-browser/blob/iceraven/docs/Logging-Crash-Information.md
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional information
|
||||||
|
description: |
|
||||||
|
If you have any additional information for us, use the field below.
|
||||||
|
Please note, you can attach screenshots or screen recordings here, by
|
||||||
|
dragging and dropping files in the field below.
|
||||||
|
validations:
|
||||||
|
required: false
|
|
@ -0,0 +1,40 @@
|
||||||
|
name: "⭐️ Feature request"
|
||||||
|
description: Suggest an idea for this project
|
||||||
|
title: "[Feature Request]: "
|
||||||
|
labels: ["🌟 feature request"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
- Please do your best to search for duplicate issues before filing a new issue so we can keep our issue board clean
|
||||||
|
- Every issue should have exactly one feature request described in it. Please do not file feedback list tickets as it is difficult to parse them and address their individual points
|
||||||
|
- Feature Requests are better when they’re open-ended instead of demanding a specific solution e.g: “I want an easier way to do X” instead of “add Y”
|
||||||
|
- Read https://github.com/fork-maintainers/iceraven-browser#i-want-to-file-an-issue for more information
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Feature Summary
|
||||||
|
description: What is the user problem or growth opportunity you want to see solved?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Feature Research
|
||||||
|
description: How do you know that this problem exists today? Why is this important?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Alternative Solution
|
||||||
|
description: Other possible solutions, if any.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Target beneficiaries
|
||||||
|
description: Who will benefit from it?
|
||||||
|
validations:
|
||||||
|
required: true
|
0
.github/workflows/disabled_workflows/build-contributor-pr.yml
vendored
Executable file → Normal file
0
.github/workflows/disabled_workflows/build-contributor-pr.yml
vendored
Executable file → Normal file
0
.github/workflows/disabled_workflows/fenix-sync-strings.yml
vendored
Executable file → Normal file
0
.github/workflows/disabled_workflows/fenix-sync-strings.yml
vendored
Executable file → Normal file
0
.github/workflows/disabled_workflows/fenix-update-nimbus-experiments.yml
vendored
Executable file → Normal file
0
.github/workflows/disabled_workflows/fenix-update-nimbus-experiments.yml
vendored
Executable file → Normal file
0
.github/workflows/disabled_workflows/glean-probe-scraper.yml
vendored
Executable file → Normal file
0
.github/workflows/disabled_workflows/glean-probe-scraper.yml
vendored
Executable file → Normal file
0
.github/workflows/disabled_workflows/reopen-for-qa-needed.yml
vendored
Executable file → Normal file
0
.github/workflows/disabled_workflows/reopen-for-qa-needed.yml
vendored
Executable file → Normal file
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
Definitely not brought to you by Mozilla!
|
Definitely not brought to you by Mozilla!
|
||||||
|
|
||||||
|
|
||||||
Iceraven Browser is a web browser for Android, based on [Mozilla's Fenix version of Firefox](https://github.com/mozilla-mobile/fenix/), [GeckoView](https://mozilla.github.io/geckoview/) and [Mozilla Android Components](https://mozac.org/).
|
Iceraven Browser is a web browser for Android, based on [Mozilla's Fenix version of Firefox](https://github.com/mozilla-mobile/fenix/), [GeckoView](https://mozilla.github.io/geckoview/) and [Mozilla Android Components](https://mozac.org/).
|
||||||
|
|
||||||
Our goal is to be a close fork of the new Firefox for Android that seeks to provide users with more options, more opportunities to customize (including a broad extension library), and more information about the pages they visit and how their browsers are interacting with those pages.
|
Our goal is to be a close fork of the new Firefox for Android that seeks to provide users with more options, more opportunities to customize (including a broad extension library), and more information about the pages they visit and how their browsers are interacting with those pages.
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit f03a5f2688acc3faffe488c75cea6658d079fea3
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,8 @@
|
||||||
|
# Maven group ID used for all components
|
||||||
|
componentsGroupId: "org.mozilla.components"
|
||||||
|
|
||||||
|
# Synchronized build configuration for all modules
|
||||||
|
jvmTargetCompatibility: 17
|
||||||
|
compileSdkVersion: 35
|
||||||
|
minSdkVersion: 21
|
||||||
|
targetSdkVersion: 34
|
|
@ -0,0 +1,15 @@
|
||||||
|
# This is an .editorconfig that ktlint will "stop" at.
|
||||||
|
|
||||||
|
# If an .editorconfig file exists in any parent directory of the checkout
|
||||||
|
# directory, ktlint will fail because it uses those settings to determine what
|
||||||
|
# the indentation should be.
|
||||||
|
|
||||||
|
root = True
|
||||||
|
|
||||||
|
[*.kt]
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
|
||||||
|
ij_kotlin_allow_trailing_comma_on_call_site=true
|
||||||
|
ij_kotlin_allow_trailing_comma=true
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
# 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/.
|
||||||
|
|
||||||
|
# Treat generated API docs as binary. We do not want them to pollute
|
||||||
|
# our diffs.
|
||||||
|
docs/api/**/* -text -diff linguist-generated=true
|
||||||
|
|
||||||
|
# Treat the public suffix list file as text
|
||||||
|
**/publicsuffixes diff
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
,ich,pop-os,12.10.2024 11:30,file:///home/ich/.config/libreoffice/4;
|
|
@ -0,0 +1,2 @@
|
||||||
|
The changelog is now hosted here:
|
||||||
|
https://mozilla-mobile.github.io/android-components/changelog/
|
|
@ -0,0 +1,325 @@
|
||||||
|
# Android components
|
||||||
|
|
||||||
|
[![Task Status](https://firefox-ci-tc.services.mozilla.com/api/github/v1/repository/mozilla-mobile/android-components/main/badge.svg)](https://firefox-ci-tc.services.mozilla.com/api/github/v1/repository/mozilla-mobile/android-components/main/latest)
|
||||||
|
[![Mergify Status](https://img.shields.io/endpoint.svg?url=https://gh.mergify.io/badges/mozilla-mobile/android-components&style=flat)](https://mergify.io)
|
||||||
|
[![chat.mozilla.org](https://img.shields.io/badge/chat-on%20matrix-51bb9c)](https://chat.mozilla.org/#/room/#android-components:mozilla.org)
|
||||||
|
|
||||||
|
_A collection of Android libraries to build browsers or browser-like applications._
|
||||||
|
|
||||||
|
ℹ️ For more information **[see the website](https://mozilla-mobile.github.io/android-components/)**.
|
||||||
|
|
||||||
|
A fully-featured reference browser implementation based on the components can be found in the [reference-browser repository](https://github.com/mozilla-mobile/reference-browser).
|
||||||
|
|
||||||
|
# Getting Involved
|
||||||
|
|
||||||
|
We encourage you to participate in this open source project. We love pull requests, bug reports, ideas, (security) code reviews or any kind of positive contribution.
|
||||||
|
|
||||||
|
Before you attempt to make a contribution please read the [Community Participation Guidelines](https://www.mozilla.org/en-US/about/governance/policies/participation/).
|
||||||
|
|
||||||
|
* Matrix: [android-components:mozilla.org chat room](https://chat.mozilla.org/#/room/#android-components:mozilla.org) ([How to connect](https://wiki.mozilla.org/Matrix#Connect_to_Matrix)).
|
||||||
|
|
||||||
|
* Localization happens on [Pontoon](https://pontoon.mozilla.org/projects/android-l10n/). Please get in touch with delphine (at) mozilla (dot) com directly for more information.
|
||||||
|
|
||||||
|
# Maven repository
|
||||||
|
|
||||||
|
All components are getting published on [maven.mozilla.org](https://maven.mozilla.org/).
|
||||||
|
To use them, you need to add the following to your project's top-level build file, in the `allprojects` block (see e.g. the [reference-browser](https://github.com/mozilla-mobile/reference-browser/blob/main/build.gradle)):
|
||||||
|
|
||||||
|
```groovy
|
||||||
|
repositories {
|
||||||
|
maven {
|
||||||
|
url "https://maven.mozilla.org/maven2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each module that uses a component needs to specify it in its build file, in the `dependencies` block. For example, to use the `Base` component (in the `support`) collection, you need:
|
||||||
|
|
||||||
|
```groovy
|
||||||
|
dependencies {
|
||||||
|
implementation 'org.mozilla.components:support-base:+'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nightly builds
|
||||||
|
|
||||||
|
Nightly builds are created every day from the `main` branch and published on [nightly.maven.mozilla.org](https://nightly.maven.mozilla.org).
|
||||||
|
|
||||||
|
# Components
|
||||||
|
|
||||||
|
* 🔴 **In Development** - Not ready to be used in shipping products.
|
||||||
|
* ⚪ **Preview** - This component is almost/partially ready and can be tested in products.
|
||||||
|
* 🔵 **Production ready** - Used by shipping products.
|
||||||
|
|
||||||
|
## Browser
|
||||||
|
|
||||||
|
High-level components for building browser(-like) apps.
|
||||||
|
|
||||||
|
* 🔵 [**Awesomebar**](components/browser/awesomebar/README.md) - A customizable [Awesome Bar](https://support.mozilla.org/en-US/kb/awesome-bar-search-firefox-bookmarks-history-tabs) implementation for browsers.
|
||||||
|
|
||||||
|
* 🔵 [**Domains**](components/browser/domains/README.md) Localized and customizable domain lists for auto-completion in browsers.
|
||||||
|
|
||||||
|
* 🔵 [**Engine-Gecko**](components/browser/engine-gecko/README.md) - *Engine* implementation based on [GeckoView](https://wiki.mozilla.org/Mobile/GeckoView).
|
||||||
|
|
||||||
|
* 🔵 [**Engine-System**](components/browser/engine-system/README.md) - *Engine* implementation based on the system's WebView.
|
||||||
|
|
||||||
|
* 🔵 [**Errorpages**](components/browser/errorpages/README.md) - Responsive browser error pages for Android apps.
|
||||||
|
|
||||||
|
* 🔵 [**Icons**](components/browser/icons/README.md) - A component for loading and storing website icons (like [Favicons](https://en.wikipedia.org/wiki/Favicon)).
|
||||||
|
|
||||||
|
* 🔵 [**Menu**](components/browser/menu/README.md) - A generic menu with customizable items primarily for browser toolbars.
|
||||||
|
|
||||||
|
* ⚪ [**Menu 2**](components/browser/menu2/README.md) - A generic menu with customizable items primarily for browser toolbars.
|
||||||
|
|
||||||
|
* 🔵 [**Session-Storage**](components/browser/session-storage/README.md) - Component for saving and restoring the browser state.
|
||||||
|
|
||||||
|
* 🔵 [**State**](components/browser/state/README.md) - Component for maintaining the centralized state of the browser and its components.
|
||||||
|
|
||||||
|
* 🔵 [**Storage-Sync**](components/browser/storage-sync/README.md) - A syncable implementation of browser storage backed by [application-services' Places lib](https://github.com/mozilla/application-services).
|
||||||
|
|
||||||
|
* 🔵 [**Tabstray**](components/browser/tabstray/README.md) - A customizable tabs tray for browsers.
|
||||||
|
|
||||||
|
* 🔵 [**Thumbnails**](components/browser/thumbnails/README.md) - A component for loading and storing website thumbnails (screenshot of the website).
|
||||||
|
|
||||||
|
* 🔵 [**Toolbar**](components/browser/toolbar/README.md) - A customizable toolbar for browsers.
|
||||||
|
|
||||||
|
## Concept
|
||||||
|
|
||||||
|
_API contracts and abstraction layers for browser components._
|
||||||
|
|
||||||
|
* 🔵 [**Awesomebar**](components/concept/awesomebar/README.md) - An abstract definition of an awesome bar component.
|
||||||
|
|
||||||
|
* 🔵 [**Engine**](components/concept/engine/README.md) - Abstraction layer that allows hiding the actual browser engine implementation.
|
||||||
|
|
||||||
|
* 🔵 [**Fetch**](components/concept/fetch/README.md) - An abstract definition of an HTTP client for fetching resources.
|
||||||
|
|
||||||
|
* 🔵 [**Push**](components/concept/push/README.md) - An abstract definition of a push service component.
|
||||||
|
|
||||||
|
* 🔵 [**Storage**](components/concept/storage/README.md) - Abstract definition of a browser storage component.
|
||||||
|
|
||||||
|
* 🔵 [**Tabstray**](components/concept/tabstray/README.md) - Abstract definition of a tabs tray component.
|
||||||
|
|
||||||
|
* 🔵 [**Toolbar**](components/concept/toolbar/README.md) - Abstract definition of a browser toolbar component.
|
||||||
|
|
||||||
|
## Feature
|
||||||
|
|
||||||
|
_Combined components to implement feature-specific use cases._
|
||||||
|
|
||||||
|
* 🔵 [**Accounts**](components/feature/accounts/README.md) - A component that connects an FxaAccountManager from [service-firefox-accounts](components/service/firefox-accounts/README.md) with [feature-tabs](components/feature/tabs/README.md) in order to facilitate authentication flows.
|
||||||
|
|
||||||
|
* 🔵 [**Accounts Push**](components/feature/accounts-push/README.md) - Feature of use cases for FxA Account that work with push support.
|
||||||
|
|
||||||
|
* 🔵 [**Autofill**](components/feature/autofill/README.md) - A component that provides support for Android's Autofill framework.
|
||||||
|
|
||||||
|
* 🔵 [**Awesomebar**](components/feature/awesomebar/README.md) - A component that connects a [concept-awesomebar](components/concept/awesomebar/README.md) implementation to a [concept-toolbar](components/concept/toolbar/README.md) implementation and provides implementations of various suggestion providers.
|
||||||
|
|
||||||
|
* 🔴 [**Containers**](components/feature/containers/README.md) - A component for working with contextual identities also known as containers.
|
||||||
|
|
||||||
|
* 🔵 [**Context Menu**](components/feature/contextmenu/README.md) - A component for displaying context menus when *long-pressing* web content.
|
||||||
|
|
||||||
|
* 🔵 [**Custom Tabs**](components/feature/customtabs/README.md) - A component for providing [Custom Tabs](https://developer.chrome.com/multidevice/android/customtabs) functionality in browsers.
|
||||||
|
|
||||||
|
* 🔵 [**Downloads**](components/feature/downloads/README.md) - A component to perform downloads using the [Android downloads manager](https://developer.android.com/reference/android/app/DownloadManager).
|
||||||
|
|
||||||
|
* 🔵 [**Intent**](components/feature/intent/README.md) - A component that provides intent processing functionality by combining various other feature modules.
|
||||||
|
|
||||||
|
* ⚪ [**Progressive Web Apps (PWA)**](components/feature/pwa/README.md) - A component that provides functionality for supporting Progressive Web Apps (PWA).
|
||||||
|
|
||||||
|
* 🔵 [**Reader View**](components/feature/readerview/README.md) - A component that provides Reader View functionality.
|
||||||
|
|
||||||
|
* 🔵 [**QR**](components/feature/qr/README.md) - A component that provides functionality for scanning QR codes.
|
||||||
|
|
||||||
|
* 🔵 [**Search**](components/feature/search/README.md) - A component that connects an (concept) engine implementation with the browser search module.
|
||||||
|
|
||||||
|
* 🔵 [**Session**](components/feature/session/README.md) - A component that connects an (concept) engine implementation with the browser session and storage modules.
|
||||||
|
|
||||||
|
* 🔵 [**Share**](components/feature/share/README.md) - Feature implementation for saving and sorting recent apps used for sharing.
|
||||||
|
|
||||||
|
* 🔵 [**Sync**](components/feature/sync/README.md) -A component that provides synchronization orchestration for groups of (concept) SyncableStore objects.
|
||||||
|
|
||||||
|
* 🔵 [**Tabs**](components/feature/tabs/README.md) - A component that connects a tabs tray implementation with the session and toolbar modules.
|
||||||
|
|
||||||
|
* 🔵 [**Tab Collections**](components/feature/tab-collections/README.md) - Feature implementation for saving, restoring and organizing collections of tabs.
|
||||||
|
|
||||||
|
* 🔵 [**Toolbar**](components/feature/toolbar/README.md) - A component that connects a (concept) toolbar implementation with the browser session module.
|
||||||
|
|
||||||
|
* 🔵 [**Top Sites**](components/feature/top-sites/README.md) - Feature implementation for saving and removing top sites.
|
||||||
|
|
||||||
|
* 🔵 [**Prompts**](components/feature/prompts/README.md) - A component that will handle all the common prompt dialogs from web content.
|
||||||
|
|
||||||
|
* 🔵 [**Push**](components/feature/push/README.md) - A component that provides Autopush messages with help from a supported push service.
|
||||||
|
|
||||||
|
* 🔵 [**Find In Page**](components/feature/findinpage/README.md) - A component that provides an UI widget for [find in page functionality](https://support.mozilla.org/en-US/kb/search-contents-current-page-text-or-links).
|
||||||
|
|
||||||
|
* 🔵 [**Remote Tabs**](components/feature/remotetabs/README.md) - Feature that provides access to other device's tabs in the same account.
|
||||||
|
|
||||||
|
* 🔵 [**Site Permissions**](components/feature/sitepermissions/README.md) - A feature for showing site permission request prompts.
|
||||||
|
|
||||||
|
* 🔵 [**WebAuthn**](components/feature/webauthn/README.md) - A feature that provides WebAuthn functionality for supported engines.
|
||||||
|
|
||||||
|
* 🔵 [**Web Notifications**](components/feature/webnotifications/README.md) - A component for displaying web notifications.
|
||||||
|
|
||||||
|
* 🔵 [**WebCompat**](components/feature/webcompat/README.md) - A feature to enable website-hotfixing via the Web Compatibility System-Addon.
|
||||||
|
|
||||||
|
* 🔵 [**WebCompat Reporter**](components/feature/webcompat-reporter/README.md) - A feature that enables users to report site issues to Mozilla's Web Compatibility team for further diagnosis.
|
||||||
|
|
||||||
|
* 🔵 [**Web Add-ons**](components/feature/addons/README.md) - A feature that provides functionality for managing add-ons.
|
||||||
|
|
||||||
|
## UI
|
||||||
|
|
||||||
|
_Generic low-level UI components for building apps._
|
||||||
|
|
||||||
|
* 🔵 [**Autocomplete**](components/ui/autocomplete/README.md) - A set of components to provide autocomplete functionality.
|
||||||
|
|
||||||
|
* 🔵 [**Colors**](components/ui/colors/README.md) - The standard set of [Photon](https://design.firefox.com/photon/) colors.
|
||||||
|
|
||||||
|
* 🔵 [**Fonts**](components/ui/fonts/README.md) - The standard set of fonts used by Mozilla Android products.
|
||||||
|
|
||||||
|
* 🔵 [**Icons**](components/ui/icons/README.md) - A collection of often used browser icons.
|
||||||
|
|
||||||
|
* 🔵 [**Tabcounter**](components/ui/tabcounter/README.md) - A button that shows the current tab count and can animate state changes.
|
||||||
|
|
||||||
|
## Service
|
||||||
|
|
||||||
|
_Components and libraries to interact with backend services._
|
||||||
|
|
||||||
|
* 🔵 [**Firefox Accounts (FxA)**](components/service/firefox-accounts/README.md) - A library for integrating with Firefox Accounts.
|
||||||
|
|
||||||
|
* 🔵 [**Firefox Sync - Logins**](components/service/sync-logins/README.md) - A library for integrating with Firefox Sync - Logins.
|
||||||
|
|
||||||
|
* 🔵 [**Firefox Sync - Autofill**](components/service/sync-autofill/README.md) - A library for integrating with Firefox Sync - Autofill.
|
||||||
|
|
||||||
|
* 🔵 [**Glean**](components/service/glean/README.md) - A client-side telemetry SDK for collecting metrics and sending them to Mozilla's telemetry service (eventually replacing [service-telemetry](components/service/telemetry/README.md)).
|
||||||
|
|
||||||
|
* 🔵 [**Location**](components/service/location/README.md) - A library for accessing Mozilla's and other location services.
|
||||||
|
|
||||||
|
* 🔴 [**Nimbus**](components/service/nimbus/README.md) - A wrapper for the Nimbus SDK.
|
||||||
|
|
||||||
|
* 🔵 [**Pocket**](components/service/pocket/README.md) - A library for communicating with the Pocket API.
|
||||||
|
|
||||||
|
* 🔵 [**Contile**](components/service/contile/README.md) - A library for communicating with the Contile services API.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
_Supporting components with generic helper code._
|
||||||
|
|
||||||
|
* 🔵 [**Android Test**](components/support/android-test/README.md) - A collection of helpers for testing components in instrumented (on device) tests (`src/androidTest`).
|
||||||
|
|
||||||
|
* 🔵 [**Base**](components/support/base/README.md) - Base component containing building blocks for components.
|
||||||
|
|
||||||
|
* 🔵 [**Ktx**](components/support/ktx/README.md) - A set of Kotlin extensions on top of the Android framework and Kotlin standard library.
|
||||||
|
|
||||||
|
* 🔵 [**Test**](components/support/test/README.md) - A collection of helpers for testing components in local unit tests (`src/test`).
|
||||||
|
|
||||||
|
* 🔵 [**Test Appservices**](components/support/test-appservices/README.md) - A component for synchronizing Application Services' unit testing dependencies used in Android Components.
|
||||||
|
|
||||||
|
* 🔵 [**Test LibState**](components/support/test-libstate/README.md) - A collection of helpers for testing functionality that relies on the lib-state component in local unit tests (`src/test`).
|
||||||
|
|
||||||
|
* 🔵 [**Utils**](components/support/utils/README.md) - Generic utility classes to be shared between projects.
|
||||||
|
|
||||||
|
* 🔵 [**Webextensions**](components/support/webextensions/README.md) - A component containing building blocks for features implemented as web extensions.
|
||||||
|
|
||||||
|
## Standalone libraries
|
||||||
|
|
||||||
|
* 🔵 [**Crash**](components/lib/crash/README.md) - A generic crash reporter component that can report crashes to multiple services.
|
||||||
|
|
||||||
|
* 🔵 [**Dataprotect**](components/lib/dataprotect/README.md) - A component using AndroidKeyStore to protect user data.
|
||||||
|
|
||||||
|
* 🔵 [**Fetch-HttpURLConnection**](components/lib/fetch-httpurlconnection/README.md) - A [concept-fetch](concept/fetch/README.md) implementation using [HttpURLConnection](https://developer.android.com/reference/java/net/HttpURLConnection.html).
|
||||||
|
|
||||||
|
* 🔵 [**Fetch-OkHttp**](components/lib/fetch-okhttp/README.md) - A [concept-fetch](concept/fetch/README.md) implementation using [OkHttp](https://github.com/square/okhttp).
|
||||||
|
|
||||||
|
* ⚪ [**JEXL**](components/lib/jexl/README.md) - Javascript Expression Language: Context-based expression parser and evaluator.
|
||||||
|
|
||||||
|
* 🔵 [**Public Suffix List**](components/lib/publicsuffixlist/README.md) - A library for reading and using the [public suffix list](https://publicsuffix.org/).
|
||||||
|
|
||||||
|
* 🔵 [**Push-Firebase**](components/lib/push-firebase/README.md) - A [concept-push](concept/push/README.md) implementation using [Firebase Cloud Messaging](https://firebase.google.com/products/cloud-messaging/).
|
||||||
|
|
||||||
|
* 🔵 [**State**](components/lib/state/README.md) - A library for maintaining application state.
|
||||||
|
|
||||||
|
## Tooling
|
||||||
|
|
||||||
|
* 🔵 [**Fetch-Tests**](components/tooling/fetch-tests/README.md) - A generic test suite for components that implement [concept-fetch](concept/fetch/README.md).
|
||||||
|
|
||||||
|
* 🔵 [**Lint**](components/tooling/lint/README.md) - Custom Lint rules for the components repository.
|
||||||
|
|
||||||
|
# Sample apps
|
||||||
|
|
||||||
|
_Sample apps using various components._
|
||||||
|
|
||||||
|
* [**Browser**](samples/browser) - A simple browser composed from browser components. This sample application is only a very basic browser. For a full-featured reference browser implementation see the **[reference-browser repository](https://github.com/mozilla-mobile/reference-browser)**.
|
||||||
|
|
||||||
|
* [**Crash**](samples/crash) - An app showing the integration of the `lib-crash` component.
|
||||||
|
|
||||||
|
* [**Firefox Accounts (FxA)**](samples/firefox-accounts) - A simple app demoing Firefox Accounts integration.
|
||||||
|
|
||||||
|
* [**Firefox Sync**](samples/sync) - A simple app demoing general Firefox Sync integration, with bookmarks and history.
|
||||||
|
|
||||||
|
* [**Firefox Sync - Logins**](samples/sync-logins) - A simple app demoing Firefox Sync (Logins) integration.
|
||||||
|
|
||||||
|
* [**DataProtect**](samples/dataprotect) - An app demoing how to use the [**Dataprotect**](components/lib/dataprotect/README.md) component to load and store encrypted data in `SharedPreferences`.
|
||||||
|
|
||||||
|
* [**Glean**](samples/glean) - An app demoing how to use the [**Glean**](components/service/glean/README.md) library to collect and send telemetry data.
|
||||||
|
|
||||||
|
* [**Toolbar**](samples/toolbar) - An app demoing multiple customized toolbars using the [**browser-toolbar**](components/browser/toolbar/README.md) component.
|
||||||
|
|
||||||
|
# Building #
|
||||||
|
|
||||||
|
## Command line ##
|
||||||
|
|
||||||
|
```
|
||||||
|
$ git clone https://github.com/mozilla-mobile/android-components.git
|
||||||
|
$ cd android-components
|
||||||
|
$ ./gradlew assemble
|
||||||
|
```
|
||||||
|
|
||||||
|
## Android Studio ##
|
||||||
|
|
||||||
|
If the environment variable `JAVA_HOME` is not defined, you will need to set it. If you would like to use the JDK installed by Android Studio, here's how to find it:
|
||||||
|
|
||||||
|
1. Open Android Studio.
|
||||||
|
2. Select "Configure".
|
||||||
|
3. Select "Default Project Structure". You should now see the Android JDK location.
|
||||||
|
4. Set the environment variable `JAVA_HOME` to the location. (How you set an environment variable depends on your OS.)
|
||||||
|
5. Restart Android Studio.
|
||||||
|
|
||||||
|
Once the environment variable is set, you can import the project into Android Studio with the default wizard options.
|
||||||
|
|
||||||
|
If your build fails, you may find you get more instructive error messages by attempting the build at the command line.
|
||||||
|
|
||||||
|
# Coding Standards #
|
||||||
|
|
||||||
|
## Style ##
|
||||||
|
We follow the style enforced by [ktlint](https://ktlint.github.io/) and [detekt](https://github.com/detekt/detekt). See [how to configure Android Studio appropriately](https://github.com/pinterest/ktlint#option-1-recommended).
|
||||||
|
|
||||||
|
To check your style, run:
|
||||||
|
|
||||||
|
```
|
||||||
|
./gradlew ktlint
|
||||||
|
./gradlew detekt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation ##
|
||||||
|
We use `README.md` files for each component.
|
||||||
|
|
||||||
|
If you fix a bug or change an API, you should update [docs/changelog.md](https://github.com/mozilla-mobile/android-components/blob/main/docs/changelog.md).
|
||||||
|
|
||||||
|
## Testing ##
|
||||||
|
You are expected to both add tests for code that you write and make sure that your changes do not
|
||||||
|
cause existing tests to fail. You may find these command lines helpful:
|
||||||
|
|
||||||
|
```
|
||||||
|
./gradlew test # Run all tests
|
||||||
|
./gradlew :support-ktx:testdebugunittest # Run unit tests for a specified module
|
||||||
|
```
|
||||||
|
|
||||||
|
See also [how to measure code coverage](https://mozac.org/contributing/code-coverage).
|
||||||
|
|
||||||
|
## Accessibility ##
|
||||||
|
If your code has user-facing changes, follow [Android accessibility best practices](https://github.com/mozilla-mobile/shared-docs/blob/main/android/accessibility_guide.md).
|
||||||
|
|
||||||
|
# License
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/
|
|
@ -0,0 +1,19 @@
|
||||||
|
android {
|
||||||
|
lint {
|
||||||
|
warningsAsErrors true
|
||||||
|
abortOnError (project.name != "support-test")
|
||||||
|
|
||||||
|
// With our L10N process its totally possible to have missing or (temporarily) extra translations.
|
||||||
|
disable 'MissingTranslation',
|
||||||
|
'ExtraTranslation',
|
||||||
|
// We do not want to enforce this as a generic rule for all languages (see #6117, #6056, #6118)
|
||||||
|
'TypographyEllipsis',
|
||||||
|
// https://github.com/mozilla-mobile/android-components/issues/10641
|
||||||
|
'UnspecifiedImmutableFlag',
|
||||||
|
// https://github.com/mozilla-mobile/android-components/issues/10643
|
||||||
|
'UnusedResources',
|
||||||
|
// "We do not impose rules on locales"
|
||||||
|
// https://github.com/mozilla-mobile/android-components/pull/11069
|
||||||
|
'TypographyDashes'
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# 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/.
|
||||||
|
|
||||||
|
|
||||||
|
# Purpose: Publish android packages to local maven repo, but only if changed since last publish.
|
||||||
|
# Dependencies: None
|
||||||
|
# Usage: ./automation/publish_to_maven_local_if_modified.py
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def fatal_err(msg):
|
||||||
|
print(f"\033[31mError: {msg}\033[0m")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def run_cmd_checked(*args, **kwargs):
|
||||||
|
"""Run a command, throwing an exception if it exits with non-zero status."""
|
||||||
|
kwargs["check"] = True
|
||||||
|
return subprocess.run(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def find_project_root():
|
||||||
|
"""Find the absolute path of the project repository root."""
|
||||||
|
# As a convention, we expect this file in [project-root]/automation/.
|
||||||
|
automation_dir = Path(__file__).parent
|
||||||
|
|
||||||
|
# Therefore the automation dir's parent is the project root we're looking for.
|
||||||
|
return automation_dir.parent
|
||||||
|
|
||||||
|
|
||||||
|
LAST_CONTENTS_HASH_FILE = ".lastAutoPublishContentsHash"
|
||||||
|
|
||||||
|
GITIGNORED_FILES_THAT_AFFECT_THE_BUILD = ["local.properties"]
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Publish android packages to local maven repo, but only if changed since last publish"
|
||||||
|
)
|
||||||
|
parser.parse_args()
|
||||||
|
|
||||||
|
root_dir = find_project_root()
|
||||||
|
if str(root_dir) != os.path.abspath(os.curdir):
|
||||||
|
fatal_err(
|
||||||
|
f"This only works if run from the repo root ({root_dir!r} != {os.path.abspath(os.curdir)!r})"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Calculate a hash reflecting the current state of the repo.
|
||||||
|
|
||||||
|
contents_hash = hashlib.sha256()
|
||||||
|
|
||||||
|
contents_hash.update(
|
||||||
|
run_cmd_checked(["git", "rev-parse", "HEAD"], capture_output=True).stdout
|
||||||
|
)
|
||||||
|
contents_hash.update(b"\x00")
|
||||||
|
|
||||||
|
# Get a diff of all tracked (staged and unstaged) files.
|
||||||
|
|
||||||
|
changes = run_cmd_checked(["git", "diff", "HEAD", "."], capture_output=True).stdout
|
||||||
|
contents_hash.update(changes)
|
||||||
|
contents_hash.update(b"\x00")
|
||||||
|
|
||||||
|
# But unfortunately it can only tell us the names of untracked
|
||||||
|
# files, and it won't tell us anything about files that are in
|
||||||
|
# .gitignore but can still affect the build.
|
||||||
|
|
||||||
|
untracked_files = []
|
||||||
|
|
||||||
|
# Get a list of all untracked files sans standard exclusions.
|
||||||
|
|
||||||
|
# -o is for getting other (i.e. untracked) files
|
||||||
|
# --exclude-standard is to handle standard Git exclusions: .git/info/exclude, .gitignore in each directory,
|
||||||
|
# and the user's global exclusion file.
|
||||||
|
changes_others = run_cmd_checked(
|
||||||
|
["git", "ls-files", "-o", "--exclude-standard"], capture_output=True
|
||||||
|
).stdout
|
||||||
|
changes_lines = iter(ln.strip() for ln in changes_others.split(b"\n"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
ln = next(changes_lines)
|
||||||
|
while ln:
|
||||||
|
untracked_files.append(ln)
|
||||||
|
ln = next(changes_lines)
|
||||||
|
except StopIteration:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Then, account for some excluded files that we care about.
|
||||||
|
untracked_files.extend(GITIGNORED_FILES_THAT_AFFECT_THE_BUILD)
|
||||||
|
|
||||||
|
# Finally, get hashes of everything.
|
||||||
|
# Skip files that don't exist, e.g. missing GITIGNORED_FILES_THAT_AFFECT_THE_BUILD. `hash-object` errors out if it gets
|
||||||
|
# a non-existent file, so we hope that disk won't change between this filter and the cmd run just below.
|
||||||
|
filtered_untracked = [nm for nm in untracked_files if os.path.isfile(nm)]
|
||||||
|
# Reading contents of the files is quite slow when there are lots of them, so delegate to `git hash-object`.
|
||||||
|
git_hash_object_cmd = ["git", "hash-object"]
|
||||||
|
git_hash_object_cmd.extend(filtered_untracked)
|
||||||
|
changes_untracked = run_cmd_checked(git_hash_object_cmd, capture_output=True).stdout
|
||||||
|
contents_hash.update(changes_untracked)
|
||||||
|
contents_hash.update(b"\x00")
|
||||||
|
|
||||||
|
contents_hash = contents_hash.hexdigest()
|
||||||
|
|
||||||
|
# If the contents hash has changed since last publish, re-publish.
|
||||||
|
last_contents_hash = ""
|
||||||
|
try:
|
||||||
|
with open(LAST_CONTENTS_HASH_FILE) as f:
|
||||||
|
last_contents_hash = f.read().strip()
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if contents_hash == last_contents_hash:
|
||||||
|
print("Contents have not changed, no need to publish")
|
||||||
|
else:
|
||||||
|
print("Contents have changed, publishing")
|
||||||
|
if sys.platform.startswith("win"):
|
||||||
|
run_cmd_checked(
|
||||||
|
["gradlew.bat", "publishToMavenLocal", f"-Plocal={time.time_ns()}"],
|
||||||
|
shell=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
run_cmd_checked(
|
||||||
|
["./gradlew", "publishToMavenLocal", f"-Plocal={time.time_ns()}"]
|
||||||
|
)
|
||||||
|
with open(LAST_CONTENTS_HASH_FILE, "w") as f:
|
||||||
|
f.write(contents_hash)
|
||||||
|
f.write("\n")
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Google Cloud Documentation: https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run
|
||||||
|
# Flank Documentation: https://flank.github.io/flank/
|
||||||
|
gcloud:
|
||||||
|
results-bucket: android-components_test_artifacts
|
||||||
|
record-video: true
|
||||||
|
timeout: 30m
|
||||||
|
async: false
|
||||||
|
num-flaky-test-attempts: 2
|
||||||
|
|
||||||
|
app: /APP/PATH
|
||||||
|
test: /TEST/PATH
|
||||||
|
|
||||||
|
auto-google-login: false
|
||||||
|
use-orchestrator: true
|
||||||
|
environment-variables:
|
||||||
|
clearPackageData: true
|
||||||
|
directories-to-pull:
|
||||||
|
- /sdcard/screenshots
|
||||||
|
performance-metrics: true
|
||||||
|
|
||||||
|
device:
|
||||||
|
- model: Pixel2.arm
|
||||||
|
version: 28
|
||||||
|
locale: en_US
|
||||||
|
|
||||||
|
flank:
|
||||||
|
project: GOOGLE_PROJECT
|
||||||
|
max-test-shards: -1
|
||||||
|
num-test-runs: 1
|
||||||
|
output-style: compact
|
||||||
|
full-junit-result: true
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Google Cloud Documentation: https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run
|
||||||
|
# Flank Documentation: https://flank.github.io/flank/
|
||||||
|
gcloud:
|
||||||
|
results-bucket: android-components_test_artifacts
|
||||||
|
record-video: true
|
||||||
|
timeout: 30m
|
||||||
|
async: false
|
||||||
|
num-flaky-test-attempts: 1
|
||||||
|
|
||||||
|
app: /app/path
|
||||||
|
test: /test/path
|
||||||
|
|
||||||
|
auto-google-login: false
|
||||||
|
use-orchestrator: true
|
||||||
|
environment-variables:
|
||||||
|
clearPackageData: true
|
||||||
|
directories-to-pull:
|
||||||
|
- /sdcard/screenshots
|
||||||
|
performance-metrics: true
|
||||||
|
|
||||||
|
device:
|
||||||
|
- model: Pixel2
|
||||||
|
version: 28
|
||||||
|
|
||||||
|
flank:
|
||||||
|
project: GOOGLE_PROJECT
|
||||||
|
max-test-shards: -1
|
||||||
|
num-test-runs: 1
|
||||||
|
output-style: compact
|
||||||
|
full-junit-result: true
|
||||||
|
repeat-tests: 1
|
|
@ -0,0 +1,144 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import xml
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from beautifultable import BeautifulTable
|
||||||
|
from junitparser import Attr, Failure, JUnitXml, TestCase, TestSuite
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(cmdln_args):
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Parse and print UI test JUnit results"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--results",
|
||||||
|
type=Path,
|
||||||
|
help="Directory containing task artifact results",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
return parser.parse_args(args=cmdln_args)
|
||||||
|
|
||||||
|
|
||||||
|
class test_suite(TestSuite):
|
||||||
|
flakes = Attr()
|
||||||
|
|
||||||
|
|
||||||
|
class test_case(TestCase):
|
||||||
|
flaky = Attr()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_print_failure_results(results):
|
||||||
|
"""
|
||||||
|
Parses the given JUnit test results and prints a formatted table of failures and flaky tests.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
results (JUnitXml): Parsed JUnit XML results.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The number of test failures.
|
||||||
|
|
||||||
|
The function processes each test suite and each test case within the suite.
|
||||||
|
If a test case has a result that is an instance of Failure, it is added to the table.
|
||||||
|
The test case is marked as 'Flaky' if the flaky attribute is set to "true", otherwise it is marked as 'Failure'.
|
||||||
|
|
||||||
|
Example of possible JUnit XML (FullJUnitReport.xml):
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="ExampleSuite" tests="2" failures="1" flakes="1" time="0.003">
|
||||||
|
<testcase classname="example.TestClass" name="testSuccess" flaky="true" time="0.001">
|
||||||
|
<failure message="Assertion failed">Expected true but was false</failure>
|
||||||
|
</testcase>
|
||||||
|
<testcase classname="example.TestClass" name="testFailure" time="0.002">
|
||||||
|
<failure message="Assertion failed">Expected true but was false</failure>
|
||||||
|
<failure message="Assertion failed">Expected true but was false</failure>
|
||||||
|
</testcase>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
"""
|
||||||
|
|
||||||
|
table = BeautifulTable(maxwidth=256)
|
||||||
|
table.columns.header = ["UI Test", "Outcome", "Details"]
|
||||||
|
table.columns.alignment = BeautifulTable.ALIGN_LEFT
|
||||||
|
table.set_style(BeautifulTable.STYLE_GRID)
|
||||||
|
|
||||||
|
failure_count = 0
|
||||||
|
|
||||||
|
# Dictionary to store the last seen failure details for each test case
|
||||||
|
last_seen_failures = {}
|
||||||
|
|
||||||
|
for suite in results:
|
||||||
|
cur_suite = test_suite.fromelem(suite)
|
||||||
|
for case in cur_suite:
|
||||||
|
cur_case = test_case.fromelem(case)
|
||||||
|
if cur_case.result:
|
||||||
|
for entry in case.result:
|
||||||
|
if isinstance(entry, Failure):
|
||||||
|
flaky_status = getattr(cur_case, "flaky", "false") == "true"
|
||||||
|
if flaky_status:
|
||||||
|
test_id = "%s#%s" % (case.classname, case.name)
|
||||||
|
details = (
|
||||||
|
entry.text.replace("\t", " ") if entry.text else ""
|
||||||
|
)
|
||||||
|
# Check if the current failure details are different from the last seen ones
|
||||||
|
if details != last_seen_failures.get(test_id, ""):
|
||||||
|
table.rows.append(
|
||||||
|
[
|
||||||
|
test_id,
|
||||||
|
"Flaky",
|
||||||
|
details,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
last_seen_failures[test_id] = details
|
||||||
|
else:
|
||||||
|
test_id = "%s#%s" % (case.classname, case.name)
|
||||||
|
details = (
|
||||||
|
entry.text.replace("\t", " ") if entry.text else ""
|
||||||
|
)
|
||||||
|
# Check if the current failure details are different from the last seen ones
|
||||||
|
if details != last_seen_failures.get(test_id, ""):
|
||||||
|
table.rows.append(
|
||||||
|
[
|
||||||
|
test_id,
|
||||||
|
"Failure",
|
||||||
|
details,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
print(f"TEST-UNEXPECTED-FAIL | {test_id} | {details}")
|
||||||
|
failure_count += 1
|
||||||
|
# Update the last seen failure details for this test case
|
||||||
|
last_seen_failures[test_id] = details
|
||||||
|
|
||||||
|
print(table)
|
||||||
|
return failure_count
|
||||||
|
|
||||||
|
|
||||||
|
def load_results_file(filename):
|
||||||
|
ret = None
|
||||||
|
try:
|
||||||
|
f = open(filename, "r")
|
||||||
|
try:
|
||||||
|
ret = JUnitXml.fromfile(f)
|
||||||
|
except xml.etree.ElementTree.ParseError as e:
|
||||||
|
print(f"Error parsing {filename} file: {e}")
|
||||||
|
finally:
|
||||||
|
f.close()
|
||||||
|
except IOError as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = parse_args(sys.argv[1:])
|
||||||
|
|
||||||
|
failure_count = 0
|
||||||
|
junitxml = load_results_file(args.results.joinpath("FullJUnitReport.xml"))
|
||||||
|
if junitxml:
|
||||||
|
failure_count = parse_print_failure_results(junitxml)
|
||||||
|
return failure_count
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
|
@ -0,0 +1,93 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(cmdln_args):
|
||||||
|
parser = argparse.ArgumentParser(description="Parse UI test logs an results")
|
||||||
|
parser.add_argument(
|
||||||
|
"--output-md",
|
||||||
|
type=argparse.FileType("w", encoding="utf-8"),
|
||||||
|
help="Output markdown file.",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--log",
|
||||||
|
type=argparse.FileType("r", encoding="utf-8"),
|
||||||
|
help="Log output of flank.",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--results", type=Path, help="Directory containing flank results", required=True
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--exit-code", type=int, help="Exit code of flank.", required=True
|
||||||
|
)
|
||||||
|
parser.add_argument("--device-type", help="Type of device ", required=True)
|
||||||
|
parser.add_argument(
|
||||||
|
"--report-treeherder-failures",
|
||||||
|
help="Report failures in treeherder format.",
|
||||||
|
required=False,
|
||||||
|
action="store_true",
|
||||||
|
)
|
||||||
|
|
||||||
|
return parser.parse_args(args=cmdln_args)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_android_args(log):
|
||||||
|
return yaml.safe_load(log.split("AndroidArgs\n")[1].split("RunTests\n")[0])
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = parse_args(sys.argv[1:])
|
||||||
|
|
||||||
|
log = args.log.read()
|
||||||
|
matrix_ids = json.loads(args.results.joinpath("matrix_ids.json").read_text())
|
||||||
|
# with args.results.joinpath("flank.yml") as f:
|
||||||
|
# flank_config = yaml.safe_load(f)
|
||||||
|
|
||||||
|
android_args = extract_android_args(log)
|
||||||
|
|
||||||
|
print = args.output_md.write
|
||||||
|
|
||||||
|
print("# Devices\n")
|
||||||
|
print(yaml.safe_dump(android_args["gcloud"]["device"]))
|
||||||
|
|
||||||
|
print("# Results\n")
|
||||||
|
print("| matrix | result | logs | details \n")
|
||||||
|
print("| --- | --- | --- | --- |\n")
|
||||||
|
for matrix, matrix_result in matrix_ids.items():
|
||||||
|
print(
|
||||||
|
"| {matrixId} | {outcome} | [logs]({webLink}) | {axes[0][details]}\n".format(
|
||||||
|
**matrix_result
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
args.report_treeherder_failures
|
||||||
|
and matrix_result["outcome"] != "success"
|
||||||
|
and matrix_result["outcome"] != "flaky"
|
||||||
|
):
|
||||||
|
# write failures to test log in format known to treeherder logviewer
|
||||||
|
sys.stdout.write(
|
||||||
|
f"TEST-UNEXPECTED-FAIL | {matrix_result['outcome']} | {matrix_result['webLink']} | {matrix_result['axes'][0]['details']}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("---\n")
|
||||||
|
print("# References & Documentation\n")
|
||||||
|
print(
|
||||||
|
"* [Automated UI Testing Documentation](https://github.com/mozilla-mobile/shared-docs/blob/main/android/ui-testing.md)\n"
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
"* Mobile Test Engineering on [Confluence](https://mozilla-hub.atlassian.net/wiki/spaces/MTE/overview) | [Slack](https://mozilla.slack.com/archives/C02KDDS9QM9) | [Alerts](https://mozilla.slack.com/archives/C0134KJ4JHL)\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
|
@ -0,0 +1,164 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# 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/.
|
||||||
|
|
||||||
|
# This script does the following:
|
||||||
|
# 1. Retrieves glcoud service account token
|
||||||
|
# 2. Activates gcloud service account
|
||||||
|
# 3. Connects to google Firebase (using TestArmada's Flank tool)
|
||||||
|
# 4. Executes UI tests
|
||||||
|
# 5. Puts test artifacts into the test_artifacts folder
|
||||||
|
|
||||||
|
# NOTE:
|
||||||
|
# Flank supports sharding across multiple devices at a time, but gcloud API
|
||||||
|
# only supports 1 defined APK per test run.
|
||||||
|
|
||||||
|
|
||||||
|
# If a command fails then do not proceed and fail this script too.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
#########################
|
||||||
|
# The command line help #
|
||||||
|
#########################
|
||||||
|
display_help() {
|
||||||
|
echo "Usage: $0 Component_Name Build_Variant [Number_Shards...]"
|
||||||
|
echo
|
||||||
|
echo "Examples:"
|
||||||
|
echo "To run component/browser tests on ARM device shard (1 test / shard)"
|
||||||
|
echo "$ execute-firebase-test.sh component arm"
|
||||||
|
echo
|
||||||
|
echo "To run component/feature tests on X86 device (on 3 shards)"
|
||||||
|
echo "$ execute-firebase-test.sh feature x86 3"
|
||||||
|
echo
|
||||||
|
echo "To run UI samples/sampleName tests"
|
||||||
|
echo "$ execute-firebase-test.sh sample-sampleName arm 1"
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
# Basic parameter check
|
||||||
|
if [[ $# -lt 2 ]]; then
|
||||||
|
echo "Your command line contains $# arguments"
|
||||||
|
display_help
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
component="$1" # browser, concept, feature
|
||||||
|
device_type="$2" # arm | x86
|
||||||
|
if [[ ! -z "$3" ]]; then
|
||||||
|
num_shards=$3
|
||||||
|
fi
|
||||||
|
|
||||||
|
JAVA_BIN="/usr/bin/java"
|
||||||
|
PATH_TEST="./automation/taskcluster/androidTest"
|
||||||
|
FLANK_BIN="/builds/worker/test-tools/flank.jar"
|
||||||
|
FLANK_CONF_ARM="${PATH_TEST}/flank-arm.yml"
|
||||||
|
FLANK_CONF_X86="${PATH_TEST}/flank-x86.yml"
|
||||||
|
ARTIFACT_DIR="/builds/worker/artifacts"
|
||||||
|
RESULTS_DIR="${ARTIFACT_DIR}/results"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "ACTIVATE SERVICE ACCT"
|
||||||
|
echo
|
||||||
|
# this is where the Google Testcloud project ID is set
|
||||||
|
gcloud config set project "$GOOGLE_PROJECT"
|
||||||
|
echo
|
||||||
|
|
||||||
|
gcloud auth activate-service-account --key-file "$GOOGLE_APPLICATION_CREDENTIALS"
|
||||||
|
echo
|
||||||
|
echo
|
||||||
|
|
||||||
|
# From now on disable exiting on error. If the tests fail we want to continue
|
||||||
|
# and try to download the artifacts. We will exit with the actual error code later.
|
||||||
|
set +e
|
||||||
|
|
||||||
|
if [[ "${device_type,,}" == "x86" ]]
|
||||||
|
then
|
||||||
|
flank_template="$FLANK_CONF_X86"
|
||||||
|
else
|
||||||
|
flank_template="$FLANK_CONF_ARM"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove samples- from the component for each APK path
|
||||||
|
samples=${component//samples-}
|
||||||
|
|
||||||
|
# If tests are for components, the path is different than for samples
|
||||||
|
if [[ "${component}" != samples-* ]]
|
||||||
|
then
|
||||||
|
# Case 1: tests for any component (but NOT samples, NOT real UI tests)
|
||||||
|
APK_APP="./samples/browser/build/outputs/apk/gecko/debug/samples-browser-gecko-debug.apk"
|
||||||
|
if [[ "${component}" == *"-"* ]]
|
||||||
|
then
|
||||||
|
regex='([a-z]*)-(.*)'
|
||||||
|
[[ "$component" =~ $regex ]]
|
||||||
|
APK_TEST="./components/${BASH_REMATCH[1]}/${BASH_REMATCH[2]}/build/outputs/apk/androidTest/debug/${component}-debug-androidTest.apk"
|
||||||
|
else
|
||||||
|
APK_TEST="./components/${component}/engine-gecko/build/outputs/apk/androidTest/debug/browser-engine-gecko-debug-androidTest.apk"
|
||||||
|
fi
|
||||||
|
elif [[ "${component}" == "samples-browser" ]]
|
||||||
|
then
|
||||||
|
# Case 2: tests for browser sample (gecko sample only)
|
||||||
|
APK_APP="./samples/${samples}/build/outputs/apk/gecko/debug/samples-${samples}-gecko-debug.apk"
|
||||||
|
APK_TEST="./samples/${samples}/build/outputs/apk/androidTest/gecko/debug/samples-{$samples}-gecko-debug-androidTest.apk"
|
||||||
|
else
|
||||||
|
# Case 3: tests for non-browser samples (i.e. samples-glean)
|
||||||
|
APK_APP="./samples/${samples}/build/outputs/apk/debug/samples-${samples}-debug.apk"
|
||||||
|
APK_TEST="./samples/${samples}/build/outputs/apk/androidTest/debug/samples-${samples}-debug-androidTest.apk"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# function to exit script with exit code from test run.
|
||||||
|
# (Only 0 if all test executions passed)
|
||||||
|
function failure_check() {
|
||||||
|
echo
|
||||||
|
echo
|
||||||
|
if [[ $exitcode -ne 0 ]]; then
|
||||||
|
echo "ERROR: UI test run failed, please check above URL"
|
||||||
|
else
|
||||||
|
echo "All UI test(s) have passed!"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "RESULTS"
|
||||||
|
echo
|
||||||
|
mkdir -p ${ARTIFACT_DIR}/github
|
||||||
|
chmod +x ${PATH_TEST}/parse-ui-test.py
|
||||||
|
chmod +x ${PATH_TEST}/parse-ui-test-fromfile.py
|
||||||
|
${PATH_TEST}/parse-ui-test-fromfile.py \
|
||||||
|
--results "${RESULTS_DIR}"
|
||||||
|
if [[ $? -ne 0 ]]; then
|
||||||
|
${PATH_TEST}/parse-ui-test.py \
|
||||||
|
--exit-code "${exitcode}" \
|
||||||
|
--log flank.log \
|
||||||
|
--results "${RESULTS_DIR}" \
|
||||||
|
--output-md "${ARTIFACT_DIR}/github/customCheckRunText.md" \
|
||||||
|
--device-type "${device_type}"
|
||||||
|
else
|
||||||
|
${PATH_TEST}/parse-ui-test.py \
|
||||||
|
--exit-code "${exitcode}" \
|
||||||
|
--log flank.log \
|
||||||
|
--results "${RESULTS_DIR}" \
|
||||||
|
--output-md "${ARTIFACT_DIR}/github/customCheckRunText.md" \
|
||||||
|
--device-type "${device_type}" \
|
||||||
|
--report-treeherder-failures
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "EXECUTE TEST(S)"
|
||||||
|
echo
|
||||||
|
|
||||||
|
set -o pipefail && $JAVA_BIN -jar $FLANK_BIN android run \
|
||||||
|
--config=$flank_template \
|
||||||
|
--max-test-shards=$num_shards \
|
||||||
|
--app=$APK_APP --test=$APK_TEST \
|
||||||
|
--local-result-dir="${RESULTS_DIR}" \
|
||||||
|
--project=$GOOGLE_PROJECT \
|
||||||
|
--client-details=commit=${MOBILE_HEAD_REV:-None},pullRequest=${PULL_REQUEST_NUMBER:-None} \
|
||||||
|
| tee flank.log
|
||||||
|
|
||||||
|
exitcode=$?
|
||||||
|
failure_check
|
||||||
|
|
||||||
|
exit $exitcode
|
|
@ -0,0 +1,356 @@
|
||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
|
import io.gitlab.arturbosch.detekt.Detekt
|
||||||
|
import io.gitlab.arturbosch.detekt.DetektCreateBaselineTask
|
||||||
|
import org.gradle.internal.logging.text.StyledTextOutput.Style
|
||||||
|
import org.gradle.internal.logging.text.StyledTextOutputFactory
|
||||||
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
import static org.gradle.api.tasks.testing.TestResult.ResultType
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORIES.each { repository ->
|
||||||
|
maven {
|
||||||
|
url repository
|
||||||
|
if (gradle.mozconfig.substs.ALLOW_INSECURE_GRADLE_REPOSITORIES) {
|
||||||
|
allowInsecureProtocol = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath ComponentsDependencies.tools_androidgradle
|
||||||
|
classpath ComponentsDependencies.tools_kotlingradle
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variables in plugins {} aren't directly supported. Hack around it by setting an
|
||||||
|
// intermediate variable which can pull from FenixDependencies.kt and be used later.
|
||||||
|
ext {
|
||||||
|
detekt_plugin = Versions.detekt
|
||||||
|
python_envs_plugin = Versions.python_envs_plugin
|
||||||
|
ksp_plugin = Versions.ksp_plugin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("io.gitlab.arturbosch.detekt").version("$detekt_plugin")
|
||||||
|
id("com.google.devtools.ksp").version("$ksp_plugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORIES.each { repository ->
|
||||||
|
maven {
|
||||||
|
url repository
|
||||||
|
if (gradle.mozconfig.substs.ALLOW_INSECURE_GRADLE_REPOSITORIES) {
|
||||||
|
allowInsecureProtocol = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
maven {
|
||||||
|
url "${gradle.mozconfig.topobjdir}/gradle/maven"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
apply plugin: 'jacoco'
|
||||||
|
|
||||||
|
// Enable Kotlin warnings as errors for all modules
|
||||||
|
tasks.withType(KotlinCompile).configureEach {
|
||||||
|
kotlinOptions.allWarningsAsErrors = true
|
||||||
|
}
|
||||||
|
|
||||||
|
project.configurations.configureEach {
|
||||||
|
// Dependencies can't depend on a different major version of Glean than A-C itself.
|
||||||
|
resolutionStrategy.eachDependency { details ->
|
||||||
|
if (details.requested.group == 'org.mozilla.telemetry'
|
||||||
|
&& details.requested.name.contains('glean') ) {
|
||||||
|
def requested = details.requested.version.tokenize(".")
|
||||||
|
def defined = Versions.mozilla_glean.tokenize(".")
|
||||||
|
// Check the major version
|
||||||
|
if (requested[0] != defined[0]) {
|
||||||
|
throw new AssertionError("Cannot resolve to a single Glean version. Requested: ${details.requested.version}, A-C uses: ${Versions.mozilla_glean}")
|
||||||
|
} else {
|
||||||
|
// Enforce that all (transitive) dependencies are using the defined Glean version
|
||||||
|
details.useVersion Versions.mozilla_glean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolutionStrategy.capabilitiesResolution.withCapability("org.mozilla.telemetry:glean-native") {
|
||||||
|
def toBeSelected = candidates.find { it.id instanceof ModuleComponentIdentifier && it.id.module.contains('geckoview') }
|
||||||
|
if (toBeSelected != null) {
|
||||||
|
select(toBeSelected)
|
||||||
|
}
|
||||||
|
because 'use GeckoView Glean instead of standalone Glean'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gradle.hasProperty('localProperties.dependencySubstitutions.geckoviewTopsrcdir')) {
|
||||||
|
if (gradle.hasProperty('localProperties.dependencySubstitutions.geckoviewTopobjdir')) {
|
||||||
|
ext.topobjdir = gradle."localProperties.dependencySubstitutions.geckoviewTopobjdir"
|
||||||
|
}
|
||||||
|
ext.topsrcdir = gradle."localProperties.dependencySubstitutions.geckoviewTopsrcdir"
|
||||||
|
apply from: "${topsrcdir}/substitute-local-geckoview.gradle"
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEvaluate {
|
||||||
|
if (it.hasProperty('android')) {
|
||||||
|
jacoco {
|
||||||
|
toolVersion = Versions.jacoco
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format test output
|
||||||
|
tasks.matching {it instanceof Test}.configureEach() {
|
||||||
|
systemProperty "robolectric.logging", "stdout"
|
||||||
|
systemProperty "logging.test-mode", "true"
|
||||||
|
systemProperty "javax.net.ssl.trustStoreType", "JKS"
|
||||||
|
|
||||||
|
testLogging.events = []
|
||||||
|
|
||||||
|
def out = services.get(StyledTextOutputFactory).create("an-ouput")
|
||||||
|
|
||||||
|
beforeSuite { descriptor ->
|
||||||
|
if (descriptor.getClassName() != null) {
|
||||||
|
out.style(Style.Header).println("\nSUITE: " + descriptor.getClassName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeTest { descriptor ->
|
||||||
|
out.style(Style.Description).println(" TEST: " + descriptor.getName())
|
||||||
|
}
|
||||||
|
|
||||||
|
onOutput { descriptor, event ->
|
||||||
|
logger.lifecycle(" " + event.message.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
afterTest { descriptor, result ->
|
||||||
|
switch (result.getResultType()) {
|
||||||
|
case ResultType.SUCCESS:
|
||||||
|
out.style(Style.Success).println(" SUCCESS")
|
||||||
|
break
|
||||||
|
|
||||||
|
case ResultType.FAILURE:
|
||||||
|
def testId = descriptor.getClassName() + "." + descriptor.getName()
|
||||||
|
out.style(Style.Failure).println(" TEST-UNEXPECTED-FAIL | " + testId + " | " + result.getException())
|
||||||
|
break
|
||||||
|
|
||||||
|
case ResultType.SKIPPED:
|
||||||
|
out.style(Style.Info).println(" SKIPPED")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
logger.lifecycle("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
lintChecks project(':tooling-lint')
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(config.jvmTargetCompatibility)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
includeAndroidResources = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
resources {
|
||||||
|
excludes += ['META-INF/atomicfu.kotlin_module', 'META-INF/AL2.0', 'META-INF/LGPL2.1']
|
||||||
|
// Required dependencies using byte-buddy; remove after this is
|
||||||
|
// fixed by: https://issuetracker.google.com/issues/170131605
|
||||||
|
excludes.add("META-INF/licenses/ASM")
|
||||||
|
|
||||||
|
pickFirsts += ['win32-x86-64/attach_hotspot_windows.dll', 'win32-x86/attach_hotspot_windows.dll']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
androidResources {
|
||||||
|
ignoreAssetsPattern "manifest.template.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType(KotlinCompile).configureEach {
|
||||||
|
kotlinOptions.freeCompilerArgs += ["-opt-in=kotlin.RequiresOptIn"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (project.hasProperty("coverage") && project.name != "support-test") {
|
||||||
|
android.buildTypes.all { buildType ->
|
||||||
|
tasks.withType(Test).configureEach() {
|
||||||
|
jacoco {
|
||||||
|
includeNoLocationClasses = true
|
||||||
|
excludes = ['jdk.internal.*']
|
||||||
|
}
|
||||||
|
|
||||||
|
finalizedBy { "jacoco${buildType.name.capitalize()}TestReport" }
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("jacoco${buildType.name.capitalize()}TestReport", JacocoReport) {
|
||||||
|
reports {
|
||||||
|
xml.required = true
|
||||||
|
html.required = true
|
||||||
|
}
|
||||||
|
|
||||||
|
def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*',
|
||||||
|
'**/*Test*.*', 'android/**/*.*', '**/*$[0-9].*']
|
||||||
|
def kotlinDebugTree = fileTree(dir: "$project.layout.buildDirectory/tmp/kotlin-classes/${buildType.name}", excludes: fileFilter)
|
||||||
|
def javaDebugTree = fileTree(dir: "$project.layout.buildDirectory/intermediates/classes/${buildType.name}", excludes: fileFilter)
|
||||||
|
def mainSrc = "$project.projectDir/src/main/java"
|
||||||
|
|
||||||
|
sourceDirectories.setFrom(files([mainSrc]))
|
||||||
|
classDirectories.setFrom(files([kotlinDebugTree, javaDebugTree]))
|
||||||
|
getExecutionData().setFrom(fileTree(project.layout.buildDirectory).include([
|
||||||
|
"jacoco/test${buildType.name.capitalize()}UnitTest.exec"
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
testCoverageEnabled true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType(KotlinCompile).configureEach {
|
||||||
|
// Translate Kotlin messages like "w: ..." and "e: ..." into
|
||||||
|
// "...: warning: ..." and "...: error: ...", to make Treeherder understand.
|
||||||
|
def listener = {
|
||||||
|
if (it.startsWith("e: warnings found")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it.startsWith('w: ') || it.startsWith('e: ')) {
|
||||||
|
def matches = (it =~ /([ew]): (.+):(\d+):(\d+) (.*)/)
|
||||||
|
if (!matches) {
|
||||||
|
logger.quiet "kotlinc message format has changed!"
|
||||||
|
if (it.startsWith('w: ')) {
|
||||||
|
// For warnings, don't continue because we don't want to throw an
|
||||||
|
// exception. For errors, we want the exception so that the new error
|
||||||
|
// message format gets translated properly.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def (_, type, file, line, column, message) = matches[0]
|
||||||
|
type = (type == 'w') ? 'warning' : 'error'
|
||||||
|
// Use logger.lifecycle, which does not go through stderr again.
|
||||||
|
logger.lifecycle "$file:$line:$column: $type: $message"
|
||||||
|
}
|
||||||
|
} as StandardOutputListener
|
||||||
|
|
||||||
|
doFirst {
|
||||||
|
logging.addStandardErrorListener(listener)
|
||||||
|
}
|
||||||
|
doLast {
|
||||||
|
logging.removeStandardErrorListener(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (findProject(":geckoview") == null) {
|
||||||
|
// Avoid adding this task if it already exists in a different root project.
|
||||||
|
tasks.register("clean", Delete) {
|
||||||
|
delete rootProject.layout.buildDirectory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
detekt {
|
||||||
|
input = files("$projectDir/components", "$projectDir/buildSrc", "$projectDir/samples")
|
||||||
|
config = files("$projectDir/config/detekt.yml")
|
||||||
|
baseline = file("$projectDir/config/detekt-baseline.xml")
|
||||||
|
|
||||||
|
reports {
|
||||||
|
html {
|
||||||
|
enabled = true
|
||||||
|
destination = file("$projectDir/build/reports/detekt.html")
|
||||||
|
}
|
||||||
|
xml {
|
||||||
|
enabled = false
|
||||||
|
}
|
||||||
|
txt {
|
||||||
|
enabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType(Detekt).configureEach() {
|
||||||
|
// Custom detekt rules should be build before
|
||||||
|
// See https://arturbosch.github.io/detekt/extensions.html#pitfalls
|
||||||
|
dependsOn(":tooling-detekt:assemble")
|
||||||
|
|
||||||
|
autoCorrect = true
|
||||||
|
|
||||||
|
exclude "**/build.gradle.kts"
|
||||||
|
exclude "**/src/androidTest/**"
|
||||||
|
exclude "**/src/iosTest/**"
|
||||||
|
exclude "**/src/test/**"
|
||||||
|
exclude "**/test/src/**"
|
||||||
|
exclude "**/build/**"
|
||||||
|
exclude "**/resources/**"
|
||||||
|
exclude "**/tmp/**"
|
||||||
|
exclude "**/tooling/fetch/tests/**"
|
||||||
|
exclude "**/tooling/fetch-tests/**"
|
||||||
|
exclude "**/src/main/assets/extensions/**"
|
||||||
|
exclude "**/docs/**"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply same path exclusions as for the main task
|
||||||
|
tasks.withType(DetektCreateBaselineTask).configureEach() {
|
||||||
|
exclude "**/src/androidTest/**"
|
||||||
|
exclude "**/src/test/**"
|
||||||
|
exclude "**/test/src/**"
|
||||||
|
exclude "**/build/**"
|
||||||
|
exclude "**/resources/**"
|
||||||
|
exclude "**/tmp/**"
|
||||||
|
exclude "**/tooling/fetch/tests/**"
|
||||||
|
exclude "**/tooling/fetch-tests/**"
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations {
|
||||||
|
ktlint
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
ktlint("com.pinterest:ktlint:${Versions.ktlint}") {
|
||||||
|
attributes {
|
||||||
|
attribute(Bundling.BUNDLING_ATTRIBUTE, getObjects().named(Bundling, Bundling.EXTERNAL))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
detektPlugins project(":tooling-detekt")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("ktlint", JavaExec) {
|
||||||
|
group = "verification"
|
||||||
|
description = "Check Kotlin code style."
|
||||||
|
classpath = configurations.ktlint
|
||||||
|
mainClass.set("com.pinterest.ktlint.Main")
|
||||||
|
args "components/**/*.kt" , "samples/**/*.kt", "!**/build/**/*.kt", "buildSrc/**/*.kt", "--baseline=ktlint-baseline.xml",
|
||||||
|
"--reporter=json,output=build/reports/ktlint/ktlint.json", "--reporter=plain"
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("ktlintFormat", JavaExec) {
|
||||||
|
group = "formatting"
|
||||||
|
description = "Fix Kotlin code style deviations."
|
||||||
|
classpath = configurations.ktlint
|
||||||
|
mainClass.set("com.pinterest.ktlint.Main")
|
||||||
|
args "-F", "components/**/*.kt" , "samples/**/*.kt", "!**/build/**/*.kt", "buildSrc/**/*.kt", "--baseline=ktlint-baseline.xml"
|
||||||
|
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("listRepositories") {
|
||||||
|
doLast {
|
||||||
|
println "Repositories:"
|
||||||
|
project.repositories.each { println "Name: " + it.name + "; url: " + it.url }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
# [Android Components](../../../README.md) > Browser > Domains
|
||||||
|
|
||||||
|
This component provides APIs for managing localized and customizable domain lists (see [Domains](#domains) and [CustomDomains](#customdomains)). It also contains auto-complete functionality for these lists (see [DomainAutoCompleteProvider](#domainautocompleteprovider)) which can be used in conjuction with our [UI autocomplete component](../../ui/autocomplete/README.md).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Setting up the dependency
|
||||||
|
|
||||||
|
Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)):
|
||||||
|
|
||||||
|
```Groovy
|
||||||
|
implementation "org.mozilla.components:browser-domains:{latest-version}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Domains
|
||||||
|
|
||||||
|
The `Domains` object is used to load the built-in localized domain lists which are shipped as part of this component. These lists are grouped by country and can be found [in our repository](src/main/assets/domains).
|
||||||
|
|
||||||
|
```Kotlin
|
||||||
|
// Load the domain lists for all countries in the default locale (fallback is US)
|
||||||
|
val domains = Domains.load(context)
|
||||||
|
```
|
||||||
|
|
||||||
|
### CustomDomains
|
||||||
|
|
||||||
|
The `CustomDomains` object can be used to manage a custom domain list which will be stored in `SharedPreferences`.
|
||||||
|
|
||||||
|
```Kotlin
|
||||||
|
// Load the custom domain list
|
||||||
|
val domains = CustomDomains.load(context)
|
||||||
|
|
||||||
|
// Save custom domains
|
||||||
|
CustomDomains.save(context, listOf("mozilla.org", "getpocket.com"))
|
||||||
|
|
||||||
|
// Remove custom domains
|
||||||
|
CustomDomains.remove(context, listOf("nolongerexists.org"))
|
||||||
|
```
|
||||||
|
|
||||||
|
### DomainAutoCompleteProvider
|
||||||
|
|
||||||
|
The class provides auto-complete functionality for both `Domains` and `CustomDomains`.
|
||||||
|
|
||||||
|
```Kotlin
|
||||||
|
// Initialize the provider
|
||||||
|
val provider = DomainAutocompleteProvider()
|
||||||
|
provider.initialize(
|
||||||
|
context,
|
||||||
|
useShippedDomains = true,
|
||||||
|
useCustomDomains = true,
|
||||||
|
loadDomainsFromDisk = true
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that when `loadDomainsFromDisk` is set to true there is no need to manually call `load` on either `Domains` or `CustomDomains`.
|
||||||
|
|
||||||
|
```Kotlin
|
||||||
|
// Autocomplete domain lists
|
||||||
|
val result = provider.autocomplete("moz")
|
||||||
|
```
|
||||||
|
|
||||||
|
The result will contain the autocompleted text (`result.text`), the URL (`result.url`), and the source of the match (`result.source`), which is either `DEFAULT_LIST` if a result was found in the shipped domain list or `CUSTOM_LIST` otherwise. The custom domain list takes precendece over the built-in shipped domain list and the API will only return the first match.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/
|
|
@ -0,0 +1,40 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
apply plugin: 'com.android.library'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion config.minSdkVersion
|
||||||
|
compileSdk config.compileSdkVersion
|
||||||
|
targetSdkVersion config.targetSdkVersion
|
||||||
|
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace 'mozilla.components.browser.domains'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(':concept-toolbar')
|
||||||
|
implementation ComponentsDependencies.kotlin_coroutines
|
||||||
|
|
||||||
|
testImplementation project(':support-test')
|
||||||
|
|
||||||
|
testImplementation ComponentsDependencies.androidx_test_junit
|
||||||
|
testImplementation ComponentsDependencies.testing_robolectric
|
||||||
|
testImplementation ComponentsDependencies.testing_coroutines
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: '../../../android-lint.gradle'
|
||||||
|
apply from: '../../../publish.gradle'
|
||||||
|
ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
|
@ -0,0 +1,4 @@
|
||||||
|
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
<manifest />
|
|
@ -0,0 +1,50 @@
|
||||||
|
google.com.br
|
||||||
|
youtube.com
|
||||||
|
google.com
|
||||||
|
facebook.com
|
||||||
|
globo.com
|
||||||
|
uol.com.br
|
||||||
|
blastingnews.com
|
||||||
|
live.com
|
||||||
|
mercadolivre.com.br
|
||||||
|
yahoo.com
|
||||||
|
blogspot.com.br
|
||||||
|
wikipedia.org
|
||||||
|
whatsapp.com
|
||||||
|
netflix.com
|
||||||
|
olx.com.br
|
||||||
|
instagram.com
|
||||||
|
msn.com
|
||||||
|
metropoles.com
|
||||||
|
fatosdesconhecidos.com.br
|
||||||
|
twitter.com
|
||||||
|
caixa.gov.br
|
||||||
|
uptodown.com
|
||||||
|
aliexpress.com
|
||||||
|
curapelanatureza.com.br
|
||||||
|
wordpress.com
|
||||||
|
abril.com.br
|
||||||
|
americanas.com.br
|
||||||
|
correios.com.br
|
||||||
|
reclameaqui.com.br
|
||||||
|
bet365.com
|
||||||
|
onclkds.com
|
||||||
|
bol.uol.com.br
|
||||||
|
techtudo.com.br
|
||||||
|
fazenda.gov.br
|
||||||
|
microsoft.com
|
||||||
|
folha.uol.com.br
|
||||||
|
linkedin.com
|
||||||
|
tumblr.com
|
||||||
|
sp.gov.br
|
||||||
|
reddit.com
|
||||||
|
bb.com.br
|
||||||
|
pinterest.com
|
||||||
|
itau.com.br
|
||||||
|
letras.mus.br
|
||||||
|
otvfoco.com.br
|
||||||
|
vagalume.com.br
|
||||||
|
portalinteressante.com
|
||||||
|
myappolicious.com.br
|
||||||
|
thewhizmarketing.com
|
||||||
|
twitch.tv
|
|
@ -0,0 +1,49 @@
|
||||||
|
google.com
|
||||||
|
youtube.com
|
||||||
|
facebook.com
|
||||||
|
reddit.com
|
||||||
|
amazon.com
|
||||||
|
wikipedia.org
|
||||||
|
yahoo.com
|
||||||
|
twitter.com
|
||||||
|
netflix.com
|
||||||
|
ebay.com
|
||||||
|
imgur.com
|
||||||
|
linkedin.com
|
||||||
|
instagram.com
|
||||||
|
diply.com
|
||||||
|
craigslist.org
|
||||||
|
live.com
|
||||||
|
office.com
|
||||||
|
twitch.tv
|
||||||
|
tumblr.com
|
||||||
|
pinterest.com
|
||||||
|
espn.com
|
||||||
|
cnn.com
|
||||||
|
bing.com
|
||||||
|
wikia.com
|
||||||
|
chase.com
|
||||||
|
imdb.com
|
||||||
|
nytimes.com
|
||||||
|
paypal.com
|
||||||
|
blogspot.com
|
||||||
|
apple.com
|
||||||
|
yelp.com
|
||||||
|
stackoverflow.com
|
||||||
|
bankofamerica.com
|
||||||
|
wordpress.com
|
||||||
|
github.com
|
||||||
|
microsoft.com
|
||||||
|
wellsfargo.com
|
||||||
|
zillow.com
|
||||||
|
salesforce.com
|
||||||
|
msn.com
|
||||||
|
walmart.com
|
||||||
|
weather.com
|
||||||
|
dropbox.com
|
||||||
|
buzzfeed.com
|
||||||
|
intuit.com
|
||||||
|
washingtonpost.com
|
||||||
|
soundcloud.com
|
||||||
|
huffingtonpost.com
|
||||||
|
indeed.com
|
|
@ -0,0 +1,50 @@
|
||||||
|
google.de
|
||||||
|
youtube.com
|
||||||
|
google.com
|
||||||
|
facebook.com
|
||||||
|
amazon.de
|
||||||
|
ebay.de
|
||||||
|
wikipedia.org
|
||||||
|
ebay-kleinanzeigen.de
|
||||||
|
web.de
|
||||||
|
yahoo.com
|
||||||
|
ok.ru
|
||||||
|
gmx.net
|
||||||
|
reddit.com
|
||||||
|
vk.com
|
||||||
|
t-online.de
|
||||||
|
twitter.com
|
||||||
|
spiegel.de
|
||||||
|
mail.ru
|
||||||
|
instagram.com
|
||||||
|
live.com
|
||||||
|
chip.de
|
||||||
|
bild.de
|
||||||
|
paypal.com
|
||||||
|
bing.com
|
||||||
|
twitch.tv
|
||||||
|
whatsapp.com
|
||||||
|
yandex.ru
|
||||||
|
gutefrage.net
|
||||||
|
mobile.de
|
||||||
|
google.ru
|
||||||
|
blogspot.de
|
||||||
|
tumblr.com
|
||||||
|
bs.to
|
||||||
|
focus.de
|
||||||
|
linkedin.com
|
||||||
|
netflix.com
|
||||||
|
wordpress.com
|
||||||
|
imgur.com
|
||||||
|
postbank.de
|
||||||
|
welt.de
|
||||||
|
streamcloud.eu
|
||||||
|
microsoft.com
|
||||||
|
immobilienscout24.de
|
||||||
|
msn.com
|
||||||
|
dict.cc
|
||||||
|
otto.de
|
||||||
|
xing.com
|
||||||
|
amazon.com
|
||||||
|
heise.de
|
||||||
|
github.com
|
|
@ -0,0 +1,50 @@
|
||||||
|
google.fr
|
||||||
|
youtube.com
|
||||||
|
google.com
|
||||||
|
facebook.com
|
||||||
|
wikipedia.org
|
||||||
|
amazon.fr
|
||||||
|
leboncoin.fr
|
||||||
|
yahoo.com
|
||||||
|
live.com
|
||||||
|
twitter.com
|
||||||
|
orange.fr
|
||||||
|
free.fr
|
||||||
|
linkedin.com
|
||||||
|
lemonde.fr
|
||||||
|
instagram.com
|
||||||
|
reddit.com
|
||||||
|
lefigaro.fr
|
||||||
|
ebay.fr
|
||||||
|
cdiscount.com
|
||||||
|
jeuxvideo.com
|
||||||
|
zone-telechargement.ws
|
||||||
|
labanquepostale.fr
|
||||||
|
blogspot.fr
|
||||||
|
allocine.fr
|
||||||
|
msn.com
|
||||||
|
commentcamarche.net
|
||||||
|
pole-emploi.fr
|
||||||
|
vk.com
|
||||||
|
sfr.fr
|
||||||
|
lequipe.fr
|
||||||
|
twitch.tv
|
||||||
|
francetvinfo.fr
|
||||||
|
20minutes.fr
|
||||||
|
pinterest.com
|
||||||
|
netflix.com
|
||||||
|
programme-tv.net
|
||||||
|
credit-agricole.fr
|
||||||
|
linternaute.com
|
||||||
|
github.com
|
||||||
|
wordpress.com
|
||||||
|
caf.fr
|
||||||
|
aliexpress.com
|
||||||
|
dailymotion.com
|
||||||
|
tumblr.com
|
||||||
|
t411.ai
|
||||||
|
stackoverflow.com
|
||||||
|
microsoft.com
|
||||||
|
meteofrance.com
|
||||||
|
onclkds.com
|
||||||
|
bfmtv.com
|
|
@ -0,0 +1,49 @@
|
||||||
|
google.co.uk
|
||||||
|
youtube.com
|
||||||
|
google.com
|
||||||
|
facebook.com
|
||||||
|
reddit.com
|
||||||
|
bbc.co.uk
|
||||||
|
amazon.co.uk
|
||||||
|
wikipedia.org
|
||||||
|
ebay.co.uk
|
||||||
|
twitter.com
|
||||||
|
live.com
|
||||||
|
yahoo.com
|
||||||
|
instagram.com
|
||||||
|
diply.com
|
||||||
|
linkedin.com
|
||||||
|
imgur.com
|
||||||
|
netflix.com
|
||||||
|
theguardian.com
|
||||||
|
dailymail.co.uk
|
||||||
|
twitch.tv
|
||||||
|
imdb.com
|
||||||
|
paypal.com
|
||||||
|
office.com
|
||||||
|
tumblr.com
|
||||||
|
www.gov.uk
|
||||||
|
wikia.com
|
||||||
|
givemesport.com
|
||||||
|
amazon.com
|
||||||
|
bing.com
|
||||||
|
wordpress.com
|
||||||
|
telegraph.co.uk
|
||||||
|
rightmove.co.uk
|
||||||
|
pinterest.com
|
||||||
|
gumtree.com
|
||||||
|
msn.com
|
||||||
|
microsoft.com
|
||||||
|
stackoverflow.com
|
||||||
|
booking.com
|
||||||
|
vk.com
|
||||||
|
tripadvisor.co.uk
|
||||||
|
lloydsbank.co.uk
|
||||||
|
apple.com
|
||||||
|
service.gov.uk
|
||||||
|
onclkds.com
|
||||||
|
github.com
|
||||||
|
independent.co.uk
|
||||||
|
bt.com
|
||||||
|
vice.com
|
||||||
|
hsbc.co.uk
|
|
@ -0,0 +1,444 @@
|
||||||
|
google.com
|
||||||
|
facebook.com
|
||||||
|
amazon.com
|
||||||
|
youtube.com
|
||||||
|
yahoo.com
|
||||||
|
ebay.com
|
||||||
|
wikipedia.org
|
||||||
|
twitter.com
|
||||||
|
reddit.com
|
||||||
|
go.com
|
||||||
|
craigslist.org
|
||||||
|
live.com
|
||||||
|
netflix.com
|
||||||
|
pinterest.com
|
||||||
|
bing.com
|
||||||
|
linkedin.com
|
||||||
|
imgur.com
|
||||||
|
espn.go.com
|
||||||
|
walmart.com
|
||||||
|
tumblr.com
|
||||||
|
target.com
|
||||||
|
paypal.com
|
||||||
|
cnn.com
|
||||||
|
chase.com
|
||||||
|
instagram.com
|
||||||
|
bestbuy.com
|
||||||
|
blogspot.com
|
||||||
|
nytimes.com
|
||||||
|
msn.com
|
||||||
|
imdb.com
|
||||||
|
apple.com
|
||||||
|
bankofamerica.com
|
||||||
|
diply.com
|
||||||
|
huffingtonpost.com
|
||||||
|
yelp.com
|
||||||
|
wellsfargo.com
|
||||||
|
etsy.com
|
||||||
|
weather.com
|
||||||
|
wordpress.com
|
||||||
|
buzzfeed.com
|
||||||
|
zillow.com
|
||||||
|
kohls.com
|
||||||
|
aol.com
|
||||||
|
homedepot.com
|
||||||
|
foxnews.com
|
||||||
|
microsoft.com
|
||||||
|
comcast.net
|
||||||
|
wikia.com
|
||||||
|
groupon.com
|
||||||
|
macys.com
|
||||||
|
washingtonpost.com
|
||||||
|
outbrain.com
|
||||||
|
xfinity.com
|
||||||
|
usps.com
|
||||||
|
hulu.com
|
||||||
|
americanexpress.com
|
||||||
|
slickdeals.net
|
||||||
|
pandora.com
|
||||||
|
office.com
|
||||||
|
cnet.com
|
||||||
|
indeed.com
|
||||||
|
capitalone.com
|
||||||
|
nfl.com
|
||||||
|
ups.com
|
||||||
|
ask.com
|
||||||
|
verizonwireless.com
|
||||||
|
newegg.com
|
||||||
|
usatoday.com
|
||||||
|
forbes.com
|
||||||
|
dailymail.co.uk
|
||||||
|
dropbox.com
|
||||||
|
att.com
|
||||||
|
costco.com
|
||||||
|
gfycat.com
|
||||||
|
lowes.com
|
||||||
|
gap.com
|
||||||
|
about.com
|
||||||
|
tripadvisor.com
|
||||||
|
fedex.com
|
||||||
|
baidu.com
|
||||||
|
vice.com
|
||||||
|
nordstrom.com
|
||||||
|
adobe.com
|
||||||
|
bbc.com
|
||||||
|
twitch.tv
|
||||||
|
allrecipes.com
|
||||||
|
retailmenot.com
|
||||||
|
stackoverflow.com
|
||||||
|
citi.com
|
||||||
|
sears.com
|
||||||
|
jcpenney.com
|
||||||
|
webmd.com
|
||||||
|
nih.gov
|
||||||
|
answers.com
|
||||||
|
foodnetwork.com
|
||||||
|
discovercard.com
|
||||||
|
cbssports.com
|
||||||
|
overstock.com
|
||||||
|
businessinsider.com
|
||||||
|
office365.com
|
||||||
|
theguardian.com
|
||||||
|
staples.com
|
||||||
|
bleacherreport.com
|
||||||
|
verizon.com
|
||||||
|
github.com
|
||||||
|
wayfair.com
|
||||||
|
salesforce.com
|
||||||
|
zulily.com
|
||||||
|
wsj.com
|
||||||
|
flickr.com
|
||||||
|
goodreads.com
|
||||||
|
realtor.com
|
||||||
|
nbcnews.com
|
||||||
|
ebates.com
|
||||||
|
ancestry.com
|
||||||
|
wunderground.com
|
||||||
|
instructure.com
|
||||||
|
people.com
|
||||||
|
stackexchange.com
|
||||||
|
drudgereport.com
|
||||||
|
fidelity.com
|
||||||
|
southwest.com
|
||||||
|
deviantart.com
|
||||||
|
thesaurus.com
|
||||||
|
intuit.com
|
||||||
|
woot.com
|
||||||
|
pch.com
|
||||||
|
soundcloud.com
|
||||||
|
force.com
|
||||||
|
samsclub.com
|
||||||
|
ign.com
|
||||||
|
qvc.com
|
||||||
|
npr.org
|
||||||
|
patch.com
|
||||||
|
dell.com
|
||||||
|
accuweather.com
|
||||||
|
vimeo.com
|
||||||
|
expedia.com
|
||||||
|
trulia.com
|
||||||
|
ca.gov
|
||||||
|
swagbucks.com
|
||||||
|
spotify.com
|
||||||
|
bedbathandbeyond.com
|
||||||
|
nypost.com
|
||||||
|
aliexpress.com
|
||||||
|
blackboard.com
|
||||||
|
ticketmaster.com
|
||||||
|
ikea.com
|
||||||
|
feedly.com
|
||||||
|
usaa.com
|
||||||
|
tmz.com
|
||||||
|
quora.com
|
||||||
|
lifehacker.com
|
||||||
|
kayak.com
|
||||||
|
reference.com
|
||||||
|
zappos.com
|
||||||
|
gizmodo.com
|
||||||
|
slate.com
|
||||||
|
faithtap.com
|
||||||
|
adp.com
|
||||||
|
abcnews.go.com
|
||||||
|
sephora.com
|
||||||
|
cbs.com
|
||||||
|
latimes.com
|
||||||
|
shutterfly.com
|
||||||
|
t-mobile.com
|
||||||
|
littlethings.com
|
||||||
|
glassdoor.com
|
||||||
|
bloomberg.com
|
||||||
|
cbsnews.com
|
||||||
|
wikihow.com
|
||||||
|
walgreens.com
|
||||||
|
usbank.com
|
||||||
|
blogger.com
|
||||||
|
weebly.com
|
||||||
|
gamestop.com
|
||||||
|
food.com
|
||||||
|
time.com
|
||||||
|
kickstarter.com
|
||||||
|
okcupid.com
|
||||||
|
aa.com
|
||||||
|
weather.gov
|
||||||
|
nametests.com
|
||||||
|
fandango.com
|
||||||
|
engadget.com
|
||||||
|
steamcommunity.com
|
||||||
|
thekitchn.com
|
||||||
|
nba.com
|
||||||
|
mashable.com
|
||||||
|
hp.com
|
||||||
|
gamefaqs.com
|
||||||
|
delta.com
|
||||||
|
coupons.com
|
||||||
|
eonline.com
|
||||||
|
surveymonkey.com
|
||||||
|
kmart.com
|
||||||
|
barnesandnoble.com
|
||||||
|
meetup.com
|
||||||
|
bhphotovideo.com
|
||||||
|
fanduel.com
|
||||||
|
quizlet.com
|
||||||
|
nydailynews.com
|
||||||
|
sbnation.com
|
||||||
|
nbcsports.com
|
||||||
|
bbc.co.uk
|
||||||
|
ew.com
|
||||||
|
nike.com
|
||||||
|
rottentomatoes.com
|
||||||
|
steampowered.com
|
||||||
|
reuters.com
|
||||||
|
qq.com
|
||||||
|
today.com
|
||||||
|
mapquest.com
|
||||||
|
audible.com
|
||||||
|
priceline.com
|
||||||
|
whitepages.com
|
||||||
|
united.com
|
||||||
|
myfitnesspal.com
|
||||||
|
icloud.com
|
||||||
|
forever21.com
|
||||||
|
theatlantic.com
|
||||||
|
microsoftstore.com
|
||||||
|
theverge.com
|
||||||
|
gawker.com
|
||||||
|
houzz.com
|
||||||
|
mayoclinic.org
|
||||||
|
rei.com
|
||||||
|
sfgate.com
|
||||||
|
lifebuzz.com
|
||||||
|
discover.com
|
||||||
|
pnc.com
|
||||||
|
pof.com
|
||||||
|
iflscience.com
|
||||||
|
popsugar.com
|
||||||
|
creditkarma.com
|
||||||
|
telegraph.co.uk
|
||||||
|
airbnb.com
|
||||||
|
buzzlie.com
|
||||||
|
cnbc.com
|
||||||
|
deadspin.com
|
||||||
|
sina.com.cn
|
||||||
|
legacy.com
|
||||||
|
thedailybeast.com
|
||||||
|
samsung.com
|
||||||
|
nextdoor.com
|
||||||
|
evite.com
|
||||||
|
shopify.com
|
||||||
|
yellowpages.com
|
||||||
|
pcmag.com
|
||||||
|
redfin.com
|
||||||
|
weibo.com
|
||||||
|
alibaba.com
|
||||||
|
cabelas.com
|
||||||
|
battle.net
|
||||||
|
foxsports.com
|
||||||
|
taobao.com
|
||||||
|
eventbrite.com
|
||||||
|
victoriassecret.com
|
||||||
|
theblaze.com
|
||||||
|
dealnews.com
|
||||||
|
cbslocal.com
|
||||||
|
cvs.com
|
||||||
|
dailymotion.com
|
||||||
|
ecollege.com
|
||||||
|
gofundme.com
|
||||||
|
fitbit.com
|
||||||
|
instructables.com
|
||||||
|
godaddy.com
|
||||||
|
babycenter.com
|
||||||
|
squarespace.com
|
||||||
|
llbean.com
|
||||||
|
dickssportinggoods.com
|
||||||
|
6pm.com
|
||||||
|
myway.com
|
||||||
|
hsn.com
|
||||||
|
wired.com
|
||||||
|
officedepot.com
|
||||||
|
ozztube.com
|
||||||
|
usmagazine.com
|
||||||
|
match.com
|
||||||
|
cracked.com
|
||||||
|
evernote.com
|
||||||
|
box.com
|
||||||
|
starbucks.com
|
||||||
|
kbb.com
|
||||||
|
mlb.com
|
||||||
|
marriott.com
|
||||||
|
si.com
|
||||||
|
jezebel.com
|
||||||
|
pbs.org
|
||||||
|
consumerreports.org
|
||||||
|
roblox.com
|
||||||
|
urbandictionary.com
|
||||||
|
kotaku.com
|
||||||
|
xbox.com
|
||||||
|
marketwatch.com
|
||||||
|
refinery29.com
|
||||||
|
wikimedia.org
|
||||||
|
tvguide.com
|
||||||
|
politico.com
|
||||||
|
barclaycardus.com
|
||||||
|
abc.go.com
|
||||||
|
mint.com
|
||||||
|
topix.com
|
||||||
|
theblackfriday.com
|
||||||
|
aarp.org
|
||||||
|
hotnewhiphop.com
|
||||||
|
yourdailydish.com
|
||||||
|
sprint.com
|
||||||
|
vox.com
|
||||||
|
cafemom.com
|
||||||
|
nbc.com
|
||||||
|
dailykos.com
|
||||||
|
azlyrics.com
|
||||||
|
autotrader.com
|
||||||
|
hilton.com
|
||||||
|
irs.gov
|
||||||
|
monster.com
|
||||||
|
mailchimp.com
|
||||||
|
webex.com
|
||||||
|
landsend.com
|
||||||
|
wix.com
|
||||||
|
usnews.com
|
||||||
|
jcrew.com
|
||||||
|
jet.com
|
||||||
|
capitalone360.com
|
||||||
|
sharepoint.com
|
||||||
|
schwab.com
|
||||||
|
ulta.com
|
||||||
|
vistaprint.com
|
||||||
|
rollingstone.com
|
||||||
|
biblegateway.com
|
||||||
|
gamespot.com
|
||||||
|
io9.com
|
||||||
|
opentable.com
|
||||||
|
hm.com
|
||||||
|
duckduckgo.com
|
||||||
|
chron.com
|
||||||
|
photobucket.com
|
||||||
|
shareasale.com
|
||||||
|
directv.com
|
||||||
|
avg.com
|
||||||
|
oracle.com
|
||||||
|
hotels.com
|
||||||
|
timewarnercable.com
|
||||||
|
chicagotribune.com
|
||||||
|
ehow.com
|
||||||
|
primewire.ag
|
||||||
|
abs-cbnnews.com
|
||||||
|
salon.com
|
||||||
|
greatergood.com
|
||||||
|
epicurious.com
|
||||||
|
fool.com
|
||||||
|
patheos.com
|
||||||
|
custhelp.com
|
||||||
|
purdue.edu
|
||||||
|
tickld.com
|
||||||
|
frys.com
|
||||||
|
indiatimes.com
|
||||||
|
amazon.co.uk
|
||||||
|
zendesk.com
|
||||||
|
tigerdirect.com
|
||||||
|
stubhub.com
|
||||||
|
healthcare.gov
|
||||||
|
archive.org
|
||||||
|
qualtrics.com
|
||||||
|
ravelry.com
|
||||||
|
cars.com
|
||||||
|
redbox.com
|
||||||
|
jalopnik.com
|
||||||
|
speedtest.net
|
||||||
|
harvard.edu
|
||||||
|
slideshare.net
|
||||||
|
kinja.com
|
||||||
|
nesn.com
|
||||||
|
michaels.com
|
||||||
|
mit.edu
|
||||||
|
bodybuilding.com
|
||||||
|
edmunds.com
|
||||||
|
nhl.com
|
||||||
|
zergnet.com
|
||||||
|
techcrunch.com
|
||||||
|
pogo.com
|
||||||
|
mozilla.org
|
||||||
|
naver.com
|
||||||
|
giphy.com
|
||||||
|
bankrate.com
|
||||||
|
msnbc.com
|
||||||
|
digitaltrends.com
|
||||||
|
fanfiction.net
|
||||||
|
skype.com
|
||||||
|
disney.go.com
|
||||||
|
norton.com
|
||||||
|
androidcentral.com
|
||||||
|
tomshardware.com
|
||||||
|
thefreedictionary.com
|
||||||
|
liveleak.com
|
||||||
|
247sports.com
|
||||||
|
merriam-webster.com
|
||||||
|
wnd.com
|
||||||
|
earthlink.net
|
||||||
|
independent.co.uk
|
||||||
|
drugs.com
|
||||||
|
rotoworld.com
|
||||||
|
nationalgeographic.com
|
||||||
|
ae.com
|
||||||
|
noaa.gov
|
||||||
|
arstechnica.com
|
||||||
|
thinkgeek.com
|
||||||
|
stanford.edu
|
||||||
|
bizjournals.com
|
||||||
|
hootsuite.com
|
||||||
|
genius.com
|
||||||
|
goodhousekeeping.com
|
||||||
|
vanguard.com
|
||||||
|
ny.gov
|
||||||
|
citibankonline.com
|
||||||
|
booking.com
|
||||||
|
mic.com
|
||||||
|
orbitz.com
|
||||||
|
dominos.com
|
||||||
|
medium.com
|
||||||
|
wow.com
|
||||||
|
urbanoutfitters.com
|
||||||
|
douban.com
|
||||||
|
timeanddate.com
|
||||||
|
draftkings.com
|
||||||
|
livestrong.com
|
||||||
|
livingsocial.com
|
||||||
|
cox.net
|
||||||
|
theonion.com
|
||||||
|
marthastewart.com
|
||||||
|
comenity.net
|
||||||
|
worldlifestyle.com
|
||||||
|
disney.com
|
||||||
|
realsimple.com
|
||||||
|
vrbo.com
|
||||||
|
playstation.com
|
||||||
|
potterybarn.com
|
||||||
|
zazzle.com
|
||||||
|
ksl.com
|
||||||
|
tdbank.com
|
||||||
|
sourceforge.net
|
||||||
|
careerbuilder.com
|
|
@ -0,0 +1,50 @@
|
||||||
|
google.com.hk
|
||||||
|
youtube.com
|
||||||
|
google.com
|
||||||
|
facebook.com
|
||||||
|
yahoo.com
|
||||||
|
discuss.com.hk
|
||||||
|
aastocks.com
|
||||||
|
wikipedia.org
|
||||||
|
baidu.com
|
||||||
|
taobao.com
|
||||||
|
pixnet.net
|
||||||
|
bastillepost.com
|
||||||
|
nextmedia.com
|
||||||
|
whatsapp.com
|
||||||
|
instagram.com
|
||||||
|
price.com.hk
|
||||||
|
ettoday.net
|
||||||
|
qq.com
|
||||||
|
hsbc.com.hk
|
||||||
|
tmall.com
|
||||||
|
live.com
|
||||||
|
hkgolden.com
|
||||||
|
reddit.com
|
||||||
|
beautyexchange.com.hk
|
||||||
|
etnet.com.hk
|
||||||
|
on.cc
|
||||||
|
amazon.com
|
||||||
|
twitter.com
|
||||||
|
uwants.com
|
||||||
|
presslogic.com
|
||||||
|
unwire.hk
|
||||||
|
gamer.com.tw
|
||||||
|
hangseng.com
|
||||||
|
hk01.com
|
||||||
|
twitch.tv
|
||||||
|
linkedin.com
|
||||||
|
teepr.com
|
||||||
|
hkjc.com
|
||||||
|
apple.com
|
||||||
|
bomb01.com
|
||||||
|
sina.com.cn
|
||||||
|
weibo.com
|
||||||
|
dcfever.com
|
||||||
|
thestandnews.com
|
||||||
|
office.com
|
||||||
|
openrice.com
|
||||||
|
tumblr.com
|
||||||
|
tvb.com
|
||||||
|
alipay.com
|
||||||
|
stackoverflow.com
|
|
@ -0,0 +1,50 @@
|
||||||
|
google.com
|
||||||
|
google.co.id
|
||||||
|
youtube.com
|
||||||
|
detik.com
|
||||||
|
tribunnews.com
|
||||||
|
facebook.com
|
||||||
|
yahoo.com
|
||||||
|
tokopedia.com
|
||||||
|
liputan6.com
|
||||||
|
kompas.com
|
||||||
|
bukalapak.com
|
||||||
|
kaskus.co.id
|
||||||
|
kapanlagi.com
|
||||||
|
wordpress.com
|
||||||
|
merdeka.com
|
||||||
|
okezone.com
|
||||||
|
elevenia.co.id
|
||||||
|
lazada.co.id
|
||||||
|
uzone.id
|
||||||
|
bintang.com
|
||||||
|
brilio.net
|
||||||
|
popads.net
|
||||||
|
instagram.com
|
||||||
|
bola.net
|
||||||
|
wikipedia.org
|
||||||
|
blogspot.com
|
||||||
|
onclkds.com
|
||||||
|
dream.co.id
|
||||||
|
viva.co.id
|
||||||
|
alodokter.com
|
||||||
|
tempo.co
|
||||||
|
suara.com
|
||||||
|
wowkeren.com
|
||||||
|
idntimes.com
|
||||||
|
bola.com
|
||||||
|
sindonews.com
|
||||||
|
republika.co.id
|
||||||
|
kompasiana.com
|
||||||
|
vemale.com
|
||||||
|
blanja.com
|
||||||
|
cnnindonesia.com
|
||||||
|
olx.co.id
|
||||||
|
lk21.org
|
||||||
|
popcash.net
|
||||||
|
blibli.com
|
||||||
|
poptm.com
|
||||||
|
nonton.movie
|
||||||
|
indexmovie.me
|
||||||
|
adexchangeprediction.com
|
||||||
|
subscene.com
|
|
@ -0,0 +1,50 @@
|
||||||
|
google.pl
|
||||||
|
youtube.com
|
||||||
|
facebook.com
|
||||||
|
google.com
|
||||||
|
allegro.pl
|
||||||
|
onet.pl
|
||||||
|
wp.pl
|
||||||
|
wikipedia.org
|
||||||
|
olx.pl
|
||||||
|
vk.com
|
||||||
|
interia.pl
|
||||||
|
wykop.pl
|
||||||
|
gazeta.pl
|
||||||
|
filmweb.pl
|
||||||
|
instagram.com
|
||||||
|
wiocha.pl
|
||||||
|
cda.pl
|
||||||
|
aliexpress.com
|
||||||
|
otomoto.pl
|
||||||
|
mbank.pl
|
||||||
|
reddit.com
|
||||||
|
ceneo.pl
|
||||||
|
tvn24.pl
|
||||||
|
twitter.com
|
||||||
|
gumtree.pl
|
||||||
|
blogspot.com
|
||||||
|
kwejk.pl
|
||||||
|
wyborcza.pl
|
||||||
|
joemonster.org
|
||||||
|
stackoverflow.com
|
||||||
|
twitch.tv
|
||||||
|
o2.pl
|
||||||
|
ipko.pl
|
||||||
|
steamcommunity.com
|
||||||
|
github.com
|
||||||
|
chomikuj.pl
|
||||||
|
centrum24.pl
|
||||||
|
linkedin.com
|
||||||
|
money.pl
|
||||||
|
librus.pl
|
||||||
|
demotywatory.pl
|
||||||
|
sport.pl
|
||||||
|
microsoft.com
|
||||||
|
zalukaj.com
|
||||||
|
wikia.com
|
||||||
|
jbzdy.pl
|
||||||
|
imgur.com
|
||||||
|
flashscore.pl
|
||||||
|
gry-online.pl
|
||||||
|
pudelek.pl
|
|
@ -0,0 +1,50 @@
|
||||||
|
vk.com
|
||||||
|
google.ru
|
||||||
|
yandex.ru
|
||||||
|
youtube.com
|
||||||
|
mail.ru
|
||||||
|
ok.ru
|
||||||
|
google.com
|
||||||
|
avito.ru
|
||||||
|
aliexpress.com
|
||||||
|
wikipedia.org
|
||||||
|
instagram.com
|
||||||
|
sberbank.ru
|
||||||
|
gismeteo.ru
|
||||||
|
rambler.ru
|
||||||
|
kinogo.club
|
||||||
|
kinopoisk.ru
|
||||||
|
drom.ru
|
||||||
|
facebook.com
|
||||||
|
pikabu.ru
|
||||||
|
drive2.ru
|
||||||
|
rutracker.org
|
||||||
|
twitch.tv
|
||||||
|
rbc.ru
|
||||||
|
hh.ru
|
||||||
|
gosuslugi.ru
|
||||||
|
lenta.ru
|
||||||
|
pochta.ru
|
||||||
|
wildberries.ru
|
||||||
|
wikia.com
|
||||||
|
4pda.ru
|
||||||
|
fb.ru
|
||||||
|
seasonvar.ru
|
||||||
|
kp.ru
|
||||||
|
znanija.com
|
||||||
|
ucoz.ru
|
||||||
|
narod.ru
|
||||||
|
mts.ru
|
||||||
|
infourok.ru
|
||||||
|
ebay.com
|
||||||
|
ozon.ru
|
||||||
|
worldoftanks.ru
|
||||||
|
mos.ru
|
||||||
|
vesti.ru
|
||||||
|
nnmclub.to
|
||||||
|
microsoft.com
|
||||||
|
rp5.ru
|
||||||
|
2gis.ru
|
||||||
|
consultant.ru
|
||||||
|
fotostrana.ru
|
||||||
|
dnevnik.ru
|
|
@ -0,0 +1,49 @@
|
||||||
|
google.com.sg
|
||||||
|
youtube.com
|
||||||
|
google.com
|
||||||
|
facebook.com
|
||||||
|
yahoo.com
|
||||||
|
wikipedia.org
|
||||||
|
reddit.com
|
||||||
|
blogspot.sg
|
||||||
|
live.com
|
||||||
|
instagram.com
|
||||||
|
qoo10.sg
|
||||||
|
whatsapp.com
|
||||||
|
linkedin.com
|
||||||
|
dbs.com.sg
|
||||||
|
amazon.com
|
||||||
|
twitter.com
|
||||||
|
wordpress.com
|
||||||
|
onclkds.com
|
||||||
|
office.com
|
||||||
|
allsingaporestuff.com
|
||||||
|
baidu.com
|
||||||
|
lazada.sg
|
||||||
|
straitstimes.com
|
||||||
|
singpass.gov.sg
|
||||||
|
google.co.id
|
||||||
|
taobao.com
|
||||||
|
tumblr.com
|
||||||
|
gomovies.to
|
||||||
|
wikia.com
|
||||||
|
hardwarezone.com.sg
|
||||||
|
nus.edu.sg
|
||||||
|
msn.com
|
||||||
|
microsoft.com
|
||||||
|
carousell.com
|
||||||
|
kissanime.ru
|
||||||
|
ocbc.com
|
||||||
|
stackoverflow.com
|
||||||
|
ntu.edu.sg
|
||||||
|
thepiratebay.org
|
||||||
|
aliexpress.com
|
||||||
|
imgur.com
|
||||||
|
dropbox.com
|
||||||
|
apple.com
|
||||||
|
channelnewsasia.com
|
||||||
|
imdb.com
|
||||||
|
twitch.tv
|
||||||
|
abs-cbn.com
|
||||||
|
jobstreet.com.sg
|
||||||
|
uob.com.sg
|
|
@ -0,0 +1,50 @@
|
||||||
|
google.com.tw
|
||||||
|
pixnet.net
|
||||||
|
youtube.com
|
||||||
|
facebook.com
|
||||||
|
ettoday.net
|
||||||
|
google.com
|
||||||
|
yahoo.com
|
||||||
|
ltn.com.tw
|
||||||
|
nownews.com
|
||||||
|
setn.com
|
||||||
|
momoshop.com.tw
|
||||||
|
wikipedia.org
|
||||||
|
ck101.com
|
||||||
|
ptt.cc
|
||||||
|
tvbs.com.tw
|
||||||
|
104.com.tw
|
||||||
|
gamer.com.tw
|
||||||
|
appledaily.com.tw
|
||||||
|
pchome.com.tw
|
||||||
|
ruten.com.tw
|
||||||
|
ctitv.com.tw
|
||||||
|
teepr.com
|
||||||
|
life.tw
|
||||||
|
blogspot.tw
|
||||||
|
dcard.tw
|
||||||
|
baidu.com
|
||||||
|
udn.com
|
||||||
|
mobile01.com
|
||||||
|
eyny.com
|
||||||
|
epochtimes.com
|
||||||
|
qoolquiz.com
|
||||||
|
bomb01.com
|
||||||
|
talk.tw
|
||||||
|
ipetgroup.com
|
||||||
|
storm.mg
|
||||||
|
123kubo.com
|
||||||
|
cmoney.tw
|
||||||
|
taobao.com
|
||||||
|
twitch.tv
|
||||||
|
instagram.com
|
||||||
|
xuite.net
|
||||||
|
sina.com.tw
|
||||||
|
1111.com.tw
|
||||||
|
businessweekly.com.tw
|
||||||
|
elle.com.tw
|
||||||
|
twitter.com
|
||||||
|
books.com.tw
|
||||||
|
591.com.tw
|
||||||
|
everydayhealth.com.tw
|
||||||
|
techbang.com
|
|
@ -0,0 +1,49 @@
|
||||||
|
google.com
|
||||||
|
youtube.com
|
||||||
|
facebook.com
|
||||||
|
reddit.com
|
||||||
|
amazon.com
|
||||||
|
wikipedia.org
|
||||||
|
yahoo.com
|
||||||
|
twitter.com
|
||||||
|
netflix.com
|
||||||
|
ebay.com
|
||||||
|
imgur.com
|
||||||
|
linkedin.com
|
||||||
|
instagram.com
|
||||||
|
diply.com
|
||||||
|
craigslist.org
|
||||||
|
live.com
|
||||||
|
office.com
|
||||||
|
twitch.tv
|
||||||
|
tumblr.com
|
||||||
|
pinterest.com
|
||||||
|
espn.com
|
||||||
|
cnn.com
|
||||||
|
bing.com
|
||||||
|
wikia.com
|
||||||
|
chase.com
|
||||||
|
imdb.com
|
||||||
|
nytimes.com
|
||||||
|
paypal.com
|
||||||
|
blogspot.com
|
||||||
|
apple.com
|
||||||
|
yelp.com
|
||||||
|
stackoverflow.com
|
||||||
|
bankofamerica.com
|
||||||
|
wordpress.com
|
||||||
|
github.com
|
||||||
|
microsoft.com
|
||||||
|
wellsfargo.com
|
||||||
|
zillow.com
|
||||||
|
salesforce.com
|
||||||
|
msn.com
|
||||||
|
walmart.com
|
||||||
|
weather.com
|
||||||
|
dropbox.com
|
||||||
|
buzzfeed.com
|
||||||
|
intuit.com
|
||||||
|
washingtonpost.com
|
||||||
|
soundcloud.com
|
||||||
|
huffingtonpost.com
|
||||||
|
indeed.com
|
|
@ -0,0 +1,68 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains functionality to manage custom domains for auto-completion.
|
||||||
|
*/
|
||||||
|
object CustomDomains {
|
||||||
|
private const val PREFERENCE_NAME = "custom_autocomplete"
|
||||||
|
private const val KEY_DOMAINS = "custom_domains"
|
||||||
|
private const val SEPARATOR = "@<;>@"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the previously added/saved custom domains from preferences.
|
||||||
|
*
|
||||||
|
* @param context the application context
|
||||||
|
* @return list of custom domains
|
||||||
|
*/
|
||||||
|
fun load(context: Context): List<String> =
|
||||||
|
preferences(context).getString(KEY_DOMAINS, "")!!
|
||||||
|
.split(SEPARATOR)
|
||||||
|
.filter { !it.isEmpty() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the provided domains to preferences.
|
||||||
|
*
|
||||||
|
* @param context the application context
|
||||||
|
* @param domains list of domains
|
||||||
|
*/
|
||||||
|
fun save(context: Context, domains: List<String>) {
|
||||||
|
preferences(context)
|
||||||
|
.edit()
|
||||||
|
.putString(KEY_DOMAINS, domains.joinToString(separator = SEPARATOR))
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the provided domain to preferences.
|
||||||
|
*
|
||||||
|
* @param context the application context
|
||||||
|
* @param domain the domain to add
|
||||||
|
*/
|
||||||
|
fun add(context: Context, domain: String) {
|
||||||
|
val domains = mutableListOf<String>()
|
||||||
|
domains.addAll(load(context))
|
||||||
|
domains.add(domain)
|
||||||
|
|
||||||
|
save(context, domains)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the provided domain from preferences.
|
||||||
|
*
|
||||||
|
* @param context the application context
|
||||||
|
* @param domains the domain to remove
|
||||||
|
*/
|
||||||
|
fun remove(context: Context, domains: List<String>) {
|
||||||
|
save(context, load(context) - domains)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun preferences(context: Context): SharedPreferences =
|
||||||
|
context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class intended for internal use which encapsulates meta data about a domain.
|
||||||
|
*/
|
||||||
|
data class Domain(val protocol: String, val hasWww: Boolean, val host: String) {
|
||||||
|
internal val url: String
|
||||||
|
get() = "$protocol${if (hasWww) "www." else "" }$host"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PROTOCOL_INDEX = 1
|
||||||
|
private const val WWW_INDEX = 2
|
||||||
|
private const val HOST_INDEX = 3
|
||||||
|
|
||||||
|
private const val DEFAULT_PROTOCOL = "http://"
|
||||||
|
|
||||||
|
private val urlMatcher = Regex("""(https?://)?(www.)?(.+)?""")
|
||||||
|
|
||||||
|
fun create(url: String): Domain {
|
||||||
|
val result = urlMatcher.find(url)
|
||||||
|
|
||||||
|
return result?.let {
|
||||||
|
val protocol = it.groups[PROTOCOL_INDEX]?.value ?: DEFAULT_PROTOCOL
|
||||||
|
val hasWww = it.groups[WWW_INDEX]?.value == "www."
|
||||||
|
val host = it.groups[HOST_INDEX]?.value ?: throw IllegalStateException()
|
||||||
|
|
||||||
|
return Domain(protocol, hasWww, host)
|
||||||
|
} ?: throw IllegalStateException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun Iterable<String>.into(): List<Domain> {
|
||||||
|
return this.map { Domain.create(it) }
|
||||||
|
}
|
|
@ -0,0 +1,151 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides autocomplete functionality for domains, based on a provided list
|
||||||
|
* of assets (see [Domains]) and/or a custom domain list managed by
|
||||||
|
* [CustomDomains].
|
||||||
|
*/
|
||||||
|
// FIXME delete this https://github.com/mozilla-mobile/android-components/issues/1358
|
||||||
|
@Deprecated(
|
||||||
|
"Use `ShippedDomainsProvider` or `CustomDomainsProvider`",
|
||||||
|
ReplaceWith(
|
||||||
|
"ShippedDomainsProvider()/CustomDomainsProvider()",
|
||||||
|
"mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider",
|
||||||
|
"mozilla.components.browser.domains.autocomplete.CustomDomainsProvider",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
class DomainAutoCompleteProvider {
|
||||||
|
|
||||||
|
object AutocompleteSource {
|
||||||
|
const val DEFAULT_LIST = "default"
|
||||||
|
const val CUSTOM_LIST = "custom"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a result of auto-completion.
|
||||||
|
*
|
||||||
|
* @property text the result text starting with the raw search text as passed
|
||||||
|
* to [autocomplete] followed by the completion text (e.g. moz => mozilla.org)
|
||||||
|
* @property url the complete url (containing the protocol) as provided
|
||||||
|
* when the domain was saved. (e.g. https://mozilla.org)
|
||||||
|
* @property source the source identifier of the autocomplete source
|
||||||
|
* @property size total number of available autocomplete domains
|
||||||
|
* in this source
|
||||||
|
*/
|
||||||
|
data class Result(val text: String, val url: String, val source: String, val size: Int)
|
||||||
|
|
||||||
|
// We compute these on worker threads; make sure results are immediately visible on the UI thread.
|
||||||
|
@Volatile
|
||||||
|
internal var customDomains = emptyList<Domain>()
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
internal var shippedDomains = emptyList<Domain>()
|
||||||
|
private var useCustomDomains = false
|
||||||
|
private var useShippedDomains = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes an autocomplete suggestion for the given text, and invokes the
|
||||||
|
* provided callback, passing the result.
|
||||||
|
*
|
||||||
|
* @param rawText text to be auto completed
|
||||||
|
* @return the result of auto-completion. If no match is found an empty
|
||||||
|
* result object is returned.
|
||||||
|
*/
|
||||||
|
@Suppress("ReturnCount")
|
||||||
|
fun autocomplete(rawText: String): Result {
|
||||||
|
if (useCustomDomains) {
|
||||||
|
val result = tryToAutocomplete(rawText, customDomains, AutocompleteSource.CUSTOM_LIST)
|
||||||
|
if (result != null) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useShippedDomains) {
|
||||||
|
val result = tryToAutocomplete(rawText, shippedDomains, AutocompleteSource.DEFAULT_LIST)
|
||||||
|
if (result != null) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result("", "", "", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes this provider instance by making sure the shipped and/or custom
|
||||||
|
* domains are loaded.
|
||||||
|
*
|
||||||
|
* @param context the application context
|
||||||
|
* @param useShippedDomains true (default) if the domains provided by this
|
||||||
|
* module should be used, otherwise false.
|
||||||
|
* @param useCustomDomains true if the custom domains provided by
|
||||||
|
* [CustomDomains] should be used, otherwise false (default).
|
||||||
|
* @param loadDomainsFromDisk true (default) if domains should be loaded,
|
||||||
|
* otherwise false. This parameter is for testing purposes only.
|
||||||
|
*/
|
||||||
|
fun initialize(
|
||||||
|
context: Context,
|
||||||
|
useShippedDomains: Boolean = true,
|
||||||
|
useCustomDomains: Boolean = false,
|
||||||
|
loadDomainsFromDisk: Boolean = true,
|
||||||
|
) {
|
||||||
|
this.useCustomDomains = useCustomDomains
|
||||||
|
this.useShippedDomains = useShippedDomains
|
||||||
|
|
||||||
|
if (!loadDomainsFromDisk) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!useCustomDomains && !useShippedDomains) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
if (useCustomDomains) {
|
||||||
|
customDomains = async { CustomDomains.load(context).into() }.await()
|
||||||
|
}
|
||||||
|
if (useShippedDomains) {
|
||||||
|
shippedDomains = async { Domains.load(context).into() }.await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("ReturnCount")
|
||||||
|
private fun tryToAutocomplete(rawText: String, domains: List<Domain>, source: String): Result? {
|
||||||
|
// Search terms are all lowercase already, we just need to lowercase the search text
|
||||||
|
val searchText = rawText.lowercase(Locale.US)
|
||||||
|
|
||||||
|
domains.forEach {
|
||||||
|
val wwwDomain = "www.${it.host}"
|
||||||
|
if (wwwDomain.startsWith(searchText)) {
|
||||||
|
return Result(getResultText(rawText, wwwDomain), it.url, source, domains.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it.host.startsWith(searchText)) {
|
||||||
|
return Result(getResultText(rawText, it.host), it.url, source, domains.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Our autocomplete list is all lower case, however the search text might
|
||||||
|
* be mixed case. Our autocomplete EditText code does more string comparison,
|
||||||
|
* which fails if the suggestion doesn't exactly match searchText (ie.
|
||||||
|
* if casing differs). It's simplest to just build a suggestion
|
||||||
|
* that exactly matches the search text - which is what this method is for:
|
||||||
|
*/
|
||||||
|
private fun getResultText(rawSearchText: String, autocomplete: String) =
|
||||||
|
rawSearchText + autocomplete.substring(rawSearchText.length)
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build.VERSION.SDK_INT
|
||||||
|
import android.os.Build.VERSION_CODES
|
||||||
|
import android.os.LocaleList
|
||||||
|
import android.text.TextUtils
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains functionality to access domain lists shipped as part of this
|
||||||
|
* module's assets.
|
||||||
|
*/
|
||||||
|
object Domains {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the domains applicable to the app's locale, plus the domains
|
||||||
|
* in the 'global' list.
|
||||||
|
*
|
||||||
|
* @param context the application context
|
||||||
|
* @return list of domains
|
||||||
|
*/
|
||||||
|
fun load(context: Context): List<String> {
|
||||||
|
return load(context, getCountriesInDefaultLocaleList())
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun load(context: Context, countries: Set<String>): List<String> {
|
||||||
|
val domains = LinkedHashSet<String>()
|
||||||
|
val availableLists = getAvailableDomainLists(context)
|
||||||
|
|
||||||
|
// First initialize the country specific lists following the default locale order
|
||||||
|
countries
|
||||||
|
.filter { availableLists.contains(it) }
|
||||||
|
.forEach { loadDomainsForLanguage(context, domains, it) }
|
||||||
|
|
||||||
|
// And then add domains from the global list
|
||||||
|
loadDomainsForLanguage(context, domains, "global")
|
||||||
|
|
||||||
|
return domains.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAvailableDomainLists(context: Context): Set<String> {
|
||||||
|
val availableDomains = LinkedHashSet<String>()
|
||||||
|
val assetManager = context.assets
|
||||||
|
val domains = try {
|
||||||
|
assetManager.list("domains") ?: emptyArray<String>()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
emptyArray<String>()
|
||||||
|
}
|
||||||
|
availableDomains.addAll(domains)
|
||||||
|
return availableDomains
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadDomainsForLanguage(context: Context, domains: MutableSet<String>, country: String) {
|
||||||
|
val assetManager = context.assets
|
||||||
|
val languageDomains = try {
|
||||||
|
assetManager.open("domains/$country").bufferedReader().readLines()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
emptyList<String>()
|
||||||
|
}
|
||||||
|
domains.addAll(languageDomains)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCountriesInDefaultLocaleList(): Set<String> {
|
||||||
|
val countries = java.util.LinkedHashSet<String>()
|
||||||
|
val addIfNotEmpty = { c: String -> if (!TextUtils.isEmpty(c)) countries.add(c.lowercase(Locale.US)) }
|
||||||
|
|
||||||
|
if (SDK_INT >= VERSION_CODES.N) {
|
||||||
|
val list = LocaleList.getDefault()
|
||||||
|
for (i in 0 until list.size()) {
|
||||||
|
addIfNotEmpty(list.get(i).country)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addIfNotEmpty(Locale.getDefault().country)
|
||||||
|
}
|
||||||
|
|
||||||
|
return countries
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains.autocomplete
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import mozilla.components.browser.domains.CustomDomains
|
||||||
|
import mozilla.components.browser.domains.Domain
|
||||||
|
import mozilla.components.browser.domains.Domains
|
||||||
|
import mozilla.components.browser.domains.into
|
||||||
|
import mozilla.components.concept.toolbar.AutocompleteProvider
|
||||||
|
import mozilla.components.concept.toolbar.AutocompleteResult
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
enum class DomainList(val listName: String) {
|
||||||
|
DEFAULT("default"),
|
||||||
|
CUSTOM("custom"),
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides autocomplete functionality for domains based on provided list of assets (see [Domains]).
|
||||||
|
*/
|
||||||
|
class ShippedDomainsProvider(override val autocompletePriority: Int = 0) :
|
||||||
|
BaseDomainAutocompleteProvider(DomainList.DEFAULT, Domains.asLoader())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides autocomplete functionality for domains based on a list managed by [CustomDomains].
|
||||||
|
*/
|
||||||
|
class CustomDomainsProvider(override val autocompletePriority: Int = 0) :
|
||||||
|
BaseDomainAutocompleteProvider(DomainList.CUSTOM, CustomDomains.asLoader())
|
||||||
|
|
||||||
|
typealias DomainsLoader = (Context) -> List<Domain>
|
||||||
|
|
||||||
|
private fun Domains.asLoader(): DomainsLoader = { context: Context -> load(context).into() }
|
||||||
|
private fun CustomDomains.asLoader(): DomainsLoader = { context: Context -> load(context).into() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides common autocomplete functionality powered by domain lists.
|
||||||
|
*
|
||||||
|
* @param list source of domains
|
||||||
|
* @param domainsLoader provider for all available domains
|
||||||
|
*/
|
||||||
|
open class BaseDomainAutocompleteProvider(
|
||||||
|
private val list: DomainList,
|
||||||
|
private val domainsLoader: DomainsLoader,
|
||||||
|
override val autocompletePriority: Int = 0,
|
||||||
|
) : AutocompleteProvider, CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||||
|
|
||||||
|
// We compute 'domains' on the worker thread; make sure it's immediately visible on the UI thread.
|
||||||
|
@Volatile
|
||||||
|
var domains: List<Domain> = emptyList()
|
||||||
|
|
||||||
|
fun initialize(context: Context) {
|
||||||
|
launch {
|
||||||
|
domains = async { domainsLoader(context) }.await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes an autocomplete suggestion for the given text, and invokes the
|
||||||
|
* provided callback, passing the result.
|
||||||
|
*
|
||||||
|
* @param query text to be auto completed
|
||||||
|
* @return the result of auto-completion, or null if no match is found.
|
||||||
|
*/
|
||||||
|
override suspend fun getAutocompleteSuggestion(query: String): AutocompleteResult? {
|
||||||
|
// Search terms are all lowercase already, we just need to lowercase the search text
|
||||||
|
val searchText = query.lowercase(Locale.US)
|
||||||
|
|
||||||
|
domains.forEach {
|
||||||
|
val wwwDomain = "www.${it.host}"
|
||||||
|
if (wwwDomain.startsWith(searchText)) {
|
||||||
|
return AutocompleteResult(
|
||||||
|
input = searchText,
|
||||||
|
text = getResultText(query, wwwDomain),
|
||||||
|
url = it.url,
|
||||||
|
source = list.listName,
|
||||||
|
totalItems = domains.size,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it.host.startsWith(searchText)) {
|
||||||
|
return AutocompleteResult(
|
||||||
|
input = searchText,
|
||||||
|
text = getResultText(query, it.host),
|
||||||
|
url = it.url,
|
||||||
|
source = list.listName,
|
||||||
|
totalItems = domains.size,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Our autocomplete list is all lower case, however the search text might
|
||||||
|
* be mixed case. Our autocomplete EditText code does more string comparison,
|
||||||
|
* which fails if the suggestion doesn't exactly match searchText (ie.
|
||||||
|
* if casing differs). It's simplest to just build a suggestion
|
||||||
|
* that exactly matches the search text - which is what this method is for:
|
||||||
|
*/
|
||||||
|
private fun getResultText(rawSearchText: String, autocomplete: String) =
|
||||||
|
rawSearchText + autocomplete.substring(rawSearchText.length)
|
||||||
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Looper.getMainLooper
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import mozilla.components.browser.domains.autocomplete.BaseDomainAutocompleteProvider
|
||||||
|
import mozilla.components.browser.domains.autocomplete.DomainList
|
||||||
|
import mozilla.components.browser.domains.autocomplete.DomainsLoader
|
||||||
|
import mozilla.components.concept.toolbar.AutocompleteProvider
|
||||||
|
import mozilla.components.support.test.robolectric.testContext
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.Shadows.shadowOf
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class BaseDomainAutocompleteProviderTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `empty provider with DEFAULT list returns nothing`() {
|
||||||
|
val provider = createAndInitProvider(testContext, DomainList.DEFAULT) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNoCompletion(provider, "m")
|
||||||
|
assertNoCompletion(provider, "mo")
|
||||||
|
assertNoCompletion(provider, "moz")
|
||||||
|
assertNoCompletion(provider, "g")
|
||||||
|
assertNoCompletion(provider, "go")
|
||||||
|
assertNoCompletion(provider, "goo")
|
||||||
|
assertNoCompletion(provider, "w")
|
||||||
|
assertNoCompletion(provider, "www")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `empty provider with CUSTOM list returns nothing`() {
|
||||||
|
val provider = createAndInitProvider(testContext, DomainList.CUSTOM) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNoCompletion(provider, "m")
|
||||||
|
assertNoCompletion(provider, "mo")
|
||||||
|
assertNoCompletion(provider, "moz")
|
||||||
|
assertNoCompletion(provider, "g")
|
||||||
|
assertNoCompletion(provider, "go")
|
||||||
|
assertNoCompletion(provider, "goo")
|
||||||
|
assertNoCompletion(provider, "w")
|
||||||
|
assertNoCompletion(provider, "www")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `non-empty provider with DEFAULT list returns completion`() {
|
||||||
|
val domains = listOf("mozilla.org", "google.com", "facebook.com").into()
|
||||||
|
val list = DomainList.DEFAULT
|
||||||
|
val domainsCount = domains.size
|
||||||
|
|
||||||
|
val provider = createAndInitProvider(testContext, list) { domains }
|
||||||
|
shadowOf(getMainLooper()).idle()
|
||||||
|
|
||||||
|
assertCompletion(provider, list, domainsCount, "m", "m", "mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "moz", "moz", "mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "www", "www", "www.mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "www.face", "www.face", "www.facebook.com", "http://facebook.com")
|
||||||
|
assertCompletion(provider, list, domainsCount, "M", "m", "Mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "MOZ", "moz", "MOZilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "www.GOO", "www.goo", "www.GOOgle.com", "http://google.com")
|
||||||
|
assertCompletion(provider, list, domainsCount, "WWW.GOOGLE.", "www.google.", "WWW.GOOGLE.com", "http://google.com")
|
||||||
|
assertCompletion(provider, list, domainsCount, "www.facebook.com", "www.facebook.com", "www.facebook.com", "http://facebook.com")
|
||||||
|
assertCompletion(provider, list, domainsCount, "facebook.com", "facebook.com", "facebook.com", "http://facebook.com")
|
||||||
|
|
||||||
|
assertNoCompletion(provider, "wwww")
|
||||||
|
assertNoCompletion(provider, "yahoo")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `non-empty provider with CUSTOM list returns completion`() {
|
||||||
|
val domains = listOf("mozilla.org", "google.com", "facebook.com").into()
|
||||||
|
val list = DomainList.CUSTOM
|
||||||
|
val domainsCount = domains.size
|
||||||
|
|
||||||
|
val provider = createAndInitProvider(testContext, list) { domains }
|
||||||
|
shadowOf(getMainLooper()).idle()
|
||||||
|
|
||||||
|
assertCompletion(provider, list, domainsCount, "m", "m", "mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "moz", "moz", "mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "www", "www", "www.mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "www.face", "www.face", "www.facebook.com", "http://facebook.com")
|
||||||
|
assertCompletion(provider, list, domainsCount, "M", "m", "Mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "MOZ", "moz", "MOZilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "www.GOO", "www.goo", "www.GOOgle.com", "http://google.com")
|
||||||
|
assertCompletion(provider, list, domainsCount, "WWW.GOOGLE.", "www.google.", "WWW.GOOGLE.com", "http://google.com")
|
||||||
|
assertCompletion(provider, list, domainsCount, "www.facebook.com", "www.facebook.com", "www.facebook.com", "http://facebook.com")
|
||||||
|
assertCompletion(provider, list, domainsCount, "facebook.com", "facebook.com", "facebook.com", "http://facebook.com")
|
||||||
|
|
||||||
|
assertNoCompletion(provider, "wwww")
|
||||||
|
assertNoCompletion(provider, "yahoo")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private fun assertCompletion(
|
||||||
|
provider: AutocompleteProvider,
|
||||||
|
domainSource: DomainList,
|
||||||
|
sourceSize: Int,
|
||||||
|
input: String,
|
||||||
|
expectedInput: String,
|
||||||
|
completion: String,
|
||||||
|
expectedUrl: String,
|
||||||
|
) = runTest {
|
||||||
|
val result = provider.getAutocompleteSuggestion(input)!!
|
||||||
|
|
||||||
|
assertTrue("Autocompletion shouldn't be empty", result.text.isNotEmpty())
|
||||||
|
|
||||||
|
assertEquals("Autocompletion input", expectedInput, result.input)
|
||||||
|
assertEquals("Autocompletion completion", completion, result.text)
|
||||||
|
assertEquals("Autocompletion source list", domainSource.listName, result.source)
|
||||||
|
assertEquals("Autocompletion url", expectedUrl, result.url)
|
||||||
|
assertEquals("Autocompletion source list size", sourceSize, result.totalItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private fun assertNoCompletion(provider: AutocompleteProvider, input: String) = runTest {
|
||||||
|
val result = provider.getAutocompleteSuggestion(input)
|
||||||
|
|
||||||
|
assertNull("Result should be null", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createAndInitProvider(context: Context, list: DomainList, loader: DomainsLoader): AutocompleteProvider =
|
||||||
|
object : BaseDomainAutocompleteProvider(list, loader) {
|
||||||
|
override val coroutineContext = super.coroutineContext + Dispatchers.Main
|
||||||
|
}.apply { initialize(context) }
|
|
@ -0,0 +1,81 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import mozilla.components.support.test.robolectric.testContext
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class CustomDomainsTest {
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
testContext.getSharedPreferences("custom_autocomplete", Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.clear()
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
@Test
|
||||||
|
fun customListIsEmptyByDefault() {
|
||||||
|
val domains = CustomDomains.load(testContext)
|
||||||
|
|
||||||
|
assertEquals(0, domains.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun saveAndRemoveDomains() {
|
||||||
|
CustomDomains.save(
|
||||||
|
testContext,
|
||||||
|
listOf(
|
||||||
|
"mozilla.org",
|
||||||
|
"example.org",
|
||||||
|
"example.com",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
var domains = CustomDomains.load(testContext)
|
||||||
|
assertEquals(3, domains.size)
|
||||||
|
|
||||||
|
CustomDomains.remove(testContext, listOf("example.org", "example.com"))
|
||||||
|
domains = CustomDomains.load(testContext)
|
||||||
|
assertEquals(1, domains.size)
|
||||||
|
assertEquals("mozilla.org", domains.elementAt(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun addAndLoadDomains() {
|
||||||
|
CustomDomains.add(testContext, "mozilla.org")
|
||||||
|
val domains = CustomDomains.load(testContext)
|
||||||
|
assertEquals(1, domains.size)
|
||||||
|
assertEquals("mozilla.org", domains.elementAt(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun saveAndLoadDomains() {
|
||||||
|
CustomDomains.save(
|
||||||
|
testContext,
|
||||||
|
listOf(
|
||||||
|
"mozilla.org",
|
||||||
|
"example.org",
|
||||||
|
"example.com",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val domains = CustomDomains.load(testContext)
|
||||||
|
|
||||||
|
assertEquals(3, domains.size)
|
||||||
|
assertEquals("mozilla.org", domains.elementAt(0))
|
||||||
|
assertEquals("example.org", domains.elementAt(1))
|
||||||
|
assertEquals("example.com", domains.elementAt(2))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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/. */
|
||||||
|
|
||||||
|
@file:Suppress("DEPRECATION")
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import mozilla.components.browser.domains.DomainAutoCompleteProvider.AutocompleteSource.CUSTOM_LIST
|
||||||
|
import mozilla.components.browser.domains.DomainAutoCompleteProvider.AutocompleteSource.DEFAULT_LIST
|
||||||
|
import mozilla.components.support.test.robolectric.testContext
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
/**
|
||||||
|
* While [DomainAutoCompleteProvider] exists (even if it's deprecated) we need to test it.
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class DomainAutoCompleteProviderTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun autocompletionWithShippedDomains() {
|
||||||
|
val provider = DomainAutoCompleteProvider().also {
|
||||||
|
it.initialize(
|
||||||
|
testContext,
|
||||||
|
useShippedDomains = true,
|
||||||
|
useCustomDomains = false,
|
||||||
|
loadDomainsFromDisk = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
it.shippedDomains = listOf("mozilla.org", "google.com", "facebook.com").into()
|
||||||
|
it.customDomains = emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val size = provider.shippedDomains.size
|
||||||
|
|
||||||
|
assertCompletion(provider, "m", DEFAULT_LIST, size, "mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, "www", DEFAULT_LIST, size, "www.mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, "www.face", DEFAULT_LIST, size, "www.facebook.com", "http://facebook.com")
|
||||||
|
assertCompletion(provider, "MOZ", DEFAULT_LIST, size, "MOZilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, "www.GOO", DEFAULT_LIST, size, "www.GOOgle.com", "http://google.com")
|
||||||
|
assertCompletion(provider, "WWW.GOOGLE.", DEFAULT_LIST, size, "WWW.GOOGLE.com", "http://google.com")
|
||||||
|
assertCompletion(provider, "www.facebook.com", DEFAULT_LIST, size, "www.facebook.com", "http://facebook.com")
|
||||||
|
assertCompletion(provider, "facebook.com", DEFAULT_LIST, size, "facebook.com", "http://facebook.com")
|
||||||
|
|
||||||
|
assertNoCompletion(provider, "wwww")
|
||||||
|
assertNoCompletion(provider, "yahoo")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun autocompletionWithCustomDomains() {
|
||||||
|
val domains = listOf("facebook.com", "google.com", "mozilla.org")
|
||||||
|
val customDomains = listOf("gap.com", "www.fanfiction.com", "https://mobile.de")
|
||||||
|
|
||||||
|
val provider = DomainAutoCompleteProvider().also {
|
||||||
|
it.initialize(
|
||||||
|
testContext,
|
||||||
|
useShippedDomains = true,
|
||||||
|
useCustomDomains = true,
|
||||||
|
loadDomainsFromDisk = false,
|
||||||
|
)
|
||||||
|
it.shippedDomains = domains.into()
|
||||||
|
it.customDomains = customDomains.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertCompletion(provider, "f", CUSTOM_LIST, customDomains.size, "fanfiction.com", "http://www.fanfiction.com")
|
||||||
|
assertCompletion(provider, "fa", CUSTOM_LIST, customDomains.size, "fanfiction.com", "http://www.fanfiction.com")
|
||||||
|
assertCompletion(provider, "fac", DEFAULT_LIST, domains.size, "facebook.com", "http://facebook.com")
|
||||||
|
|
||||||
|
assertCompletion(provider, "g", CUSTOM_LIST, customDomains.size, "gap.com", "http://gap.com")
|
||||||
|
assertCompletion(provider, "go", DEFAULT_LIST, domains.size, "google.com", "http://google.com")
|
||||||
|
assertCompletion(provider, "ga", CUSTOM_LIST, customDomains.size, "gap.com", "http://gap.com")
|
||||||
|
|
||||||
|
assertCompletion(provider, "m", CUSTOM_LIST, customDomains.size, "mobile.de", "https://mobile.de")
|
||||||
|
assertCompletion(provider, "mo", CUSTOM_LIST, customDomains.size, "mobile.de", "https://mobile.de")
|
||||||
|
assertCompletion(provider, "mob", CUSTOM_LIST, customDomains.size, "mobile.de", "https://mobile.de")
|
||||||
|
assertCompletion(provider, "moz", DEFAULT_LIST, domains.size, "mozilla.org", "http://mozilla.org")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun autocompletionWithoutDomains() {
|
||||||
|
val filter = DomainAutoCompleteProvider()
|
||||||
|
assertNoCompletion(filter, "mozilla")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertCompletion(
|
||||||
|
provider: DomainAutoCompleteProvider,
|
||||||
|
text: String,
|
||||||
|
domainSource: String,
|
||||||
|
sourceSize: Int,
|
||||||
|
completion: String,
|
||||||
|
expectedUrl: String,
|
||||||
|
) {
|
||||||
|
val result = provider.autocomplete(text)
|
||||||
|
|
||||||
|
assertFalse(result.text.isEmpty())
|
||||||
|
|
||||||
|
assertEquals(completion, result.text)
|
||||||
|
assertEquals(domainSource, result.source)
|
||||||
|
assertEquals(expectedUrl, result.url)
|
||||||
|
assertEquals(sourceSize, result.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertNoCompletion(provider: DomainAutoCompleteProvider, text: String) {
|
||||||
|
val result = provider.autocomplete(text)
|
||||||
|
|
||||||
|
assertTrue(result.text.isEmpty())
|
||||||
|
assertTrue(result.url.isEmpty())
|
||||||
|
assertTrue(result.source.isEmpty())
|
||||||
|
assertEquals(0, result.size)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class DomainTest {
|
||||||
|
@Test
|
||||||
|
fun domainCreation() {
|
||||||
|
val firstItem = Domain.create("https://mozilla.com")
|
||||||
|
|
||||||
|
Assert.assertTrue(firstItem.protocol == "https://")
|
||||||
|
Assert.assertFalse(firstItem.hasWww)
|
||||||
|
Assert.assertTrue(firstItem.host == "mozilla.com")
|
||||||
|
|
||||||
|
val secondItem = Domain.create("www.mozilla.com")
|
||||||
|
|
||||||
|
Assert.assertTrue(secondItem.protocol == "http://")
|
||||||
|
Assert.assertTrue(secondItem.hasWww)
|
||||||
|
Assert.assertTrue(secondItem.host == "mozilla.com")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun domainCanCreateUrl() {
|
||||||
|
val firstItem = Domain.create("https://mozilla.com")
|
||||||
|
Assert.assertEquals("https://mozilla.com", firstItem.url)
|
||||||
|
|
||||||
|
val secondItem = Domain.create("www.mozilla.com")
|
||||||
|
Assert.assertEquals("http://www.mozilla.com", secondItem.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalStateException::class)
|
||||||
|
fun domainCreationWithBadURLThrowsException() {
|
||||||
|
Domain.create("http://www.")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import mozilla.components.support.test.robolectric.testContext
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class DomainsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun loadDomains() {
|
||||||
|
val domains = Domains.load(testContext, setOf("us"))
|
||||||
|
Assert.assertFalse(domains.isEmpty())
|
||||||
|
Assert.assertTrue(domains.contains("reddit.com"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun loadDomainsWithDefaultCountries() {
|
||||||
|
val domains = Domains.load(testContext)
|
||||||
|
Assert.assertFalse(domains.isEmpty())
|
||||||
|
// From the global list
|
||||||
|
Assert.assertTrue(domains.contains("mozilla.org"))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import mozilla.components.browser.domains.autocomplete.BaseDomainAutocompleteProvider
|
||||||
|
import mozilla.components.browser.domains.autocomplete.CustomDomainsProvider
|
||||||
|
import mozilla.components.browser.domains.autocomplete.DomainList
|
||||||
|
import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ProvidersTest {
|
||||||
|
@Test
|
||||||
|
fun autocompletionWithShippedDomains() {
|
||||||
|
val provider = ShippedDomainsProvider()
|
||||||
|
provider.domains = listOf("mozilla.org", "google.com", "facebook.com").into()
|
||||||
|
|
||||||
|
val size = provider.domains.size
|
||||||
|
|
||||||
|
assertCompletion(provider, "m", DomainList.DEFAULT, size, "mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, "www", DomainList.DEFAULT, size, "www.mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, "www.face", DomainList.DEFAULT, size, "www.facebook.com", "http://facebook.com")
|
||||||
|
assertCompletion(provider, "MOZ", DomainList.DEFAULT, size, "MOZilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, "www.GOO", DomainList.DEFAULT, size, "www.GOOgle.com", "http://google.com")
|
||||||
|
assertCompletion(provider, "WWW.GOOGLE.", DomainList.DEFAULT, size, "WWW.GOOGLE.com", "http://google.com")
|
||||||
|
assertCompletion(provider, "www.facebook.com", DomainList.DEFAULT, size, "www.facebook.com", "http://facebook.com")
|
||||||
|
assertCompletion(provider, "facebook.com", DomainList.DEFAULT, size, "facebook.com", "http://facebook.com")
|
||||||
|
|
||||||
|
assertNoCompletion(provider, "wwww")
|
||||||
|
assertNoCompletion(provider, "yahoo")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun autocompletionWithCustomDomains() {
|
||||||
|
val customDomains = listOf("gap.com", "www.fanfiction.com", "https://mobile.de")
|
||||||
|
|
||||||
|
val provider = CustomDomainsProvider()
|
||||||
|
provider.domains = customDomains.into()
|
||||||
|
|
||||||
|
assertCompletion(provider, "f", DomainList.CUSTOM, customDomains.size, "fanfiction.com", "http://www.fanfiction.com")
|
||||||
|
assertCompletion(provider, "fa", DomainList.CUSTOM, customDomains.size, "fanfiction.com", "http://www.fanfiction.com")
|
||||||
|
|
||||||
|
assertCompletion(provider, "g", DomainList.CUSTOM, customDomains.size, "gap.com", "http://gap.com")
|
||||||
|
assertCompletion(provider, "ga", DomainList.CUSTOM, customDomains.size, "gap.com", "http://gap.com")
|
||||||
|
|
||||||
|
assertCompletion(provider, "m", DomainList.CUSTOM, customDomains.size, "mobile.de", "https://mobile.de")
|
||||||
|
assertCompletion(provider, "mo", DomainList.CUSTOM, customDomains.size, "mobile.de", "https://mobile.de")
|
||||||
|
assertCompletion(provider, "mob", DomainList.CUSTOM, customDomains.size, "mobile.de", "https://mobile.de")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun autocompletionWithoutDomains() {
|
||||||
|
val filter = CustomDomainsProvider()
|
||||||
|
assertNoCompletion(filter, "mozilla")
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private fun assertCompletion(
|
||||||
|
provider: BaseDomainAutocompleteProvider,
|
||||||
|
text: String,
|
||||||
|
domainSource: DomainList,
|
||||||
|
sourceSize: Int,
|
||||||
|
completion: String,
|
||||||
|
expectedUrl: String,
|
||||||
|
) = runTest {
|
||||||
|
val result = provider.getAutocompleteSuggestion(text)!!
|
||||||
|
assertFalse(result.text.isEmpty())
|
||||||
|
|
||||||
|
assertEquals(completion, result.text)
|
||||||
|
assertEquals(domainSource.listName, result.source)
|
||||||
|
assertEquals(expectedUrl, result.url)
|
||||||
|
assertEquals(sourceSize, result.totalItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private fun assertNoCompletion(provider: BaseDomainAutocompleteProvider, text: String) = runTest {
|
||||||
|
assertNull(provider.getAutocompleteSuggestion(text))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
sdk=34
|
|
@ -0,0 +1,41 @@
|
||||||
|
# [Android Components](../../../README.md) > Browser > Engine-Gecko
|
||||||
|
|
||||||
|
[*Engine*](../../concept/engine/README.md) implementation based on [GeckoView](https://wiki.mozilla.org/Mobile/GeckoView).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Setting up the dependency
|
||||||
|
|
||||||
|
Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)):
|
||||||
|
|
||||||
|
```Groovy
|
||||||
|
implementation "org.mozilla.components:browser-engine-gecko:{latest-version}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration with the Glean SDK
|
||||||
|
|
||||||
|
#### Before using this component
|
||||||
|
Products sending telemetry and using this component *must request* a data-review following [this process](https://wiki.mozilla.org/Firefox/Data_Collection).
|
||||||
|
|
||||||
|
The [Glean SDK](../../../components/service/glean/README.md) can be used to collect [Gecko Telemetry](https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/index.html).
|
||||||
|
Applications using both this component and the Glean SDK should setup the Gecko Telemetry delegate
|
||||||
|
as shown below:
|
||||||
|
|
||||||
|
```Kotlin
|
||||||
|
val builder = GeckoRuntimeSettings.Builder()
|
||||||
|
val runtimeSettings = builder
|
||||||
|
.telemetryDelegate(GeckoGleanAdapter()) // Sets up the delegate!
|
||||||
|
.build()
|
||||||
|
// Create the Gecko runtime.
|
||||||
|
GeckoRuntime.create(context, runtimeSettings)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Adding new metrics
|
||||||
|
|
||||||
|
New Gecko metrics can be added as described [in the Firefox Telemetry docs](https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/start/adding-a-new-probe.html).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/
|
|
@ -0,0 +1,195 @@
|
||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORIES.each { repository ->
|
||||||
|
maven {
|
||||||
|
url repository
|
||||||
|
if (gradle.mozconfig.substs.ALLOW_INSECURE_GRADLE_REPOSITORIES) {
|
||||||
|
allowInsecureProtocol = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath "${ApplicationServicesConfig.groupId}:tooling-nimbus-gradle:${ApplicationServicesConfig.version}"
|
||||||
|
classpath "org.mozilla.telemetry:glean-gradle-plugin:${Versions.mozilla_glean}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id "com.jetbrains.python.envs" version "$python_envs_plugin"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'com.android.library'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion config.minSdkVersion
|
||||||
|
compileSdk config.compileSdkVersion
|
||||||
|
targetSdkVersion config.targetSdkVersion
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
resources {
|
||||||
|
excludes += ['META-INF/proguard/androidx-annotations.pro']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig true
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace 'mozilla.components.browser.engine.gecko'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set configuration for the Glean parser to extract metrics.yaml
|
||||||
|
// file from AAR dependencies of this project rather than look
|
||||||
|
// for it into the project directory.
|
||||||
|
ext.allowMetricsFromAAR = true
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(':concept-engine')
|
||||||
|
implementation project(':concept-fetch')
|
||||||
|
implementation project(':support-ktx')
|
||||||
|
implementation project(':support-utils')
|
||||||
|
implementation(project(':service-nimbus')) {
|
||||||
|
exclude group: 'org.mozilla.telemetry', module: 'glean-native'
|
||||||
|
}
|
||||||
|
implementation ComponentsDependencies.kotlin_coroutines
|
||||||
|
|
||||||
|
if (findProject(":geckoview") != null) {
|
||||||
|
api project(':geckoview')
|
||||||
|
} else {
|
||||||
|
api getGeckoViewDependency()
|
||||||
|
}
|
||||||
|
|
||||||
|
implementation ComponentsDependencies.androidx_core_ktx
|
||||||
|
implementation ComponentsDependencies.androidx_paging
|
||||||
|
implementation ComponentsDependencies.androidx_datastore_preferences
|
||||||
|
implementation ComponentsDependencies.androidx_lifecycle_livedata
|
||||||
|
|
||||||
|
testImplementation ComponentsDependencies.androidx_test_core
|
||||||
|
testImplementation ComponentsDependencies.androidx_test_junit
|
||||||
|
testImplementation ComponentsDependencies.testing_robolectric
|
||||||
|
testImplementation ComponentsDependencies.testing_coroutines
|
||||||
|
testImplementation ComponentsDependencies.testing_mockwebserver
|
||||||
|
testImplementation project(':support-test')
|
||||||
|
testImplementation project(':tooling-fetch-tests')
|
||||||
|
|
||||||
|
// We only compile against Glean. It's up to the app to add those dependencies
|
||||||
|
// if it wants to collect GeckoView telemetry through the Glean SDK.
|
||||||
|
compileOnly ComponentsDependencies.mozilla_glean
|
||||||
|
testImplementation ComponentsDependencies.mozilla_glean
|
||||||
|
testImplementation ComponentsDependencies.androidx_work_testing
|
||||||
|
|
||||||
|
androidTestImplementation ComponentsDependencies.androidx_test_core
|
||||||
|
androidTestImplementation ComponentsDependencies.androidx_test_runner
|
||||||
|
androidTestImplementation ComponentsDependencies.androidx_test_rules
|
||||||
|
androidTestImplementation project(':tooling-fetch-tests')
|
||||||
|
}
|
||||||
|
|
||||||
|
ext.gleanNamespace = "mozilla.telemetry.glean"
|
||||||
|
apply plugin: "org.mozilla.telemetry.glean-gradle-plugin"
|
||||||
|
apply from: '../../../android-lint.gradle'
|
||||||
|
apply from: '../../../publish.gradle'
|
||||||
|
apply plugin: "org.mozilla.appservices.nimbus-gradle-plugin"
|
||||||
|
nimbus {
|
||||||
|
// The path to the Nimbus feature manifest file
|
||||||
|
manifestFile = "geckoview.fml.yaml"
|
||||||
|
|
||||||
|
channels = [
|
||||||
|
debug: "debug",
|
||||||
|
release: "release",
|
||||||
|
]
|
||||||
|
|
||||||
|
// This is an optional value, and updates the plugin to use a copy of application
|
||||||
|
// services. The path should be relative to the root project directory.
|
||||||
|
// *NOTE*: This example will not work for all projects, but should work for Fenix, Focus, and Android Components
|
||||||
|
applicationServicesDir = gradle.hasProperty('localProperties.autoPublish.application-services.dir')
|
||||||
|
? gradle.getProperty('localProperties.autoPublish.application-services.dir') : null
|
||||||
|
}
|
||||||
|
ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
|
||||||
|
|
||||||
|
// Non-official versions are like "61.0a1", where "a1" is the milestone.
|
||||||
|
// This simply strips that off, leaving "61.0" in this example.
|
||||||
|
def getAppVersionWithoutMilestone() {
|
||||||
|
return gradle.mozconfig.substs.MOZ_APP_VERSION.replaceFirst(/a[0-9]/, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mimic Python: open(os.path.join(buildconfig.topobjdir, 'buildid.h')).readline().split()[2]
|
||||||
|
def getBuildId() {
|
||||||
|
if (System.env.MOZ_BUILD_DATE) {
|
||||||
|
if (System.env.MOZ_BUILD_DATE.length() == 14) {
|
||||||
|
return System.env.MOZ_BUILD_DATE
|
||||||
|
}
|
||||||
|
logger.warn("Ignoring invalid MOZ_BUILD_DATE: ${System.env.MOZ_BUILD_DATE}")
|
||||||
|
}
|
||||||
|
return file("${gradle.mozconfig.topobjdir}/buildid.h").getText('utf-8').split()[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
def getVersionNumber() {
|
||||||
|
def appVersion = getAppVersionWithoutMilestone()
|
||||||
|
def parts = appVersion.split('\\.')
|
||||||
|
def version = parts[0] + "." + parts[1] + "." + getBuildId()
|
||||||
|
|
||||||
|
if (!gradle.mozconfig.substs.MOZILLA_OFFICIAL && !gradle.mozconfig.substs.MOZ_ANDROID_FAT_AAR_ARCHITECTURES) {
|
||||||
|
// Use -SNAPSHOT versions locally to enable the local GeckoView substitution flow.
|
||||||
|
version += "-SNAPSHOT"
|
||||||
|
}
|
||||||
|
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
def getArtifactSuffix() {
|
||||||
|
def suffix = ""
|
||||||
|
|
||||||
|
// Release artifacts don't specify the channel, for the sake of simplicity.
|
||||||
|
if (gradle.mozconfig.substs.MOZ_UPDATE_CHANNEL != 'release') {
|
||||||
|
suffix += "-${gradle.mozconfig.substs.MOZ_UPDATE_CHANNEL}"
|
||||||
|
}
|
||||||
|
|
||||||
|
return suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
def getArtifactId() {
|
||||||
|
def id = "geckoview" + getArtifactSuffix()
|
||||||
|
|
||||||
|
if (!gradle.mozconfig.substs.MOZ_ANDROID_GECKOVIEW_LITE) {
|
||||||
|
id += "-omni"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gradle.mozconfig.substs.MOZILLA_OFFICIAL && !gradle.mozconfig.substs.MOZ_ANDROID_FAT_AAR_ARCHITECTURES) {
|
||||||
|
// In automation, per-architecture artifacts identify
|
||||||
|
// the architecture; multi-architecture artifacts don't.
|
||||||
|
// When building locally, we produce a "skinny AAR" with
|
||||||
|
// one target architecture masquerading as a "fat AAR"
|
||||||
|
// to enable Gradle composite builds to substitute this
|
||||||
|
// project into consumers easily.
|
||||||
|
id += "-${gradle.mozconfig.substs.ANDROID_CPU_ARCH}"
|
||||||
|
}
|
||||||
|
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
def getGeckoViewDependency() {
|
||||||
|
// on try, relax geckoview version pin to allow for --use-existing-task
|
||||||
|
if ('https://hg.mozilla.org/try' == System.env.GECKO_HEAD_REPOSITORY) {
|
||||||
|
rootProject.logger.lifecycle("Getting geckoview on try: ${getArtifactId()}:+")
|
||||||
|
return "org.mozilla.geckoview:${getArtifactId()}:+"
|
||||||
|
}
|
||||||
|
rootProject.logger.lifecycle("Getting geckoview: ${getArtifactId()}:${getVersionNumber()}")
|
||||||
|
return "org.mozilla.geckoview:${getArtifactId()}:${getVersionNumber()}"
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Metrics definitions have moved
|
||||||
|
|
||||||
|
Metrics definitions for projects using `engine-gecko` moved to the Glean Dictionary.
|
||||||
|
|
||||||
|
For Firefox for Android those definitions can be found at:
|
||||||
|
[https://dictionary.telemetry.mozilla.org/apps/fenix](https://dictionary.telemetry.mozilla.org/apps/fenix)
|
||||||
|
|
||||||
|
This file is kept only for historical reference.
|
|
@ -0,0 +1,24 @@
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
about:
|
||||||
|
description: GeckoView features configurable via Nimbus
|
||||||
|
android:
|
||||||
|
package: mozilla.components.browser.engine.gecko
|
||||||
|
class: .GeckoNimbus
|
||||||
|
channels:
|
||||||
|
- debug
|
||||||
|
- release
|
||||||
|
features:
|
||||||
|
pdfjs:
|
||||||
|
description: "PDF.js features"
|
||||||
|
variables:
|
||||||
|
download-button:
|
||||||
|
description: "Download button"
|
||||||
|
type: Boolean
|
||||||
|
default: true
|
||||||
|
|
||||||
|
open-in-app-button:
|
||||||
|
description: "Open in app button"
|
||||||
|
type: Boolean
|
||||||
|
default: true
|
|
@ -0,0 +1,30 @@
|
||||||
|
# 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/.
|
||||||
|
|
||||||
|
# IMPORTANT NOTE: this file is here only as a safety measure, to make
|
||||||
|
# sure the correct code is generated even though the GeckoView AAR file
|
||||||
|
# reports an empty metrics.yaml file. The metric in this file is currently
|
||||||
|
# disabled and not supposed to collect any data.
|
||||||
|
---
|
||||||
|
|
||||||
|
$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
|
||||||
|
|
||||||
|
test.glean.geckoview:
|
||||||
|
streaming:
|
||||||
|
type: timing_distribution
|
||||||
|
gecko_datapoint: TELEMETRY_TEST_STREAMING
|
||||||
|
disabled: true
|
||||||
|
description: |
|
||||||
|
A test-only, disabled metric. This is required to guarantee
|
||||||
|
that a `GleanGeckoHistogramMapping` is always generated, even
|
||||||
|
though the GeckoView AAR exports no metric. Please note that
|
||||||
|
the data-review field below contains no review, since this
|
||||||
|
metric is disabled and not allowed to collect any data.
|
||||||
|
bugs:
|
||||||
|
- https://bugzilla.mozilla.org/1566374
|
||||||
|
data_reviews:
|
||||||
|
- https://bugzilla.mozilla.org/1566374
|
||||||
|
notification_emails:
|
||||||
|
- glean-team@mozilla.com
|
||||||
|
expires: never
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
|
@ -0,0 +1,126 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.fetch.geckoview
|
||||||
|
|
||||||
|
import androidx.test.annotation.UiThreadTest
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.filters.MediumTest
|
||||||
|
import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient
|
||||||
|
import mozilla.components.concept.fetch.Client
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@MediumTest
|
||||||
|
class GeckoViewFetchTestCases : mozilla.components.tooling.fetch.tests.FetchTestCases() {
|
||||||
|
override fun createNewClient(): Client = GeckoViewFetchClient(ApplicationProvider.getApplicationContext())
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
fun clientInstance() {
|
||||||
|
assertTrue(createNewClient() is GeckoViewFetchClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200WithGzippedBody() {
|
||||||
|
super.get200WithGzippedBody()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200OverridingDefaultHeaders() {
|
||||||
|
super.get200OverridingDefaultHeaders()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200WithDuplicatedCacheControlRequestHeaders() {
|
||||||
|
super.get200WithDuplicatedCacheControlRequestHeaders()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200WithDuplicatedCacheControlResponseHeaders() {
|
||||||
|
super.get200WithDuplicatedCacheControlResponseHeaders()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200WithHeaders() {
|
||||||
|
super.get200WithHeaders()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200WithReadTimeout() {
|
||||||
|
super.get200WithReadTimeout()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200WithStringBody() {
|
||||||
|
super.get200WithStringBody()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get302FollowRedirects() {
|
||||||
|
super.get302FollowRedirects()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get302FollowRedirectsDisabled() {
|
||||||
|
super.get302FollowRedirectsDisabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get404WithBody() {
|
||||||
|
super.get404WithBody()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun post200WithBody() {
|
||||||
|
super.post200WithBody()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun put201FileUpload() {
|
||||||
|
super.put201FileUpload()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200WithCookiePolicy() {
|
||||||
|
super.get200WithCookiePolicy()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200WithContentTypeCharset() {
|
||||||
|
super.get200WithContentTypeCharset()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200WithCacheControl() {
|
||||||
|
super.get200WithCacheControl()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun getThrowsIOExceptionWhenHostNotReachable() {
|
||||||
|
super.getThrowsIOExceptionWhenHostNotReachable()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun getDataUri() {
|
||||||
|
super.getDataUri()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
<manifest />
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,73 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko
|
||||||
|
|
||||||
|
import android.util.JsonReader
|
||||||
|
import android.util.JsonWriter
|
||||||
|
import mozilla.components.concept.engine.EngineSessionState
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.mozilla.geckoview.GeckoSession
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
private const val GECKO_STATE_KEY = "GECKO_STATE"
|
||||||
|
|
||||||
|
class GeckoEngineSessionState internal constructor(
|
||||||
|
internal val actualState: GeckoSession.SessionState?,
|
||||||
|
) : EngineSessionState {
|
||||||
|
override fun writeTo(writer: JsonWriter) {
|
||||||
|
with(writer) {
|
||||||
|
beginObject()
|
||||||
|
|
||||||
|
name(GECKO_STATE_KEY)
|
||||||
|
value(actualState.toString())
|
||||||
|
|
||||||
|
endObject()
|
||||||
|
flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromJSON(json: JSONObject): GeckoEngineSessionState = try {
|
||||||
|
val state = json.getString(GECKO_STATE_KEY)
|
||||||
|
|
||||||
|
GeckoEngineSessionState(
|
||||||
|
GeckoSession.SessionState.fromString(state),
|
||||||
|
)
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
GeckoEngineSessionState(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a [GeckoEngineSessionState] from the given [JsonReader].
|
||||||
|
*/
|
||||||
|
fun from(reader: JsonReader): GeckoEngineSessionState = try {
|
||||||
|
reader.beginObject()
|
||||||
|
|
||||||
|
val rawState = if (reader.hasNext()) {
|
||||||
|
val key = reader.nextName()
|
||||||
|
if (key != GECKO_STATE_KEY) {
|
||||||
|
throw AssertionError("Unknown state key: $key")
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.nextString()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.endObject()
|
||||||
|
|
||||||
|
GeckoEngineSessionState(
|
||||||
|
rawState?.let { GeckoSession.SessionState.fromString(it) },
|
||||||
|
)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
GeckoEngineSessionState(null)
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
// Internally GeckoView uses org.json and currently may throw JSONException in certain cases
|
||||||
|
// https://github.com/mozilla-mobile/android-components/issues/9332
|
||||||
|
GeckoEngineSessionState(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,247 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import mozilla.components.browser.engine.gecko.activity.GeckoViewActivityContextDelegate
|
||||||
|
import mozilla.components.browser.engine.gecko.selection.GeckoSelectionActionDelegate
|
||||||
|
import mozilla.components.concept.engine.EngineSession
|
||||||
|
import mozilla.components.concept.engine.EngineView
|
||||||
|
import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
|
||||||
|
import mozilla.components.concept.engine.selection.SelectionActionDelegate
|
||||||
|
import org.mozilla.geckoview.BasicSelectionActionDelegate
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
import org.mozilla.geckoview.GeckoSession
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
import androidx.core.view.OnApplyWindowInsetsListener as AndroidxOnApplyWindowInsetsListener
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gecko-based EngineView implementation.
|
||||||
|
*/
|
||||||
|
class GeckoEngineView @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0,
|
||||||
|
) : FrameLayout(context, attrs, defStyleAttr), EngineView {
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal var geckoView = object : NestedGeckoView(context) {
|
||||||
|
|
||||||
|
override fun onAttachedToWindow() {
|
||||||
|
try {
|
||||||
|
super.onAttachedToWindow()
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
// This is to debug "display already acquired" crashes
|
||||||
|
val otherActivityClassName =
|
||||||
|
this.session?.accessibility?.view?.context?.javaClass?.simpleName
|
||||||
|
val otherActivityClassHashcode =
|
||||||
|
this.session?.accessibility?.view?.context?.hashCode()
|
||||||
|
val activityClassName = context.javaClass.simpleName
|
||||||
|
val activityClassHashCode = context.hashCode()
|
||||||
|
val msg = "ATTACH VIEW: Current activity: $activityClassName hashcode " +
|
||||||
|
"$activityClassHashCode Other activity: $otherActivityClassName " +
|
||||||
|
"hashcode $otherActivityClassHashcode"
|
||||||
|
throw IllegalStateException(msg, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromWindow() {
|
||||||
|
// We are releasing the session before GeckoView gets detached from the window. Otherwise
|
||||||
|
// GeckoView will close the session automatically and we do not want that.
|
||||||
|
releaseSession()
|
||||||
|
|
||||||
|
super.onDetachedFromWindow()
|
||||||
|
}
|
||||||
|
}.apply {
|
||||||
|
// Explicitly mark this view as important for autofill. The default "auto" doesn't seem to trigger any
|
||||||
|
// autofill behavior for us here.
|
||||||
|
@Suppress("WrongConstant")
|
||||||
|
ViewCompat.setImportantForAutofill(this, View.IMPORTANT_FOR_ACCESSIBILITY_YES)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun setColorScheme(preferredColorScheme: PreferredColorScheme) {
|
||||||
|
var colorScheme = preferredColorScheme
|
||||||
|
if (preferredColorScheme == PreferredColorScheme.System) {
|
||||||
|
colorScheme =
|
||||||
|
if (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
||||||
|
== Configuration.UI_MODE_NIGHT_YES
|
||||||
|
) {
|
||||||
|
PreferredColorScheme.Dark
|
||||||
|
} else {
|
||||||
|
PreferredColorScheme.Light
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colorScheme == PreferredColorScheme.Dark) {
|
||||||
|
geckoView.coverUntilFirstPaint(DARK_COVER)
|
||||||
|
} else {
|
||||||
|
geckoView.coverUntilFirstPaint(Color.WHITE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal var currentSession: GeckoEngineSession? = null
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal var currentSelection: BasicSelectionActionDelegate? = null
|
||||||
|
|
||||||
|
override var selectionActionDelegate: SelectionActionDelegate? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
addView(geckoView)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* With the current design, we have a [NestedGeckoView] inside this
|
||||||
|
* [GeckoEngineView]. In our supported embedders, we wrap this with the
|
||||||
|
* AndroidX `SwipeRefreshLayout` to enable features like Pull-To-Refresh:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* SwipeRefreshLayout
|
||||||
|
* └── GeckoEngineView
|
||||||
|
* └── NestedGeckoView
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* `SwipeRefreshLayout` only looks at the direct child to see if it has nested scrolling
|
||||||
|
* enabled. As we embed [NestedGeckoView] inside [GeckoEngineView], we change the hierarchy
|
||||||
|
* so that [NestedGeckoView] is no longer the direct child of `SwipeRefreshLayout`.
|
||||||
|
*
|
||||||
|
* To fix this we enable nested scrolling on the GeckoEngineView to emulate this
|
||||||
|
* information. This is required information for `View.requestDisallowInterceptTouchEvent`
|
||||||
|
* to work correctly in the [NestedGeckoView].
|
||||||
|
*/
|
||||||
|
isNestedScrollingEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the content of the given session.
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
override fun render(session: EngineSession) {
|
||||||
|
val internalSession = session as GeckoEngineSession
|
||||||
|
currentSession = session
|
||||||
|
|
||||||
|
if (geckoView.session != internalSession.geckoSession) {
|
||||||
|
geckoView.session?.let {
|
||||||
|
// Release a previously assigned session. Otherwise GeckoView will close it
|
||||||
|
// automatically.
|
||||||
|
detachSelectionActionDelegate(it)
|
||||||
|
geckoView.releaseSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
geckoView.setSession(internalSession.geckoSession)
|
||||||
|
attachSelectionActionDelegate(internalSession.geckoSession)
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
// This is to debug "display already acquired" crashes
|
||||||
|
val otherActivityClassName =
|
||||||
|
internalSession.geckoSession.accessibility.view?.context?.javaClass?.simpleName
|
||||||
|
val otherActivityClassHashcode =
|
||||||
|
internalSession.geckoSession.accessibility.view?.context?.hashCode()
|
||||||
|
val activityClassName = context.javaClass.simpleName
|
||||||
|
val activityClassHashCode = context.hashCode()
|
||||||
|
val msg = "SET SESSION: Current activity: $activityClassName hashcode " +
|
||||||
|
"$activityClassHashCode Other activity: $otherActivityClassName " +
|
||||||
|
"hashcode $otherActivityClassHashcode"
|
||||||
|
throw IllegalStateException(msg, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun attachSelectionActionDelegate(session: GeckoSession) {
|
||||||
|
val delegate = GeckoSelectionActionDelegate.maybeCreate(context, selectionActionDelegate)
|
||||||
|
if (delegate != null) {
|
||||||
|
session.selectionActionDelegate = delegate
|
||||||
|
currentSelection = delegate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun detachSelectionActionDelegate(session: GeckoSession?) {
|
||||||
|
if (currentSelection != null) {
|
||||||
|
session?.selectionActionDelegate = null
|
||||||
|
currentSelection = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun release() {
|
||||||
|
detachSelectionActionDelegate(currentSession?.geckoSession)
|
||||||
|
|
||||||
|
currentSession = null
|
||||||
|
|
||||||
|
geckoView.releaseSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromWindow() {
|
||||||
|
super.onDetachedFromWindow()
|
||||||
|
|
||||||
|
release()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun canClearSelection() = !currentSelection?.selection?.text.isNullOrEmpty()
|
||||||
|
|
||||||
|
override fun canScrollVerticallyUp() = currentSession?.let { it.scrollY > 0 } != false
|
||||||
|
|
||||||
|
override fun canScrollVerticallyDown() =
|
||||||
|
true // waiting for this issue https://bugzilla.mozilla.org/show_bug.cgi?id=1507569
|
||||||
|
|
||||||
|
override fun getInputResultDetail() = geckoView.inputResultDetail
|
||||||
|
|
||||||
|
override fun setVerticalClipping(clippingHeight: Int) {
|
||||||
|
geckoView.setVerticalClipping(clippingHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setDynamicToolbarMaxHeight(height: Int) {
|
||||||
|
geckoView.setDynamicToolbarMaxHeight(height)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setActivityContext(context: Context?) {
|
||||||
|
geckoView.activityContextDelegate = GeckoViewActivityContextDelegate(WeakReference(context))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun captureThumbnail(onFinish: (Bitmap?) -> Unit) {
|
||||||
|
val geckoResult = geckoView.capturePixels()
|
||||||
|
geckoResult.then(
|
||||||
|
{ bitmap ->
|
||||||
|
onFinish(bitmap)
|
||||||
|
GeckoResult()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onFinish(null)
|
||||||
|
GeckoResult<Void>()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearSelection() {
|
||||||
|
currentSelection?.clearSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setVisibility(visibility: Int) {
|
||||||
|
// GeckoView doesn't react to onVisibilityChanged so we need to propagate ourselves for now:
|
||||||
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1630775
|
||||||
|
// We do this to prevent the content from resizing when the view is not visible:
|
||||||
|
// https://github.com/mozilla-mobile/android-components/issues/6664
|
||||||
|
geckoView.visibility = visibility
|
||||||
|
super.setVisibility(visibility)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addWindowInsetsListener(
|
||||||
|
key: String,
|
||||||
|
listener: AndroidxOnApplyWindowInsetsListener?,
|
||||||
|
) = geckoView.addWindowInsetsListener(key, listener)
|
||||||
|
|
||||||
|
override fun removeWindowInsetsListener(key: String) = geckoView.removeWindowInsetsListener(key)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
internal const val DARK_COVER = 0xFF2A2A2E.toInt()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.CoroutineStart
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import mozilla.components.concept.engine.CancellableOperation
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for a GeckoResult to be complete in a co-routine.
|
||||||
|
*/
|
||||||
|
suspend fun <T> GeckoResult<T>.await() = suspendCoroutine<T?> { continuation ->
|
||||||
|
then(
|
||||||
|
{
|
||||||
|
continuation.resume(it)
|
||||||
|
GeckoResult<Void>()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
continuation.resumeWithException(it)
|
||||||
|
GeckoResult<Void>()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a [GeckoResult] to a [CancellableOperation].
|
||||||
|
*/
|
||||||
|
fun <T> GeckoResult<T>.asCancellableOperation(): CancellableOperation {
|
||||||
|
val geckoResult = this
|
||||||
|
return object : CancellableOperation {
|
||||||
|
override fun cancel(): Deferred<Boolean> {
|
||||||
|
val result = CompletableDeferred<Boolean>()
|
||||||
|
geckoResult.cancel().then(
|
||||||
|
{
|
||||||
|
result.complete(it ?: false)
|
||||||
|
GeckoResult<Void>()
|
||||||
|
},
|
||||||
|
{ throwable ->
|
||||||
|
result.completeExceptionally(throwable)
|
||||||
|
GeckoResult<Void>()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a GeckoResult from a co-routine.
|
||||||
|
*/
|
||||||
|
@Suppress("TooGenericExceptionCaught")
|
||||||
|
fun <T> CoroutineScope.launchGeckoResult(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
block: suspend CoroutineScope.() -> T,
|
||||||
|
) = GeckoResult<T>().apply {
|
||||||
|
launch(context, start) {
|
||||||
|
try {
|
||||||
|
val value = block()
|
||||||
|
complete(value)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
completeExceptionally(exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import mozilla.components.browser.engine.gecko.content.blocking.GeckoTrackingProtectionException
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.geckoTrackingProtectionPermission
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.isExcludedForTrackingProtection
|
||||||
|
import mozilla.components.concept.engine.EngineSession
|
||||||
|
import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
|
||||||
|
import mozilla.components.concept.engine.content.blocking.TrackingProtectionExceptionStorage
|
||||||
|
import mozilla.components.support.ktx.kotlin.getOrigin
|
||||||
|
import mozilla.components.support.ktx.kotlin.stripDefaultPort
|
||||||
|
import org.mozilla.geckoview.GeckoRuntime
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_ALLOW
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_DENY
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [TrackingProtectionExceptionStorage] implementation to store tracking protection exceptions.
|
||||||
|
*/
|
||||||
|
internal class GeckoTrackingProtectionExceptionStorage(
|
||||||
|
private val runtime: GeckoRuntime,
|
||||||
|
) : TrackingProtectionExceptionStorage {
|
||||||
|
internal var scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
|
override fun contains(session: EngineSession, onResult: (Boolean) -> Unit) {
|
||||||
|
val url = (session as GeckoEngineSession).currentUrl
|
||||||
|
if (!url.isNullOrEmpty()) {
|
||||||
|
getPermissions(url) { permissions ->
|
||||||
|
val contains = permissions.isNotEmpty()
|
||||||
|
onResult(contains)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onResult(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchAll(onResult: (List<TrackingProtectionException>) -> Unit) {
|
||||||
|
runtime.storageController.allPermissions.accept { permissions ->
|
||||||
|
val trackingExceptions = permissions.filterTrackingProtectionExceptions()
|
||||||
|
onResult(trackingExceptions.map { exceptions -> exceptions.toTrackingProtectionException() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<ContentPermission>?.filterTrackingProtectionExceptions() =
|
||||||
|
this.orEmpty().filter { it.isExcludedForTrackingProtection }
|
||||||
|
|
||||||
|
private fun List<ContentPermission>?.filterTrackingProtectionExceptions(url: String) =
|
||||||
|
this.orEmpty()
|
||||||
|
.filter {
|
||||||
|
it.isExcludedForTrackingProtection && it.uri.getOrigin().orEmpty()
|
||||||
|
.stripDefaultPort() == url
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun add(session: EngineSession, persistInPrivateMode: Boolean) {
|
||||||
|
val geckoEngineSession = (session as GeckoEngineSession)
|
||||||
|
if (persistInPrivateMode) {
|
||||||
|
addPersistentPrivateException(geckoEngineSession)
|
||||||
|
} else {
|
||||||
|
geckoEngineSession.geckoTrackingProtectionPermission?.let {
|
||||||
|
runtime.storageController.setPermission(it, VALUE_ALLOW)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onExcludedOnTrackingProtectionChange(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun addPersistentPrivateException(geckoEngineSession: GeckoEngineSession) {
|
||||||
|
val permission = geckoEngineSession.geckoTrackingProtectionPermission
|
||||||
|
permission?.let {
|
||||||
|
runtime.storageController.setPrivateBrowsingPermanentPermission(it, VALUE_ALLOW)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun remove(session: EngineSession) {
|
||||||
|
val geckoEngineSession = (session as GeckoEngineSession)
|
||||||
|
val url = geckoEngineSession.currentUrl ?: return
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onExcludedOnTrackingProtectionChange(false)
|
||||||
|
}
|
||||||
|
remove(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun remove(exception: TrackingProtectionException) {
|
||||||
|
if (exception is GeckoTrackingProtectionException) {
|
||||||
|
remove(exception.contentPermission)
|
||||||
|
} else {
|
||||||
|
remove(exception.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun remove(url: String) {
|
||||||
|
val storage = runtime.storageController
|
||||||
|
getPermissions(url) { permissions ->
|
||||||
|
permissions.forEach { geckoPermissions ->
|
||||||
|
storage.setPermission(geckoPermissions, VALUE_DENY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a workaround until https://bugzilla.mozilla.org/show_bug.cgi?id=1723280 gets addressed
|
||||||
|
private fun getPermissions(url: String, onFinish: (List<ContentPermission>) -> Unit) {
|
||||||
|
val localUrl = url.getOrigin().orEmpty().stripDefaultPort()
|
||||||
|
val storage = runtime.storageController
|
||||||
|
if (localUrl.isNotEmpty()) {
|
||||||
|
storage.allPermissions.accept { permissions ->
|
||||||
|
onFinish(permissions.filterTrackingProtectionExceptions(localUrl))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onFinish(emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun remove(contentPermission: ContentPermission) {
|
||||||
|
runtime.storageController.setPermission(contentPermission, VALUE_DENY)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeAll(activeSessions: List<EngineSession>?, onRemove: () -> Unit) {
|
||||||
|
val storage = runtime.storageController
|
||||||
|
activeSessions?.forEach { engineSession ->
|
||||||
|
engineSession.notifyObservers {
|
||||||
|
onExcludedOnTrackingProtectionChange(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
storage.allPermissions.accept { permissions ->
|
||||||
|
val trackingExceptions = permissions.filterTrackingProtectionExceptions()
|
||||||
|
trackingExceptions.forEach {
|
||||||
|
storage.setPermission(it, VALUE_DENY)
|
||||||
|
}
|
||||||
|
onRemove.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ContentPermission.toTrackingProtectionException(): GeckoTrackingProtectionException {
|
||||||
|
return GeckoTrackingProtectionException(uri, privateMode, this)
|
||||||
|
}
|
|
@ -0,0 +1,257 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import androidx.core.view.NestedScrollingChild
|
||||||
|
import androidx.core.view.NestedScrollingChildHelper
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import mozilla.components.concept.engine.InputResultDetail
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
import org.mozilla.geckoview.GeckoView
|
||||||
|
import org.mozilla.geckoview.PanZoomController
|
||||||
|
|
||||||
|
/**
|
||||||
|
* geckoView that supports nested scrolls (for using in a CoordinatorLayout).
|
||||||
|
*
|
||||||
|
* This code is a simplified version of the NestedScrollView implementation
|
||||||
|
* which can be found in the support library:
|
||||||
|
* [android.support.v4.widget.NestedScrollView]
|
||||||
|
*
|
||||||
|
* Based on:
|
||||||
|
* https://github.com/takahirom/webview-in-coordinatorlayout
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Suppress("ClickableViewAccessibility")
|
||||||
|
open class NestedGeckoView(context: Context) : GeckoView(context), NestedScrollingChild {
|
||||||
|
@VisibleForTesting
|
||||||
|
internal var lastY: Int = 0
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal val scrollOffset = IntArray(2)
|
||||||
|
|
||||||
|
private val scrollConsumed = IntArray(2)
|
||||||
|
|
||||||
|
private var gestureCanReachParent = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the initial scroll.
|
||||||
|
*/
|
||||||
|
enum class InitialScrollDirection {
|
||||||
|
NOT_YET,
|
||||||
|
DOWN,
|
||||||
|
UP,
|
||||||
|
}
|
||||||
|
|
||||||
|
private var initialScrollDirection = InitialScrollDirection.NOT_YET
|
||||||
|
|
||||||
|
private var initialDownY: Float = 0f
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal var nestedOffsetY: Int = 0
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal var childHelper: NestedScrollingChildHelper = NestedScrollingChildHelper(this)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How user's MotionEvent will be handled.
|
||||||
|
*
|
||||||
|
* @see InputResultDetail
|
||||||
|
*/
|
||||||
|
internal var inputResultDetail = InputResultDetail.newInstance(true)
|
||||||
|
|
||||||
|
init {
|
||||||
|
isNestedScrollingEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("ComplexMethod")
|
||||||
|
override fun onTouchEvent(ev: MotionEvent): Boolean {
|
||||||
|
val event = MotionEvent.obtain(ev)
|
||||||
|
val action = ev.actionMasked
|
||||||
|
val eventY = event.y.toInt()
|
||||||
|
|
||||||
|
when (action) {
|
||||||
|
MotionEvent.ACTION_MOVE -> {
|
||||||
|
val allowScroll = !shouldPinOnScreen() && inputResultDetail.isTouchHandledByBrowser()
|
||||||
|
|
||||||
|
var deltaY = lastY - eventY
|
||||||
|
|
||||||
|
if (allowScroll && dispatchNestedPreScroll(0, deltaY, scrollConsumed, scrollOffset)) {
|
||||||
|
deltaY -= scrollConsumed[1]
|
||||||
|
event.offsetLocation(0f, (-scrollOffset[1]).toFloat())
|
||||||
|
nestedOffsetY += scrollOffset[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
lastY = eventY - scrollOffset[1]
|
||||||
|
|
||||||
|
if (allowScroll && dispatchNestedScroll(0, scrollOffset[1], 0, deltaY, scrollOffset)) {
|
||||||
|
lastY -= scrollOffset[1]
|
||||||
|
event.offsetLocation(0f, scrollOffset[1].toFloat())
|
||||||
|
nestedOffsetY += scrollOffset[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initialScrollDirection == InitialScrollDirection.NOT_YET) {
|
||||||
|
if (event.y > initialDownY) {
|
||||||
|
initialScrollDirection = InitialScrollDirection.UP
|
||||||
|
} else if (event.y < initialDownY) {
|
||||||
|
initialScrollDirection = InitialScrollDirection.DOWN
|
||||||
|
} else {
|
||||||
|
// We may want to disallow pull-to-refresh in the case of
|
||||||
|
// scrolling left or right initially?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MotionEvent.ACTION_DOWN -> {
|
||||||
|
// A new gesture started. Ask GV if it can handle this.
|
||||||
|
parent?.requestDisallowInterceptTouchEvent(true)
|
||||||
|
updateInputResult(event)
|
||||||
|
|
||||||
|
initialScrollDirection = InitialScrollDirection.NOT_YET
|
||||||
|
nestedOffsetY = 0
|
||||||
|
lastY = eventY
|
||||||
|
initialDownY = event.y
|
||||||
|
|
||||||
|
// The event should be handled either by onTouchEvent,
|
||||||
|
// either by onTouchEventForResult, never by both.
|
||||||
|
// Early return if we sent it to updateInputResult(..) which calls onTouchEventForResult.
|
||||||
|
event.recycle()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't care about other touch events
|
||||||
|
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||||
|
// inputResultDetail needs to be reset here and not in the next ACTION_DOWN, because
|
||||||
|
// its value is used by other features that poll for the value via
|
||||||
|
// `EngineView.getInputResultDetail`. Not resetting this in ACTION_CANCEL/ACTION_UP
|
||||||
|
// would then mean we send stale information to those features from a previous
|
||||||
|
// gesture's result.
|
||||||
|
inputResultDetail = InputResultDetail.newInstance(true)
|
||||||
|
stopNestedScroll()
|
||||||
|
|
||||||
|
// Allow touch event interception here so that the next ACTION_DOWN event can be properly
|
||||||
|
// intercepted by the parent.
|
||||||
|
parent?.requestDisallowInterceptTouchEvent(false)
|
||||||
|
gestureCanReachParent = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute event handler from parent class in all cases
|
||||||
|
val eventHandled = callSuperOnTouchEvent(event)
|
||||||
|
|
||||||
|
// Recycle previously obtained event
|
||||||
|
event.recycle()
|
||||||
|
|
||||||
|
return eventHandled
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun callSuperOnTouchEvent(event: MotionEvent): Boolean {
|
||||||
|
return super.onTouchEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("WrongThread") // Lint complains startNestedScroll() needs to be called on the main thread
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun updateInputResult(event: MotionEvent) {
|
||||||
|
val eventAction = event.actionMasked
|
||||||
|
superOnTouchEventForDetailResult(event)
|
||||||
|
.accept {
|
||||||
|
// Since the response from APZ is async, we could theoretically have a response
|
||||||
|
// which is out of time when we get the ACTION_MOVE events, and we do not want
|
||||||
|
// to forward this to the parent pre-emptively.
|
||||||
|
if (!gestureCanReachParent) {
|
||||||
|
return@accept
|
||||||
|
}
|
||||||
|
|
||||||
|
inputResultDetail = inputResultDetail.copy(
|
||||||
|
it?.handledResult(),
|
||||||
|
it?.scrollableDirections(),
|
||||||
|
it?.overscrollDirections(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (eventAction == MotionEvent.ACTION_DOWN) {
|
||||||
|
// Gesture can reach the parent only if the content is already at the top
|
||||||
|
gestureCanReachParent = inputResultDetail.canOverscrollTop()
|
||||||
|
|
||||||
|
when (initialScrollDirection) {
|
||||||
|
InitialScrollDirection.NOT_YET -> {
|
||||||
|
// If the event wasn't used in GeckoView, allow touch event interception.
|
||||||
|
if (gestureCanReachParent && !inputResultDetail.isTouchHandledByWebsite()) {
|
||||||
|
parent?.requestDisallowInterceptTouchEvent(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InitialScrollDirection.UP -> {
|
||||||
|
// If we received the InputResultDetail from Gecko after we've sent the
|
||||||
|
// first touch move event to Gecko, that means there's at least a touch
|
||||||
|
// event listener whether to prevent the event or not, or CSS touch-action
|
||||||
|
// properties in the contents, thus if the result isn't
|
||||||
|
// `INPUT_HANDLED_CONTENT`, we can allow pull-to-refresh.
|
||||||
|
if (gestureCanReachParent && !inputResultDetail.isTouchHandledByWebsite()) {
|
||||||
|
parent?.requestDisallowInterceptTouchEvent(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InitialScrollDirection.DOWN -> {
|
||||||
|
parent?.requestDisallowInterceptTouchEvent(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal open fun superOnTouchEventForDetailResult(
|
||||||
|
event: MotionEvent,
|
||||||
|
): GeckoResult<PanZoomController.InputResultDetail> =
|
||||||
|
super.onTouchEventForDetailResult(event)
|
||||||
|
|
||||||
|
override fun setNestedScrollingEnabled(enabled: Boolean) {
|
||||||
|
childHelper.isNestedScrollingEnabled = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isNestedScrollingEnabled(): Boolean {
|
||||||
|
return childHelper.isNestedScrollingEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startNestedScroll(axes: Int): Boolean {
|
||||||
|
return childHelper.startNestedScroll(axes)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stopNestedScroll() {
|
||||||
|
childHelper.stopNestedScroll()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hasNestedScrollingParent(): Boolean {
|
||||||
|
return childHelper.hasNestedScrollingParent()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispatchNestedScroll(
|
||||||
|
dxConsumed: Int,
|
||||||
|
dyConsumed: Int,
|
||||||
|
dxUnconsumed: Int,
|
||||||
|
dyUnconsumed: Int,
|
||||||
|
offsetInWindow: IntArray?,
|
||||||
|
): Boolean {
|
||||||
|
return childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?): Boolean {
|
||||||
|
return childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispatchNestedFling(velocityX: Float, velocityY: Float, consumed: Boolean): Boolean {
|
||||||
|
return childHelper.dispatchNestedFling(velocityX, velocityY, consumed)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float): Boolean {
|
||||||
|
return childHelper.dispatchNestedPreFling(velocityX, velocityY)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.activity
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Intent
|
||||||
|
import mozilla.components.concept.engine.activity.ActivityDelegate
|
||||||
|
import mozilla.components.support.base.log.logger.Logger
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
import org.mozilla.geckoview.GeckoRuntime
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper for the [ActivityDelegate] to communicate with the Gecko-based delegate.
|
||||||
|
*/
|
||||||
|
internal class GeckoActivityDelegate(
|
||||||
|
private val delegateRef: WeakReference<ActivityDelegate>,
|
||||||
|
) : GeckoRuntime.ActivityDelegate {
|
||||||
|
|
||||||
|
private val logger = Logger(GeckoActivityDelegate::javaClass.name)
|
||||||
|
|
||||||
|
override fun onStartActivityForResult(intent: PendingIntent): GeckoResult<Intent> {
|
||||||
|
val result: GeckoResult<Intent> = GeckoResult()
|
||||||
|
val delegate = delegateRef.get()
|
||||||
|
|
||||||
|
if (delegate == null) {
|
||||||
|
logger.warn("No activity delegate attached. Cannot request FIDO auth.")
|
||||||
|
|
||||||
|
result.completeExceptionally(RuntimeException("Activity for result failed; no delegate attached."))
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate.startIntentSenderForResult(intent.intentSender) { data ->
|
||||||
|
if (data != null) {
|
||||||
|
result.complete(data)
|
||||||
|
} else {
|
||||||
|
result.completeExceptionally(RuntimeException("Activity for result failed."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.activity
|
||||||
|
|
||||||
|
import mozilla.components.concept.engine.activity.OrientationDelegate
|
||||||
|
import org.mozilla.geckoview.AllowOrDeny
|
||||||
|
import org.mozilla.geckoview.AllowOrDeny.ALLOW
|
||||||
|
import org.mozilla.geckoview.AllowOrDeny.DENY
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
import org.mozilla.geckoview.OrientationController
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default [OrientationController.OrientationDelegate] implementation that delegates both the behavior
|
||||||
|
* and the returned value to a [OrientationDelegate].
|
||||||
|
*/
|
||||||
|
internal class GeckoScreenOrientationDelegate(
|
||||||
|
private val delegate: OrientationDelegate,
|
||||||
|
) : OrientationController.OrientationDelegate {
|
||||||
|
override fun onOrientationLock(requestedOrientation: Int): GeckoResult<AllowOrDeny> {
|
||||||
|
val result = GeckoResult<AllowOrDeny>()
|
||||||
|
|
||||||
|
when (delegate.onOrientationLock(requestedOrientation)) {
|
||||||
|
true -> result.complete(ALLOW)
|
||||||
|
false -> result.complete(DENY)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOrientationUnlock() {
|
||||||
|
delegate.onOrientationUnlock()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.activity
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import mozilla.components.support.base.log.logger.Logger
|
||||||
|
import org.mozilla.geckoview.GeckoView
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GeckoViewActivityContextDelegate provides an active Activity to GeckoView or null. Not to be confused
|
||||||
|
* with the runtime delegate of [GeckoActivityDelegate], which is tightly coupled to webauthn.
|
||||||
|
* See bug 1806191 for more information on delegate differences.
|
||||||
|
*
|
||||||
|
* @param contextRef A reference to an active Activity context or null for GeckoView to use.
|
||||||
|
*/
|
||||||
|
class GeckoViewActivityContextDelegate(
|
||||||
|
private val contextRef: WeakReference<Context?>,
|
||||||
|
) : GeckoView.ActivityContextDelegate {
|
||||||
|
private val logger = Logger("GeckoViewActivityContextDelegate")
|
||||||
|
init {
|
||||||
|
if (contextRef.get() == null) {
|
||||||
|
logger.warn("Activity context is null.")
|
||||||
|
} else if (contextRef.get() !is Activity) {
|
||||||
|
logger.warn("A non-activity context was set.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by GeckoView to get an Activity context for operations such as printing.
|
||||||
|
* @return An active Activity context or null.
|
||||||
|
*/
|
||||||
|
override fun getActivityContext(): Context? {
|
||||||
|
val context = contextRef.get()
|
||||||
|
if ((context == null)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.autofill
|
||||||
|
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.toAutocompleteAddress
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.toCreditCardEntry
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.toLoginEntry
|
||||||
|
import mozilla.components.concept.storage.CreditCard
|
||||||
|
import mozilla.components.concept.storage.CreditCardsAddressesStorageDelegate
|
||||||
|
import mozilla.components.concept.storage.Login
|
||||||
|
import mozilla.components.concept.storage.LoginStorageDelegate
|
||||||
|
import org.mozilla.geckoview.Autocomplete
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gecko credit card and login storage delegate that handles runtime storage requests. This allows
|
||||||
|
* the Gecko runtime to call the underlying storage to handle requests for fetching, saving and
|
||||||
|
* updating of autocomplete items in the storage.
|
||||||
|
*
|
||||||
|
* @param creditCardsAddressesStorageDelegate An instance of [CreditCardsAddressesStorageDelegate].
|
||||||
|
* Provides methods for retrieving [CreditCard]s from the underlying storage.
|
||||||
|
* @param loginStorageDelegate An instance of [LoginStorageDelegate].
|
||||||
|
* Provides read/write methods for the [Login] storage.
|
||||||
|
*/
|
||||||
|
class GeckoAutocompleteStorageDelegate(
|
||||||
|
private val creditCardsAddressesStorageDelegate: CreditCardsAddressesStorageDelegate,
|
||||||
|
private val loginStorageDelegate: LoginStorageDelegate,
|
||||||
|
) : Autocomplete.StorageDelegate {
|
||||||
|
|
||||||
|
override fun onAddressFetch(): GeckoResult<Array<Autocomplete.Address>>? {
|
||||||
|
val result = GeckoResult<Array<Autocomplete.Address>>()
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
GlobalScope.launch(IO) {
|
||||||
|
val addresses = creditCardsAddressesStorageDelegate.onAddressesFetch()
|
||||||
|
.map { it.toAutocompleteAddress() }
|
||||||
|
.toTypedArray()
|
||||||
|
|
||||||
|
result.complete(addresses)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreditCardFetch(): GeckoResult<Array<Autocomplete.CreditCard>> {
|
||||||
|
val result = GeckoResult<Array<Autocomplete.CreditCard>>()
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
GlobalScope.launch(IO) {
|
||||||
|
val key = creditCardsAddressesStorageDelegate.getOrGenerateKey()
|
||||||
|
|
||||||
|
val creditCards = creditCardsAddressesStorageDelegate.onCreditCardsFetch()
|
||||||
|
.mapNotNull {
|
||||||
|
val plaintextCardNumber =
|
||||||
|
creditCardsAddressesStorageDelegate.decrypt(key, it.encryptedCardNumber)?.number
|
||||||
|
|
||||||
|
if (plaintextCardNumber == null) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
Autocomplete.CreditCard.Builder()
|
||||||
|
.guid(it.guid)
|
||||||
|
.name(it.billingName)
|
||||||
|
.number(plaintextCardNumber)
|
||||||
|
.expirationMonth(it.expiryMonth.toString())
|
||||||
|
.expirationYear(it.expiryYear.toString())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toTypedArray()
|
||||||
|
|
||||||
|
result.complete(creditCards)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreditCardSave(creditCard: Autocomplete.CreditCard) {
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
GlobalScope.launch(IO) {
|
||||||
|
creditCardsAddressesStorageDelegate.onCreditCardSave(creditCard.toCreditCardEntry())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoginSave(login: Autocomplete.LoginEntry) {
|
||||||
|
loginStorageDelegate.onLoginSave(login.toLoginEntry())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoginFetch(domain: String): GeckoResult<Array<Autocomplete.LoginEntry>> {
|
||||||
|
val result = GeckoResult<Array<Autocomplete.LoginEntry>>()
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
GlobalScope.launch(IO) {
|
||||||
|
val storedLogins = loginStorageDelegate.onLoginFetch(domain)
|
||||||
|
|
||||||
|
val logins = storedLogins.await()
|
||||||
|
.map { it.toLoginEntry() }
|
||||||
|
.toTypedArray()
|
||||||
|
|
||||||
|
result.complete(logins)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.content.blocking
|
||||||
|
|
||||||
|
import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a site that will be ignored by the tracking protection policies.
|
||||||
|
* @property url The url of the site to be ignored.
|
||||||
|
* @property privateMode Indicates if this exception should persisted in private mode.
|
||||||
|
* @property contentPermission The associated gecko content permission of this exception.
|
||||||
|
*/
|
||||||
|
data class GeckoTrackingProtectionException(
|
||||||
|
override val url: String,
|
||||||
|
val privateMode: Boolean = false,
|
||||||
|
val contentPermission: ContentPermission,
|
||||||
|
) : TrackingProtectionException
|
|
@ -0,0 +1,128 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.cookiebanners
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import mozilla.components.browser.engine.gecko.await
|
||||||
|
import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingMode
|
||||||
|
import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingMode.DISABLED
|
||||||
|
import mozilla.components.concept.engine.cookiehandling.CookieBannersStorage
|
||||||
|
import mozilla.components.support.base.log.logger.Logger
|
||||||
|
import org.mozilla.geckoview.GeckoRuntime
|
||||||
|
import org.mozilla.geckoview.StorageController
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A storage to store [CookieBannerHandlingMode] using GeckoView APIs.
|
||||||
|
*/
|
||||||
|
class GeckoCookieBannersStorage(
|
||||||
|
runtime: GeckoRuntime,
|
||||||
|
private val reportSiteDomainsRepository: ReportSiteDomainsRepository,
|
||||||
|
) : CookieBannersStorage {
|
||||||
|
|
||||||
|
private val geckoStorage: StorageController = runtime.storageController
|
||||||
|
private val mainScope = CoroutineScope(Dispatchers.Main)
|
||||||
|
|
||||||
|
override suspend fun addException(
|
||||||
|
uri: String,
|
||||||
|
privateBrowsing: Boolean,
|
||||||
|
) {
|
||||||
|
setGeckoException(uri, DISABLED, privateBrowsing)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun isSiteDomainReported(siteDomain: String): Boolean {
|
||||||
|
return reportSiteDomainsRepository.isSiteDomainReported(siteDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun saveSiteDomain(siteDomain: String) {
|
||||||
|
reportSiteDomainsRepository.saveSiteDomain(siteDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun addPersistentExceptionInPrivateMode(uri: String) {
|
||||||
|
setPersistentPrivateGeckoException(uri, DISABLED)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findExceptionFor(
|
||||||
|
uri: String,
|
||||||
|
privateBrowsing: Boolean,
|
||||||
|
): CookieBannerHandlingMode? {
|
||||||
|
return queryExceptionInGecko(uri, privateBrowsing)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun hasException(uri: String, privateBrowsing: Boolean): Boolean? {
|
||||||
|
val result = findExceptionFor(uri, privateBrowsing)
|
||||||
|
return if (result != null) {
|
||||||
|
result == DISABLED
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun removeException(uri: String, privateBrowsing: Boolean) {
|
||||||
|
removeGeckoException(uri, privateBrowsing)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun removeGeckoException(uri: String, privateBrowsing: Boolean) {
|
||||||
|
geckoStorage.removeCookieBannerModeForDomain(uri, privateBrowsing)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun setGeckoException(
|
||||||
|
uri: String,
|
||||||
|
mode: CookieBannerHandlingMode,
|
||||||
|
privateBrowsing: Boolean,
|
||||||
|
) {
|
||||||
|
geckoStorage.setCookieBannerModeForDomain(
|
||||||
|
uri,
|
||||||
|
mode.mode,
|
||||||
|
privateBrowsing,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun setPersistentPrivateGeckoException(
|
||||||
|
uri: String,
|
||||||
|
mode: CookieBannerHandlingMode,
|
||||||
|
) {
|
||||||
|
geckoStorage.setCookieBannerModeAndPersistInPrivateBrowsingForDomain(
|
||||||
|
uri,
|
||||||
|
mode.mode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
@Suppress("TooGenericExceptionCaught")
|
||||||
|
internal suspend fun queryExceptionInGecko(
|
||||||
|
uri: String,
|
||||||
|
privateBrowsing: Boolean,
|
||||||
|
): CookieBannerHandlingMode? {
|
||||||
|
return try {
|
||||||
|
withContext(mainScope.coroutineContext) {
|
||||||
|
geckoStorage.getCookieBannerModeForDomain(uri, privateBrowsing).await()
|
||||||
|
?.toCookieBannerHandlingMode() ?: throw IllegalArgumentException(
|
||||||
|
"An error happened trying to find cookie banners mode for the " +
|
||||||
|
"uri $uri and private browsing mode $privateBrowsing",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// This normally happen on internal sites like about:config or ip sites.
|
||||||
|
val disabledErrors = listOf("NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS", "NS_ERROR_HOST_IS_IP_ADDRESS")
|
||||||
|
if (disabledErrors.any { (e.message ?: "").contains(it) }) {
|
||||||
|
Logger("GeckoCookieBannersStorage").error("Unable to query cookie banners exception", e)
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun Int.toCookieBannerHandlingMode(): CookieBannerHandlingMode {
|
||||||
|
return CookieBannerHandlingMode.entries.first { it.mode == this }
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.cookiebanners
|
||||||
|
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.core.emptyPreferences
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import mozilla.components.browser.engine.gecko.cookiebanners.ReportSiteDomainsRepository.PreferencesKeys.REPORT_SITE_DOMAINS
|
||||||
|
import mozilla.components.support.base.log.logger.Logger
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A repository to save reported site domains with the datastore API.
|
||||||
|
*/
|
||||||
|
class ReportSiteDomainsRepository(
|
||||||
|
private val dataStore: DataStore<Preferences>,
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val SEPARATOR = "@<;>@"
|
||||||
|
const val REPORT_SITE_DOMAINS_REPOSITORY_NAME = "report_site_domains_preferences"
|
||||||
|
const val PREFERENCE_KEY_NAME = "report_site_domains"
|
||||||
|
}
|
||||||
|
|
||||||
|
private object PreferencesKeys {
|
||||||
|
val REPORT_SITE_DOMAINS = stringPreferencesKey(PREFERENCE_KEY_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given site's domain url is saved locally.
|
||||||
|
* @param siteDomain the [siteDomain] that will be checked.
|
||||||
|
*/
|
||||||
|
suspend fun isSiteDomainReported(siteDomain: String): Boolean {
|
||||||
|
return dataStore.data
|
||||||
|
.catch { exception ->
|
||||||
|
if (exception is IOException) {
|
||||||
|
Logger.error("Error reading preferences.", exception)
|
||||||
|
emit(emptyPreferences())
|
||||||
|
} else {
|
||||||
|
throw exception
|
||||||
|
}
|
||||||
|
}.map { preferences ->
|
||||||
|
val reportSiteDomainsString = preferences[REPORT_SITE_DOMAINS] ?: ""
|
||||||
|
val reportSiteDomainsList =
|
||||||
|
reportSiteDomainsString.split(SEPARATOR).filter { it.isNotEmpty() }
|
||||||
|
reportSiteDomainsList.contains(siteDomain)
|
||||||
|
}.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the given site's domain url in datastore to keep it persistent locally.
|
||||||
|
* This method gets called after the site domain was reported with Nimbus.
|
||||||
|
* @param siteDomain the [siteDomain] that will be saved.
|
||||||
|
*/
|
||||||
|
suspend fun saveSiteDomain(siteDomain: String) {
|
||||||
|
dataStore.edit { preferences ->
|
||||||
|
val siteDomainsPreferences = preferences[REPORT_SITE_DOMAINS] ?: ""
|
||||||
|
val siteDomainsList = siteDomainsPreferences.split(SEPARATOR).filter { it.isNotEmpty() }
|
||||||
|
if (siteDomainsList.contains(siteDomain)) {
|
||||||
|
return@edit
|
||||||
|
}
|
||||||
|
val domains = mutableListOf<String>()
|
||||||
|
domains.addAll(siteDomainsList)
|
||||||
|
domains.add(siteDomain)
|
||||||
|
preferences[REPORT_SITE_DOMAINS] = domains.joinToString(SEPARATOR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 mozilla.components.browser.engine.gecko.ext
|
||||||
|
|
||||||
|
import mozilla.components.concept.storage.Address
|
||||||
|
import org.mozilla.geckoview.Autocomplete
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a GeckoView [Autocomplete.Address] to an Android Components [Address].
|
||||||
|
*/
|
||||||
|
fun Autocomplete.Address.toAddress() = Address(
|
||||||
|
guid = guid ?: "",
|
||||||
|
name = name,
|
||||||
|
organization = organization,
|
||||||
|
streetAddress = streetAddress,
|
||||||
|
addressLevel3 = addressLevel3,
|
||||||
|
addressLevel2 = addressLevel2,
|
||||||
|
addressLevel1 = addressLevel1,
|
||||||
|
postalCode = postalCode,
|
||||||
|
country = country,
|
||||||
|
tel = tel,
|
||||||
|
email = email,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an Android Components [Address] to a GeckoView [Autocomplete.Address].
|
||||||
|
*/
|
||||||
|
fun Address.toAutocompleteAddress() = Autocomplete.Address.Builder()
|
||||||
|
.guid(guid)
|
||||||
|
.name(name)
|
||||||
|
.organization(organization)
|
||||||
|
.streetAddress(streetAddress)
|
||||||
|
.addressLevel3(addressLevel3)
|
||||||
|
.addressLevel2(addressLevel2)
|
||||||
|
.addressLevel1(addressLevel1)
|
||||||
|
.postalCode(postalCode)
|
||||||
|
.country(country)
|
||||||
|
.tel(tel)
|
||||||
|
.email(email)
|
||||||
|
.build()
|
|
@ -0,0 +1,32 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.ext
|
||||||
|
|
||||||
|
import mozilla.components.concept.storage.CreditCardEntry
|
||||||
|
import mozilla.components.support.utils.creditCardIIN
|
||||||
|
import org.mozilla.geckoview.Autocomplete
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a GeckoView [Autocomplete.CreditCard] to an Android Components [CreditCardEntry].
|
||||||
|
*/
|
||||||
|
fun Autocomplete.CreditCard.toCreditCardEntry() = CreditCardEntry(
|
||||||
|
guid = guid,
|
||||||
|
name = name,
|
||||||
|
number = number,
|
||||||
|
expiryMonth = expirationMonth,
|
||||||
|
expiryYear = expirationYear,
|
||||||
|
cardType = number.creditCardIIN()?.creditCardIssuerNetwork?.name ?: "",
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an Android Components [CreditCardEntry] to a GeckoView [Autocomplete.CreditCard].
|
||||||
|
*/
|
||||||
|
fun CreditCardEntry.toAutocompleteCreditCard() = Autocomplete.CreditCard.Builder()
|
||||||
|
.guid(guid)
|
||||||
|
.name(name)
|
||||||
|
.number(number)
|
||||||
|
.expirationMonth(expiryMonth)
|
||||||
|
.expirationYear(expiryYear)
|
||||||
|
.build()
|
|
@ -0,0 +1,31 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.ext
|
||||||
|
|
||||||
|
import mozilla.components.browser.engine.gecko.prompt.GeckoChoice
|
||||||
|
import mozilla.components.concept.engine.prompt.Choice
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a GeckoView [GeckoChoice] to an Android Components [Choice].
|
||||||
|
*/
|
||||||
|
private fun GeckoChoice.toChoice(): Choice {
|
||||||
|
val choiceChildren = items?.map { it.toChoice() }?.toTypedArray()
|
||||||
|
// On the GeckoView docs states that label is a @NonNull, but on run-time
|
||||||
|
// we are getting null values
|
||||||
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1771149
|
||||||
|
@Suppress("USELESS_ELVIS")
|
||||||
|
return Choice(id, !disabled, label ?: "", selected, separator, choiceChildren)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an array of [GeckoChoice] to Choice array.
|
||||||
|
* @return array of Choice
|
||||||
|
*/
|
||||||
|
fun convertToChoices(
|
||||||
|
geckoChoices: Array<out GeckoChoice>,
|
||||||
|
): Array<Choice> = geckoChoices.map { geckoChoice ->
|
||||||
|
val choice = geckoChoice.toChoice()
|
||||||
|
choice
|
||||||
|
}.toTypedArray()
|
|
@ -0,0 +1,25 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.ext
|
||||||
|
|
||||||
|
import mozilla.components.browser.engine.gecko.GeckoEngineSession
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_ALLOW
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_TRACKING
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this Gecko permission is a tracking protection permission and it is excluded
|
||||||
|
* from the tracking protection policies.
|
||||||
|
*/
|
||||||
|
val ContentPermission.isExcludedForTrackingProtection: Boolean
|
||||||
|
get() = this.permission == PERMISSION_TRACKING &&
|
||||||
|
value == VALUE_ALLOW
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the tracking protection permission for the given [GeckoEngineSession].
|
||||||
|
* This is available after every onLocationChange call.
|
||||||
|
*/
|
||||||
|
val GeckoEngineSession.geckoTrackingProtectionPermission: ContentPermission?
|
||||||
|
get() = this.geckoPermissions.find { it.permission == PERMISSION_TRACKING }
|
|
@ -0,0 +1,43 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.ext
|
||||||
|
|
||||||
|
import mozilla.components.concept.storage.Login
|
||||||
|
import mozilla.components.concept.storage.LoginEntry
|
||||||
|
import org.mozilla.geckoview.Autocomplete
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a GeckoView [Autocomplete.LoginEntry] to an Android Components [LoginEntry].
|
||||||
|
*/
|
||||||
|
fun Autocomplete.LoginEntry.toLoginEntry() = LoginEntry(
|
||||||
|
origin = origin,
|
||||||
|
formActionOrigin = formActionOrigin,
|
||||||
|
httpRealm = httpRealm,
|
||||||
|
username = username,
|
||||||
|
password = password,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an Android Components [Login] to a GeckoView [Autocomplete.LoginEntry].
|
||||||
|
*/
|
||||||
|
fun Login.toLoginEntry() = Autocomplete.LoginEntry.Builder()
|
||||||
|
.guid(guid)
|
||||||
|
.origin(origin)
|
||||||
|
.formActionOrigin(formActionOrigin)
|
||||||
|
.httpRealm(httpRealm)
|
||||||
|
.username(username)
|
||||||
|
.password(password)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an Android Components [LoginEntry] to a GeckoView [Autocomplete.LoginEntry].
|
||||||
|
*/
|
||||||
|
fun LoginEntry.toLoginEntry() = Autocomplete.LoginEntry.Builder()
|
||||||
|
.origin(origin)
|
||||||
|
.formActionOrigin(formActionOrigin)
|
||||||
|
.httpRealm(httpRealm)
|
||||||
|
.username(username)
|
||||||
|
.password(password)
|
||||||
|
.build()
|
|
@ -0,0 +1,82 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.ext
|
||||||
|
|
||||||
|
import mozilla.components.concept.engine.EngineSession
|
||||||
|
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
|
||||||
|
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory
|
||||||
|
import org.mozilla.geckoview.ContentBlocking
|
||||||
|
import org.mozilla.geckoview.GeckoRuntimeSettings
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a [TrackingProtectionPolicy] into a GeckoView setting that can be used with [GeckoRuntimeSettings.Builder].
|
||||||
|
* Also contains the cookie banner handling settings for regular and private browsing.
|
||||||
|
*/
|
||||||
|
@Suppress("SpreadOperator")
|
||||||
|
fun TrackingProtectionPolicy.toContentBlockingSetting(
|
||||||
|
safeBrowsingPolicy: Array<EngineSession.SafeBrowsingPolicy> = arrayOf(EngineSession.SafeBrowsingPolicy.RECOMMENDED),
|
||||||
|
cookieBannerHandlingMode: EngineSession.CookieBannerHandlingMode = EngineSession.CookieBannerHandlingMode.DISABLED,
|
||||||
|
cookieBannerHandlingModePrivateBrowsing: EngineSession.CookieBannerHandlingMode =
|
||||||
|
EngineSession.CookieBannerHandlingMode.REJECT_ALL,
|
||||||
|
cookieBannerHandlingDetectOnlyMode: Boolean = false,
|
||||||
|
cookieBannerGlobalRulesEnabled: Boolean = false,
|
||||||
|
cookieBannerGlobalRulesSubFramesEnabled: Boolean = false,
|
||||||
|
queryParameterStripping: Boolean = false,
|
||||||
|
queryParameterStrippingPrivateBrowsing: Boolean = false,
|
||||||
|
queryParameterStrippingAllowList: String = "",
|
||||||
|
queryParameterStrippingStripList: String = "",
|
||||||
|
) = ContentBlocking.Settings.Builder().apply {
|
||||||
|
enhancedTrackingProtectionLevel(getEtpLevel())
|
||||||
|
antiTracking(getAntiTrackingPolicy())
|
||||||
|
cookieBehavior(cookiePolicy.id)
|
||||||
|
cookieBehaviorPrivateMode(cookiePolicyPrivateMode.id)
|
||||||
|
cookiePurging(cookiePurging)
|
||||||
|
safeBrowsing(safeBrowsingPolicy.sumOf { it.id })
|
||||||
|
strictSocialTrackingProtection(getStrictSocialTrackingProtection())
|
||||||
|
cookieBannerHandlingMode(cookieBannerHandlingMode.mode)
|
||||||
|
cookieBannerHandlingModePrivateBrowsing(cookieBannerHandlingModePrivateBrowsing.mode)
|
||||||
|
cookieBannerHandlingDetectOnlyMode(cookieBannerHandlingDetectOnlyMode)
|
||||||
|
cookieBannerGlobalRulesEnabled(cookieBannerGlobalRulesEnabled)
|
||||||
|
cookieBannerGlobalRulesSubFramesEnabled(cookieBannerGlobalRulesSubFramesEnabled)
|
||||||
|
queryParameterStrippingEnabled(queryParameterStripping)
|
||||||
|
queryParameterStrippingPrivateBrowsingEnabled(queryParameterStrippingPrivateBrowsing)
|
||||||
|
queryParameterStrippingAllowList(*queryParameterStrippingAllowList.split(",").toTypedArray())
|
||||||
|
queryParameterStrippingStripList(*queryParameterStrippingStripList.split(",").toTypedArray())
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether [TrackingCategory.STRICT] is enabled in the [TrackingProtectionPolicy].
|
||||||
|
*/
|
||||||
|
internal fun TrackingProtectionPolicy.getStrictSocialTrackingProtection(): Boolean {
|
||||||
|
return strictSocialTrackingProtection ?: trackingCategories.contains(TrackingCategory.STRICT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the [TrackingProtectionPolicy] categories as an Enhanced Tracking Protection level for GeckoView.
|
||||||
|
*/
|
||||||
|
internal fun TrackingProtectionPolicy.getEtpLevel(): Int {
|
||||||
|
return when {
|
||||||
|
trackingCategories.contains(TrackingCategory.NONE) -> ContentBlocking.EtpLevel.NONE
|
||||||
|
else -> ContentBlocking.EtpLevel.STRICT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the [TrackingProtectionPolicy] as a tracking policy for GeckoView.
|
||||||
|
*/
|
||||||
|
internal fun TrackingProtectionPolicy.getAntiTrackingPolicy(): Int {
|
||||||
|
/**
|
||||||
|
* The [TrackingProtectionPolicy.TrackingCategory.SCRIPTS_AND_SUB_RESOURCES] is an
|
||||||
|
* artificial category, created with the sole purpose of going around this bug
|
||||||
|
* https://bugzilla.mozilla.org/show_bug.cgi?id=1579264, for this reason we have to
|
||||||
|
* remove its value from the valid anti tracking categories, when is present.
|
||||||
|
*/
|
||||||
|
val total = trackingCategories.sumOf { it.id }
|
||||||
|
return if (contains(TrackingCategory.SCRIPTS_AND_SUB_RESOURCES)) {
|
||||||
|
total - TrackingCategory.SCRIPTS_AND_SUB_RESOURCES.id
|
||||||
|
} else {
|
||||||
|
total
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,139 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.fetch
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import mozilla.components.concept.fetch.Client
|
||||||
|
import mozilla.components.concept.fetch.Headers
|
||||||
|
import mozilla.components.concept.fetch.MutableHeaders
|
||||||
|
import mozilla.components.concept.fetch.Request
|
||||||
|
import mozilla.components.concept.fetch.Response
|
||||||
|
import mozilla.components.concept.fetch.Response.Companion.SUCCESS
|
||||||
|
import mozilla.components.concept.fetch.isBlobUri
|
||||||
|
import mozilla.components.concept.fetch.isDataUri
|
||||||
|
import org.mozilla.geckoview.GeckoRuntime
|
||||||
|
import org.mozilla.geckoview.GeckoWebExecutor
|
||||||
|
import org.mozilla.geckoview.WebRequest
|
||||||
|
import org.mozilla.geckoview.WebRequest.CACHE_MODE_DEFAULT
|
||||||
|
import org.mozilla.geckoview.WebRequest.CACHE_MODE_RELOAD
|
||||||
|
import org.mozilla.geckoview.WebRequestError
|
||||||
|
import org.mozilla.geckoview.WebResponse
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.SocketTimeoutException
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.TimeoutException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GeckoView ([GeckoWebExecutor]) based implementation of [Client].
|
||||||
|
*/
|
||||||
|
class GeckoViewFetchClient(
|
||||||
|
context: Context,
|
||||||
|
runtime: GeckoRuntime = GeckoRuntime.getDefault(context),
|
||||||
|
private val maxReadTimeOut: Pair<Long, TimeUnit> = Pair(MAX_READ_TIMEOUT_MINUTES, TimeUnit.MINUTES),
|
||||||
|
) : Client() {
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal var executor: GeckoWebExecutor = GeckoWebExecutor(runtime)
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun fetch(request: Request): Response {
|
||||||
|
if (request.isDataUri()) {
|
||||||
|
return fetchDataUri(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
val webRequest = request.toWebRequest()
|
||||||
|
|
||||||
|
val readTimeOut = request.readTimeout ?: maxReadTimeOut
|
||||||
|
val readTimeOutMillis = readTimeOut.let { (timeout, unit) ->
|
||||||
|
unit.toMillis(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val webResponse = executor.fetch(webRequest, request.fetchFlags).poll(readTimeOutMillis)
|
||||||
|
webResponse?.toResponse() ?: throw IOException("Fetch failed with null response")
|
||||||
|
} catch (e: TimeoutException) {
|
||||||
|
throw SocketTimeoutException()
|
||||||
|
} catch (e: WebRequestError) {
|
||||||
|
throw IOException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val Request.fetchFlags: Int
|
||||||
|
get() {
|
||||||
|
var fetchFlags = 0
|
||||||
|
if (cookiePolicy == Request.CookiePolicy.OMIT) {
|
||||||
|
fetchFlags += GeckoWebExecutor.FETCH_FLAGS_ANONYMOUS
|
||||||
|
}
|
||||||
|
if (private) {
|
||||||
|
fetchFlags += GeckoWebExecutor.FETCH_FLAGS_PRIVATE
|
||||||
|
}
|
||||||
|
if (redirect == Request.Redirect.MANUAL) {
|
||||||
|
fetchFlags += GeckoWebExecutor.FETCH_FLAGS_NO_REDIRECTS
|
||||||
|
}
|
||||||
|
return fetchFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val MAX_READ_TIMEOUT_MINUTES = 5L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Request.toWebRequest(): WebRequest = WebRequest.Builder(url)
|
||||||
|
.method(method.name)
|
||||||
|
.addHeadersFrom(this)
|
||||||
|
.addBodyFrom(this)
|
||||||
|
.referrer(referrerUrl)
|
||||||
|
.cacheMode(if (useCaches) CACHE_MODE_DEFAULT else CACHE_MODE_RELOAD)
|
||||||
|
.beConservative(conservative)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private fun WebRequest.Builder.addHeadersFrom(request: Request): WebRequest.Builder {
|
||||||
|
request.headers?.forEach { header ->
|
||||||
|
addHeader(header.name, header.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun WebRequest.Builder.addBodyFrom(request: Request): WebRequest.Builder {
|
||||||
|
request.body?.let { body ->
|
||||||
|
body.useStream { inStream ->
|
||||||
|
val bytes = inStream.readBytes()
|
||||||
|
val buffer = ByteBuffer.allocateDirect(bytes.size)
|
||||||
|
buffer.put(bytes)
|
||||||
|
this.body(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun WebResponse.toResponse(): Response {
|
||||||
|
val isDataUri = uri.startsWith("data:")
|
||||||
|
val isBlobUri = uri.startsWith("blob:")
|
||||||
|
val headers = translateHeaders(this)
|
||||||
|
// We use the same API for blobs, data URLs and HTTP requests, but blobs won't receive a status code.
|
||||||
|
// If no exception is thrown we assume success.
|
||||||
|
val status = if (isBlobUri || isDataUri) SUCCESS else statusCode
|
||||||
|
return Response(
|
||||||
|
uri,
|
||||||
|
status,
|
||||||
|
headers,
|
||||||
|
body?.let {
|
||||||
|
Response.Body(it, headers["Content-Type"])
|
||||||
|
} ?: Response.Body.empty(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun translateHeaders(webResponse: WebResponse): Headers {
|
||||||
|
val headers = MutableHeaders()
|
||||||
|
webResponse.headers.forEach { (k, v) ->
|
||||||
|
v.split(",").forEach { headers.append(k, it.trim()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.integration
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import mozilla.components.support.utils.ext.registerReceiverCompat
|
||||||
|
import org.mozilla.geckoview.GeckoRuntime
|
||||||
|
import androidx.core.os.LocaleListCompat as LocaleList
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to set the locales setting for geckoview, updating from the locale of the device.
|
||||||
|
*/
|
||||||
|
class LocaleSettingUpdater(
|
||||||
|
private val context: Context,
|
||||||
|
private val runtime: GeckoRuntime,
|
||||||
|
) : SettingUpdater<Array<String>>() {
|
||||||
|
|
||||||
|
override var value: Array<String> = findValue()
|
||||||
|
set(value) {
|
||||||
|
runtime.settings.locales = value
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
private val localeChangedReceiver by lazy {
|
||||||
|
object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
|
updateValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun registerForUpdates() {
|
||||||
|
context.registerReceiverCompat(
|
||||||
|
localeChangedReceiver,
|
||||||
|
IntentFilter(Intent.ACTION_LOCALE_CHANGED),
|
||||||
|
ContextCompat.RECEIVER_NOT_EXPORTED,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unregisterForUpdates() {
|
||||||
|
context.unregisterReceiver(localeChangedReceiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findValue(): Array<String> {
|
||||||
|
val localeList = LocaleList.getAdjustedDefault()
|
||||||
|
return arrayOfNulls<Unit>(localeList.size())
|
||||||
|
.mapIndexedNotNull { i, _ -> localeList.get(i)?.toLanguageTag() }
|
||||||
|
.toTypedArray()
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue