Compare commits
No commits in common. "v131" and "v125" have entirely different histories.
|
@ -1,78 +0,0 @@
|
||||||
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
|
|
|
@ -1,40 +0,0 @@
|
||||||
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
Normal file → Executable file
0
.github/workflows/disabled_workflows/build-contributor-pr.yml
vendored
Normal file → Executable file
0
.github/workflows/disabled_workflows/fenix-sync-strings.yml
vendored
Normal file → Executable file
0
.github/workflows/disabled_workflows/fenix-sync-strings.yml
vendored
Normal file → Executable file
0
.github/workflows/disabled_workflows/fenix-update-nimbus-experiments.yml
vendored
Normal file → Executable file
0
.github/workflows/disabled_workflows/fenix-update-nimbus-experiments.yml
vendored
Normal file → Executable file
0
.github/workflows/disabled_workflows/glean-probe-scraper.yml
vendored
Normal file → Executable file
0
.github/workflows/disabled_workflows/glean-probe-scraper.yml
vendored
Normal file → Executable file
0
.github/workflows/disabled_workflows/reopen-for-qa-needed.yml
vendored
Normal file → Executable file
0
.github/workflows/disabled_workflows/reopen-for-qa-needed.yml
vendored
Normal file → Executable file
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
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.
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit f03a5f2688acc3faffe488c75cea6658d079fea3
|
File diff suppressed because it is too large
Load Diff
|
@ -1,8 +0,0 @@
|
||||||
# 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
|
|
|
@ -1,15 +0,0 @@
|
||||||
# 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
|
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
,ich,pop-os,12.10.2024 11:30,file:///home/ich/.config/libreoffice/4;
|
|
|
@ -1,2 +0,0 @@
|
||||||
The changelog is now hosted here:
|
|
||||||
https://mozilla-mobile.github.io/android-components/changelog/
|
|
|
@ -1,325 +0,0 @@
|
||||||
# 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/
|
|
|
@ -1,19 +0,0 @@
|
||||||
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'
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,133 +0,0 @@
|
||||||
#!/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")
|
|
|
@ -1,31 +0,0 @@
|
||||||
# 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
|
|
|
@ -1,31 +0,0 @@
|
||||||
# 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
|
|
|
@ -1,144 +0,0 @@
|
||||||
#!/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())
|
|
|
@ -1,93 +0,0 @@
|
||||||
#!/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()
|
|
|
@ -1,164 +0,0 @@
|
||||||
#!/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
|
|
|
@ -1,356 +0,0 @@
|
||||||
// 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 }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,67 +0,0 @@
|
||||||
# [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/
|
|
|
@ -1,40 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
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)
|
|
|
@ -1,21 +0,0 @@
|
||||||
# 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
|
|
|
@ -1,4 +0,0 @@
|
||||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
|
||||||
<manifest />
|
|
|
@ -1,50 +0,0 @@
|
||||||
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
|
|
|
@ -1,49 +0,0 @@
|
||||||
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
|
|
|
@ -1,50 +0,0 @@
|
||||||
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
|
|
|
@ -1,50 +0,0 @@
|
||||||
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
|
|
|
@ -1,49 +0,0 @@
|
||||||
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
|
|
|
@ -1,444 +0,0 @@
|
||||||
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
|
|
|
@ -1,50 +0,0 @@
|
||||||
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
|
|
|
@ -1,50 +0,0 @@
|
||||||
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
|
|
|
@ -1,50 +0,0 @@
|
||||||
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
|
|
|
@ -1,50 +0,0 @@
|
||||||
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
|
|
|
@ -1,49 +0,0 @@
|
||||||
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
|
|
|
@ -1,50 +0,0 @@
|
||||||
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
|
|
|
@ -1,49 +0,0 @@
|
||||||
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
|
|
|
@ -1,68 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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)
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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) }
|
|
||||||
}
|
|
|
@ -1,151 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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)
|
|
||||||
}
|
|
|
@ -1,84 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,110 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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)
|
|
||||||
}
|
|
|
@ -1,140 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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) }
|
|
|
@ -1,81 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,116 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
@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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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.")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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"))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,85 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
sdk=34
|
|
|
@ -1,41 +0,0 @@
|
||||||
# [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/
|
|
|
@ -1,195 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
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()}"
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
# 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.
|
|
|
@ -1,24 +0,0 @@
|
||||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
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
|
|
|
@ -1,30 +0,0 @@
|
||||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
# 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
|
|
|
@ -1,21 +0,0 @@
|
||||||
# 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
|
|
|
@ -1,126 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
|
||||||
<manifest />
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,73 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,247 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,146 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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)
|
|
||||||
}
|
|
|
@ -1,257 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,110 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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
|
|
|
@ -1,128 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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 }
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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()
|
|
|
@ -1,32 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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()
|
|
|
@ -1,31 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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()
|
|
|
@ -1,25 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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 }
|
|
|
@ -1,43 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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()
|
|
|
@ -1,82 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,139 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package 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