Compare commits
2 Commits
Author | SHA1 | Date |
---|---|---|
harvey186 | ff6bef97c7 | |
harvey186 | 7a6de5f3e6 |
23
README.md
23
README.md
|
@ -1,8 +1,9 @@
|
||||||
# LeOSium Browser! [![CI](https://github.com/fork-maintainers/iceraven-browser/actions/workflows/ci.yml/badge.svg)](https://github.com/fork-maintainers/iceraven-browser/actions/workflows/ci.yml) ![Release](https://img.shields.io/github/v/release/fork-maintainers/iceraven-browser)
|
# Iceraven Browser! [![CI](https://github.com/fork-maintainers/iceraven-browser/actions/workflows/ci.yml/badge.svg)](https://github.com/fork-maintainers/iceraven-browser/actions/workflows/ci.yml) ![Release](https://img.shields.io/github/v/release/fork-maintainers/iceraven-browser)
|
||||||
|
|
||||||
Definitely not brought to you by Mozilla!
|
Definitely not brought to you by Mozilla!
|
||||||
|
|
||||||
LeOSium 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.
|
||||||
|
|
||||||
|
@ -11,15 +12,15 @@ Notable features include:
|
||||||
* The ability to *attempt* to install a much longer list of add-ons than Mozilla's Fenix version of Firefox accepts. Currently the browser queries [this AMO collection](https://addons.mozilla.org/en-US/firefox/collections/16201230/What-I-want-on-Fenix/) **Most of them will not work**, because they depend on code that Mozilla is still working on writing in `android-components`, but you may attempt to install them. If you don't see an add-on you want, you can [request it](https://github.com/fork-maintainers/iceraven-browser/issues/new).
|
* The ability to *attempt* to install a much longer list of add-ons than Mozilla's Fenix version of Firefox accepts. Currently the browser queries [this AMO collection](https://addons.mozilla.org/en-US/firefox/collections/16201230/What-I-want-on-Fenix/) **Most of them will not work**, because they depend on code that Mozilla is still working on writing in `android-components`, but you may attempt to install them. If you don't see an add-on you want, you can [request it](https://github.com/fork-maintainers/iceraven-browser/issues/new).
|
||||||
* Option to suspend tabs to avoid being killed for memory (https://bugzilla.mozilla.org/show_bug.cgi?id=1807364)
|
* Option to suspend tabs to avoid being killed for memory (https://bugzilla.mozilla.org/show_bug.cgi?id=1807364)
|
||||||
* Option not to display recently visited websites at HomePage
|
* Option not to display recently visited websites at HomePage
|
||||||
* **No warranties or guarantees of security or updates or even stability**! Note that LeOSium Browser includes some unstable code written by Mozilla, with our own added modifications on top, all shipped with the stable version of GeckoView engine. Hence, the browser may contain bugs introduced upstream. Binaries are currently built automatically by our Github release automation. These binaries are signed with a debug key. When we finally publish this somewhere official like F-droid, we will sign the apks with a proper key suitable for public release. Due to the current way we create the releases and sign them, you may not want to rely on such "alpha" quality software as your primary web browser, as it will have bugs. So, use this browser only if you are comfortable with these limitations/potential risks.
|
* **No warranties or guarantees of security or updates or even stability**! Note that Iceraven Browser includes some unstable code written by Mozilla, with our own added modifications on top, all shipped with the stable version of GeckoView engine. Hence, the browser may contain bugs introduced upstream. Binaries are currently built automatically by our Github release automation. These binaries are signed with a debug key. When we finally publish this somewhere official like F-droid, we will sign the apks with a proper key suitable for public release. Due to the current way we create the releases and sign them, you may not want to rely on such "alpha" quality software as your primary web browser, as it will have bugs. So, use this browser only if you are comfortable with these limitations/potential risks.
|
||||||
|
|
||||||
**Note/Disclaimer:** LeOSium Browser could not exist without the hardworking folks at the Mozilla Corporation who work on the Mozilla Android Components and Firefox projects, but it is not an official Mozilla product, and is not provided, endorsed, vetted, approved, or secured by Mozilla.
|
**Note/Disclaimer:** Iceraven Browser could not exist without the hardworking folks at the Mozilla Corporation who work on the Mozilla Android Components and Firefox projects, but it is not an official Mozilla product, and is not provided, endorsed, vetted, approved, or secured by Mozilla.
|
||||||
|
|
||||||
In addition, we intend to try to cut down on telemetry and proprietary code to as great of an extent as possible as long as doing so does not compromise the user experience or make the fork too hard to maintain. Right now, we believe that no telemetry should be being sent to Mozilla anymore, but we cannot guarantee this; data may still be sent. Because of the way we have implemented this, the app may still appear to contain trackers when analyzed by tools that look for the presence of known tracking libraries. These detected trackers should actually be non-functional substitutes, many of which are sourced [from here](https://gitlab.com/relan/fennecbuild/-/blob/master/fenix-liberate.patch). **If you catch the app actually sending data to Mozilla, Adjust, Leanplum, Firebase, or any other such service, please open an issue!** Presumably any data that reaches Mozilla is governed by Mozilla's privacy policy, but as LeOSium Browser is, again **not a Mozilla product**, we can make no promises.
|
In addition, we intend to try to cut down on telemetry and proprietary code to as great of an extent as possible as long as doing so does not compromise the user experience or make the fork too hard to maintain. Right now, we believe that no telemetry should be being sent to Mozilla anymore, but we cannot guarantee this; data may still be sent. Because of the way we have implemented this, the app may still appear to contain trackers when analyzed by tools that look for the presence of known tracking libraries. These detected trackers should actually be non-functional substitutes, many of which are sourced [from here](https://gitlab.com/relan/fennecbuild/-/blob/master/fenix-liberate.patch). **If you catch the app actually sending data to Mozilla, Adjust, Leanplum, Firebase, or any other such service, please open an issue!** Presumably any data that reaches Mozilla is governed by Mozilla's privacy policy, but as Iceraven Browser is, again **not a Mozilla product**, we can make no promises.
|
||||||
|
|
||||||
LeOSium Browser combines the power of Fenix (of which we are a fork) and the spirit of Fennec, with a respectful nod toward the grand tradition of Netscape Navigator, from which all Gecko-based projects came, including the earliest of our predecessors, the old Mozilla Phoenix and Mozilla Firefox desktop browsers.
|
Iceraven Browser combines the power of Fenix (of which we are a fork) and the spirit of Fennec, with a respectful nod toward the grand tradition of Netscape Navigator, from which all Gecko-based projects came, including the earliest of our predecessors, the old Mozilla Phoenix and Mozilla Firefox desktop browsers.
|
||||||
|
|
||||||
That said, LeOSium Browser is an independent all-volunteer project, and has no affiliation with Netscape, Netscape Navigator, Mozilla, Mozilla Firefox, Mozila Phoenix, Debian, Debian Iceweasel, Parabola GNU/Linux-libre Iceweasel, America Online, or Verizon, among others. :) Basically, if you don't like the browser, it's not their fault. :)
|
That said, Iceraven Browser is an independent all-volunteer project, and has no affiliation with Netscape, Netscape Navigator, Mozilla, Mozilla Firefox, Mozila Phoenix, Debian, Debian Iceweasel, Parabola GNU/Linux-libre Iceweasel, America Online, or Verizon, among others. :) Basically, if you don't like the browser, it's not their fault. :)
|
||||||
|
|
||||||
## 📥 Installation
|
## 📥 Installation
|
||||||
|
|
||||||
|
@ -27,9 +28,11 @@ Right now, releases are published as `.apk` files, through Github. You should do
|
||||||
|
|
||||||
1. **Determine what version you need**. If you have a newer, 64-bit device, or a device with more than 4 GB of memory, you probably want the `arm64-v8a` version. **Any ordinary phone or tablet should be able to use the `armeabi-v7a` version**, but it will be limited to using no more than 4 GB of memory. You almost certainly don't want the `x86` or `x86_64` versions; they are in case you are running Android on a PC.
|
1. **Determine what version you need**. If you have a newer, 64-bit device, or a device with more than 4 GB of memory, you probably want the `arm64-v8a` version. **Any ordinary phone or tablet should be able to use the `armeabi-v7a` version**, but it will be limited to using no more than 4 GB of memory. You almost certainly don't want the `x86` or `x86_64` versions; they are in case you are running Android on a PC.
|
||||||
|
|
||||||
3. **Install the APK**. You will need to enable installation of apps from "unknown" (to Google) sources, and installatiuon of apps *by* whatever app you used to open the downloaded APK (i.e. your browser or file manager). Android will try to dissuade you from doing this, and suggest that it is dangerous. LeOSium is a browser for people who enjoy danger.
|
2. [**Download the APK for the latest release from the Releases page**](https://github.com/fork-maintainers/iceraven-browser/releases). Make sure to pick the version you chose in step 1.
|
||||||
|
|
||||||
4. **Enjoy LeOSium**. Make sure to install the add-ons that are essential for you in the main menu under "Add-Ons". You may want to set LeOSium as your device's default browser app. If you do this, it will be able to provide so-called "Chrome" [custom tabs](https://developers.google.com/web/android/custom-tabs) for other applications, allowing you to use your add-ons there.
|
3. **Install the APK**. You will need to enable installation of apps from "unknown" (to Google) sources, and installatiuon of apps *by* whatever app you used to open the downloaded APK (i.e. your browser or file manager). Android will try to dissuade you from doing this, and suggest that it is dangerous. Iceraven is a browser for people who enjoy danger.
|
||||||
|
|
||||||
|
4. **Enjoy Iceraven**. Make sure to install the add-ons that are essential for you in the main menu under "Add-Ons". You may want to set Iceraven as your device's default browser app. If you do this, it will be able to provide so-called "Chrome" [custom tabs](https://developers.google.com/web/android/custom-tabs) for other applications, allowing you to use your add-ons there.
|
||||||
|
|
||||||
## 🔨 Building
|
## 🔨 Building
|
||||||
|
|
||||||
|
@ -76,7 +79,7 @@ cd iceraven-browser
|
||||||
echo "autosignReleaseWithDebugKey=" >> local.properties
|
echo "autosignReleaseWithDebugKey=" >> local.properties
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Build the project. To build the LeOSium-branded release APKs, you can do:
|
6. Build the project. To build the Iceraven-branded release APKs, you can do:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./gradlew app:assemblefenixForkRelease -PversionName="$(git describe --tags HEAD)"
|
./gradlew app:assemblefenixForkRelease -PversionName="$(git describe --tags HEAD)"
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,8 @@
|
||||||
|
# Maven group ID used for all components
|
||||||
|
componentsGroupId: "org.mozilla.components"
|
||||||
|
|
||||||
|
# Synchronized build configuration for all modules
|
||||||
|
jvmTargetCompatibility: 17
|
||||||
|
compileSdkVersion: 35
|
||||||
|
minSdkVersion: 21
|
||||||
|
targetSdkVersion: 34
|
|
@ -0,0 +1,15 @@
|
||||||
|
# This is an .editorconfig that ktlint will "stop" at.
|
||||||
|
|
||||||
|
# If an .editorconfig file exists in any parent directory of the checkout
|
||||||
|
# directory, ktlint will fail because it uses those settings to determine what
|
||||||
|
# the indentation should be.
|
||||||
|
|
||||||
|
root = True
|
||||||
|
|
||||||
|
[*.kt]
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
|
||||||
|
ij_kotlin_allow_trailing_comma_on_call_site=true
|
||||||
|
ij_kotlin_allow_trailing_comma=true
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
# Treat generated API docs as binary. We do not want them to pollute
|
||||||
|
# our diffs.
|
||||||
|
docs/api/**/* -text -diff linguist-generated=true
|
||||||
|
|
||||||
|
# Treat the public suffix list file as text
|
||||||
|
**/publicsuffixes diff
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
,ich,pop-os,12.10.2024 11:30,file:///home/ich/.config/libreoffice/4;
|
|
@ -0,0 +1,2 @@
|
||||||
|
The changelog is now hosted here:
|
||||||
|
https://mozilla-mobile.github.io/android-components/changelog/
|
|
@ -0,0 +1,325 @@
|
||||||
|
# Android components
|
||||||
|
|
||||||
|
[![Task Status](https://firefox-ci-tc.services.mozilla.com/api/github/v1/repository/mozilla-mobile/android-components/main/badge.svg)](https://firefox-ci-tc.services.mozilla.com/api/github/v1/repository/mozilla-mobile/android-components/main/latest)
|
||||||
|
[![Mergify Status](https://img.shields.io/endpoint.svg?url=https://gh.mergify.io/badges/mozilla-mobile/android-components&style=flat)](https://mergify.io)
|
||||||
|
[![chat.mozilla.org](https://img.shields.io/badge/chat-on%20matrix-51bb9c)](https://chat.mozilla.org/#/room/#android-components:mozilla.org)
|
||||||
|
|
||||||
|
_A collection of Android libraries to build browsers or browser-like applications._
|
||||||
|
|
||||||
|
ℹ️ For more information **[see the website](https://mozilla-mobile.github.io/android-components/)**.
|
||||||
|
|
||||||
|
A fully-featured reference browser implementation based on the components can be found in the [reference-browser repository](https://github.com/mozilla-mobile/reference-browser).
|
||||||
|
|
||||||
|
# Getting Involved
|
||||||
|
|
||||||
|
We encourage you to participate in this open source project. We love pull requests, bug reports, ideas, (security) code reviews or any kind of positive contribution.
|
||||||
|
|
||||||
|
Before you attempt to make a contribution please read the [Community Participation Guidelines](https://www.mozilla.org/en-US/about/governance/policies/participation/).
|
||||||
|
|
||||||
|
* Matrix: [android-components:mozilla.org chat room](https://chat.mozilla.org/#/room/#android-components:mozilla.org) ([How to connect](https://wiki.mozilla.org/Matrix#Connect_to_Matrix)).
|
||||||
|
|
||||||
|
* Localization happens on [Pontoon](https://pontoon.mozilla.org/projects/android-l10n/). Please get in touch with delphine (at) mozilla (dot) com directly for more information.
|
||||||
|
|
||||||
|
# Maven repository
|
||||||
|
|
||||||
|
All components are getting published on [maven.mozilla.org](https://maven.mozilla.org/).
|
||||||
|
To use them, you need to add the following to your project's top-level build file, in the `allprojects` block (see e.g. the [reference-browser](https://github.com/mozilla-mobile/reference-browser/blob/main/build.gradle)):
|
||||||
|
|
||||||
|
```groovy
|
||||||
|
repositories {
|
||||||
|
maven {
|
||||||
|
url "https://maven.mozilla.org/maven2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each module that uses a component needs to specify it in its build file, in the `dependencies` block. For example, to use the `Base` component (in the `support`) collection, you need:
|
||||||
|
|
||||||
|
```groovy
|
||||||
|
dependencies {
|
||||||
|
implementation 'org.mozilla.components:support-base:+'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nightly builds
|
||||||
|
|
||||||
|
Nightly builds are created every day from the `main` branch and published on [nightly.maven.mozilla.org](https://nightly.maven.mozilla.org).
|
||||||
|
|
||||||
|
# Components
|
||||||
|
|
||||||
|
* 🔴 **In Development** - Not ready to be used in shipping products.
|
||||||
|
* ⚪ **Preview** - This component is almost/partially ready and can be tested in products.
|
||||||
|
* 🔵 **Production ready** - Used by shipping products.
|
||||||
|
|
||||||
|
## Browser
|
||||||
|
|
||||||
|
High-level components for building browser(-like) apps.
|
||||||
|
|
||||||
|
* 🔵 [**Awesomebar**](components/browser/awesomebar/README.md) - A customizable [Awesome Bar](https://support.mozilla.org/en-US/kb/awesome-bar-search-firefox-bookmarks-history-tabs) implementation for browsers.
|
||||||
|
|
||||||
|
* 🔵 [**Domains**](components/browser/domains/README.md) Localized and customizable domain lists for auto-completion in browsers.
|
||||||
|
|
||||||
|
* 🔵 [**Engine-Gecko**](components/browser/engine-gecko/README.md) - *Engine* implementation based on [GeckoView](https://wiki.mozilla.org/Mobile/GeckoView).
|
||||||
|
|
||||||
|
* 🔵 [**Engine-System**](components/browser/engine-system/README.md) - *Engine* implementation based on the system's WebView.
|
||||||
|
|
||||||
|
* 🔵 [**Errorpages**](components/browser/errorpages/README.md) - Responsive browser error pages for Android apps.
|
||||||
|
|
||||||
|
* 🔵 [**Icons**](components/browser/icons/README.md) - A component for loading and storing website icons (like [Favicons](https://en.wikipedia.org/wiki/Favicon)).
|
||||||
|
|
||||||
|
* 🔵 [**Menu**](components/browser/menu/README.md) - A generic menu with customizable items primarily for browser toolbars.
|
||||||
|
|
||||||
|
* ⚪ [**Menu 2**](components/browser/menu2/README.md) - A generic menu with customizable items primarily for browser toolbars.
|
||||||
|
|
||||||
|
* 🔵 [**Session-Storage**](components/browser/session-storage/README.md) - Component for saving and restoring the browser state.
|
||||||
|
|
||||||
|
* 🔵 [**State**](components/browser/state/README.md) - Component for maintaining the centralized state of the browser and its components.
|
||||||
|
|
||||||
|
* 🔵 [**Storage-Sync**](components/browser/storage-sync/README.md) - A syncable implementation of browser storage backed by [application-services' Places lib](https://github.com/mozilla/application-services).
|
||||||
|
|
||||||
|
* 🔵 [**Tabstray**](components/browser/tabstray/README.md) - A customizable tabs tray for browsers.
|
||||||
|
|
||||||
|
* 🔵 [**Thumbnails**](components/browser/thumbnails/README.md) - A component for loading and storing website thumbnails (screenshot of the website).
|
||||||
|
|
||||||
|
* 🔵 [**Toolbar**](components/browser/toolbar/README.md) - A customizable toolbar for browsers.
|
||||||
|
|
||||||
|
## Concept
|
||||||
|
|
||||||
|
_API contracts and abstraction layers for browser components._
|
||||||
|
|
||||||
|
* 🔵 [**Awesomebar**](components/concept/awesomebar/README.md) - An abstract definition of an awesome bar component.
|
||||||
|
|
||||||
|
* 🔵 [**Engine**](components/concept/engine/README.md) - Abstraction layer that allows hiding the actual browser engine implementation.
|
||||||
|
|
||||||
|
* 🔵 [**Fetch**](components/concept/fetch/README.md) - An abstract definition of an HTTP client for fetching resources.
|
||||||
|
|
||||||
|
* 🔵 [**Push**](components/concept/push/README.md) - An abstract definition of a push service component.
|
||||||
|
|
||||||
|
* 🔵 [**Storage**](components/concept/storage/README.md) - Abstract definition of a browser storage component.
|
||||||
|
|
||||||
|
* 🔵 [**Tabstray**](components/concept/tabstray/README.md) - Abstract definition of a tabs tray component.
|
||||||
|
|
||||||
|
* 🔵 [**Toolbar**](components/concept/toolbar/README.md) - Abstract definition of a browser toolbar component.
|
||||||
|
|
||||||
|
## Feature
|
||||||
|
|
||||||
|
_Combined components to implement feature-specific use cases._
|
||||||
|
|
||||||
|
* 🔵 [**Accounts**](components/feature/accounts/README.md) - A component that connects an FxaAccountManager from [service-firefox-accounts](components/service/firefox-accounts/README.md) with [feature-tabs](components/feature/tabs/README.md) in order to facilitate authentication flows.
|
||||||
|
|
||||||
|
* 🔵 [**Accounts Push**](components/feature/accounts-push/README.md) - Feature of use cases for FxA Account that work with push support.
|
||||||
|
|
||||||
|
* 🔵 [**Autofill**](components/feature/autofill/README.md) - A component that provides support for Android's Autofill framework.
|
||||||
|
|
||||||
|
* 🔵 [**Awesomebar**](components/feature/awesomebar/README.md) - A component that connects a [concept-awesomebar](components/concept/awesomebar/README.md) implementation to a [concept-toolbar](components/concept/toolbar/README.md) implementation and provides implementations of various suggestion providers.
|
||||||
|
|
||||||
|
* 🔴 [**Containers**](components/feature/containers/README.md) - A component for working with contextual identities also known as containers.
|
||||||
|
|
||||||
|
* 🔵 [**Context Menu**](components/feature/contextmenu/README.md) - A component for displaying context menus when *long-pressing* web content.
|
||||||
|
|
||||||
|
* 🔵 [**Custom Tabs**](components/feature/customtabs/README.md) - A component for providing [Custom Tabs](https://developer.chrome.com/multidevice/android/customtabs) functionality in browsers.
|
||||||
|
|
||||||
|
* 🔵 [**Downloads**](components/feature/downloads/README.md) - A component to perform downloads using the [Android downloads manager](https://developer.android.com/reference/android/app/DownloadManager).
|
||||||
|
|
||||||
|
* 🔵 [**Intent**](components/feature/intent/README.md) - A component that provides intent processing functionality by combining various other feature modules.
|
||||||
|
|
||||||
|
* ⚪ [**Progressive Web Apps (PWA)**](components/feature/pwa/README.md) - A component that provides functionality for supporting Progressive Web Apps (PWA).
|
||||||
|
|
||||||
|
* 🔵 [**Reader View**](components/feature/readerview/README.md) - A component that provides Reader View functionality.
|
||||||
|
|
||||||
|
* 🔵 [**QR**](components/feature/qr/README.md) - A component that provides functionality for scanning QR codes.
|
||||||
|
|
||||||
|
* 🔵 [**Search**](components/feature/search/README.md) - A component that connects an (concept) engine implementation with the browser search module.
|
||||||
|
|
||||||
|
* 🔵 [**Session**](components/feature/session/README.md) - A component that connects an (concept) engine implementation with the browser session and storage modules.
|
||||||
|
|
||||||
|
* 🔵 [**Share**](components/feature/share/README.md) - Feature implementation for saving and sorting recent apps used for sharing.
|
||||||
|
|
||||||
|
* 🔵 [**Sync**](components/feature/sync/README.md) -A component that provides synchronization orchestration for groups of (concept) SyncableStore objects.
|
||||||
|
|
||||||
|
* 🔵 [**Tabs**](components/feature/tabs/README.md) - A component that connects a tabs tray implementation with the session and toolbar modules.
|
||||||
|
|
||||||
|
* 🔵 [**Tab Collections**](components/feature/tab-collections/README.md) - Feature implementation for saving, restoring and organizing collections of tabs.
|
||||||
|
|
||||||
|
* 🔵 [**Toolbar**](components/feature/toolbar/README.md) - A component that connects a (concept) toolbar implementation with the browser session module.
|
||||||
|
|
||||||
|
* 🔵 [**Top Sites**](components/feature/top-sites/README.md) - Feature implementation for saving and removing top sites.
|
||||||
|
|
||||||
|
* 🔵 [**Prompts**](components/feature/prompts/README.md) - A component that will handle all the common prompt dialogs from web content.
|
||||||
|
|
||||||
|
* 🔵 [**Push**](components/feature/push/README.md) - A component that provides Autopush messages with help from a supported push service.
|
||||||
|
|
||||||
|
* 🔵 [**Find In Page**](components/feature/findinpage/README.md) - A component that provides an UI widget for [find in page functionality](https://support.mozilla.org/en-US/kb/search-contents-current-page-text-or-links).
|
||||||
|
|
||||||
|
* 🔵 [**Remote Tabs**](components/feature/remotetabs/README.md) - Feature that provides access to other device's tabs in the same account.
|
||||||
|
|
||||||
|
* 🔵 [**Site Permissions**](components/feature/sitepermissions/README.md) - A feature for showing site permission request prompts.
|
||||||
|
|
||||||
|
* 🔵 [**WebAuthn**](components/feature/webauthn/README.md) - A feature that provides WebAuthn functionality for supported engines.
|
||||||
|
|
||||||
|
* 🔵 [**Web Notifications**](components/feature/webnotifications/README.md) - A component for displaying web notifications.
|
||||||
|
|
||||||
|
* 🔵 [**WebCompat**](components/feature/webcompat/README.md) - A feature to enable website-hotfixing via the Web Compatibility System-Addon.
|
||||||
|
|
||||||
|
* 🔵 [**WebCompat Reporter**](components/feature/webcompat-reporter/README.md) - A feature that enables users to report site issues to Mozilla's Web Compatibility team for further diagnosis.
|
||||||
|
|
||||||
|
* 🔵 [**Web Add-ons**](components/feature/addons/README.md) - A feature that provides functionality for managing add-ons.
|
||||||
|
|
||||||
|
## UI
|
||||||
|
|
||||||
|
_Generic low-level UI components for building apps._
|
||||||
|
|
||||||
|
* 🔵 [**Autocomplete**](components/ui/autocomplete/README.md) - A set of components to provide autocomplete functionality.
|
||||||
|
|
||||||
|
* 🔵 [**Colors**](components/ui/colors/README.md) - The standard set of [Photon](https://design.firefox.com/photon/) colors.
|
||||||
|
|
||||||
|
* 🔵 [**Fonts**](components/ui/fonts/README.md) - The standard set of fonts used by Mozilla Android products.
|
||||||
|
|
||||||
|
* 🔵 [**Icons**](components/ui/icons/README.md) - A collection of often used browser icons.
|
||||||
|
|
||||||
|
* 🔵 [**Tabcounter**](components/ui/tabcounter/README.md) - A button that shows the current tab count and can animate state changes.
|
||||||
|
|
||||||
|
## Service
|
||||||
|
|
||||||
|
_Components and libraries to interact with backend services._
|
||||||
|
|
||||||
|
* 🔵 [**Firefox Accounts (FxA)**](components/service/firefox-accounts/README.md) - A library for integrating with Firefox Accounts.
|
||||||
|
|
||||||
|
* 🔵 [**Firefox Sync - Logins**](components/service/sync-logins/README.md) - A library for integrating with Firefox Sync - Logins.
|
||||||
|
|
||||||
|
* 🔵 [**Firefox Sync - Autofill**](components/service/sync-autofill/README.md) - A library for integrating with Firefox Sync - Autofill.
|
||||||
|
|
||||||
|
* 🔵 [**Glean**](components/service/glean/README.md) - A client-side telemetry SDK for collecting metrics and sending them to Mozilla's telemetry service (eventually replacing [service-telemetry](components/service/telemetry/README.md)).
|
||||||
|
|
||||||
|
* 🔵 [**Location**](components/service/location/README.md) - A library for accessing Mozilla's and other location services.
|
||||||
|
|
||||||
|
* 🔴 [**Nimbus**](components/service/nimbus/README.md) - A wrapper for the Nimbus SDK.
|
||||||
|
|
||||||
|
* 🔵 [**Pocket**](components/service/pocket/README.md) - A library for communicating with the Pocket API.
|
||||||
|
|
||||||
|
* 🔵 [**Contile**](components/service/contile/README.md) - A library for communicating with the Contile services API.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
_Supporting components with generic helper code._
|
||||||
|
|
||||||
|
* 🔵 [**Android Test**](components/support/android-test/README.md) - A collection of helpers for testing components in instrumented (on device) tests (`src/androidTest`).
|
||||||
|
|
||||||
|
* 🔵 [**Base**](components/support/base/README.md) - Base component containing building blocks for components.
|
||||||
|
|
||||||
|
* 🔵 [**Ktx**](components/support/ktx/README.md) - A set of Kotlin extensions on top of the Android framework and Kotlin standard library.
|
||||||
|
|
||||||
|
* 🔵 [**Test**](components/support/test/README.md) - A collection of helpers for testing components in local unit tests (`src/test`).
|
||||||
|
|
||||||
|
* 🔵 [**Test Appservices**](components/support/test-appservices/README.md) - A component for synchronizing Application Services' unit testing dependencies used in Android Components.
|
||||||
|
|
||||||
|
* 🔵 [**Test LibState**](components/support/test-libstate/README.md) - A collection of helpers for testing functionality that relies on the lib-state component in local unit tests (`src/test`).
|
||||||
|
|
||||||
|
* 🔵 [**Utils**](components/support/utils/README.md) - Generic utility classes to be shared between projects.
|
||||||
|
|
||||||
|
* 🔵 [**Webextensions**](components/support/webextensions/README.md) - A component containing building blocks for features implemented as web extensions.
|
||||||
|
|
||||||
|
## Standalone libraries
|
||||||
|
|
||||||
|
* 🔵 [**Crash**](components/lib/crash/README.md) - A generic crash reporter component that can report crashes to multiple services.
|
||||||
|
|
||||||
|
* 🔵 [**Dataprotect**](components/lib/dataprotect/README.md) - A component using AndroidKeyStore to protect user data.
|
||||||
|
|
||||||
|
* 🔵 [**Fetch-HttpURLConnection**](components/lib/fetch-httpurlconnection/README.md) - A [concept-fetch](concept/fetch/README.md) implementation using [HttpURLConnection](https://developer.android.com/reference/java/net/HttpURLConnection.html).
|
||||||
|
|
||||||
|
* 🔵 [**Fetch-OkHttp**](components/lib/fetch-okhttp/README.md) - A [concept-fetch](concept/fetch/README.md) implementation using [OkHttp](https://github.com/square/okhttp).
|
||||||
|
|
||||||
|
* ⚪ [**JEXL**](components/lib/jexl/README.md) - Javascript Expression Language: Context-based expression parser and evaluator.
|
||||||
|
|
||||||
|
* 🔵 [**Public Suffix List**](components/lib/publicsuffixlist/README.md) - A library for reading and using the [public suffix list](https://publicsuffix.org/).
|
||||||
|
|
||||||
|
* 🔵 [**Push-Firebase**](components/lib/push-firebase/README.md) - A [concept-push](concept/push/README.md) implementation using [Firebase Cloud Messaging](https://firebase.google.com/products/cloud-messaging/).
|
||||||
|
|
||||||
|
* 🔵 [**State**](components/lib/state/README.md) - A library for maintaining application state.
|
||||||
|
|
||||||
|
## Tooling
|
||||||
|
|
||||||
|
* 🔵 [**Fetch-Tests**](components/tooling/fetch-tests/README.md) - A generic test suite for components that implement [concept-fetch](concept/fetch/README.md).
|
||||||
|
|
||||||
|
* 🔵 [**Lint**](components/tooling/lint/README.md) - Custom Lint rules for the components repository.
|
||||||
|
|
||||||
|
# Sample apps
|
||||||
|
|
||||||
|
_Sample apps using various components._
|
||||||
|
|
||||||
|
* [**Browser**](samples/browser) - A simple browser composed from browser components. This sample application is only a very basic browser. For a full-featured reference browser implementation see the **[reference-browser repository](https://github.com/mozilla-mobile/reference-browser)**.
|
||||||
|
|
||||||
|
* [**Crash**](samples/crash) - An app showing the integration of the `lib-crash` component.
|
||||||
|
|
||||||
|
* [**Firefox Accounts (FxA)**](samples/firefox-accounts) - A simple app demoing Firefox Accounts integration.
|
||||||
|
|
||||||
|
* [**Firefox Sync**](samples/sync) - A simple app demoing general Firefox Sync integration, with bookmarks and history.
|
||||||
|
|
||||||
|
* [**Firefox Sync - Logins**](samples/sync-logins) - A simple app demoing Firefox Sync (Logins) integration.
|
||||||
|
|
||||||
|
* [**DataProtect**](samples/dataprotect) - An app demoing how to use the [**Dataprotect**](components/lib/dataprotect/README.md) component to load and store encrypted data in `SharedPreferences`.
|
||||||
|
|
||||||
|
* [**Glean**](samples/glean) - An app demoing how to use the [**Glean**](components/service/glean/README.md) library to collect and send telemetry data.
|
||||||
|
|
||||||
|
* [**Toolbar**](samples/toolbar) - An app demoing multiple customized toolbars using the [**browser-toolbar**](components/browser/toolbar/README.md) component.
|
||||||
|
|
||||||
|
# Building #
|
||||||
|
|
||||||
|
## Command line ##
|
||||||
|
|
||||||
|
```
|
||||||
|
$ git clone https://github.com/mozilla-mobile/android-components.git
|
||||||
|
$ cd android-components
|
||||||
|
$ ./gradlew assemble
|
||||||
|
```
|
||||||
|
|
||||||
|
## Android Studio ##
|
||||||
|
|
||||||
|
If the environment variable `JAVA_HOME` is not defined, you will need to set it. If you would like to use the JDK installed by Android Studio, here's how to find it:
|
||||||
|
|
||||||
|
1. Open Android Studio.
|
||||||
|
2. Select "Configure".
|
||||||
|
3. Select "Default Project Structure". You should now see the Android JDK location.
|
||||||
|
4. Set the environment variable `JAVA_HOME` to the location. (How you set an environment variable depends on your OS.)
|
||||||
|
5. Restart Android Studio.
|
||||||
|
|
||||||
|
Once the environment variable is set, you can import the project into Android Studio with the default wizard options.
|
||||||
|
|
||||||
|
If your build fails, you may find you get more instructive error messages by attempting the build at the command line.
|
||||||
|
|
||||||
|
# Coding Standards #
|
||||||
|
|
||||||
|
## Style ##
|
||||||
|
We follow the style enforced by [ktlint](https://ktlint.github.io/) and [detekt](https://github.com/detekt/detekt). See [how to configure Android Studio appropriately](https://github.com/pinterest/ktlint#option-1-recommended).
|
||||||
|
|
||||||
|
To check your style, run:
|
||||||
|
|
||||||
|
```
|
||||||
|
./gradlew ktlint
|
||||||
|
./gradlew detekt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation ##
|
||||||
|
We use `README.md` files for each component.
|
||||||
|
|
||||||
|
If you fix a bug or change an API, you should update [docs/changelog.md](https://github.com/mozilla-mobile/android-components/blob/main/docs/changelog.md).
|
||||||
|
|
||||||
|
## Testing ##
|
||||||
|
You are expected to both add tests for code that you write and make sure that your changes do not
|
||||||
|
cause existing tests to fail. You may find these command lines helpful:
|
||||||
|
|
||||||
|
```
|
||||||
|
./gradlew test # Run all tests
|
||||||
|
./gradlew :support-ktx:testdebugunittest # Run unit tests for a specified module
|
||||||
|
```
|
||||||
|
|
||||||
|
See also [how to measure code coverage](https://mozac.org/contributing/code-coverage).
|
||||||
|
|
||||||
|
## Accessibility ##
|
||||||
|
If your code has user-facing changes, follow [Android accessibility best practices](https://github.com/mozilla-mobile/shared-docs/blob/main/android/accessibility_guide.md).
|
||||||
|
|
||||||
|
# License
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/
|
|
@ -0,0 +1,19 @@
|
||||||
|
android {
|
||||||
|
lint {
|
||||||
|
warningsAsErrors true
|
||||||
|
abortOnError (project.name != "support-test")
|
||||||
|
|
||||||
|
// With our L10N process its totally possible to have missing or (temporarily) extra translations.
|
||||||
|
disable 'MissingTranslation',
|
||||||
|
'ExtraTranslation',
|
||||||
|
// We do not want to enforce this as a generic rule for all languages (see #6117, #6056, #6118)
|
||||||
|
'TypographyEllipsis',
|
||||||
|
// https://github.com/mozilla-mobile/android-components/issues/10641
|
||||||
|
'UnspecifiedImmutableFlag',
|
||||||
|
// https://github.com/mozilla-mobile/android-components/issues/10643
|
||||||
|
'UnusedResources',
|
||||||
|
// "We do not impose rules on locales"
|
||||||
|
// https://github.com/mozilla-mobile/android-components/pull/11069
|
||||||
|
'TypographyDashes'
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
|
||||||
|
# Purpose: Publish android packages to local maven repo, but only if changed since last publish.
|
||||||
|
# Dependencies: None
|
||||||
|
# Usage: ./automation/publish_to_maven_local_if_modified.py
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def fatal_err(msg):
|
||||||
|
print(f"\033[31mError: {msg}\033[0m")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def run_cmd_checked(*args, **kwargs):
|
||||||
|
"""Run a command, throwing an exception if it exits with non-zero status."""
|
||||||
|
kwargs["check"] = True
|
||||||
|
return subprocess.run(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def find_project_root():
|
||||||
|
"""Find the absolute path of the project repository root."""
|
||||||
|
# As a convention, we expect this file in [project-root]/automation/.
|
||||||
|
automation_dir = Path(__file__).parent
|
||||||
|
|
||||||
|
# Therefore the automation dir's parent is the project root we're looking for.
|
||||||
|
return automation_dir.parent
|
||||||
|
|
||||||
|
|
||||||
|
LAST_CONTENTS_HASH_FILE = ".lastAutoPublishContentsHash"
|
||||||
|
|
||||||
|
GITIGNORED_FILES_THAT_AFFECT_THE_BUILD = ["local.properties"]
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Publish android packages to local maven repo, but only if changed since last publish"
|
||||||
|
)
|
||||||
|
parser.parse_args()
|
||||||
|
|
||||||
|
root_dir = find_project_root()
|
||||||
|
if str(root_dir) != os.path.abspath(os.curdir):
|
||||||
|
fatal_err(
|
||||||
|
f"This only works if run from the repo root ({root_dir!r} != {os.path.abspath(os.curdir)!r})"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Calculate a hash reflecting the current state of the repo.
|
||||||
|
|
||||||
|
contents_hash = hashlib.sha256()
|
||||||
|
|
||||||
|
contents_hash.update(
|
||||||
|
run_cmd_checked(["git", "rev-parse", "HEAD"], capture_output=True).stdout
|
||||||
|
)
|
||||||
|
contents_hash.update(b"\x00")
|
||||||
|
|
||||||
|
# Get a diff of all tracked (staged and unstaged) files.
|
||||||
|
|
||||||
|
changes = run_cmd_checked(["git", "diff", "HEAD", "."], capture_output=True).stdout
|
||||||
|
contents_hash.update(changes)
|
||||||
|
contents_hash.update(b"\x00")
|
||||||
|
|
||||||
|
# But unfortunately it can only tell us the names of untracked
|
||||||
|
# files, and it won't tell us anything about files that are in
|
||||||
|
# .gitignore but can still affect the build.
|
||||||
|
|
||||||
|
untracked_files = []
|
||||||
|
|
||||||
|
# Get a list of all untracked files sans standard exclusions.
|
||||||
|
|
||||||
|
# -o is for getting other (i.e. untracked) files
|
||||||
|
# --exclude-standard is to handle standard Git exclusions: .git/info/exclude, .gitignore in each directory,
|
||||||
|
# and the user's global exclusion file.
|
||||||
|
changes_others = run_cmd_checked(
|
||||||
|
["git", "ls-files", "-o", "--exclude-standard"], capture_output=True
|
||||||
|
).stdout
|
||||||
|
changes_lines = iter(ln.strip() for ln in changes_others.split(b"\n"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
ln = next(changes_lines)
|
||||||
|
while ln:
|
||||||
|
untracked_files.append(ln)
|
||||||
|
ln = next(changes_lines)
|
||||||
|
except StopIteration:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Then, account for some excluded files that we care about.
|
||||||
|
untracked_files.extend(GITIGNORED_FILES_THAT_AFFECT_THE_BUILD)
|
||||||
|
|
||||||
|
# Finally, get hashes of everything.
|
||||||
|
# Skip files that don't exist, e.g. missing GITIGNORED_FILES_THAT_AFFECT_THE_BUILD. `hash-object` errors out if it gets
|
||||||
|
# a non-existent file, so we hope that disk won't change between this filter and the cmd run just below.
|
||||||
|
filtered_untracked = [nm for nm in untracked_files if os.path.isfile(nm)]
|
||||||
|
# Reading contents of the files is quite slow when there are lots of them, so delegate to `git hash-object`.
|
||||||
|
git_hash_object_cmd = ["git", "hash-object"]
|
||||||
|
git_hash_object_cmd.extend(filtered_untracked)
|
||||||
|
changes_untracked = run_cmd_checked(git_hash_object_cmd, capture_output=True).stdout
|
||||||
|
contents_hash.update(changes_untracked)
|
||||||
|
contents_hash.update(b"\x00")
|
||||||
|
|
||||||
|
contents_hash = contents_hash.hexdigest()
|
||||||
|
|
||||||
|
# If the contents hash has changed since last publish, re-publish.
|
||||||
|
last_contents_hash = ""
|
||||||
|
try:
|
||||||
|
with open(LAST_CONTENTS_HASH_FILE) as f:
|
||||||
|
last_contents_hash = f.read().strip()
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if contents_hash == last_contents_hash:
|
||||||
|
print("Contents have not changed, no need to publish")
|
||||||
|
else:
|
||||||
|
print("Contents have changed, publishing")
|
||||||
|
if sys.platform.startswith("win"):
|
||||||
|
run_cmd_checked(
|
||||||
|
["gradlew.bat", "publishToMavenLocal", f"-Plocal={time.time_ns()}"],
|
||||||
|
shell=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
run_cmd_checked(
|
||||||
|
["./gradlew", "publishToMavenLocal", f"-Plocal={time.time_ns()}"]
|
||||||
|
)
|
||||||
|
with open(LAST_CONTENTS_HASH_FILE, "w") as f:
|
||||||
|
f.write(contents_hash)
|
||||||
|
f.write("\n")
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Google Cloud Documentation: https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run
|
||||||
|
# Flank Documentation: https://flank.github.io/flank/
|
||||||
|
gcloud:
|
||||||
|
results-bucket: android-components_test_artifacts
|
||||||
|
record-video: true
|
||||||
|
timeout: 30m
|
||||||
|
async: false
|
||||||
|
num-flaky-test-attempts: 2
|
||||||
|
|
||||||
|
app: /APP/PATH
|
||||||
|
test: /TEST/PATH
|
||||||
|
|
||||||
|
auto-google-login: false
|
||||||
|
use-orchestrator: true
|
||||||
|
environment-variables:
|
||||||
|
clearPackageData: true
|
||||||
|
directories-to-pull:
|
||||||
|
- /sdcard/screenshots
|
||||||
|
performance-metrics: true
|
||||||
|
|
||||||
|
device:
|
||||||
|
- model: Pixel2.arm
|
||||||
|
version: 28
|
||||||
|
locale: en_US
|
||||||
|
|
||||||
|
flank:
|
||||||
|
project: GOOGLE_PROJECT
|
||||||
|
max-test-shards: -1
|
||||||
|
num-test-runs: 1
|
||||||
|
output-style: compact
|
||||||
|
full-junit-result: true
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Google Cloud Documentation: https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run
|
||||||
|
# Flank Documentation: https://flank.github.io/flank/
|
||||||
|
gcloud:
|
||||||
|
results-bucket: android-components_test_artifacts
|
||||||
|
record-video: true
|
||||||
|
timeout: 30m
|
||||||
|
async: false
|
||||||
|
num-flaky-test-attempts: 1
|
||||||
|
|
||||||
|
app: /app/path
|
||||||
|
test: /test/path
|
||||||
|
|
||||||
|
auto-google-login: false
|
||||||
|
use-orchestrator: true
|
||||||
|
environment-variables:
|
||||||
|
clearPackageData: true
|
||||||
|
directories-to-pull:
|
||||||
|
- /sdcard/screenshots
|
||||||
|
performance-metrics: true
|
||||||
|
|
||||||
|
device:
|
||||||
|
- model: Pixel2
|
||||||
|
version: 28
|
||||||
|
|
||||||
|
flank:
|
||||||
|
project: GOOGLE_PROJECT
|
||||||
|
max-test-shards: -1
|
||||||
|
num-test-runs: 1
|
||||||
|
output-style: compact
|
||||||
|
full-junit-result: true
|
||||||
|
repeat-tests: 1
|
|
@ -0,0 +1,144 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import xml
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from beautifultable import BeautifulTable
|
||||||
|
from junitparser import Attr, Failure, JUnitXml, TestCase, TestSuite
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(cmdln_args):
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Parse and print UI test JUnit results"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--results",
|
||||||
|
type=Path,
|
||||||
|
help="Directory containing task artifact results",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
return parser.parse_args(args=cmdln_args)
|
||||||
|
|
||||||
|
|
||||||
|
class test_suite(TestSuite):
|
||||||
|
flakes = Attr()
|
||||||
|
|
||||||
|
|
||||||
|
class test_case(TestCase):
|
||||||
|
flaky = Attr()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_print_failure_results(results):
|
||||||
|
"""
|
||||||
|
Parses the given JUnit test results and prints a formatted table of failures and flaky tests.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
results (JUnitXml): Parsed JUnit XML results.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The number of test failures.
|
||||||
|
|
||||||
|
The function processes each test suite and each test case within the suite.
|
||||||
|
If a test case has a result that is an instance of Failure, it is added to the table.
|
||||||
|
The test case is marked as 'Flaky' if the flaky attribute is set to "true", otherwise it is marked as 'Failure'.
|
||||||
|
|
||||||
|
Example of possible JUnit XML (FullJUnitReport.xml):
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="ExampleSuite" tests="2" failures="1" flakes="1" time="0.003">
|
||||||
|
<testcase classname="example.TestClass" name="testSuccess" flaky="true" time="0.001">
|
||||||
|
<failure message="Assertion failed">Expected true but was false</failure>
|
||||||
|
</testcase>
|
||||||
|
<testcase classname="example.TestClass" name="testFailure" time="0.002">
|
||||||
|
<failure message="Assertion failed">Expected true but was false</failure>
|
||||||
|
<failure message="Assertion failed">Expected true but was false</failure>
|
||||||
|
</testcase>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
"""
|
||||||
|
|
||||||
|
table = BeautifulTable(maxwidth=256)
|
||||||
|
table.columns.header = ["UI Test", "Outcome", "Details"]
|
||||||
|
table.columns.alignment = BeautifulTable.ALIGN_LEFT
|
||||||
|
table.set_style(BeautifulTable.STYLE_GRID)
|
||||||
|
|
||||||
|
failure_count = 0
|
||||||
|
|
||||||
|
# Dictionary to store the last seen failure details for each test case
|
||||||
|
last_seen_failures = {}
|
||||||
|
|
||||||
|
for suite in results:
|
||||||
|
cur_suite = test_suite.fromelem(suite)
|
||||||
|
for case in cur_suite:
|
||||||
|
cur_case = test_case.fromelem(case)
|
||||||
|
if cur_case.result:
|
||||||
|
for entry in case.result:
|
||||||
|
if isinstance(entry, Failure):
|
||||||
|
flaky_status = getattr(cur_case, "flaky", "false") == "true"
|
||||||
|
if flaky_status:
|
||||||
|
test_id = "%s#%s" % (case.classname, case.name)
|
||||||
|
details = (
|
||||||
|
entry.text.replace("\t", " ") if entry.text else ""
|
||||||
|
)
|
||||||
|
# Check if the current failure details are different from the last seen ones
|
||||||
|
if details != last_seen_failures.get(test_id, ""):
|
||||||
|
table.rows.append(
|
||||||
|
[
|
||||||
|
test_id,
|
||||||
|
"Flaky",
|
||||||
|
details,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
last_seen_failures[test_id] = details
|
||||||
|
else:
|
||||||
|
test_id = "%s#%s" % (case.classname, case.name)
|
||||||
|
details = (
|
||||||
|
entry.text.replace("\t", " ") if entry.text else ""
|
||||||
|
)
|
||||||
|
# Check if the current failure details are different from the last seen ones
|
||||||
|
if details != last_seen_failures.get(test_id, ""):
|
||||||
|
table.rows.append(
|
||||||
|
[
|
||||||
|
test_id,
|
||||||
|
"Failure",
|
||||||
|
details,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
print(f"TEST-UNEXPECTED-FAIL | {test_id} | {details}")
|
||||||
|
failure_count += 1
|
||||||
|
# Update the last seen failure details for this test case
|
||||||
|
last_seen_failures[test_id] = details
|
||||||
|
|
||||||
|
print(table)
|
||||||
|
return failure_count
|
||||||
|
|
||||||
|
|
||||||
|
def load_results_file(filename):
|
||||||
|
ret = None
|
||||||
|
try:
|
||||||
|
f = open(filename, "r")
|
||||||
|
try:
|
||||||
|
ret = JUnitXml.fromfile(f)
|
||||||
|
except xml.etree.ElementTree.ParseError as e:
|
||||||
|
print(f"Error parsing {filename} file: {e}")
|
||||||
|
finally:
|
||||||
|
f.close()
|
||||||
|
except IOError as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = parse_args(sys.argv[1:])
|
||||||
|
|
||||||
|
failure_count = 0
|
||||||
|
junitxml = load_results_file(args.results.joinpath("FullJUnitReport.xml"))
|
||||||
|
if junitxml:
|
||||||
|
failure_count = parse_print_failure_results(junitxml)
|
||||||
|
return failure_count
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
|
@ -0,0 +1,93 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(cmdln_args):
|
||||||
|
parser = argparse.ArgumentParser(description="Parse UI test logs an results")
|
||||||
|
parser.add_argument(
|
||||||
|
"--output-md",
|
||||||
|
type=argparse.FileType("w", encoding="utf-8"),
|
||||||
|
help="Output markdown file.",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--log",
|
||||||
|
type=argparse.FileType("r", encoding="utf-8"),
|
||||||
|
help="Log output of flank.",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--results", type=Path, help="Directory containing flank results", required=True
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--exit-code", type=int, help="Exit code of flank.", required=True
|
||||||
|
)
|
||||||
|
parser.add_argument("--device-type", help="Type of device ", required=True)
|
||||||
|
parser.add_argument(
|
||||||
|
"--report-treeherder-failures",
|
||||||
|
help="Report failures in treeherder format.",
|
||||||
|
required=False,
|
||||||
|
action="store_true",
|
||||||
|
)
|
||||||
|
|
||||||
|
return parser.parse_args(args=cmdln_args)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_android_args(log):
|
||||||
|
return yaml.safe_load(log.split("AndroidArgs\n")[1].split("RunTests\n")[0])
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = parse_args(sys.argv[1:])
|
||||||
|
|
||||||
|
log = args.log.read()
|
||||||
|
matrix_ids = json.loads(args.results.joinpath("matrix_ids.json").read_text())
|
||||||
|
# with args.results.joinpath("flank.yml") as f:
|
||||||
|
# flank_config = yaml.safe_load(f)
|
||||||
|
|
||||||
|
android_args = extract_android_args(log)
|
||||||
|
|
||||||
|
print = args.output_md.write
|
||||||
|
|
||||||
|
print("# Devices\n")
|
||||||
|
print(yaml.safe_dump(android_args["gcloud"]["device"]))
|
||||||
|
|
||||||
|
print("# Results\n")
|
||||||
|
print("| matrix | result | logs | details \n")
|
||||||
|
print("| --- | --- | --- | --- |\n")
|
||||||
|
for matrix, matrix_result in matrix_ids.items():
|
||||||
|
print(
|
||||||
|
"| {matrixId} | {outcome} | [logs]({webLink}) | {axes[0][details]}\n".format(
|
||||||
|
**matrix_result
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
args.report_treeherder_failures
|
||||||
|
and matrix_result["outcome"] != "success"
|
||||||
|
and matrix_result["outcome"] != "flaky"
|
||||||
|
):
|
||||||
|
# write failures to test log in format known to treeherder logviewer
|
||||||
|
sys.stdout.write(
|
||||||
|
f"TEST-UNEXPECTED-FAIL | {matrix_result['outcome']} | {matrix_result['webLink']} | {matrix_result['axes'][0]['details']}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("---\n")
|
||||||
|
print("# References & Documentation\n")
|
||||||
|
print(
|
||||||
|
"* [Automated UI Testing Documentation](https://github.com/mozilla-mobile/shared-docs/blob/main/android/ui-testing.md)\n"
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
"* Mobile Test Engineering on [Confluence](https://mozilla-hub.atlassian.net/wiki/spaces/MTE/overview) | [Slack](https://mozilla.slack.com/archives/C02KDDS9QM9) | [Alerts](https://mozilla.slack.com/archives/C0134KJ4JHL)\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
|
@ -0,0 +1,164 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
# This script does the following:
|
||||||
|
# 1. Retrieves glcoud service account token
|
||||||
|
# 2. Activates gcloud service account
|
||||||
|
# 3. Connects to google Firebase (using TestArmada's Flank tool)
|
||||||
|
# 4. Executes UI tests
|
||||||
|
# 5. Puts test artifacts into the test_artifacts folder
|
||||||
|
|
||||||
|
# NOTE:
|
||||||
|
# Flank supports sharding across multiple devices at a time, but gcloud API
|
||||||
|
# only supports 1 defined APK per test run.
|
||||||
|
|
||||||
|
|
||||||
|
# If a command fails then do not proceed and fail this script too.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
#########################
|
||||||
|
# The command line help #
|
||||||
|
#########################
|
||||||
|
display_help() {
|
||||||
|
echo "Usage: $0 Component_Name Build_Variant [Number_Shards...]"
|
||||||
|
echo
|
||||||
|
echo "Examples:"
|
||||||
|
echo "To run component/browser tests on ARM device shard (1 test / shard)"
|
||||||
|
echo "$ execute-firebase-test.sh component arm"
|
||||||
|
echo
|
||||||
|
echo "To run component/feature tests on X86 device (on 3 shards)"
|
||||||
|
echo "$ execute-firebase-test.sh feature x86 3"
|
||||||
|
echo
|
||||||
|
echo "To run UI samples/sampleName tests"
|
||||||
|
echo "$ execute-firebase-test.sh sample-sampleName arm 1"
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
# Basic parameter check
|
||||||
|
if [[ $# -lt 2 ]]; then
|
||||||
|
echo "Your command line contains $# arguments"
|
||||||
|
display_help
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
component="$1" # browser, concept, feature
|
||||||
|
device_type="$2" # arm | x86
|
||||||
|
if [[ ! -z "$3" ]]; then
|
||||||
|
num_shards=$3
|
||||||
|
fi
|
||||||
|
|
||||||
|
JAVA_BIN="/usr/bin/java"
|
||||||
|
PATH_TEST="./automation/taskcluster/androidTest"
|
||||||
|
FLANK_BIN="/builds/worker/test-tools/flank.jar"
|
||||||
|
FLANK_CONF_ARM="${PATH_TEST}/flank-arm.yml"
|
||||||
|
FLANK_CONF_X86="${PATH_TEST}/flank-x86.yml"
|
||||||
|
ARTIFACT_DIR="/builds/worker/artifacts"
|
||||||
|
RESULTS_DIR="${ARTIFACT_DIR}/results"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "ACTIVATE SERVICE ACCT"
|
||||||
|
echo
|
||||||
|
# this is where the Google Testcloud project ID is set
|
||||||
|
gcloud config set project "$GOOGLE_PROJECT"
|
||||||
|
echo
|
||||||
|
|
||||||
|
gcloud auth activate-service-account --key-file "$GOOGLE_APPLICATION_CREDENTIALS"
|
||||||
|
echo
|
||||||
|
echo
|
||||||
|
|
||||||
|
# From now on disable exiting on error. If the tests fail we want to continue
|
||||||
|
# and try to download the artifacts. We will exit with the actual error code later.
|
||||||
|
set +e
|
||||||
|
|
||||||
|
if [[ "${device_type,,}" == "x86" ]]
|
||||||
|
then
|
||||||
|
flank_template="$FLANK_CONF_X86"
|
||||||
|
else
|
||||||
|
flank_template="$FLANK_CONF_ARM"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove samples- from the component for each APK path
|
||||||
|
samples=${component//samples-}
|
||||||
|
|
||||||
|
# If tests are for components, the path is different than for samples
|
||||||
|
if [[ "${component}" != samples-* ]]
|
||||||
|
then
|
||||||
|
# Case 1: tests for any component (but NOT samples, NOT real UI tests)
|
||||||
|
APK_APP="./samples/browser/build/outputs/apk/gecko/debug/samples-browser-gecko-debug.apk"
|
||||||
|
if [[ "${component}" == *"-"* ]]
|
||||||
|
then
|
||||||
|
regex='([a-z]*)-(.*)'
|
||||||
|
[[ "$component" =~ $regex ]]
|
||||||
|
APK_TEST="./components/${BASH_REMATCH[1]}/${BASH_REMATCH[2]}/build/outputs/apk/androidTest/debug/${component}-debug-androidTest.apk"
|
||||||
|
else
|
||||||
|
APK_TEST="./components/${component}/engine-gecko/build/outputs/apk/androidTest/debug/browser-engine-gecko-debug-androidTest.apk"
|
||||||
|
fi
|
||||||
|
elif [[ "${component}" == "samples-browser" ]]
|
||||||
|
then
|
||||||
|
# Case 2: tests for browser sample (gecko sample only)
|
||||||
|
APK_APP="./samples/${samples}/build/outputs/apk/gecko/debug/samples-${samples}-gecko-debug.apk"
|
||||||
|
APK_TEST="./samples/${samples}/build/outputs/apk/androidTest/gecko/debug/samples-{$samples}-gecko-debug-androidTest.apk"
|
||||||
|
else
|
||||||
|
# Case 3: tests for non-browser samples (i.e. samples-glean)
|
||||||
|
APK_APP="./samples/${samples}/build/outputs/apk/debug/samples-${samples}-debug.apk"
|
||||||
|
APK_TEST="./samples/${samples}/build/outputs/apk/androidTest/debug/samples-${samples}-debug-androidTest.apk"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# function to exit script with exit code from test run.
|
||||||
|
# (Only 0 if all test executions passed)
|
||||||
|
function failure_check() {
|
||||||
|
echo
|
||||||
|
echo
|
||||||
|
if [[ $exitcode -ne 0 ]]; then
|
||||||
|
echo "ERROR: UI test run failed, please check above URL"
|
||||||
|
else
|
||||||
|
echo "All UI test(s) have passed!"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "RESULTS"
|
||||||
|
echo
|
||||||
|
mkdir -p ${ARTIFACT_DIR}/github
|
||||||
|
chmod +x ${PATH_TEST}/parse-ui-test.py
|
||||||
|
chmod +x ${PATH_TEST}/parse-ui-test-fromfile.py
|
||||||
|
${PATH_TEST}/parse-ui-test-fromfile.py \
|
||||||
|
--results "${RESULTS_DIR}"
|
||||||
|
if [[ $? -ne 0 ]]; then
|
||||||
|
${PATH_TEST}/parse-ui-test.py \
|
||||||
|
--exit-code "${exitcode}" \
|
||||||
|
--log flank.log \
|
||||||
|
--results "${RESULTS_DIR}" \
|
||||||
|
--output-md "${ARTIFACT_DIR}/github/customCheckRunText.md" \
|
||||||
|
--device-type "${device_type}"
|
||||||
|
else
|
||||||
|
${PATH_TEST}/parse-ui-test.py \
|
||||||
|
--exit-code "${exitcode}" \
|
||||||
|
--log flank.log \
|
||||||
|
--results "${RESULTS_DIR}" \
|
||||||
|
--output-md "${ARTIFACT_DIR}/github/customCheckRunText.md" \
|
||||||
|
--device-type "${device_type}" \
|
||||||
|
--report-treeherder-failures
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "EXECUTE TEST(S)"
|
||||||
|
echo
|
||||||
|
|
||||||
|
set -o pipefail && $JAVA_BIN -jar $FLANK_BIN android run \
|
||||||
|
--config=$flank_template \
|
||||||
|
--max-test-shards=$num_shards \
|
||||||
|
--app=$APK_APP --test=$APK_TEST \
|
||||||
|
--local-result-dir="${RESULTS_DIR}" \
|
||||||
|
--project=$GOOGLE_PROJECT \
|
||||||
|
--client-details=commit=${MOBILE_HEAD_REV:-None},pullRequest=${PULL_REQUEST_NUMBER:-None} \
|
||||||
|
| tee flank.log
|
||||||
|
|
||||||
|
exitcode=$?
|
||||||
|
failure_check
|
||||||
|
|
||||||
|
exit $exitcode
|
|
@ -0,0 +1,356 @@
|
||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
|
import io.gitlab.arturbosch.detekt.Detekt
|
||||||
|
import io.gitlab.arturbosch.detekt.DetektCreateBaselineTask
|
||||||
|
import org.gradle.internal.logging.text.StyledTextOutput.Style
|
||||||
|
import org.gradle.internal.logging.text.StyledTextOutputFactory
|
||||||
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
import static org.gradle.api.tasks.testing.TestResult.ResultType
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORIES.each { repository ->
|
||||||
|
maven {
|
||||||
|
url repository
|
||||||
|
if (gradle.mozconfig.substs.ALLOW_INSECURE_GRADLE_REPOSITORIES) {
|
||||||
|
allowInsecureProtocol = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath ComponentsDependencies.tools_androidgradle
|
||||||
|
classpath ComponentsDependencies.tools_kotlingradle
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variables in plugins {} aren't directly supported. Hack around it by setting an
|
||||||
|
// intermediate variable which can pull from FenixDependencies.kt and be used later.
|
||||||
|
ext {
|
||||||
|
detekt_plugin = Versions.detekt
|
||||||
|
python_envs_plugin = Versions.python_envs_plugin
|
||||||
|
ksp_plugin = Versions.ksp_plugin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("io.gitlab.arturbosch.detekt").version("$detekt_plugin")
|
||||||
|
id("com.google.devtools.ksp").version("$ksp_plugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORIES.each { repository ->
|
||||||
|
maven {
|
||||||
|
url repository
|
||||||
|
if (gradle.mozconfig.substs.ALLOW_INSECURE_GRADLE_REPOSITORIES) {
|
||||||
|
allowInsecureProtocol = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
maven {
|
||||||
|
url "${gradle.mozconfig.topobjdir}/gradle/maven"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
apply plugin: 'jacoco'
|
||||||
|
|
||||||
|
// Enable Kotlin warnings as errors for all modules
|
||||||
|
tasks.withType(KotlinCompile).configureEach {
|
||||||
|
kotlinOptions.allWarningsAsErrors = true
|
||||||
|
}
|
||||||
|
|
||||||
|
project.configurations.configureEach {
|
||||||
|
// Dependencies can't depend on a different major version of Glean than A-C itself.
|
||||||
|
resolutionStrategy.eachDependency { details ->
|
||||||
|
if (details.requested.group == 'org.mozilla.telemetry'
|
||||||
|
&& details.requested.name.contains('glean') ) {
|
||||||
|
def requested = details.requested.version.tokenize(".")
|
||||||
|
def defined = Versions.mozilla_glean.tokenize(".")
|
||||||
|
// Check the major version
|
||||||
|
if (requested[0] != defined[0]) {
|
||||||
|
throw new AssertionError("Cannot resolve to a single Glean version. Requested: ${details.requested.version}, A-C uses: ${Versions.mozilla_glean}")
|
||||||
|
} else {
|
||||||
|
// Enforce that all (transitive) dependencies are using the defined Glean version
|
||||||
|
details.useVersion Versions.mozilla_glean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolutionStrategy.capabilitiesResolution.withCapability("org.mozilla.telemetry:glean-native") {
|
||||||
|
def toBeSelected = candidates.find { it.id instanceof ModuleComponentIdentifier && it.id.module.contains('geckoview') }
|
||||||
|
if (toBeSelected != null) {
|
||||||
|
select(toBeSelected)
|
||||||
|
}
|
||||||
|
because 'use GeckoView Glean instead of standalone Glean'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gradle.hasProperty('localProperties.dependencySubstitutions.geckoviewTopsrcdir')) {
|
||||||
|
if (gradle.hasProperty('localProperties.dependencySubstitutions.geckoviewTopobjdir')) {
|
||||||
|
ext.topobjdir = gradle."localProperties.dependencySubstitutions.geckoviewTopobjdir"
|
||||||
|
}
|
||||||
|
ext.topsrcdir = gradle."localProperties.dependencySubstitutions.geckoviewTopsrcdir"
|
||||||
|
apply from: "${topsrcdir}/substitute-local-geckoview.gradle"
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEvaluate {
|
||||||
|
if (it.hasProperty('android')) {
|
||||||
|
jacoco {
|
||||||
|
toolVersion = Versions.jacoco
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format test output
|
||||||
|
tasks.matching {it instanceof Test}.configureEach() {
|
||||||
|
systemProperty "robolectric.logging", "stdout"
|
||||||
|
systemProperty "logging.test-mode", "true"
|
||||||
|
systemProperty "javax.net.ssl.trustStoreType", "JKS"
|
||||||
|
|
||||||
|
testLogging.events = []
|
||||||
|
|
||||||
|
def out = services.get(StyledTextOutputFactory).create("an-ouput")
|
||||||
|
|
||||||
|
beforeSuite { descriptor ->
|
||||||
|
if (descriptor.getClassName() != null) {
|
||||||
|
out.style(Style.Header).println("\nSUITE: " + descriptor.getClassName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeTest { descriptor ->
|
||||||
|
out.style(Style.Description).println(" TEST: " + descriptor.getName())
|
||||||
|
}
|
||||||
|
|
||||||
|
onOutput { descriptor, event ->
|
||||||
|
logger.lifecycle(" " + event.message.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
afterTest { descriptor, result ->
|
||||||
|
switch (result.getResultType()) {
|
||||||
|
case ResultType.SUCCESS:
|
||||||
|
out.style(Style.Success).println(" SUCCESS")
|
||||||
|
break
|
||||||
|
|
||||||
|
case ResultType.FAILURE:
|
||||||
|
def testId = descriptor.getClassName() + "." + descriptor.getName()
|
||||||
|
out.style(Style.Failure).println(" TEST-UNEXPECTED-FAIL | " + testId + " | " + result.getException())
|
||||||
|
break
|
||||||
|
|
||||||
|
case ResultType.SKIPPED:
|
||||||
|
out.style(Style.Info).println(" SKIPPED")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
logger.lifecycle("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
lintChecks project(':tooling-lint')
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(config.jvmTargetCompatibility)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
includeAndroidResources = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
resources {
|
||||||
|
excludes += ['META-INF/atomicfu.kotlin_module', 'META-INF/AL2.0', 'META-INF/LGPL2.1']
|
||||||
|
// Required dependencies using byte-buddy; remove after this is
|
||||||
|
// fixed by: https://issuetracker.google.com/issues/170131605
|
||||||
|
excludes.add("META-INF/licenses/ASM")
|
||||||
|
|
||||||
|
pickFirsts += ['win32-x86-64/attach_hotspot_windows.dll', 'win32-x86/attach_hotspot_windows.dll']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
androidResources {
|
||||||
|
ignoreAssetsPattern "manifest.template.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType(KotlinCompile).configureEach {
|
||||||
|
kotlinOptions.freeCompilerArgs += ["-opt-in=kotlin.RequiresOptIn"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (project.hasProperty("coverage") && project.name != "support-test") {
|
||||||
|
android.buildTypes.all { buildType ->
|
||||||
|
tasks.withType(Test).configureEach() {
|
||||||
|
jacoco {
|
||||||
|
includeNoLocationClasses = true
|
||||||
|
excludes = ['jdk.internal.*']
|
||||||
|
}
|
||||||
|
|
||||||
|
finalizedBy { "jacoco${buildType.name.capitalize()}TestReport" }
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("jacoco${buildType.name.capitalize()}TestReport", JacocoReport) {
|
||||||
|
reports {
|
||||||
|
xml.required = true
|
||||||
|
html.required = true
|
||||||
|
}
|
||||||
|
|
||||||
|
def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*',
|
||||||
|
'**/*Test*.*', 'android/**/*.*', '**/*$[0-9].*']
|
||||||
|
def kotlinDebugTree = fileTree(dir: "$project.layout.buildDirectory/tmp/kotlin-classes/${buildType.name}", excludes: fileFilter)
|
||||||
|
def javaDebugTree = fileTree(dir: "$project.layout.buildDirectory/intermediates/classes/${buildType.name}", excludes: fileFilter)
|
||||||
|
def mainSrc = "$project.projectDir/src/main/java"
|
||||||
|
|
||||||
|
sourceDirectories.setFrom(files([mainSrc]))
|
||||||
|
classDirectories.setFrom(files([kotlinDebugTree, javaDebugTree]))
|
||||||
|
getExecutionData().setFrom(fileTree(project.layout.buildDirectory).include([
|
||||||
|
"jacoco/test${buildType.name.capitalize()}UnitTest.exec"
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
testCoverageEnabled true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType(KotlinCompile).configureEach {
|
||||||
|
// Translate Kotlin messages like "w: ..." and "e: ..." into
|
||||||
|
// "...: warning: ..." and "...: error: ...", to make Treeherder understand.
|
||||||
|
def listener = {
|
||||||
|
if (it.startsWith("e: warnings found")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it.startsWith('w: ') || it.startsWith('e: ')) {
|
||||||
|
def matches = (it =~ /([ew]): (.+):(\d+):(\d+) (.*)/)
|
||||||
|
if (!matches) {
|
||||||
|
logger.quiet "kotlinc message format has changed!"
|
||||||
|
if (it.startsWith('w: ')) {
|
||||||
|
// For warnings, don't continue because we don't want to throw an
|
||||||
|
// exception. For errors, we want the exception so that the new error
|
||||||
|
// message format gets translated properly.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def (_, type, file, line, column, message) = matches[0]
|
||||||
|
type = (type == 'w') ? 'warning' : 'error'
|
||||||
|
// Use logger.lifecycle, which does not go through stderr again.
|
||||||
|
logger.lifecycle "$file:$line:$column: $type: $message"
|
||||||
|
}
|
||||||
|
} as StandardOutputListener
|
||||||
|
|
||||||
|
doFirst {
|
||||||
|
logging.addStandardErrorListener(listener)
|
||||||
|
}
|
||||||
|
doLast {
|
||||||
|
logging.removeStandardErrorListener(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (findProject(":geckoview") == null) {
|
||||||
|
// Avoid adding this task if it already exists in a different root project.
|
||||||
|
tasks.register("clean", Delete) {
|
||||||
|
delete rootProject.layout.buildDirectory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
detekt {
|
||||||
|
input = files("$projectDir/components", "$projectDir/buildSrc", "$projectDir/samples")
|
||||||
|
config = files("$projectDir/config/detekt.yml")
|
||||||
|
baseline = file("$projectDir/config/detekt-baseline.xml")
|
||||||
|
|
||||||
|
reports {
|
||||||
|
html {
|
||||||
|
enabled = true
|
||||||
|
destination = file("$projectDir/build/reports/detekt.html")
|
||||||
|
}
|
||||||
|
xml {
|
||||||
|
enabled = false
|
||||||
|
}
|
||||||
|
txt {
|
||||||
|
enabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType(Detekt).configureEach() {
|
||||||
|
// Custom detekt rules should be build before
|
||||||
|
// See https://arturbosch.github.io/detekt/extensions.html#pitfalls
|
||||||
|
dependsOn(":tooling-detekt:assemble")
|
||||||
|
|
||||||
|
autoCorrect = true
|
||||||
|
|
||||||
|
exclude "**/build.gradle.kts"
|
||||||
|
exclude "**/src/androidTest/**"
|
||||||
|
exclude "**/src/iosTest/**"
|
||||||
|
exclude "**/src/test/**"
|
||||||
|
exclude "**/test/src/**"
|
||||||
|
exclude "**/build/**"
|
||||||
|
exclude "**/resources/**"
|
||||||
|
exclude "**/tmp/**"
|
||||||
|
exclude "**/tooling/fetch/tests/**"
|
||||||
|
exclude "**/tooling/fetch-tests/**"
|
||||||
|
exclude "**/src/main/assets/extensions/**"
|
||||||
|
exclude "**/docs/**"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply same path exclusions as for the main task
|
||||||
|
tasks.withType(DetektCreateBaselineTask).configureEach() {
|
||||||
|
exclude "**/src/androidTest/**"
|
||||||
|
exclude "**/src/test/**"
|
||||||
|
exclude "**/test/src/**"
|
||||||
|
exclude "**/build/**"
|
||||||
|
exclude "**/resources/**"
|
||||||
|
exclude "**/tmp/**"
|
||||||
|
exclude "**/tooling/fetch/tests/**"
|
||||||
|
exclude "**/tooling/fetch-tests/**"
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations {
|
||||||
|
ktlint
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
ktlint("com.pinterest:ktlint:${Versions.ktlint}") {
|
||||||
|
attributes {
|
||||||
|
attribute(Bundling.BUNDLING_ATTRIBUTE, getObjects().named(Bundling, Bundling.EXTERNAL))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
detektPlugins project(":tooling-detekt")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("ktlint", JavaExec) {
|
||||||
|
group = "verification"
|
||||||
|
description = "Check Kotlin code style."
|
||||||
|
classpath = configurations.ktlint
|
||||||
|
mainClass.set("com.pinterest.ktlint.Main")
|
||||||
|
args "components/**/*.kt" , "samples/**/*.kt", "!**/build/**/*.kt", "buildSrc/**/*.kt", "--baseline=ktlint-baseline.xml",
|
||||||
|
"--reporter=json,output=build/reports/ktlint/ktlint.json", "--reporter=plain"
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("ktlintFormat", JavaExec) {
|
||||||
|
group = "formatting"
|
||||||
|
description = "Fix Kotlin code style deviations."
|
||||||
|
classpath = configurations.ktlint
|
||||||
|
mainClass.set("com.pinterest.ktlint.Main")
|
||||||
|
args "-F", "components/**/*.kt" , "samples/**/*.kt", "!**/build/**/*.kt", "buildSrc/**/*.kt", "--baseline=ktlint-baseline.xml"
|
||||||
|
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("listRepositories") {
|
||||||
|
doLast {
|
||||||
|
println "Repositories:"
|
||||||
|
project.repositories.each { println "Name: " + it.name + "; url: " + it.url }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
# [Android Components](../../../README.md) > Browser > Domains
|
||||||
|
|
||||||
|
This component provides APIs for managing localized and customizable domain lists (see [Domains](#domains) and [CustomDomains](#customdomains)). It also contains auto-complete functionality for these lists (see [DomainAutoCompleteProvider](#domainautocompleteprovider)) which can be used in conjuction with our [UI autocomplete component](../../ui/autocomplete/README.md).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Setting up the dependency
|
||||||
|
|
||||||
|
Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)):
|
||||||
|
|
||||||
|
```Groovy
|
||||||
|
implementation "org.mozilla.components:browser-domains:{latest-version}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Domains
|
||||||
|
|
||||||
|
The `Domains` object is used to load the built-in localized domain lists which are shipped as part of this component. These lists are grouped by country and can be found [in our repository](src/main/assets/domains).
|
||||||
|
|
||||||
|
```Kotlin
|
||||||
|
// Load the domain lists for all countries in the default locale (fallback is US)
|
||||||
|
val domains = Domains.load(context)
|
||||||
|
```
|
||||||
|
|
||||||
|
### CustomDomains
|
||||||
|
|
||||||
|
The `CustomDomains` object can be used to manage a custom domain list which will be stored in `SharedPreferences`.
|
||||||
|
|
||||||
|
```Kotlin
|
||||||
|
// Load the custom domain list
|
||||||
|
val domains = CustomDomains.load(context)
|
||||||
|
|
||||||
|
// Save custom domains
|
||||||
|
CustomDomains.save(context, listOf("mozilla.org", "getpocket.com"))
|
||||||
|
|
||||||
|
// Remove custom domains
|
||||||
|
CustomDomains.remove(context, listOf("nolongerexists.org"))
|
||||||
|
```
|
||||||
|
|
||||||
|
### DomainAutoCompleteProvider
|
||||||
|
|
||||||
|
The class provides auto-complete functionality for both `Domains` and `CustomDomains`.
|
||||||
|
|
||||||
|
```Kotlin
|
||||||
|
// Initialize the provider
|
||||||
|
val provider = DomainAutocompleteProvider()
|
||||||
|
provider.initialize(
|
||||||
|
context,
|
||||||
|
useShippedDomains = true,
|
||||||
|
useCustomDomains = true,
|
||||||
|
loadDomainsFromDisk = true
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that when `loadDomainsFromDisk` is set to true there is no need to manually call `load` on either `Domains` or `CustomDomains`.
|
||||||
|
|
||||||
|
```Kotlin
|
||||||
|
// Autocomplete domain lists
|
||||||
|
val result = provider.autocomplete("moz")
|
||||||
|
```
|
||||||
|
|
||||||
|
The result will contain the autocompleted text (`result.text`), the URL (`result.url`), and the source of the match (`result.source`), which is either `DEFAULT_LIST` if a result was found in the shipped domain list or `CUSTOM_LIST` otherwise. The custom domain list takes precendece over the built-in shipped domain list and the API will only return the first match.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/
|
|
@ -0,0 +1,40 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
apply plugin: 'com.android.library'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion config.minSdkVersion
|
||||||
|
compileSdk config.compileSdkVersion
|
||||||
|
targetSdkVersion config.targetSdkVersion
|
||||||
|
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace 'mozilla.components.browser.domains'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(':concept-toolbar')
|
||||||
|
implementation ComponentsDependencies.kotlin_coroutines
|
||||||
|
|
||||||
|
testImplementation project(':support-test')
|
||||||
|
|
||||||
|
testImplementation ComponentsDependencies.androidx_test_junit
|
||||||
|
testImplementation ComponentsDependencies.testing_robolectric
|
||||||
|
testImplementation ComponentsDependencies.testing_coroutines
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: '../../../android-lint.gradle'
|
||||||
|
apply from: '../../../publish.gradle'
|
||||||
|
ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
|
@ -0,0 +1,4 @@
|
||||||
|
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
<manifest />
|
|
@ -0,0 +1,50 @@
|
||||||
|
google.com.br
|
||||||
|
youtube.com
|
||||||
|
google.com
|
||||||
|
facebook.com
|
||||||
|
globo.com
|
||||||
|
uol.com.br
|
||||||
|
blastingnews.com
|
||||||
|
live.com
|
||||||
|
mercadolivre.com.br
|
||||||
|
yahoo.com
|
||||||
|
blogspot.com.br
|
||||||
|
wikipedia.org
|
||||||
|
whatsapp.com
|
||||||
|
netflix.com
|
||||||
|
olx.com.br
|
||||||
|
instagram.com
|
||||||
|
msn.com
|
||||||
|
metropoles.com
|
||||||
|
fatosdesconhecidos.com.br
|
||||||
|
twitter.com
|
||||||
|
caixa.gov.br
|
||||||
|
uptodown.com
|
||||||
|
aliexpress.com
|
||||||
|
curapelanatureza.com.br
|
||||||
|
wordpress.com
|
||||||
|
abril.com.br
|
||||||
|
americanas.com.br
|
||||||
|
correios.com.br
|
||||||
|
reclameaqui.com.br
|
||||||
|
bet365.com
|
||||||
|
onclkds.com
|
||||||
|
bol.uol.com.br
|
||||||
|
techtudo.com.br
|
||||||
|
fazenda.gov.br
|
||||||
|
microsoft.com
|
||||||
|
folha.uol.com.br
|
||||||
|
linkedin.com
|
||||||
|
tumblr.com
|
||||||
|
sp.gov.br
|
||||||
|
reddit.com
|
||||||
|
bb.com.br
|
||||||
|
pinterest.com
|
||||||
|
itau.com.br
|
||||||
|
letras.mus.br
|
||||||
|
otvfoco.com.br
|
||||||
|
vagalume.com.br
|
||||||
|
portalinteressante.com
|
||||||
|
myappolicious.com.br
|
||||||
|
thewhizmarketing.com
|
||||||
|
twitch.tv
|
|
@ -0,0 +1,49 @@
|
||||||
|
google.com
|
||||||
|
youtube.com
|
||||||
|
facebook.com
|
||||||
|
reddit.com
|
||||||
|
amazon.com
|
||||||
|
wikipedia.org
|
||||||
|
yahoo.com
|
||||||
|
twitter.com
|
||||||
|
netflix.com
|
||||||
|
ebay.com
|
||||||
|
imgur.com
|
||||||
|
linkedin.com
|
||||||
|
instagram.com
|
||||||
|
diply.com
|
||||||
|
craigslist.org
|
||||||
|
live.com
|
||||||
|
office.com
|
||||||
|
twitch.tv
|
||||||
|
tumblr.com
|
||||||
|
pinterest.com
|
||||||
|
espn.com
|
||||||
|
cnn.com
|
||||||
|
bing.com
|
||||||
|
wikia.com
|
||||||
|
chase.com
|
||||||
|
imdb.com
|
||||||
|
nytimes.com
|
||||||
|
paypal.com
|
||||||
|
blogspot.com
|
||||||
|
apple.com
|
||||||
|
yelp.com
|
||||||
|
stackoverflow.com
|
||||||
|
bankofamerica.com
|
||||||
|
wordpress.com
|
||||||
|
github.com
|
||||||
|
microsoft.com
|
||||||
|
wellsfargo.com
|
||||||
|
zillow.com
|
||||||
|
salesforce.com
|
||||||
|
msn.com
|
||||||
|
walmart.com
|
||||||
|
weather.com
|
||||||
|
dropbox.com
|
||||||
|
buzzfeed.com
|
||||||
|
intuit.com
|
||||||
|
washingtonpost.com
|
||||||
|
soundcloud.com
|
||||||
|
huffingtonpost.com
|
||||||
|
indeed.com
|
|
@ -0,0 +1,50 @@
|
||||||
|
google.de
|
||||||
|
youtube.com
|
||||||
|
google.com
|
||||||
|
facebook.com
|
||||||
|
amazon.de
|
||||||
|
ebay.de
|
||||||
|
wikipedia.org
|
||||||
|
ebay-kleinanzeigen.de
|
||||||
|
web.de
|
||||||
|
yahoo.com
|
||||||
|
ok.ru
|
||||||
|
gmx.net
|
||||||
|
reddit.com
|
||||||
|
vk.com
|
||||||
|
t-online.de
|
||||||
|
twitter.com
|
||||||
|
spiegel.de
|
||||||
|
mail.ru
|
||||||
|
instagram.com
|
||||||
|
live.com
|
||||||
|
chip.de
|
||||||
|
bild.de
|
||||||
|
paypal.com
|
||||||
|
bing.com
|
||||||
|
twitch.tv
|
||||||
|
whatsapp.com
|
||||||
|
yandex.ru
|
||||||
|
gutefrage.net
|
||||||
|
mobile.de
|
||||||
|
google.ru
|
||||||
|
blogspot.de
|
||||||
|
tumblr.com
|
||||||
|
bs.to
|
||||||
|
focus.de
|
||||||
|
linkedin.com
|
||||||
|
netflix.com
|
||||||
|
wordpress.com
|
||||||
|
imgur.com
|
||||||
|
postbank.de
|
||||||
|
welt.de
|
||||||
|
streamcloud.eu
|
||||||
|
microsoft.com
|
||||||
|
immobilienscout24.de
|
||||||
|
msn.com
|
||||||
|
dict.cc
|
||||||
|
otto.de
|
||||||
|
xing.com
|
||||||
|
amazon.com
|
||||||
|
heise.de
|
||||||
|
github.com
|
|
@ -0,0 +1,50 @@
|
||||||
|
google.fr
|
||||||
|
youtube.com
|
||||||
|
google.com
|
||||||
|
facebook.com
|
||||||
|
wikipedia.org
|
||||||
|
amazon.fr
|
||||||
|
leboncoin.fr
|
||||||
|
yahoo.com
|
||||||
|
live.com
|
||||||
|
twitter.com
|
||||||
|
orange.fr
|
||||||
|
free.fr
|
||||||
|
linkedin.com
|
||||||
|
lemonde.fr
|
||||||
|
instagram.com
|
||||||
|
reddit.com
|
||||||
|
lefigaro.fr
|
||||||
|
ebay.fr
|
||||||
|
cdiscount.com
|
||||||
|
jeuxvideo.com
|
||||||
|
zone-telechargement.ws
|
||||||
|
labanquepostale.fr
|
||||||
|
blogspot.fr
|
||||||
|
allocine.fr
|
||||||
|
msn.com
|
||||||
|
commentcamarche.net
|
||||||
|
pole-emploi.fr
|
||||||
|
vk.com
|
||||||
|
sfr.fr
|
||||||
|
lequipe.fr
|
||||||
|
twitch.tv
|
||||||
|
francetvinfo.fr
|
||||||
|
20minutes.fr
|
||||||
|
pinterest.com
|
||||||
|
netflix.com
|
||||||
|
programme-tv.net
|
||||||
|
credit-agricole.fr
|
||||||
|
linternaute.com
|
||||||
|
github.com
|
||||||
|
wordpress.com
|
||||||
|
caf.fr
|
||||||
|
aliexpress.com
|
||||||
|
dailymotion.com
|
||||||
|
tumblr.com
|
||||||
|
t411.ai
|
||||||
|
stackoverflow.com
|
||||||
|
microsoft.com
|
||||||
|
meteofrance.com
|
||||||
|
onclkds.com
|
||||||
|
bfmtv.com
|
|
@ -0,0 +1,49 @@
|
||||||
|
google.co.uk
|
||||||
|
youtube.com
|
||||||
|
google.com
|
||||||
|
facebook.com
|
||||||
|
reddit.com
|
||||||
|
bbc.co.uk
|
||||||
|
amazon.co.uk
|
||||||
|
wikipedia.org
|
||||||
|
ebay.co.uk
|
||||||
|
twitter.com
|
||||||
|
live.com
|
||||||
|
yahoo.com
|
||||||
|
instagram.com
|
||||||
|
diply.com
|
||||||
|
linkedin.com
|
||||||
|
imgur.com
|
||||||
|
netflix.com
|
||||||
|
theguardian.com
|
||||||
|
dailymail.co.uk
|
||||||
|
twitch.tv
|
||||||
|
imdb.com
|
||||||
|
paypal.com
|
||||||
|
office.com
|
||||||
|
tumblr.com
|
||||||
|
www.gov.uk
|
||||||
|
wikia.com
|
||||||
|
givemesport.com
|
||||||
|
amazon.com
|
||||||
|
bing.com
|
||||||
|
wordpress.com
|
||||||
|
telegraph.co.uk
|
||||||
|
rightmove.co.uk
|
||||||
|
pinterest.com
|
||||||
|
gumtree.com
|
||||||
|
msn.com
|
||||||
|
microsoft.com
|
||||||
|
stackoverflow.com
|
||||||
|
booking.com
|
||||||
|
vk.com
|
||||||
|
tripadvisor.co.uk
|
||||||
|
lloydsbank.co.uk
|
||||||
|
apple.com
|
||||||
|
service.gov.uk
|
||||||
|
onclkds.com
|
||||||
|
github.com
|
||||||
|
independent.co.uk
|
||||||
|
bt.com
|
||||||
|
vice.com
|
||||||
|
hsbc.co.uk
|
|
@ -0,0 +1,444 @@
|
||||||
|
google.com
|
||||||
|
facebook.com
|
||||||
|
amazon.com
|
||||||
|
youtube.com
|
||||||
|
yahoo.com
|
||||||
|
ebay.com
|
||||||
|
wikipedia.org
|
||||||
|
twitter.com
|
||||||
|
reddit.com
|
||||||
|
go.com
|
||||||
|
craigslist.org
|
||||||
|
live.com
|
||||||
|
netflix.com
|
||||||
|
pinterest.com
|
||||||
|
bing.com
|
||||||
|
linkedin.com
|
||||||
|
imgur.com
|
||||||
|
espn.go.com
|
||||||
|
walmart.com
|
||||||
|
tumblr.com
|
||||||
|
target.com
|
||||||
|
paypal.com
|
||||||
|
cnn.com
|
||||||
|
chase.com
|
||||||
|
instagram.com
|
||||||
|
bestbuy.com
|
||||||
|
blogspot.com
|
||||||
|
nytimes.com
|
||||||
|
msn.com
|
||||||
|
imdb.com
|
||||||
|
apple.com
|
||||||
|
bankofamerica.com
|
||||||
|
diply.com
|
||||||
|
huffingtonpost.com
|
||||||
|
yelp.com
|
||||||
|
wellsfargo.com
|
||||||
|
etsy.com
|
||||||
|
weather.com
|
||||||
|
wordpress.com
|
||||||
|
buzzfeed.com
|
||||||
|
zillow.com
|
||||||
|
kohls.com
|
||||||
|
aol.com
|
||||||
|
homedepot.com
|
||||||
|
foxnews.com
|
||||||
|
microsoft.com
|
||||||
|
comcast.net
|
||||||
|
wikia.com
|
||||||
|
groupon.com
|
||||||
|
macys.com
|
||||||
|
washingtonpost.com
|
||||||
|
outbrain.com
|
||||||
|
xfinity.com
|
||||||
|
usps.com
|
||||||
|
hulu.com
|
||||||
|
americanexpress.com
|
||||||
|
slickdeals.net
|
||||||
|
pandora.com
|
||||||
|
office.com
|
||||||
|
cnet.com
|
||||||
|
indeed.com
|
||||||
|
capitalone.com
|
||||||
|
nfl.com
|
||||||
|
ups.com
|
||||||
|
ask.com
|
||||||
|
verizonwireless.com
|
||||||
|
newegg.com
|
||||||
|
usatoday.com
|
||||||
|
forbes.com
|
||||||
|
dailymail.co.uk
|
||||||
|
dropbox.com
|
||||||
|
att.com
|
||||||
|
costco.com
|
||||||
|
gfycat.com
|
||||||
|
lowes.com
|
||||||
|
gap.com
|
||||||
|
about.com
|
||||||
|
tripadvisor.com
|
||||||
|
fedex.com
|
||||||
|
baidu.com
|
||||||
|
vice.com
|
||||||
|
nordstrom.com
|
||||||
|
adobe.com
|
||||||
|
bbc.com
|
||||||
|
twitch.tv
|
||||||
|
allrecipes.com
|
||||||
|
retailmenot.com
|
||||||
|
stackoverflow.com
|
||||||
|
citi.com
|
||||||
|
sears.com
|
||||||
|
jcpenney.com
|
||||||
|
webmd.com
|
||||||
|
nih.gov
|
||||||
|
answers.com
|
||||||
|
foodnetwork.com
|
||||||
|
discovercard.com
|
||||||
|
cbssports.com
|
||||||
|
overstock.com
|
||||||
|
businessinsider.com
|
||||||
|
office365.com
|
||||||
|
theguardian.com
|
||||||
|
staples.com
|
||||||
|
bleacherreport.com
|
||||||
|
verizon.com
|
||||||
|
github.com
|
||||||
|
wayfair.com
|
||||||
|
salesforce.com
|
||||||
|
zulily.com
|
||||||
|
wsj.com
|
||||||
|
flickr.com
|
||||||
|
goodreads.com
|
||||||
|
realtor.com
|
||||||
|
nbcnews.com
|
||||||
|
ebates.com
|
||||||
|
ancestry.com
|
||||||
|
wunderground.com
|
||||||
|
instructure.com
|
||||||
|
people.com
|
||||||
|
stackexchange.com
|
||||||
|
drudgereport.com
|
||||||
|
fidelity.com
|
||||||
|
southwest.com
|
||||||
|
deviantart.com
|
||||||
|
thesaurus.com
|
||||||
|
intuit.com
|
||||||
|
woot.com
|
||||||
|
pch.com
|
||||||
|
soundcloud.com
|
||||||
|
force.com
|
||||||
|
samsclub.com
|
||||||
|
ign.com
|
||||||
|
qvc.com
|
||||||
|
npr.org
|
||||||
|
patch.com
|
||||||
|
dell.com
|
||||||
|
accuweather.com
|
||||||
|
vimeo.com
|
||||||
|
expedia.com
|
||||||
|
trulia.com
|
||||||
|
ca.gov
|
||||||
|
swagbucks.com
|
||||||
|
spotify.com
|
||||||
|
bedbathandbeyond.com
|
||||||
|
nypost.com
|
||||||
|
aliexpress.com
|
||||||
|
blackboard.com
|
||||||
|
ticketmaster.com
|
||||||
|
ikea.com
|
||||||
|
feedly.com
|
||||||
|
usaa.com
|
||||||
|
tmz.com
|
||||||
|
quora.com
|
||||||
|
lifehacker.com
|
||||||
|
kayak.com
|
||||||
|
reference.com
|
||||||
|
zappos.com
|
||||||
|
gizmodo.com
|
||||||
|
slate.com
|
||||||
|
faithtap.com
|
||||||
|
adp.com
|
||||||
|
abcnews.go.com
|
||||||
|
sephora.com
|
||||||
|
cbs.com
|
||||||
|
latimes.com
|
||||||
|
shutterfly.com
|
||||||
|
t-mobile.com
|
||||||
|
littlethings.com
|
||||||
|
glassdoor.com
|
||||||
|
bloomberg.com
|
||||||
|
cbsnews.com
|
||||||
|
wikihow.com
|
||||||
|
walgreens.com
|
||||||
|
usbank.com
|
||||||
|
blogger.com
|
||||||
|
weebly.com
|
||||||
|
gamestop.com
|
||||||
|
food.com
|
||||||
|
time.com
|
||||||
|
kickstarter.com
|
||||||
|
okcupid.com
|
||||||
|
aa.com
|
||||||
|
weather.gov
|
||||||
|
nametests.com
|
||||||
|
fandango.com
|
||||||
|
engadget.com
|
||||||
|
steamcommunity.com
|
||||||
|
thekitchn.com
|
||||||
|
nba.com
|
||||||
|
mashable.com
|
||||||
|
hp.com
|
||||||
|
gamefaqs.com
|
||||||
|
delta.com
|
||||||
|
coupons.com
|
||||||
|
eonline.com
|
||||||
|
surveymonkey.com
|
||||||
|
kmart.com
|
||||||
|
barnesandnoble.com
|
||||||
|
meetup.com
|
||||||
|
bhphotovideo.com
|
||||||
|
fanduel.com
|
||||||
|
quizlet.com
|
||||||
|
nydailynews.com
|
||||||
|
sbnation.com
|
||||||
|
nbcsports.com
|
||||||
|
bbc.co.uk
|
||||||
|
ew.com
|
||||||
|
nike.com
|
||||||
|
rottentomatoes.com
|
||||||
|
steampowered.com
|
||||||
|
reuters.com
|
||||||
|
qq.com
|
||||||
|
today.com
|
||||||
|
mapquest.com
|
||||||
|
audible.com
|
||||||
|
priceline.com
|
||||||
|
whitepages.com
|
||||||
|
united.com
|
||||||
|
myfitnesspal.com
|
||||||
|
icloud.com
|
||||||
|
forever21.com
|
||||||
|
theatlantic.com
|
||||||
|
microsoftstore.com
|
||||||
|
theverge.com
|
||||||
|
gawker.com
|
||||||
|
houzz.com
|
||||||
|
mayoclinic.org
|
||||||
|
rei.com
|
||||||
|
sfgate.com
|
||||||
|
lifebuzz.com
|
||||||
|
discover.com
|
||||||
|
pnc.com
|
||||||
|
pof.com
|
||||||
|
iflscience.com
|
||||||
|
popsugar.com
|
||||||
|
creditkarma.com
|
||||||
|
telegraph.co.uk
|
||||||
|
airbnb.com
|
||||||
|
buzzlie.com
|
||||||
|
cnbc.com
|
||||||
|
deadspin.com
|
||||||
|
sina.com.cn
|
||||||
|
legacy.com
|
||||||
|
thedailybeast.com
|
||||||
|
samsung.com
|
||||||
|
nextdoor.com
|
||||||
|
evite.com
|
||||||
|
shopify.com
|
||||||
|
yellowpages.com
|
||||||
|
pcmag.com
|
||||||
|
redfin.com
|
||||||
|
weibo.com
|
||||||
|
alibaba.com
|
||||||
|
cabelas.com
|
||||||
|
battle.net
|
||||||
|
foxsports.com
|
||||||
|
taobao.com
|
||||||
|
eventbrite.com
|
||||||
|
victoriassecret.com
|
||||||
|
theblaze.com
|
||||||
|
dealnews.com
|
||||||
|
cbslocal.com
|
||||||
|
cvs.com
|
||||||
|
dailymotion.com
|
||||||
|
ecollege.com
|
||||||
|
gofundme.com
|
||||||
|
fitbit.com
|
||||||
|
instructables.com
|
||||||
|
godaddy.com
|
||||||
|
babycenter.com
|
||||||
|
squarespace.com
|
||||||
|
llbean.com
|
||||||
|
dickssportinggoods.com
|
||||||
|
6pm.com
|
||||||
|
myway.com
|
||||||
|
hsn.com
|
||||||
|
wired.com
|
||||||
|
officedepot.com
|
||||||
|
ozztube.com
|
||||||
|
usmagazine.com
|
||||||
|
match.com
|
||||||
|
cracked.com
|
||||||
|
evernote.com
|
||||||
|
box.com
|
||||||
|
starbucks.com
|
||||||
|
kbb.com
|
||||||
|
mlb.com
|
||||||
|
marriott.com
|
||||||
|
si.com
|
||||||
|
jezebel.com
|
||||||
|
pbs.org
|
||||||
|
consumerreports.org
|
||||||
|
roblox.com
|
||||||
|
urbandictionary.com
|
||||||
|
kotaku.com
|
||||||
|
xbox.com
|
||||||
|
marketwatch.com
|
||||||
|
refinery29.com
|
||||||
|
wikimedia.org
|
||||||
|
tvguide.com
|
||||||
|
politico.com
|
||||||
|
barclaycardus.com
|
||||||
|
abc.go.com
|
||||||
|
mint.com
|
||||||
|
topix.com
|
||||||
|
theblackfriday.com
|
||||||
|
aarp.org
|
||||||
|
hotnewhiphop.com
|
||||||
|
yourdailydish.com
|
||||||
|
sprint.com
|
||||||
|
vox.com
|
||||||
|
cafemom.com
|
||||||
|
nbc.com
|
||||||
|
dailykos.com
|
||||||
|
azlyrics.com
|
||||||
|
autotrader.com
|
||||||
|
hilton.com
|
||||||
|
irs.gov
|
||||||
|
monster.com
|
||||||
|
mailchimp.com
|
||||||
|
webex.com
|
||||||
|
landsend.com
|
||||||
|
wix.com
|
||||||
|
usnews.com
|
||||||
|
jcrew.com
|
||||||
|
jet.com
|
||||||
|
capitalone360.com
|
||||||
|
sharepoint.com
|
||||||
|
schwab.com
|
||||||
|
ulta.com
|
||||||
|
vistaprint.com
|
||||||
|
rollingstone.com
|
||||||
|
biblegateway.com
|
||||||
|
gamespot.com
|
||||||
|
io9.com
|
||||||
|
opentable.com
|
||||||
|
hm.com
|
||||||
|
duckduckgo.com
|
||||||
|
chron.com
|
||||||
|
photobucket.com
|
||||||
|
shareasale.com
|
||||||
|
directv.com
|
||||||
|
avg.com
|
||||||
|
oracle.com
|
||||||
|
hotels.com
|
||||||
|
timewarnercable.com
|
||||||
|
chicagotribune.com
|
||||||
|
ehow.com
|
||||||
|
primewire.ag
|
||||||
|
abs-cbnnews.com
|
||||||
|
salon.com
|
||||||
|
greatergood.com
|
||||||
|
epicurious.com
|
||||||
|
fool.com
|
||||||
|
patheos.com
|
||||||
|
custhelp.com
|
||||||
|
purdue.edu
|
||||||
|
tickld.com
|
||||||
|
frys.com
|
||||||
|
indiatimes.com
|
||||||
|
amazon.co.uk
|
||||||
|
zendesk.com
|
||||||
|
tigerdirect.com
|
||||||
|
stubhub.com
|
||||||
|
healthcare.gov
|
||||||
|
archive.org
|
||||||
|
qualtrics.com
|
||||||
|
ravelry.com
|
||||||
|
cars.com
|
||||||
|
redbox.com
|
||||||
|
jalopnik.com
|
||||||
|
speedtest.net
|
||||||
|
harvard.edu
|
||||||
|
slideshare.net
|
||||||
|
kinja.com
|
||||||
|
nesn.com
|
||||||
|
michaels.com
|
||||||
|
mit.edu
|
||||||
|
bodybuilding.com
|
||||||
|
edmunds.com
|
||||||
|
nhl.com
|
||||||
|
zergnet.com
|
||||||
|
techcrunch.com
|
||||||
|
pogo.com
|
||||||
|
mozilla.org
|
||||||
|
naver.com
|
||||||
|
giphy.com
|
||||||
|
bankrate.com
|
||||||
|
msnbc.com
|
||||||
|
digitaltrends.com
|
||||||
|
fanfiction.net
|
||||||
|
skype.com
|
||||||
|
disney.go.com
|
||||||
|
norton.com
|
||||||
|
androidcentral.com
|
||||||
|
tomshardware.com
|
||||||
|
thefreedictionary.com
|
||||||
|
liveleak.com
|
||||||
|
247sports.com
|
||||||
|
merriam-webster.com
|
||||||
|
wnd.com
|
||||||
|
earthlink.net
|
||||||
|
independent.co.uk
|
||||||
|
drugs.com
|
||||||
|
rotoworld.com
|
||||||
|
nationalgeographic.com
|
||||||
|
ae.com
|
||||||
|
noaa.gov
|
||||||
|
arstechnica.com
|
||||||
|
thinkgeek.com
|
||||||
|
stanford.edu
|
||||||
|
bizjournals.com
|
||||||
|
hootsuite.com
|
||||||
|
genius.com
|
||||||
|
goodhousekeeping.com
|
||||||
|
vanguard.com
|
||||||
|
ny.gov
|
||||||
|
citibankonline.com
|
||||||
|
booking.com
|
||||||
|
mic.com
|
||||||
|
orbitz.com
|
||||||
|
dominos.com
|
||||||
|
medium.com
|
||||||
|
wow.com
|
||||||
|
urbanoutfitters.com
|
||||||
|
douban.com
|
||||||
|
timeanddate.com
|
||||||
|
draftkings.com
|
||||||
|
livestrong.com
|
||||||
|
livingsocial.com
|
||||||
|
cox.net
|
||||||
|
theonion.com
|
||||||
|
marthastewart.com
|
||||||
|
comenity.net
|
||||||
|
worldlifestyle.com
|
||||||
|
disney.com
|
||||||
|
realsimple.com
|
||||||
|
vrbo.com
|
||||||
|
playstation.com
|
||||||
|
potterybarn.com
|
||||||
|
zazzle.com
|
||||||
|
ksl.com
|
||||||
|
tdbank.com
|
||||||
|
sourceforge.net
|
||||||
|
careerbuilder.com
|
|
@ -0,0 +1,50 @@
|
||||||
|
google.com.hk
|
||||||
|
youtube.com
|
||||||
|
google.com
|
||||||
|
facebook.com
|
||||||
|
yahoo.com
|
||||||
|
discuss.com.hk
|
||||||
|
aastocks.com
|
||||||
|
wikipedia.org
|
||||||
|
baidu.com
|
||||||
|
taobao.com
|
||||||
|
pixnet.net
|
||||||
|
bastillepost.com
|
||||||
|
nextmedia.com
|
||||||
|
whatsapp.com
|
||||||
|
instagram.com
|
||||||
|
price.com.hk
|
||||||
|
ettoday.net
|
||||||
|
qq.com
|
||||||
|
hsbc.com.hk
|
||||||
|
tmall.com
|
||||||
|
live.com
|
||||||
|
hkgolden.com
|
||||||
|
reddit.com
|
||||||
|
beautyexchange.com.hk
|
||||||
|
etnet.com.hk
|
||||||
|
on.cc
|
||||||
|
amazon.com
|
||||||
|
twitter.com
|
||||||
|
uwants.com
|
||||||
|
presslogic.com
|
||||||
|
unwire.hk
|
||||||
|
gamer.com.tw
|
||||||
|
hangseng.com
|
||||||
|
hk01.com
|
||||||
|
twitch.tv
|
||||||
|
linkedin.com
|
||||||
|
teepr.com
|
||||||
|
hkjc.com
|
||||||
|
apple.com
|
||||||
|
bomb01.com
|
||||||
|
sina.com.cn
|
||||||
|
weibo.com
|
||||||
|
dcfever.com
|
||||||
|
thestandnews.com
|
||||||
|
office.com
|
||||||
|
openrice.com
|
||||||
|
tumblr.com
|
||||||
|
tvb.com
|
||||||
|
alipay.com
|
||||||
|
stackoverflow.com
|
|
@ -0,0 +1,50 @@
|
||||||
|
google.com
|
||||||
|
google.co.id
|
||||||
|
youtube.com
|
||||||
|
detik.com
|
||||||
|
tribunnews.com
|
||||||
|
facebook.com
|
||||||
|
yahoo.com
|
||||||
|
tokopedia.com
|
||||||
|
liputan6.com
|
||||||
|
kompas.com
|
||||||
|
bukalapak.com
|
||||||
|
kaskus.co.id
|
||||||
|
kapanlagi.com
|
||||||
|
wordpress.com
|
||||||
|
merdeka.com
|
||||||
|
okezone.com
|
||||||
|
elevenia.co.id
|
||||||
|
lazada.co.id
|
||||||
|
uzone.id
|
||||||
|
bintang.com
|
||||||
|
brilio.net
|
||||||
|
popads.net
|
||||||
|
instagram.com
|
||||||
|
bola.net
|
||||||
|
wikipedia.org
|
||||||
|
blogspot.com
|
||||||
|
onclkds.com
|
||||||
|
dream.co.id
|
||||||
|
viva.co.id
|
||||||
|
alodokter.com
|
||||||
|
tempo.co
|
||||||
|
suara.com
|
||||||
|
wowkeren.com
|
||||||
|
idntimes.com
|
||||||
|
bola.com
|
||||||
|
sindonews.com
|
||||||
|
republika.co.id
|
||||||
|
kompasiana.com
|
||||||
|
vemale.com
|
||||||
|
blanja.com
|
||||||
|
cnnindonesia.com
|
||||||
|
olx.co.id
|
||||||
|
lk21.org
|
||||||
|
popcash.net
|
||||||
|
blibli.com
|
||||||
|
poptm.com
|
||||||
|
nonton.movie
|
||||||
|
indexmovie.me
|
||||||
|
adexchangeprediction.com
|
||||||
|
subscene.com
|
|
@ -0,0 +1,50 @@
|
||||||
|
google.pl
|
||||||
|
youtube.com
|
||||||
|
facebook.com
|
||||||
|
google.com
|
||||||
|
allegro.pl
|
||||||
|
onet.pl
|
||||||
|
wp.pl
|
||||||
|
wikipedia.org
|
||||||
|
olx.pl
|
||||||
|
vk.com
|
||||||
|
interia.pl
|
||||||
|
wykop.pl
|
||||||
|
gazeta.pl
|
||||||
|
filmweb.pl
|
||||||
|
instagram.com
|
||||||
|
wiocha.pl
|
||||||
|
cda.pl
|
||||||
|
aliexpress.com
|
||||||
|
otomoto.pl
|
||||||
|
mbank.pl
|
||||||
|
reddit.com
|
||||||
|
ceneo.pl
|
||||||
|
tvn24.pl
|
||||||
|
twitter.com
|
||||||
|
gumtree.pl
|
||||||
|
blogspot.com
|
||||||
|
kwejk.pl
|
||||||
|
wyborcza.pl
|
||||||
|
joemonster.org
|
||||||
|
stackoverflow.com
|
||||||
|
twitch.tv
|
||||||
|
o2.pl
|
||||||
|
ipko.pl
|
||||||
|
steamcommunity.com
|
||||||
|
github.com
|
||||||
|
chomikuj.pl
|
||||||
|
centrum24.pl
|
||||||
|
linkedin.com
|
||||||
|
money.pl
|
||||||
|
librus.pl
|
||||||
|
demotywatory.pl
|
||||||
|
sport.pl
|
||||||
|
microsoft.com
|
||||||
|
zalukaj.com
|
||||||
|
wikia.com
|
||||||
|
jbzdy.pl
|
||||||
|
imgur.com
|
||||||
|
flashscore.pl
|
||||||
|
gry-online.pl
|
||||||
|
pudelek.pl
|
|
@ -0,0 +1,50 @@
|
||||||
|
vk.com
|
||||||
|
google.ru
|
||||||
|
yandex.ru
|
||||||
|
youtube.com
|
||||||
|
mail.ru
|
||||||
|
ok.ru
|
||||||
|
google.com
|
||||||
|
avito.ru
|
||||||
|
aliexpress.com
|
||||||
|
wikipedia.org
|
||||||
|
instagram.com
|
||||||
|
sberbank.ru
|
||||||
|
gismeteo.ru
|
||||||
|
rambler.ru
|
||||||
|
kinogo.club
|
||||||
|
kinopoisk.ru
|
||||||
|
drom.ru
|
||||||
|
facebook.com
|
||||||
|
pikabu.ru
|
||||||
|
drive2.ru
|
||||||
|
rutracker.org
|
||||||
|
twitch.tv
|
||||||
|
rbc.ru
|
||||||
|
hh.ru
|
||||||
|
gosuslugi.ru
|
||||||
|
lenta.ru
|
||||||
|
pochta.ru
|
||||||
|
wildberries.ru
|
||||||
|
wikia.com
|
||||||
|
4pda.ru
|
||||||
|
fb.ru
|
||||||
|
seasonvar.ru
|
||||||
|
kp.ru
|
||||||
|
znanija.com
|
||||||
|
ucoz.ru
|
||||||
|
narod.ru
|
||||||
|
mts.ru
|
||||||
|
infourok.ru
|
||||||
|
ebay.com
|
||||||
|
ozon.ru
|
||||||
|
worldoftanks.ru
|
||||||
|
mos.ru
|
||||||
|
vesti.ru
|
||||||
|
nnmclub.to
|
||||||
|
microsoft.com
|
||||||
|
rp5.ru
|
||||||
|
2gis.ru
|
||||||
|
consultant.ru
|
||||||
|
fotostrana.ru
|
||||||
|
dnevnik.ru
|
|
@ -0,0 +1,49 @@
|
||||||
|
google.com.sg
|
||||||
|
youtube.com
|
||||||
|
google.com
|
||||||
|
facebook.com
|
||||||
|
yahoo.com
|
||||||
|
wikipedia.org
|
||||||
|
reddit.com
|
||||||
|
blogspot.sg
|
||||||
|
live.com
|
||||||
|
instagram.com
|
||||||
|
qoo10.sg
|
||||||
|
whatsapp.com
|
||||||
|
linkedin.com
|
||||||
|
dbs.com.sg
|
||||||
|
amazon.com
|
||||||
|
twitter.com
|
||||||
|
wordpress.com
|
||||||
|
onclkds.com
|
||||||
|
office.com
|
||||||
|
allsingaporestuff.com
|
||||||
|
baidu.com
|
||||||
|
lazada.sg
|
||||||
|
straitstimes.com
|
||||||
|
singpass.gov.sg
|
||||||
|
google.co.id
|
||||||
|
taobao.com
|
||||||
|
tumblr.com
|
||||||
|
gomovies.to
|
||||||
|
wikia.com
|
||||||
|
hardwarezone.com.sg
|
||||||
|
nus.edu.sg
|
||||||
|
msn.com
|
||||||
|
microsoft.com
|
||||||
|
carousell.com
|
||||||
|
kissanime.ru
|
||||||
|
ocbc.com
|
||||||
|
stackoverflow.com
|
||||||
|
ntu.edu.sg
|
||||||
|
thepiratebay.org
|
||||||
|
aliexpress.com
|
||||||
|
imgur.com
|
||||||
|
dropbox.com
|
||||||
|
apple.com
|
||||||
|
channelnewsasia.com
|
||||||
|
imdb.com
|
||||||
|
twitch.tv
|
||||||
|
abs-cbn.com
|
||||||
|
jobstreet.com.sg
|
||||||
|
uob.com.sg
|
|
@ -0,0 +1,50 @@
|
||||||
|
google.com.tw
|
||||||
|
pixnet.net
|
||||||
|
youtube.com
|
||||||
|
facebook.com
|
||||||
|
ettoday.net
|
||||||
|
google.com
|
||||||
|
yahoo.com
|
||||||
|
ltn.com.tw
|
||||||
|
nownews.com
|
||||||
|
setn.com
|
||||||
|
momoshop.com.tw
|
||||||
|
wikipedia.org
|
||||||
|
ck101.com
|
||||||
|
ptt.cc
|
||||||
|
tvbs.com.tw
|
||||||
|
104.com.tw
|
||||||
|
gamer.com.tw
|
||||||
|
appledaily.com.tw
|
||||||
|
pchome.com.tw
|
||||||
|
ruten.com.tw
|
||||||
|
ctitv.com.tw
|
||||||
|
teepr.com
|
||||||
|
life.tw
|
||||||
|
blogspot.tw
|
||||||
|
dcard.tw
|
||||||
|
baidu.com
|
||||||
|
udn.com
|
||||||
|
mobile01.com
|
||||||
|
eyny.com
|
||||||
|
epochtimes.com
|
||||||
|
qoolquiz.com
|
||||||
|
bomb01.com
|
||||||
|
talk.tw
|
||||||
|
ipetgroup.com
|
||||||
|
storm.mg
|
||||||
|
123kubo.com
|
||||||
|
cmoney.tw
|
||||||
|
taobao.com
|
||||||
|
twitch.tv
|
||||||
|
instagram.com
|
||||||
|
xuite.net
|
||||||
|
sina.com.tw
|
||||||
|
1111.com.tw
|
||||||
|
businessweekly.com.tw
|
||||||
|
elle.com.tw
|
||||||
|
twitter.com
|
||||||
|
books.com.tw
|
||||||
|
591.com.tw
|
||||||
|
everydayhealth.com.tw
|
||||||
|
techbang.com
|
|
@ -0,0 +1,49 @@
|
||||||
|
google.com
|
||||||
|
youtube.com
|
||||||
|
facebook.com
|
||||||
|
reddit.com
|
||||||
|
amazon.com
|
||||||
|
wikipedia.org
|
||||||
|
yahoo.com
|
||||||
|
twitter.com
|
||||||
|
netflix.com
|
||||||
|
ebay.com
|
||||||
|
imgur.com
|
||||||
|
linkedin.com
|
||||||
|
instagram.com
|
||||||
|
diply.com
|
||||||
|
craigslist.org
|
||||||
|
live.com
|
||||||
|
office.com
|
||||||
|
twitch.tv
|
||||||
|
tumblr.com
|
||||||
|
pinterest.com
|
||||||
|
espn.com
|
||||||
|
cnn.com
|
||||||
|
bing.com
|
||||||
|
wikia.com
|
||||||
|
chase.com
|
||||||
|
imdb.com
|
||||||
|
nytimes.com
|
||||||
|
paypal.com
|
||||||
|
blogspot.com
|
||||||
|
apple.com
|
||||||
|
yelp.com
|
||||||
|
stackoverflow.com
|
||||||
|
bankofamerica.com
|
||||||
|
wordpress.com
|
||||||
|
github.com
|
||||||
|
microsoft.com
|
||||||
|
wellsfargo.com
|
||||||
|
zillow.com
|
||||||
|
salesforce.com
|
||||||
|
msn.com
|
||||||
|
walmart.com
|
||||||
|
weather.com
|
||||||
|
dropbox.com
|
||||||
|
buzzfeed.com
|
||||||
|
intuit.com
|
||||||
|
washingtonpost.com
|
||||||
|
soundcloud.com
|
||||||
|
huffingtonpost.com
|
||||||
|
indeed.com
|
|
@ -0,0 +1,68 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains functionality to manage custom domains for auto-completion.
|
||||||
|
*/
|
||||||
|
object CustomDomains {
|
||||||
|
private const val PREFERENCE_NAME = "custom_autocomplete"
|
||||||
|
private const val KEY_DOMAINS = "custom_domains"
|
||||||
|
private const val SEPARATOR = "@<;>@"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the previously added/saved custom domains from preferences.
|
||||||
|
*
|
||||||
|
* @param context the application context
|
||||||
|
* @return list of custom domains
|
||||||
|
*/
|
||||||
|
fun load(context: Context): List<String> =
|
||||||
|
preferences(context).getString(KEY_DOMAINS, "")!!
|
||||||
|
.split(SEPARATOR)
|
||||||
|
.filter { !it.isEmpty() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the provided domains to preferences.
|
||||||
|
*
|
||||||
|
* @param context the application context
|
||||||
|
* @param domains list of domains
|
||||||
|
*/
|
||||||
|
fun save(context: Context, domains: List<String>) {
|
||||||
|
preferences(context)
|
||||||
|
.edit()
|
||||||
|
.putString(KEY_DOMAINS, domains.joinToString(separator = SEPARATOR))
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the provided domain to preferences.
|
||||||
|
*
|
||||||
|
* @param context the application context
|
||||||
|
* @param domain the domain to add
|
||||||
|
*/
|
||||||
|
fun add(context: Context, domain: String) {
|
||||||
|
val domains = mutableListOf<String>()
|
||||||
|
domains.addAll(load(context))
|
||||||
|
domains.add(domain)
|
||||||
|
|
||||||
|
save(context, domains)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the provided domain from preferences.
|
||||||
|
*
|
||||||
|
* @param context the application context
|
||||||
|
* @param domains the domain to remove
|
||||||
|
*/
|
||||||
|
fun remove(context: Context, domains: List<String>) {
|
||||||
|
save(context, load(context) - domains)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun preferences(context: Context): SharedPreferences =
|
||||||
|
context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class intended for internal use which encapsulates meta data about a domain.
|
||||||
|
*/
|
||||||
|
data class Domain(val protocol: String, val hasWww: Boolean, val host: String) {
|
||||||
|
internal val url: String
|
||||||
|
get() = "$protocol${if (hasWww) "www." else "" }$host"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PROTOCOL_INDEX = 1
|
||||||
|
private const val WWW_INDEX = 2
|
||||||
|
private const val HOST_INDEX = 3
|
||||||
|
|
||||||
|
private const val DEFAULT_PROTOCOL = "http://"
|
||||||
|
|
||||||
|
private val urlMatcher = Regex("""(https?://)?(www.)?(.+)?""")
|
||||||
|
|
||||||
|
fun create(url: String): Domain {
|
||||||
|
val result = urlMatcher.find(url)
|
||||||
|
|
||||||
|
return result?.let {
|
||||||
|
val protocol = it.groups[PROTOCOL_INDEX]?.value ?: DEFAULT_PROTOCOL
|
||||||
|
val hasWww = it.groups[WWW_INDEX]?.value == "www."
|
||||||
|
val host = it.groups[HOST_INDEX]?.value ?: throw IllegalStateException()
|
||||||
|
|
||||||
|
return Domain(protocol, hasWww, host)
|
||||||
|
} ?: throw IllegalStateException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun Iterable<String>.into(): List<Domain> {
|
||||||
|
return this.map { Domain.create(it) }
|
||||||
|
}
|
|
@ -0,0 +1,151 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides autocomplete functionality for domains, based on a provided list
|
||||||
|
* of assets (see [Domains]) and/or a custom domain list managed by
|
||||||
|
* [CustomDomains].
|
||||||
|
*/
|
||||||
|
// FIXME delete this https://github.com/mozilla-mobile/android-components/issues/1358
|
||||||
|
@Deprecated(
|
||||||
|
"Use `ShippedDomainsProvider` or `CustomDomainsProvider`",
|
||||||
|
ReplaceWith(
|
||||||
|
"ShippedDomainsProvider()/CustomDomainsProvider()",
|
||||||
|
"mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider",
|
||||||
|
"mozilla.components.browser.domains.autocomplete.CustomDomainsProvider",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
class DomainAutoCompleteProvider {
|
||||||
|
|
||||||
|
object AutocompleteSource {
|
||||||
|
const val DEFAULT_LIST = "default"
|
||||||
|
const val CUSTOM_LIST = "custom"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a result of auto-completion.
|
||||||
|
*
|
||||||
|
* @property text the result text starting with the raw search text as passed
|
||||||
|
* to [autocomplete] followed by the completion text (e.g. moz => mozilla.org)
|
||||||
|
* @property url the complete url (containing the protocol) as provided
|
||||||
|
* when the domain was saved. (e.g. https://mozilla.org)
|
||||||
|
* @property source the source identifier of the autocomplete source
|
||||||
|
* @property size total number of available autocomplete domains
|
||||||
|
* in this source
|
||||||
|
*/
|
||||||
|
data class Result(val text: String, val url: String, val source: String, val size: Int)
|
||||||
|
|
||||||
|
// We compute these on worker threads; make sure results are immediately visible on the UI thread.
|
||||||
|
@Volatile
|
||||||
|
internal var customDomains = emptyList<Domain>()
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
internal var shippedDomains = emptyList<Domain>()
|
||||||
|
private var useCustomDomains = false
|
||||||
|
private var useShippedDomains = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes an autocomplete suggestion for the given text, and invokes the
|
||||||
|
* provided callback, passing the result.
|
||||||
|
*
|
||||||
|
* @param rawText text to be auto completed
|
||||||
|
* @return the result of auto-completion. If no match is found an empty
|
||||||
|
* result object is returned.
|
||||||
|
*/
|
||||||
|
@Suppress("ReturnCount")
|
||||||
|
fun autocomplete(rawText: String): Result {
|
||||||
|
if (useCustomDomains) {
|
||||||
|
val result = tryToAutocomplete(rawText, customDomains, AutocompleteSource.CUSTOM_LIST)
|
||||||
|
if (result != null) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useShippedDomains) {
|
||||||
|
val result = tryToAutocomplete(rawText, shippedDomains, AutocompleteSource.DEFAULT_LIST)
|
||||||
|
if (result != null) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result("", "", "", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes this provider instance by making sure the shipped and/or custom
|
||||||
|
* domains are loaded.
|
||||||
|
*
|
||||||
|
* @param context the application context
|
||||||
|
* @param useShippedDomains true (default) if the domains provided by this
|
||||||
|
* module should be used, otherwise false.
|
||||||
|
* @param useCustomDomains true if the custom domains provided by
|
||||||
|
* [CustomDomains] should be used, otherwise false (default).
|
||||||
|
* @param loadDomainsFromDisk true (default) if domains should be loaded,
|
||||||
|
* otherwise false. This parameter is for testing purposes only.
|
||||||
|
*/
|
||||||
|
fun initialize(
|
||||||
|
context: Context,
|
||||||
|
useShippedDomains: Boolean = true,
|
||||||
|
useCustomDomains: Boolean = false,
|
||||||
|
loadDomainsFromDisk: Boolean = true,
|
||||||
|
) {
|
||||||
|
this.useCustomDomains = useCustomDomains
|
||||||
|
this.useShippedDomains = useShippedDomains
|
||||||
|
|
||||||
|
if (!loadDomainsFromDisk) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!useCustomDomains && !useShippedDomains) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
if (useCustomDomains) {
|
||||||
|
customDomains = async { CustomDomains.load(context).into() }.await()
|
||||||
|
}
|
||||||
|
if (useShippedDomains) {
|
||||||
|
shippedDomains = async { Domains.load(context).into() }.await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("ReturnCount")
|
||||||
|
private fun tryToAutocomplete(rawText: String, domains: List<Domain>, source: String): Result? {
|
||||||
|
// Search terms are all lowercase already, we just need to lowercase the search text
|
||||||
|
val searchText = rawText.lowercase(Locale.US)
|
||||||
|
|
||||||
|
domains.forEach {
|
||||||
|
val wwwDomain = "www.${it.host}"
|
||||||
|
if (wwwDomain.startsWith(searchText)) {
|
||||||
|
return Result(getResultText(rawText, wwwDomain), it.url, source, domains.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it.host.startsWith(searchText)) {
|
||||||
|
return Result(getResultText(rawText, it.host), it.url, source, domains.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Our autocomplete list is all lower case, however the search text might
|
||||||
|
* be mixed case. Our autocomplete EditText code does more string comparison,
|
||||||
|
* which fails if the suggestion doesn't exactly match searchText (ie.
|
||||||
|
* if casing differs). It's simplest to just build a suggestion
|
||||||
|
* that exactly matches the search text - which is what this method is for:
|
||||||
|
*/
|
||||||
|
private fun getResultText(rawSearchText: String, autocomplete: String) =
|
||||||
|
rawSearchText + autocomplete.substring(rawSearchText.length)
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build.VERSION.SDK_INT
|
||||||
|
import android.os.Build.VERSION_CODES
|
||||||
|
import android.os.LocaleList
|
||||||
|
import android.text.TextUtils
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains functionality to access domain lists shipped as part of this
|
||||||
|
* module's assets.
|
||||||
|
*/
|
||||||
|
object Domains {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the domains applicable to the app's locale, plus the domains
|
||||||
|
* in the 'global' list.
|
||||||
|
*
|
||||||
|
* @param context the application context
|
||||||
|
* @return list of domains
|
||||||
|
*/
|
||||||
|
fun load(context: Context): List<String> {
|
||||||
|
return load(context, getCountriesInDefaultLocaleList())
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun load(context: Context, countries: Set<String>): List<String> {
|
||||||
|
val domains = LinkedHashSet<String>()
|
||||||
|
val availableLists = getAvailableDomainLists(context)
|
||||||
|
|
||||||
|
// First initialize the country specific lists following the default locale order
|
||||||
|
countries
|
||||||
|
.filter { availableLists.contains(it) }
|
||||||
|
.forEach { loadDomainsForLanguage(context, domains, it) }
|
||||||
|
|
||||||
|
// And then add domains from the global list
|
||||||
|
loadDomainsForLanguage(context, domains, "global")
|
||||||
|
|
||||||
|
return domains.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAvailableDomainLists(context: Context): Set<String> {
|
||||||
|
val availableDomains = LinkedHashSet<String>()
|
||||||
|
val assetManager = context.assets
|
||||||
|
val domains = try {
|
||||||
|
assetManager.list("domains") ?: emptyArray<String>()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
emptyArray<String>()
|
||||||
|
}
|
||||||
|
availableDomains.addAll(domains)
|
||||||
|
return availableDomains
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadDomainsForLanguage(context: Context, domains: MutableSet<String>, country: String) {
|
||||||
|
val assetManager = context.assets
|
||||||
|
val languageDomains = try {
|
||||||
|
assetManager.open("domains/$country").bufferedReader().readLines()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
emptyList<String>()
|
||||||
|
}
|
||||||
|
domains.addAll(languageDomains)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCountriesInDefaultLocaleList(): Set<String> {
|
||||||
|
val countries = java.util.LinkedHashSet<String>()
|
||||||
|
val addIfNotEmpty = { c: String -> if (!TextUtils.isEmpty(c)) countries.add(c.lowercase(Locale.US)) }
|
||||||
|
|
||||||
|
if (SDK_INT >= VERSION_CODES.N) {
|
||||||
|
val list = LocaleList.getDefault()
|
||||||
|
for (i in 0 until list.size()) {
|
||||||
|
addIfNotEmpty(list.get(i).country)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addIfNotEmpty(Locale.getDefault().country)
|
||||||
|
}
|
||||||
|
|
||||||
|
return countries
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains.autocomplete
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import mozilla.components.browser.domains.CustomDomains
|
||||||
|
import mozilla.components.browser.domains.Domain
|
||||||
|
import mozilla.components.browser.domains.Domains
|
||||||
|
import mozilla.components.browser.domains.into
|
||||||
|
import mozilla.components.concept.toolbar.AutocompleteProvider
|
||||||
|
import mozilla.components.concept.toolbar.AutocompleteResult
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
enum class DomainList(val listName: String) {
|
||||||
|
DEFAULT("default"),
|
||||||
|
CUSTOM("custom"),
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides autocomplete functionality for domains based on provided list of assets (see [Domains]).
|
||||||
|
*/
|
||||||
|
class ShippedDomainsProvider(override val autocompletePriority: Int = 0) :
|
||||||
|
BaseDomainAutocompleteProvider(DomainList.DEFAULT, Domains.asLoader())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides autocomplete functionality for domains based on a list managed by [CustomDomains].
|
||||||
|
*/
|
||||||
|
class CustomDomainsProvider(override val autocompletePriority: Int = 0) :
|
||||||
|
BaseDomainAutocompleteProvider(DomainList.CUSTOM, CustomDomains.asLoader())
|
||||||
|
|
||||||
|
typealias DomainsLoader = (Context) -> List<Domain>
|
||||||
|
|
||||||
|
private fun Domains.asLoader(): DomainsLoader = { context: Context -> load(context).into() }
|
||||||
|
private fun CustomDomains.asLoader(): DomainsLoader = { context: Context -> load(context).into() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides common autocomplete functionality powered by domain lists.
|
||||||
|
*
|
||||||
|
* @param list source of domains
|
||||||
|
* @param domainsLoader provider for all available domains
|
||||||
|
*/
|
||||||
|
open class BaseDomainAutocompleteProvider(
|
||||||
|
private val list: DomainList,
|
||||||
|
private val domainsLoader: DomainsLoader,
|
||||||
|
override val autocompletePriority: Int = 0,
|
||||||
|
) : AutocompleteProvider, CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||||
|
|
||||||
|
// We compute 'domains' on the worker thread; make sure it's immediately visible on the UI thread.
|
||||||
|
@Volatile
|
||||||
|
var domains: List<Domain> = emptyList()
|
||||||
|
|
||||||
|
fun initialize(context: Context) {
|
||||||
|
launch {
|
||||||
|
domains = async { domainsLoader(context) }.await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes an autocomplete suggestion for the given text, and invokes the
|
||||||
|
* provided callback, passing the result.
|
||||||
|
*
|
||||||
|
* @param query text to be auto completed
|
||||||
|
* @return the result of auto-completion, or null if no match is found.
|
||||||
|
*/
|
||||||
|
override suspend fun getAutocompleteSuggestion(query: String): AutocompleteResult? {
|
||||||
|
// Search terms are all lowercase already, we just need to lowercase the search text
|
||||||
|
val searchText = query.lowercase(Locale.US)
|
||||||
|
|
||||||
|
domains.forEach {
|
||||||
|
val wwwDomain = "www.${it.host}"
|
||||||
|
if (wwwDomain.startsWith(searchText)) {
|
||||||
|
return AutocompleteResult(
|
||||||
|
input = searchText,
|
||||||
|
text = getResultText(query, wwwDomain),
|
||||||
|
url = it.url,
|
||||||
|
source = list.listName,
|
||||||
|
totalItems = domains.size,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it.host.startsWith(searchText)) {
|
||||||
|
return AutocompleteResult(
|
||||||
|
input = searchText,
|
||||||
|
text = getResultText(query, it.host),
|
||||||
|
url = it.url,
|
||||||
|
source = list.listName,
|
||||||
|
totalItems = domains.size,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Our autocomplete list is all lower case, however the search text might
|
||||||
|
* be mixed case. Our autocomplete EditText code does more string comparison,
|
||||||
|
* which fails if the suggestion doesn't exactly match searchText (ie.
|
||||||
|
* if casing differs). It's simplest to just build a suggestion
|
||||||
|
* that exactly matches the search text - which is what this method is for:
|
||||||
|
*/
|
||||||
|
private fun getResultText(rawSearchText: String, autocomplete: String) =
|
||||||
|
rawSearchText + autocomplete.substring(rawSearchText.length)
|
||||||
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Looper.getMainLooper
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import mozilla.components.browser.domains.autocomplete.BaseDomainAutocompleteProvider
|
||||||
|
import mozilla.components.browser.domains.autocomplete.DomainList
|
||||||
|
import mozilla.components.browser.domains.autocomplete.DomainsLoader
|
||||||
|
import mozilla.components.concept.toolbar.AutocompleteProvider
|
||||||
|
import mozilla.components.support.test.robolectric.testContext
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.Shadows.shadowOf
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class BaseDomainAutocompleteProviderTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `empty provider with DEFAULT list returns nothing`() {
|
||||||
|
val provider = createAndInitProvider(testContext, DomainList.DEFAULT) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNoCompletion(provider, "m")
|
||||||
|
assertNoCompletion(provider, "mo")
|
||||||
|
assertNoCompletion(provider, "moz")
|
||||||
|
assertNoCompletion(provider, "g")
|
||||||
|
assertNoCompletion(provider, "go")
|
||||||
|
assertNoCompletion(provider, "goo")
|
||||||
|
assertNoCompletion(provider, "w")
|
||||||
|
assertNoCompletion(provider, "www")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `empty provider with CUSTOM list returns nothing`() {
|
||||||
|
val provider = createAndInitProvider(testContext, DomainList.CUSTOM) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNoCompletion(provider, "m")
|
||||||
|
assertNoCompletion(provider, "mo")
|
||||||
|
assertNoCompletion(provider, "moz")
|
||||||
|
assertNoCompletion(provider, "g")
|
||||||
|
assertNoCompletion(provider, "go")
|
||||||
|
assertNoCompletion(provider, "goo")
|
||||||
|
assertNoCompletion(provider, "w")
|
||||||
|
assertNoCompletion(provider, "www")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `non-empty provider with DEFAULT list returns completion`() {
|
||||||
|
val domains = listOf("mozilla.org", "google.com", "facebook.com").into()
|
||||||
|
val list = DomainList.DEFAULT
|
||||||
|
val domainsCount = domains.size
|
||||||
|
|
||||||
|
val provider = createAndInitProvider(testContext, list) { domains }
|
||||||
|
shadowOf(getMainLooper()).idle()
|
||||||
|
|
||||||
|
assertCompletion(provider, list, domainsCount, "m", "m", "mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "moz", "moz", "mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "www", "www", "www.mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "www.face", "www.face", "www.facebook.com", "http://facebook.com")
|
||||||
|
assertCompletion(provider, list, domainsCount, "M", "m", "Mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "MOZ", "moz", "MOZilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "www.GOO", "www.goo", "www.GOOgle.com", "http://google.com")
|
||||||
|
assertCompletion(provider, list, domainsCount, "WWW.GOOGLE.", "www.google.", "WWW.GOOGLE.com", "http://google.com")
|
||||||
|
assertCompletion(provider, list, domainsCount, "www.facebook.com", "www.facebook.com", "www.facebook.com", "http://facebook.com")
|
||||||
|
assertCompletion(provider, list, domainsCount, "facebook.com", "facebook.com", "facebook.com", "http://facebook.com")
|
||||||
|
|
||||||
|
assertNoCompletion(provider, "wwww")
|
||||||
|
assertNoCompletion(provider, "yahoo")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `non-empty provider with CUSTOM list returns completion`() {
|
||||||
|
val domains = listOf("mozilla.org", "google.com", "facebook.com").into()
|
||||||
|
val list = DomainList.CUSTOM
|
||||||
|
val domainsCount = domains.size
|
||||||
|
|
||||||
|
val provider = createAndInitProvider(testContext, list) { domains }
|
||||||
|
shadowOf(getMainLooper()).idle()
|
||||||
|
|
||||||
|
assertCompletion(provider, list, domainsCount, "m", "m", "mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "moz", "moz", "mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "www", "www", "www.mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "www.face", "www.face", "www.facebook.com", "http://facebook.com")
|
||||||
|
assertCompletion(provider, list, domainsCount, "M", "m", "Mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "MOZ", "moz", "MOZilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "www.GOO", "www.goo", "www.GOOgle.com", "http://google.com")
|
||||||
|
assertCompletion(provider, list, domainsCount, "WWW.GOOGLE.", "www.google.", "WWW.GOOGLE.com", "http://google.com")
|
||||||
|
assertCompletion(provider, list, domainsCount, "www.facebook.com", "www.facebook.com", "www.facebook.com", "http://facebook.com")
|
||||||
|
assertCompletion(provider, list, domainsCount, "facebook.com", "facebook.com", "facebook.com", "http://facebook.com")
|
||||||
|
|
||||||
|
assertNoCompletion(provider, "wwww")
|
||||||
|
assertNoCompletion(provider, "yahoo")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private fun assertCompletion(
|
||||||
|
provider: AutocompleteProvider,
|
||||||
|
domainSource: DomainList,
|
||||||
|
sourceSize: Int,
|
||||||
|
input: String,
|
||||||
|
expectedInput: String,
|
||||||
|
completion: String,
|
||||||
|
expectedUrl: String,
|
||||||
|
) = runTest {
|
||||||
|
val result = provider.getAutocompleteSuggestion(input)!!
|
||||||
|
|
||||||
|
assertTrue("Autocompletion shouldn't be empty", result.text.isNotEmpty())
|
||||||
|
|
||||||
|
assertEquals("Autocompletion input", expectedInput, result.input)
|
||||||
|
assertEquals("Autocompletion completion", completion, result.text)
|
||||||
|
assertEquals("Autocompletion source list", domainSource.listName, result.source)
|
||||||
|
assertEquals("Autocompletion url", expectedUrl, result.url)
|
||||||
|
assertEquals("Autocompletion source list size", sourceSize, result.totalItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private fun assertNoCompletion(provider: AutocompleteProvider, input: String) = runTest {
|
||||||
|
val result = provider.getAutocompleteSuggestion(input)
|
||||||
|
|
||||||
|
assertNull("Result should be null", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createAndInitProvider(context: Context, list: DomainList, loader: DomainsLoader): AutocompleteProvider =
|
||||||
|
object : BaseDomainAutocompleteProvider(list, loader) {
|
||||||
|
override val coroutineContext = super.coroutineContext + Dispatchers.Main
|
||||||
|
}.apply { initialize(context) }
|
|
@ -0,0 +1,81 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import mozilla.components.support.test.robolectric.testContext
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class CustomDomainsTest {
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
testContext.getSharedPreferences("custom_autocomplete", Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.clear()
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
@Test
|
||||||
|
fun customListIsEmptyByDefault() {
|
||||||
|
val domains = CustomDomains.load(testContext)
|
||||||
|
|
||||||
|
assertEquals(0, domains.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun saveAndRemoveDomains() {
|
||||||
|
CustomDomains.save(
|
||||||
|
testContext,
|
||||||
|
listOf(
|
||||||
|
"mozilla.org",
|
||||||
|
"example.org",
|
||||||
|
"example.com",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
var domains = CustomDomains.load(testContext)
|
||||||
|
assertEquals(3, domains.size)
|
||||||
|
|
||||||
|
CustomDomains.remove(testContext, listOf("example.org", "example.com"))
|
||||||
|
domains = CustomDomains.load(testContext)
|
||||||
|
assertEquals(1, domains.size)
|
||||||
|
assertEquals("mozilla.org", domains.elementAt(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun addAndLoadDomains() {
|
||||||
|
CustomDomains.add(testContext, "mozilla.org")
|
||||||
|
val domains = CustomDomains.load(testContext)
|
||||||
|
assertEquals(1, domains.size)
|
||||||
|
assertEquals("mozilla.org", domains.elementAt(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun saveAndLoadDomains() {
|
||||||
|
CustomDomains.save(
|
||||||
|
testContext,
|
||||||
|
listOf(
|
||||||
|
"mozilla.org",
|
||||||
|
"example.org",
|
||||||
|
"example.com",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val domains = CustomDomains.load(testContext)
|
||||||
|
|
||||||
|
assertEquals(3, domains.size)
|
||||||
|
assertEquals("mozilla.org", domains.elementAt(0))
|
||||||
|
assertEquals("example.org", domains.elementAt(1))
|
||||||
|
assertEquals("example.com", domains.elementAt(2))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
@file:Suppress("DEPRECATION")
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import mozilla.components.browser.domains.DomainAutoCompleteProvider.AutocompleteSource.CUSTOM_LIST
|
||||||
|
import mozilla.components.browser.domains.DomainAutoCompleteProvider.AutocompleteSource.DEFAULT_LIST
|
||||||
|
import mozilla.components.support.test.robolectric.testContext
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
/**
|
||||||
|
* While [DomainAutoCompleteProvider] exists (even if it's deprecated) we need to test it.
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class DomainAutoCompleteProviderTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun autocompletionWithShippedDomains() {
|
||||||
|
val provider = DomainAutoCompleteProvider().also {
|
||||||
|
it.initialize(
|
||||||
|
testContext,
|
||||||
|
useShippedDomains = true,
|
||||||
|
useCustomDomains = false,
|
||||||
|
loadDomainsFromDisk = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
it.shippedDomains = listOf("mozilla.org", "google.com", "facebook.com").into()
|
||||||
|
it.customDomains = emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val size = provider.shippedDomains.size
|
||||||
|
|
||||||
|
assertCompletion(provider, "m", DEFAULT_LIST, size, "mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, "www", DEFAULT_LIST, size, "www.mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, "www.face", DEFAULT_LIST, size, "www.facebook.com", "http://facebook.com")
|
||||||
|
assertCompletion(provider, "MOZ", DEFAULT_LIST, size, "MOZilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, "www.GOO", DEFAULT_LIST, size, "www.GOOgle.com", "http://google.com")
|
||||||
|
assertCompletion(provider, "WWW.GOOGLE.", DEFAULT_LIST, size, "WWW.GOOGLE.com", "http://google.com")
|
||||||
|
assertCompletion(provider, "www.facebook.com", DEFAULT_LIST, size, "www.facebook.com", "http://facebook.com")
|
||||||
|
assertCompletion(provider, "facebook.com", DEFAULT_LIST, size, "facebook.com", "http://facebook.com")
|
||||||
|
|
||||||
|
assertNoCompletion(provider, "wwww")
|
||||||
|
assertNoCompletion(provider, "yahoo")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun autocompletionWithCustomDomains() {
|
||||||
|
val domains = listOf("facebook.com", "google.com", "mozilla.org")
|
||||||
|
val customDomains = listOf("gap.com", "www.fanfiction.com", "https://mobile.de")
|
||||||
|
|
||||||
|
val provider = DomainAutoCompleteProvider().also {
|
||||||
|
it.initialize(
|
||||||
|
testContext,
|
||||||
|
useShippedDomains = true,
|
||||||
|
useCustomDomains = true,
|
||||||
|
loadDomainsFromDisk = false,
|
||||||
|
)
|
||||||
|
it.shippedDomains = domains.into()
|
||||||
|
it.customDomains = customDomains.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertCompletion(provider, "f", CUSTOM_LIST, customDomains.size, "fanfiction.com", "http://www.fanfiction.com")
|
||||||
|
assertCompletion(provider, "fa", CUSTOM_LIST, customDomains.size, "fanfiction.com", "http://www.fanfiction.com")
|
||||||
|
assertCompletion(provider, "fac", DEFAULT_LIST, domains.size, "facebook.com", "http://facebook.com")
|
||||||
|
|
||||||
|
assertCompletion(provider, "g", CUSTOM_LIST, customDomains.size, "gap.com", "http://gap.com")
|
||||||
|
assertCompletion(provider, "go", DEFAULT_LIST, domains.size, "google.com", "http://google.com")
|
||||||
|
assertCompletion(provider, "ga", CUSTOM_LIST, customDomains.size, "gap.com", "http://gap.com")
|
||||||
|
|
||||||
|
assertCompletion(provider, "m", CUSTOM_LIST, customDomains.size, "mobile.de", "https://mobile.de")
|
||||||
|
assertCompletion(provider, "mo", CUSTOM_LIST, customDomains.size, "mobile.de", "https://mobile.de")
|
||||||
|
assertCompletion(provider, "mob", CUSTOM_LIST, customDomains.size, "mobile.de", "https://mobile.de")
|
||||||
|
assertCompletion(provider, "moz", DEFAULT_LIST, domains.size, "mozilla.org", "http://mozilla.org")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun autocompletionWithoutDomains() {
|
||||||
|
val filter = DomainAutoCompleteProvider()
|
||||||
|
assertNoCompletion(filter, "mozilla")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertCompletion(
|
||||||
|
provider: DomainAutoCompleteProvider,
|
||||||
|
text: String,
|
||||||
|
domainSource: String,
|
||||||
|
sourceSize: Int,
|
||||||
|
completion: String,
|
||||||
|
expectedUrl: String,
|
||||||
|
) {
|
||||||
|
val result = provider.autocomplete(text)
|
||||||
|
|
||||||
|
assertFalse(result.text.isEmpty())
|
||||||
|
|
||||||
|
assertEquals(completion, result.text)
|
||||||
|
assertEquals(domainSource, result.source)
|
||||||
|
assertEquals(expectedUrl, result.url)
|
||||||
|
assertEquals(sourceSize, result.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertNoCompletion(provider: DomainAutoCompleteProvider, text: String) {
|
||||||
|
val result = provider.autocomplete(text)
|
||||||
|
|
||||||
|
assertTrue(result.text.isEmpty())
|
||||||
|
assertTrue(result.url.isEmpty())
|
||||||
|
assertTrue(result.source.isEmpty())
|
||||||
|
assertEquals(0, result.size)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class DomainTest {
|
||||||
|
@Test
|
||||||
|
fun domainCreation() {
|
||||||
|
val firstItem = Domain.create("https://mozilla.com")
|
||||||
|
|
||||||
|
Assert.assertTrue(firstItem.protocol == "https://")
|
||||||
|
Assert.assertFalse(firstItem.hasWww)
|
||||||
|
Assert.assertTrue(firstItem.host == "mozilla.com")
|
||||||
|
|
||||||
|
val secondItem = Domain.create("www.mozilla.com")
|
||||||
|
|
||||||
|
Assert.assertTrue(secondItem.protocol == "http://")
|
||||||
|
Assert.assertTrue(secondItem.hasWww)
|
||||||
|
Assert.assertTrue(secondItem.host == "mozilla.com")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun domainCanCreateUrl() {
|
||||||
|
val firstItem = Domain.create("https://mozilla.com")
|
||||||
|
Assert.assertEquals("https://mozilla.com", firstItem.url)
|
||||||
|
|
||||||
|
val secondItem = Domain.create("www.mozilla.com")
|
||||||
|
Assert.assertEquals("http://www.mozilla.com", secondItem.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalStateException::class)
|
||||||
|
fun domainCreationWithBadURLThrowsException() {
|
||||||
|
Domain.create("http://www.")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import mozilla.components.support.test.robolectric.testContext
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class DomainsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun loadDomains() {
|
||||||
|
val domains = Domains.load(testContext, setOf("us"))
|
||||||
|
Assert.assertFalse(domains.isEmpty())
|
||||||
|
Assert.assertTrue(domains.contains("reddit.com"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun loadDomainsWithDefaultCountries() {
|
||||||
|
val domains = Domains.load(testContext)
|
||||||
|
Assert.assertFalse(domains.isEmpty())
|
||||||
|
// From the global list
|
||||||
|
Assert.assertTrue(domains.contains("mozilla.org"))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import mozilla.components.browser.domains.autocomplete.BaseDomainAutocompleteProvider
|
||||||
|
import mozilla.components.browser.domains.autocomplete.CustomDomainsProvider
|
||||||
|
import mozilla.components.browser.domains.autocomplete.DomainList
|
||||||
|
import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ProvidersTest {
|
||||||
|
@Test
|
||||||
|
fun autocompletionWithShippedDomains() {
|
||||||
|
val provider = ShippedDomainsProvider()
|
||||||
|
provider.domains = listOf("mozilla.org", "google.com", "facebook.com").into()
|
||||||
|
|
||||||
|
val size = provider.domains.size
|
||||||
|
|
||||||
|
assertCompletion(provider, "m", DomainList.DEFAULT, size, "mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, "www", DomainList.DEFAULT, size, "www.mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, "www.face", DomainList.DEFAULT, size, "www.facebook.com", "http://facebook.com")
|
||||||
|
assertCompletion(provider, "MOZ", DomainList.DEFAULT, size, "MOZilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, "www.GOO", DomainList.DEFAULT, size, "www.GOOgle.com", "http://google.com")
|
||||||
|
assertCompletion(provider, "WWW.GOOGLE.", DomainList.DEFAULT, size, "WWW.GOOGLE.com", "http://google.com")
|
||||||
|
assertCompletion(provider, "www.facebook.com", DomainList.DEFAULT, size, "www.facebook.com", "http://facebook.com")
|
||||||
|
assertCompletion(provider, "facebook.com", DomainList.DEFAULT, size, "facebook.com", "http://facebook.com")
|
||||||
|
|
||||||
|
assertNoCompletion(provider, "wwww")
|
||||||
|
assertNoCompletion(provider, "yahoo")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun autocompletionWithCustomDomains() {
|
||||||
|
val customDomains = listOf("gap.com", "www.fanfiction.com", "https://mobile.de")
|
||||||
|
|
||||||
|
val provider = CustomDomainsProvider()
|
||||||
|
provider.domains = customDomains.into()
|
||||||
|
|
||||||
|
assertCompletion(provider, "f", DomainList.CUSTOM, customDomains.size, "fanfiction.com", "http://www.fanfiction.com")
|
||||||
|
assertCompletion(provider, "fa", DomainList.CUSTOM, customDomains.size, "fanfiction.com", "http://www.fanfiction.com")
|
||||||
|
|
||||||
|
assertCompletion(provider, "g", DomainList.CUSTOM, customDomains.size, "gap.com", "http://gap.com")
|
||||||
|
assertCompletion(provider, "ga", DomainList.CUSTOM, customDomains.size, "gap.com", "http://gap.com")
|
||||||
|
|
||||||
|
assertCompletion(provider, "m", DomainList.CUSTOM, customDomains.size, "mobile.de", "https://mobile.de")
|
||||||
|
assertCompletion(provider, "mo", DomainList.CUSTOM, customDomains.size, "mobile.de", "https://mobile.de")
|
||||||
|
assertCompletion(provider, "mob", DomainList.CUSTOM, customDomains.size, "mobile.de", "https://mobile.de")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun autocompletionWithoutDomains() {
|
||||||
|
val filter = CustomDomainsProvider()
|
||||||
|
assertNoCompletion(filter, "mozilla")
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private fun assertCompletion(
|
||||||
|
provider: BaseDomainAutocompleteProvider,
|
||||||
|
text: String,
|
||||||
|
domainSource: DomainList,
|
||||||
|
sourceSize: Int,
|
||||||
|
completion: String,
|
||||||
|
expectedUrl: String,
|
||||||
|
) = runTest {
|
||||||
|
val result = provider.getAutocompleteSuggestion(text)!!
|
||||||
|
assertFalse(result.text.isEmpty())
|
||||||
|
|
||||||
|
assertEquals(completion, result.text)
|
||||||
|
assertEquals(domainSource.listName, result.source)
|
||||||
|
assertEquals(expectedUrl, result.url)
|
||||||
|
assertEquals(sourceSize, result.totalItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private fun assertNoCompletion(provider: BaseDomainAutocompleteProvider, text: String) = runTest {
|
||||||
|
assertNull(provider.getAutocompleteSuggestion(text))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
sdk=34
|
|
@ -0,0 +1,41 @@
|
||||||
|
# [Android Components](../../../README.md) > Browser > Engine-Gecko
|
||||||
|
|
||||||
|
[*Engine*](../../concept/engine/README.md) implementation based on [GeckoView](https://wiki.mozilla.org/Mobile/GeckoView).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Setting up the dependency
|
||||||
|
|
||||||
|
Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)):
|
||||||
|
|
||||||
|
```Groovy
|
||||||
|
implementation "org.mozilla.components:browser-engine-gecko:{latest-version}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration with the Glean SDK
|
||||||
|
|
||||||
|
#### Before using this component
|
||||||
|
Products sending telemetry and using this component *must request* a data-review following [this process](https://wiki.mozilla.org/Firefox/Data_Collection).
|
||||||
|
|
||||||
|
The [Glean SDK](../../../components/service/glean/README.md) can be used to collect [Gecko Telemetry](https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/index.html).
|
||||||
|
Applications using both this component and the Glean SDK should setup the Gecko Telemetry delegate
|
||||||
|
as shown below:
|
||||||
|
|
||||||
|
```Kotlin
|
||||||
|
val builder = GeckoRuntimeSettings.Builder()
|
||||||
|
val runtimeSettings = builder
|
||||||
|
.telemetryDelegate(GeckoGleanAdapter()) // Sets up the delegate!
|
||||||
|
.build()
|
||||||
|
// Create the Gecko runtime.
|
||||||
|
GeckoRuntime.create(context, runtimeSettings)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Adding new metrics
|
||||||
|
|
||||||
|
New Gecko metrics can be added as described [in the Firefox Telemetry docs](https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/start/adding-a-new-probe.html).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/
|
|
@ -0,0 +1,195 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORIES.each { repository ->
|
||||||
|
maven {
|
||||||
|
url repository
|
||||||
|
if (gradle.mozconfig.substs.ALLOW_INSECURE_GRADLE_REPOSITORIES) {
|
||||||
|
allowInsecureProtocol = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath "${ApplicationServicesConfig.groupId}:tooling-nimbus-gradle:${ApplicationServicesConfig.version}"
|
||||||
|
classpath "org.mozilla.telemetry:glean-gradle-plugin:${Versions.mozilla_glean}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id "com.jetbrains.python.envs" version "$python_envs_plugin"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'com.android.library'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion config.minSdkVersion
|
||||||
|
compileSdk config.compileSdkVersion
|
||||||
|
targetSdkVersion config.targetSdkVersion
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
resources {
|
||||||
|
excludes += ['META-INF/proguard/androidx-annotations.pro']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig true
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace 'mozilla.components.browser.engine.gecko'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set configuration for the Glean parser to extract metrics.yaml
|
||||||
|
// file from AAR dependencies of this project rather than look
|
||||||
|
// for it into the project directory.
|
||||||
|
ext.allowMetricsFromAAR = true
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(':concept-engine')
|
||||||
|
implementation project(':concept-fetch')
|
||||||
|
implementation project(':support-ktx')
|
||||||
|
implementation project(':support-utils')
|
||||||
|
implementation(project(':service-nimbus')) {
|
||||||
|
exclude group: 'org.mozilla.telemetry', module: 'glean-native'
|
||||||
|
}
|
||||||
|
implementation ComponentsDependencies.kotlin_coroutines
|
||||||
|
|
||||||
|
if (findProject(":geckoview") != null) {
|
||||||
|
api project(':geckoview')
|
||||||
|
} else {
|
||||||
|
api getGeckoViewDependency()
|
||||||
|
}
|
||||||
|
|
||||||
|
implementation ComponentsDependencies.androidx_core_ktx
|
||||||
|
implementation ComponentsDependencies.androidx_paging
|
||||||
|
implementation ComponentsDependencies.androidx_datastore_preferences
|
||||||
|
implementation ComponentsDependencies.androidx_lifecycle_livedata
|
||||||
|
|
||||||
|
testImplementation ComponentsDependencies.androidx_test_core
|
||||||
|
testImplementation ComponentsDependencies.androidx_test_junit
|
||||||
|
testImplementation ComponentsDependencies.testing_robolectric
|
||||||
|
testImplementation ComponentsDependencies.testing_coroutines
|
||||||
|
testImplementation ComponentsDependencies.testing_mockwebserver
|
||||||
|
testImplementation project(':support-test')
|
||||||
|
testImplementation project(':tooling-fetch-tests')
|
||||||
|
|
||||||
|
// We only compile against Glean. It's up to the app to add those dependencies
|
||||||
|
// if it wants to collect GeckoView telemetry through the Glean SDK.
|
||||||
|
compileOnly ComponentsDependencies.mozilla_glean
|
||||||
|
testImplementation ComponentsDependencies.mozilla_glean
|
||||||
|
testImplementation ComponentsDependencies.androidx_work_testing
|
||||||
|
|
||||||
|
androidTestImplementation ComponentsDependencies.androidx_test_core
|
||||||
|
androidTestImplementation ComponentsDependencies.androidx_test_runner
|
||||||
|
androidTestImplementation ComponentsDependencies.androidx_test_rules
|
||||||
|
androidTestImplementation project(':tooling-fetch-tests')
|
||||||
|
}
|
||||||
|
|
||||||
|
ext.gleanNamespace = "mozilla.telemetry.glean"
|
||||||
|
apply plugin: "org.mozilla.telemetry.glean-gradle-plugin"
|
||||||
|
apply from: '../../../android-lint.gradle'
|
||||||
|
apply from: '../../../publish.gradle'
|
||||||
|
apply plugin: "org.mozilla.appservices.nimbus-gradle-plugin"
|
||||||
|
nimbus {
|
||||||
|
// The path to the Nimbus feature manifest file
|
||||||
|
manifestFile = "geckoview.fml.yaml"
|
||||||
|
|
||||||
|
channels = [
|
||||||
|
debug: "debug",
|
||||||
|
release: "release",
|
||||||
|
]
|
||||||
|
|
||||||
|
// This is an optional value, and updates the plugin to use a copy of application
|
||||||
|
// services. The path should be relative to the root project directory.
|
||||||
|
// *NOTE*: This example will not work for all projects, but should work for Fenix, Focus, and Android Components
|
||||||
|
applicationServicesDir = gradle.hasProperty('localProperties.autoPublish.application-services.dir')
|
||||||
|
? gradle.getProperty('localProperties.autoPublish.application-services.dir') : null
|
||||||
|
}
|
||||||
|
ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
|
||||||
|
|
||||||
|
// Non-official versions are like "61.0a1", where "a1" is the milestone.
|
||||||
|
// This simply strips that off, leaving "61.0" in this example.
|
||||||
|
def getAppVersionWithoutMilestone() {
|
||||||
|
return gradle.mozconfig.substs.MOZ_APP_VERSION.replaceFirst(/a[0-9]/, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mimic Python: open(os.path.join(buildconfig.topobjdir, 'buildid.h')).readline().split()[2]
|
||||||
|
def getBuildId() {
|
||||||
|
if (System.env.MOZ_BUILD_DATE) {
|
||||||
|
if (System.env.MOZ_BUILD_DATE.length() == 14) {
|
||||||
|
return System.env.MOZ_BUILD_DATE
|
||||||
|
}
|
||||||
|
logger.warn("Ignoring invalid MOZ_BUILD_DATE: ${System.env.MOZ_BUILD_DATE}")
|
||||||
|
}
|
||||||
|
return file("${gradle.mozconfig.topobjdir}/buildid.h").getText('utf-8').split()[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
def getVersionNumber() {
|
||||||
|
def appVersion = getAppVersionWithoutMilestone()
|
||||||
|
def parts = appVersion.split('\\.')
|
||||||
|
def version = parts[0] + "." + parts[1] + "." + getBuildId()
|
||||||
|
|
||||||
|
if (!gradle.mozconfig.substs.MOZILLA_OFFICIAL && !gradle.mozconfig.substs.MOZ_ANDROID_FAT_AAR_ARCHITECTURES) {
|
||||||
|
// Use -SNAPSHOT versions locally to enable the local GeckoView substitution flow.
|
||||||
|
version += "-SNAPSHOT"
|
||||||
|
}
|
||||||
|
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
def getArtifactSuffix() {
|
||||||
|
def suffix = ""
|
||||||
|
|
||||||
|
// Release artifacts don't specify the channel, for the sake of simplicity.
|
||||||
|
if (gradle.mozconfig.substs.MOZ_UPDATE_CHANNEL != 'release') {
|
||||||
|
suffix += "-${gradle.mozconfig.substs.MOZ_UPDATE_CHANNEL}"
|
||||||
|
}
|
||||||
|
|
||||||
|
return suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
def getArtifactId() {
|
||||||
|
def id = "geckoview" + getArtifactSuffix()
|
||||||
|
|
||||||
|
if (!gradle.mozconfig.substs.MOZ_ANDROID_GECKOVIEW_LITE) {
|
||||||
|
id += "-omni"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gradle.mozconfig.substs.MOZILLA_OFFICIAL && !gradle.mozconfig.substs.MOZ_ANDROID_FAT_AAR_ARCHITECTURES) {
|
||||||
|
// In automation, per-architecture artifacts identify
|
||||||
|
// the architecture; multi-architecture artifacts don't.
|
||||||
|
// When building locally, we produce a "skinny AAR" with
|
||||||
|
// one target architecture masquerading as a "fat AAR"
|
||||||
|
// to enable Gradle composite builds to substitute this
|
||||||
|
// project into consumers easily.
|
||||||
|
id += "-${gradle.mozconfig.substs.ANDROID_CPU_ARCH}"
|
||||||
|
}
|
||||||
|
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
def getGeckoViewDependency() {
|
||||||
|
// on try, relax geckoview version pin to allow for --use-existing-task
|
||||||
|
if ('https://hg.mozilla.org/try' == System.env.GECKO_HEAD_REPOSITORY) {
|
||||||
|
rootProject.logger.lifecycle("Getting geckoview on try: ${getArtifactId()}:+")
|
||||||
|
return "org.mozilla.geckoview:${getArtifactId()}:+"
|
||||||
|
}
|
||||||
|
rootProject.logger.lifecycle("Getting geckoview: ${getArtifactId()}:${getVersionNumber()}")
|
||||||
|
return "org.mozilla.geckoview:${getArtifactId()}:${getVersionNumber()}"
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Metrics definitions have moved
|
||||||
|
|
||||||
|
Metrics definitions for projects using `engine-gecko` moved to the Glean Dictionary.
|
||||||
|
|
||||||
|
For Firefox for Android those definitions can be found at:
|
||||||
|
[https://dictionary.telemetry.mozilla.org/apps/fenix](https://dictionary.telemetry.mozilla.org/apps/fenix)
|
||||||
|
|
||||||
|
This file is kept only for historical reference.
|
|
@ -0,0 +1,24 @@
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
about:
|
||||||
|
description: GeckoView features configurable via Nimbus
|
||||||
|
android:
|
||||||
|
package: mozilla.components.browser.engine.gecko
|
||||||
|
class: .GeckoNimbus
|
||||||
|
channels:
|
||||||
|
- debug
|
||||||
|
- release
|
||||||
|
features:
|
||||||
|
pdfjs:
|
||||||
|
description: "PDF.js features"
|
||||||
|
variables:
|
||||||
|
download-button:
|
||||||
|
description: "Download button"
|
||||||
|
type: Boolean
|
||||||
|
default: true
|
||||||
|
|
||||||
|
open-in-app-button:
|
||||||
|
description: "Open in app button"
|
||||||
|
type: Boolean
|
||||||
|
default: true
|
|
@ -0,0 +1,30 @@
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
# IMPORTANT NOTE: this file is here only as a safety measure, to make
|
||||||
|
# sure the correct code is generated even though the GeckoView AAR file
|
||||||
|
# reports an empty metrics.yaml file. The metric in this file is currently
|
||||||
|
# disabled and not supposed to collect any data.
|
||||||
|
---
|
||||||
|
|
||||||
|
$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
|
||||||
|
|
||||||
|
test.glean.geckoview:
|
||||||
|
streaming:
|
||||||
|
type: timing_distribution
|
||||||
|
gecko_datapoint: TELEMETRY_TEST_STREAMING
|
||||||
|
disabled: true
|
||||||
|
description: |
|
||||||
|
A test-only, disabled metric. This is required to guarantee
|
||||||
|
that a `GleanGeckoHistogramMapping` is always generated, even
|
||||||
|
though the GeckoView AAR exports no metric. Please note that
|
||||||
|
the data-review field below contains no review, since this
|
||||||
|
metric is disabled and not allowed to collect any data.
|
||||||
|
bugs:
|
||||||
|
- https://bugzilla.mozilla.org/1566374
|
||||||
|
data_reviews:
|
||||||
|
- https://bugzilla.mozilla.org/1566374
|
||||||
|
notification_emails:
|
||||||
|
- glean-team@mozilla.com
|
||||||
|
expires: never
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
|
@ -0,0 +1,126 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.fetch.geckoview
|
||||||
|
|
||||||
|
import androidx.test.annotation.UiThreadTest
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.filters.MediumTest
|
||||||
|
import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient
|
||||||
|
import mozilla.components.concept.fetch.Client
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@MediumTest
|
||||||
|
class GeckoViewFetchTestCases : mozilla.components.tooling.fetch.tests.FetchTestCases() {
|
||||||
|
override fun createNewClient(): Client = GeckoViewFetchClient(ApplicationProvider.getApplicationContext())
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
fun clientInstance() {
|
||||||
|
assertTrue(createNewClient() is GeckoViewFetchClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200WithGzippedBody() {
|
||||||
|
super.get200WithGzippedBody()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200OverridingDefaultHeaders() {
|
||||||
|
super.get200OverridingDefaultHeaders()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200WithDuplicatedCacheControlRequestHeaders() {
|
||||||
|
super.get200WithDuplicatedCacheControlRequestHeaders()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200WithDuplicatedCacheControlResponseHeaders() {
|
||||||
|
super.get200WithDuplicatedCacheControlResponseHeaders()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200WithHeaders() {
|
||||||
|
super.get200WithHeaders()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200WithReadTimeout() {
|
||||||
|
super.get200WithReadTimeout()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200WithStringBody() {
|
||||||
|
super.get200WithStringBody()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get302FollowRedirects() {
|
||||||
|
super.get302FollowRedirects()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get302FollowRedirectsDisabled() {
|
||||||
|
super.get302FollowRedirectsDisabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get404WithBody() {
|
||||||
|
super.get404WithBody()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun post200WithBody() {
|
||||||
|
super.post200WithBody()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun put201FileUpload() {
|
||||||
|
super.put201FileUpload()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200WithCookiePolicy() {
|
||||||
|
super.get200WithCookiePolicy()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200WithContentTypeCharset() {
|
||||||
|
super.get200WithContentTypeCharset()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200WithCacheControl() {
|
||||||
|
super.get200WithCacheControl()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun getThrowsIOExceptionWhenHostNotReachable() {
|
||||||
|
super.getThrowsIOExceptionWhenHostNotReachable()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun getDataUri() {
|
||||||
|
super.getDataUri()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
<manifest />
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,73 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko
|
||||||
|
|
||||||
|
import android.util.JsonReader
|
||||||
|
import android.util.JsonWriter
|
||||||
|
import mozilla.components.concept.engine.EngineSessionState
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.mozilla.geckoview.GeckoSession
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
private const val GECKO_STATE_KEY = "GECKO_STATE"
|
||||||
|
|
||||||
|
class GeckoEngineSessionState internal constructor(
|
||||||
|
internal val actualState: GeckoSession.SessionState?,
|
||||||
|
) : EngineSessionState {
|
||||||
|
override fun writeTo(writer: JsonWriter) {
|
||||||
|
with(writer) {
|
||||||
|
beginObject()
|
||||||
|
|
||||||
|
name(GECKO_STATE_KEY)
|
||||||
|
value(actualState.toString())
|
||||||
|
|
||||||
|
endObject()
|
||||||
|
flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromJSON(json: JSONObject): GeckoEngineSessionState = try {
|
||||||
|
val state = json.getString(GECKO_STATE_KEY)
|
||||||
|
|
||||||
|
GeckoEngineSessionState(
|
||||||
|
GeckoSession.SessionState.fromString(state),
|
||||||
|
)
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
GeckoEngineSessionState(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a [GeckoEngineSessionState] from the given [JsonReader].
|
||||||
|
*/
|
||||||
|
fun from(reader: JsonReader): GeckoEngineSessionState = try {
|
||||||
|
reader.beginObject()
|
||||||
|
|
||||||
|
val rawState = if (reader.hasNext()) {
|
||||||
|
val key = reader.nextName()
|
||||||
|
if (key != GECKO_STATE_KEY) {
|
||||||
|
throw AssertionError("Unknown state key: $key")
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.nextString()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.endObject()
|
||||||
|
|
||||||
|
GeckoEngineSessionState(
|
||||||
|
rawState?.let { GeckoSession.SessionState.fromString(it) },
|
||||||
|
)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
GeckoEngineSessionState(null)
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
// Internally GeckoView uses org.json and currently may throw JSONException in certain cases
|
||||||
|
// https://github.com/mozilla-mobile/android-components/issues/9332
|
||||||
|
GeckoEngineSessionState(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,247 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import mozilla.components.browser.engine.gecko.activity.GeckoViewActivityContextDelegate
|
||||||
|
import mozilla.components.browser.engine.gecko.selection.GeckoSelectionActionDelegate
|
||||||
|
import mozilla.components.concept.engine.EngineSession
|
||||||
|
import mozilla.components.concept.engine.EngineView
|
||||||
|
import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
|
||||||
|
import mozilla.components.concept.engine.selection.SelectionActionDelegate
|
||||||
|
import org.mozilla.geckoview.BasicSelectionActionDelegate
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
import org.mozilla.geckoview.GeckoSession
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
import androidx.core.view.OnApplyWindowInsetsListener as AndroidxOnApplyWindowInsetsListener
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gecko-based EngineView implementation.
|
||||||
|
*/
|
||||||
|
class GeckoEngineView @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0,
|
||||||
|
) : FrameLayout(context, attrs, defStyleAttr), EngineView {
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal var geckoView = object : NestedGeckoView(context) {
|
||||||
|
|
||||||
|
override fun onAttachedToWindow() {
|
||||||
|
try {
|
||||||
|
super.onAttachedToWindow()
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
// This is to debug "display already acquired" crashes
|
||||||
|
val otherActivityClassName =
|
||||||
|
this.session?.accessibility?.view?.context?.javaClass?.simpleName
|
||||||
|
val otherActivityClassHashcode =
|
||||||
|
this.session?.accessibility?.view?.context?.hashCode()
|
||||||
|
val activityClassName = context.javaClass.simpleName
|
||||||
|
val activityClassHashCode = context.hashCode()
|
||||||
|
val msg = "ATTACH VIEW: Current activity: $activityClassName hashcode " +
|
||||||
|
"$activityClassHashCode Other activity: $otherActivityClassName " +
|
||||||
|
"hashcode $otherActivityClassHashcode"
|
||||||
|
throw IllegalStateException(msg, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromWindow() {
|
||||||
|
// We are releasing the session before GeckoView gets detached from the window. Otherwise
|
||||||
|
// GeckoView will close the session automatically and we do not want that.
|
||||||
|
releaseSession()
|
||||||
|
|
||||||
|
super.onDetachedFromWindow()
|
||||||
|
}
|
||||||
|
}.apply {
|
||||||
|
// Explicitly mark this view as important for autofill. The default "auto" doesn't seem to trigger any
|
||||||
|
// autofill behavior for us here.
|
||||||
|
@Suppress("WrongConstant")
|
||||||
|
ViewCompat.setImportantForAutofill(this, View.IMPORTANT_FOR_ACCESSIBILITY_YES)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun setColorScheme(preferredColorScheme: PreferredColorScheme) {
|
||||||
|
var colorScheme = preferredColorScheme
|
||||||
|
if (preferredColorScheme == PreferredColorScheme.System) {
|
||||||
|
colorScheme =
|
||||||
|
if (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
||||||
|
== Configuration.UI_MODE_NIGHT_YES
|
||||||
|
) {
|
||||||
|
PreferredColorScheme.Dark
|
||||||
|
} else {
|
||||||
|
PreferredColorScheme.Light
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colorScheme == PreferredColorScheme.Dark) {
|
||||||
|
geckoView.coverUntilFirstPaint(DARK_COVER)
|
||||||
|
} else {
|
||||||
|
geckoView.coverUntilFirstPaint(Color.WHITE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal var currentSession: GeckoEngineSession? = null
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal var currentSelection: BasicSelectionActionDelegate? = null
|
||||||
|
|
||||||
|
override var selectionActionDelegate: SelectionActionDelegate? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
addView(geckoView)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* With the current design, we have a [NestedGeckoView] inside this
|
||||||
|
* [GeckoEngineView]. In our supported embedders, we wrap this with the
|
||||||
|
* AndroidX `SwipeRefreshLayout` to enable features like Pull-To-Refresh:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* SwipeRefreshLayout
|
||||||
|
* └── GeckoEngineView
|
||||||
|
* └── NestedGeckoView
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* `SwipeRefreshLayout` only looks at the direct child to see if it has nested scrolling
|
||||||
|
* enabled. As we embed [NestedGeckoView] inside [GeckoEngineView], we change the hierarchy
|
||||||
|
* so that [NestedGeckoView] is no longer the direct child of `SwipeRefreshLayout`.
|
||||||
|
*
|
||||||
|
* To fix this we enable nested scrolling on the GeckoEngineView to emulate this
|
||||||
|
* information. This is required information for `View.requestDisallowInterceptTouchEvent`
|
||||||
|
* to work correctly in the [NestedGeckoView].
|
||||||
|
*/
|
||||||
|
isNestedScrollingEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the content of the given session.
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
override fun render(session: EngineSession) {
|
||||||
|
val internalSession = session as GeckoEngineSession
|
||||||
|
currentSession = session
|
||||||
|
|
||||||
|
if (geckoView.session != internalSession.geckoSession) {
|
||||||
|
geckoView.session?.let {
|
||||||
|
// Release a previously assigned session. Otherwise GeckoView will close it
|
||||||
|
// automatically.
|
||||||
|
detachSelectionActionDelegate(it)
|
||||||
|
geckoView.releaseSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
geckoView.setSession(internalSession.geckoSession)
|
||||||
|
attachSelectionActionDelegate(internalSession.geckoSession)
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
// This is to debug "display already acquired" crashes
|
||||||
|
val otherActivityClassName =
|
||||||
|
internalSession.geckoSession.accessibility.view?.context?.javaClass?.simpleName
|
||||||
|
val otherActivityClassHashcode =
|
||||||
|
internalSession.geckoSession.accessibility.view?.context?.hashCode()
|
||||||
|
val activityClassName = context.javaClass.simpleName
|
||||||
|
val activityClassHashCode = context.hashCode()
|
||||||
|
val msg = "SET SESSION: Current activity: $activityClassName hashcode " +
|
||||||
|
"$activityClassHashCode Other activity: $otherActivityClassName " +
|
||||||
|
"hashcode $otherActivityClassHashcode"
|
||||||
|
throw IllegalStateException(msg, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun attachSelectionActionDelegate(session: GeckoSession) {
|
||||||
|
val delegate = GeckoSelectionActionDelegate.maybeCreate(context, selectionActionDelegate)
|
||||||
|
if (delegate != null) {
|
||||||
|
session.selectionActionDelegate = delegate
|
||||||
|
currentSelection = delegate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun detachSelectionActionDelegate(session: GeckoSession?) {
|
||||||
|
if (currentSelection != null) {
|
||||||
|
session?.selectionActionDelegate = null
|
||||||
|
currentSelection = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun release() {
|
||||||
|
detachSelectionActionDelegate(currentSession?.geckoSession)
|
||||||
|
|
||||||
|
currentSession = null
|
||||||
|
|
||||||
|
geckoView.releaseSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromWindow() {
|
||||||
|
super.onDetachedFromWindow()
|
||||||
|
|
||||||
|
release()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun canClearSelection() = !currentSelection?.selection?.text.isNullOrEmpty()
|
||||||
|
|
||||||
|
override fun canScrollVerticallyUp() = currentSession?.let { it.scrollY > 0 } != false
|
||||||
|
|
||||||
|
override fun canScrollVerticallyDown() =
|
||||||
|
true // waiting for this issue https://bugzilla.mozilla.org/show_bug.cgi?id=1507569
|
||||||
|
|
||||||
|
override fun getInputResultDetail() = geckoView.inputResultDetail
|
||||||
|
|
||||||
|
override fun setVerticalClipping(clippingHeight: Int) {
|
||||||
|
geckoView.setVerticalClipping(clippingHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setDynamicToolbarMaxHeight(height: Int) {
|
||||||
|
geckoView.setDynamicToolbarMaxHeight(height)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setActivityContext(context: Context?) {
|
||||||
|
geckoView.activityContextDelegate = GeckoViewActivityContextDelegate(WeakReference(context))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun captureThumbnail(onFinish: (Bitmap?) -> Unit) {
|
||||||
|
val geckoResult = geckoView.capturePixels()
|
||||||
|
geckoResult.then(
|
||||||
|
{ bitmap ->
|
||||||
|
onFinish(bitmap)
|
||||||
|
GeckoResult()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onFinish(null)
|
||||||
|
GeckoResult<Void>()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearSelection() {
|
||||||
|
currentSelection?.clearSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setVisibility(visibility: Int) {
|
||||||
|
// GeckoView doesn't react to onVisibilityChanged so we need to propagate ourselves for now:
|
||||||
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1630775
|
||||||
|
// We do this to prevent the content from resizing when the view is not visible:
|
||||||
|
// https://github.com/mozilla-mobile/android-components/issues/6664
|
||||||
|
geckoView.visibility = visibility
|
||||||
|
super.setVisibility(visibility)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addWindowInsetsListener(
|
||||||
|
key: String,
|
||||||
|
listener: AndroidxOnApplyWindowInsetsListener?,
|
||||||
|
) = geckoView.addWindowInsetsListener(key, listener)
|
||||||
|
|
||||||
|
override fun removeWindowInsetsListener(key: String) = geckoView.removeWindowInsetsListener(key)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
internal const val DARK_COVER = 0xFF2A2A2E.toInt()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.CoroutineStart
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import mozilla.components.concept.engine.CancellableOperation
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for a GeckoResult to be complete in a co-routine.
|
||||||
|
*/
|
||||||
|
suspend fun <T> GeckoResult<T>.await() = suspendCoroutine<T?> { continuation ->
|
||||||
|
then(
|
||||||
|
{
|
||||||
|
continuation.resume(it)
|
||||||
|
GeckoResult<Void>()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
continuation.resumeWithException(it)
|
||||||
|
GeckoResult<Void>()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a [GeckoResult] to a [CancellableOperation].
|
||||||
|
*/
|
||||||
|
fun <T> GeckoResult<T>.asCancellableOperation(): CancellableOperation {
|
||||||
|
val geckoResult = this
|
||||||
|
return object : CancellableOperation {
|
||||||
|
override fun cancel(): Deferred<Boolean> {
|
||||||
|
val result = CompletableDeferred<Boolean>()
|
||||||
|
geckoResult.cancel().then(
|
||||||
|
{
|
||||||
|
result.complete(it ?: false)
|
||||||
|
GeckoResult<Void>()
|
||||||
|
},
|
||||||
|
{ throwable ->
|
||||||
|
result.completeExceptionally(throwable)
|
||||||
|
GeckoResult<Void>()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a GeckoResult from a co-routine.
|
||||||
|
*/
|
||||||
|
@Suppress("TooGenericExceptionCaught")
|
||||||
|
fun <T> CoroutineScope.launchGeckoResult(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
block: suspend CoroutineScope.() -> T,
|
||||||
|
) = GeckoResult<T>().apply {
|
||||||
|
launch(context, start) {
|
||||||
|
try {
|
||||||
|
val value = block()
|
||||||
|
complete(value)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
completeExceptionally(exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import mozilla.components.browser.engine.gecko.content.blocking.GeckoTrackingProtectionException
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.geckoTrackingProtectionPermission
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.isExcludedForTrackingProtection
|
||||||
|
import mozilla.components.concept.engine.EngineSession
|
||||||
|
import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
|
||||||
|
import mozilla.components.concept.engine.content.blocking.TrackingProtectionExceptionStorage
|
||||||
|
import mozilla.components.support.ktx.kotlin.getOrigin
|
||||||
|
import mozilla.components.support.ktx.kotlin.stripDefaultPort
|
||||||
|
import org.mozilla.geckoview.GeckoRuntime
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_ALLOW
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_DENY
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [TrackingProtectionExceptionStorage] implementation to store tracking protection exceptions.
|
||||||
|
*/
|
||||||
|
internal class GeckoTrackingProtectionExceptionStorage(
|
||||||
|
private val runtime: GeckoRuntime,
|
||||||
|
) : TrackingProtectionExceptionStorage {
|
||||||
|
internal var scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
|
override fun contains(session: EngineSession, onResult: (Boolean) -> Unit) {
|
||||||
|
val url = (session as GeckoEngineSession).currentUrl
|
||||||
|
if (!url.isNullOrEmpty()) {
|
||||||
|
getPermissions(url) { permissions ->
|
||||||
|
val contains = permissions.isNotEmpty()
|
||||||
|
onResult(contains)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onResult(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchAll(onResult: (List<TrackingProtectionException>) -> Unit) {
|
||||||
|
runtime.storageController.allPermissions.accept { permissions ->
|
||||||
|
val trackingExceptions = permissions.filterTrackingProtectionExceptions()
|
||||||
|
onResult(trackingExceptions.map { exceptions -> exceptions.toTrackingProtectionException() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<ContentPermission>?.filterTrackingProtectionExceptions() =
|
||||||
|
this.orEmpty().filter { it.isExcludedForTrackingProtection }
|
||||||
|
|
||||||
|
private fun List<ContentPermission>?.filterTrackingProtectionExceptions(url: String) =
|
||||||
|
this.orEmpty()
|
||||||
|
.filter {
|
||||||
|
it.isExcludedForTrackingProtection && it.uri.getOrigin().orEmpty()
|
||||||
|
.stripDefaultPort() == url
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun add(session: EngineSession, persistInPrivateMode: Boolean) {
|
||||||
|
val geckoEngineSession = (session as GeckoEngineSession)
|
||||||
|
if (persistInPrivateMode) {
|
||||||
|
addPersistentPrivateException(geckoEngineSession)
|
||||||
|
} else {
|
||||||
|
geckoEngineSession.geckoTrackingProtectionPermission?.let {
|
||||||
|
runtime.storageController.setPermission(it, VALUE_ALLOW)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onExcludedOnTrackingProtectionChange(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun addPersistentPrivateException(geckoEngineSession: GeckoEngineSession) {
|
||||||
|
val permission = geckoEngineSession.geckoTrackingProtectionPermission
|
||||||
|
permission?.let {
|
||||||
|
runtime.storageController.setPrivateBrowsingPermanentPermission(it, VALUE_ALLOW)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun remove(session: EngineSession) {
|
||||||
|
val geckoEngineSession = (session as GeckoEngineSession)
|
||||||
|
val url = geckoEngineSession.currentUrl ?: return
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onExcludedOnTrackingProtectionChange(false)
|
||||||
|
}
|
||||||
|
remove(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun remove(exception: TrackingProtectionException) {
|
||||||
|
if (exception is GeckoTrackingProtectionException) {
|
||||||
|
remove(exception.contentPermission)
|
||||||
|
} else {
|
||||||
|
remove(exception.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun remove(url: String) {
|
||||||
|
val storage = runtime.storageController
|
||||||
|
getPermissions(url) { permissions ->
|
||||||
|
permissions.forEach { geckoPermissions ->
|
||||||
|
storage.setPermission(geckoPermissions, VALUE_DENY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a workaround until https://bugzilla.mozilla.org/show_bug.cgi?id=1723280 gets addressed
|
||||||
|
private fun getPermissions(url: String, onFinish: (List<ContentPermission>) -> Unit) {
|
||||||
|
val localUrl = url.getOrigin().orEmpty().stripDefaultPort()
|
||||||
|
val storage = runtime.storageController
|
||||||
|
if (localUrl.isNotEmpty()) {
|
||||||
|
storage.allPermissions.accept { permissions ->
|
||||||
|
onFinish(permissions.filterTrackingProtectionExceptions(localUrl))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onFinish(emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun remove(contentPermission: ContentPermission) {
|
||||||
|
runtime.storageController.setPermission(contentPermission, VALUE_DENY)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeAll(activeSessions: List<EngineSession>?, onRemove: () -> Unit) {
|
||||||
|
val storage = runtime.storageController
|
||||||
|
activeSessions?.forEach { engineSession ->
|
||||||
|
engineSession.notifyObservers {
|
||||||
|
onExcludedOnTrackingProtectionChange(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
storage.allPermissions.accept { permissions ->
|
||||||
|
val trackingExceptions = permissions.filterTrackingProtectionExceptions()
|
||||||
|
trackingExceptions.forEach {
|
||||||
|
storage.setPermission(it, VALUE_DENY)
|
||||||
|
}
|
||||||
|
onRemove.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ContentPermission.toTrackingProtectionException(): GeckoTrackingProtectionException {
|
||||||
|
return GeckoTrackingProtectionException(uri, privateMode, this)
|
||||||
|
}
|
|
@ -0,0 +1,257 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import androidx.core.view.NestedScrollingChild
|
||||||
|
import androidx.core.view.NestedScrollingChildHelper
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import mozilla.components.concept.engine.InputResultDetail
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
import org.mozilla.geckoview.GeckoView
|
||||||
|
import org.mozilla.geckoview.PanZoomController
|
||||||
|
|
||||||
|
/**
|
||||||
|
* geckoView that supports nested scrolls (for using in a CoordinatorLayout).
|
||||||
|
*
|
||||||
|
* This code is a simplified version of the NestedScrollView implementation
|
||||||
|
* which can be found in the support library:
|
||||||
|
* [android.support.v4.widget.NestedScrollView]
|
||||||
|
*
|
||||||
|
* Based on:
|
||||||
|
* https://github.com/takahirom/webview-in-coordinatorlayout
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Suppress("ClickableViewAccessibility")
|
||||||
|
open class NestedGeckoView(context: Context) : GeckoView(context), NestedScrollingChild {
|
||||||
|
@VisibleForTesting
|
||||||
|
internal var lastY: Int = 0
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal val scrollOffset = IntArray(2)
|
||||||
|
|
||||||
|
private val scrollConsumed = IntArray(2)
|
||||||
|
|
||||||
|
private var gestureCanReachParent = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the initial scroll.
|
||||||
|
*/
|
||||||
|
enum class InitialScrollDirection {
|
||||||
|
NOT_YET,
|
||||||
|
DOWN,
|
||||||
|
UP,
|
||||||
|
}
|
||||||
|
|
||||||
|
private var initialScrollDirection = InitialScrollDirection.NOT_YET
|
||||||
|
|
||||||
|
private var initialDownY: Float = 0f
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal var nestedOffsetY: Int = 0
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal var childHelper: NestedScrollingChildHelper = NestedScrollingChildHelper(this)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How user's MotionEvent will be handled.
|
||||||
|
*
|
||||||
|
* @see InputResultDetail
|
||||||
|
*/
|
||||||
|
internal var inputResultDetail = InputResultDetail.newInstance(true)
|
||||||
|
|
||||||
|
init {
|
||||||
|
isNestedScrollingEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("ComplexMethod")
|
||||||
|
override fun onTouchEvent(ev: MotionEvent): Boolean {
|
||||||
|
val event = MotionEvent.obtain(ev)
|
||||||
|
val action = ev.actionMasked
|
||||||
|
val eventY = event.y.toInt()
|
||||||
|
|
||||||
|
when (action) {
|
||||||
|
MotionEvent.ACTION_MOVE -> {
|
||||||
|
val allowScroll = !shouldPinOnScreen() && inputResultDetail.isTouchHandledByBrowser()
|
||||||
|
|
||||||
|
var deltaY = lastY - eventY
|
||||||
|
|
||||||
|
if (allowScroll && dispatchNestedPreScroll(0, deltaY, scrollConsumed, scrollOffset)) {
|
||||||
|
deltaY -= scrollConsumed[1]
|
||||||
|
event.offsetLocation(0f, (-scrollOffset[1]).toFloat())
|
||||||
|
nestedOffsetY += scrollOffset[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
lastY = eventY - scrollOffset[1]
|
||||||
|
|
||||||
|
if (allowScroll && dispatchNestedScroll(0, scrollOffset[1], 0, deltaY, scrollOffset)) {
|
||||||
|
lastY -= scrollOffset[1]
|
||||||
|
event.offsetLocation(0f, scrollOffset[1].toFloat())
|
||||||
|
nestedOffsetY += scrollOffset[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initialScrollDirection == InitialScrollDirection.NOT_YET) {
|
||||||
|
if (event.y > initialDownY) {
|
||||||
|
initialScrollDirection = InitialScrollDirection.UP
|
||||||
|
} else if (event.y < initialDownY) {
|
||||||
|
initialScrollDirection = InitialScrollDirection.DOWN
|
||||||
|
} else {
|
||||||
|
// We may want to disallow pull-to-refresh in the case of
|
||||||
|
// scrolling left or right initially?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MotionEvent.ACTION_DOWN -> {
|
||||||
|
// A new gesture started. Ask GV if it can handle this.
|
||||||
|
parent?.requestDisallowInterceptTouchEvent(true)
|
||||||
|
updateInputResult(event)
|
||||||
|
|
||||||
|
initialScrollDirection = InitialScrollDirection.NOT_YET
|
||||||
|
nestedOffsetY = 0
|
||||||
|
lastY = eventY
|
||||||
|
initialDownY = event.y
|
||||||
|
|
||||||
|
// The event should be handled either by onTouchEvent,
|
||||||
|
// either by onTouchEventForResult, never by both.
|
||||||
|
// Early return if we sent it to updateInputResult(..) which calls onTouchEventForResult.
|
||||||
|
event.recycle()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't care about other touch events
|
||||||
|
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||||
|
// inputResultDetail needs to be reset here and not in the next ACTION_DOWN, because
|
||||||
|
// its value is used by other features that poll for the value via
|
||||||
|
// `EngineView.getInputResultDetail`. Not resetting this in ACTION_CANCEL/ACTION_UP
|
||||||
|
// would then mean we send stale information to those features from a previous
|
||||||
|
// gesture's result.
|
||||||
|
inputResultDetail = InputResultDetail.newInstance(true)
|
||||||
|
stopNestedScroll()
|
||||||
|
|
||||||
|
// Allow touch event interception here so that the next ACTION_DOWN event can be properly
|
||||||
|
// intercepted by the parent.
|
||||||
|
parent?.requestDisallowInterceptTouchEvent(false)
|
||||||
|
gestureCanReachParent = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute event handler from parent class in all cases
|
||||||
|
val eventHandled = callSuperOnTouchEvent(event)
|
||||||
|
|
||||||
|
// Recycle previously obtained event
|
||||||
|
event.recycle()
|
||||||
|
|
||||||
|
return eventHandled
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun callSuperOnTouchEvent(event: MotionEvent): Boolean {
|
||||||
|
return super.onTouchEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("WrongThread") // Lint complains startNestedScroll() needs to be called on the main thread
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun updateInputResult(event: MotionEvent) {
|
||||||
|
val eventAction = event.actionMasked
|
||||||
|
superOnTouchEventForDetailResult(event)
|
||||||
|
.accept {
|
||||||
|
// Since the response from APZ is async, we could theoretically have a response
|
||||||
|
// which is out of time when we get the ACTION_MOVE events, and we do not want
|
||||||
|
// to forward this to the parent pre-emptively.
|
||||||
|
if (!gestureCanReachParent) {
|
||||||
|
return@accept
|
||||||
|
}
|
||||||
|
|
||||||
|
inputResultDetail = inputResultDetail.copy(
|
||||||
|
it?.handledResult(),
|
||||||
|
it?.scrollableDirections(),
|
||||||
|
it?.overscrollDirections(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (eventAction == MotionEvent.ACTION_DOWN) {
|
||||||
|
// Gesture can reach the parent only if the content is already at the top
|
||||||
|
gestureCanReachParent = inputResultDetail.canOverscrollTop()
|
||||||
|
|
||||||
|
when (initialScrollDirection) {
|
||||||
|
InitialScrollDirection.NOT_YET -> {
|
||||||
|
// If the event wasn't used in GeckoView, allow touch event interception.
|
||||||
|
if (gestureCanReachParent && !inputResultDetail.isTouchHandledByWebsite()) {
|
||||||
|
parent?.requestDisallowInterceptTouchEvent(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InitialScrollDirection.UP -> {
|
||||||
|
// If we received the InputResultDetail from Gecko after we've sent the
|
||||||
|
// first touch move event to Gecko, that means there's at least a touch
|
||||||
|
// event listener whether to prevent the event or not, or CSS touch-action
|
||||||
|
// properties in the contents, thus if the result isn't
|
||||||
|
// `INPUT_HANDLED_CONTENT`, we can allow pull-to-refresh.
|
||||||
|
if (gestureCanReachParent && !inputResultDetail.isTouchHandledByWebsite()) {
|
||||||
|
parent?.requestDisallowInterceptTouchEvent(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InitialScrollDirection.DOWN -> {
|
||||||
|
parent?.requestDisallowInterceptTouchEvent(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal open fun superOnTouchEventForDetailResult(
|
||||||
|
event: MotionEvent,
|
||||||
|
): GeckoResult<PanZoomController.InputResultDetail> =
|
||||||
|
super.onTouchEventForDetailResult(event)
|
||||||
|
|
||||||
|
override fun setNestedScrollingEnabled(enabled: Boolean) {
|
||||||
|
childHelper.isNestedScrollingEnabled = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isNestedScrollingEnabled(): Boolean {
|
||||||
|
return childHelper.isNestedScrollingEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startNestedScroll(axes: Int): Boolean {
|
||||||
|
return childHelper.startNestedScroll(axes)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stopNestedScroll() {
|
||||||
|
childHelper.stopNestedScroll()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hasNestedScrollingParent(): Boolean {
|
||||||
|
return childHelper.hasNestedScrollingParent()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispatchNestedScroll(
|
||||||
|
dxConsumed: Int,
|
||||||
|
dyConsumed: Int,
|
||||||
|
dxUnconsumed: Int,
|
||||||
|
dyUnconsumed: Int,
|
||||||
|
offsetInWindow: IntArray?,
|
||||||
|
): Boolean {
|
||||||
|
return childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?): Boolean {
|
||||||
|
return childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispatchNestedFling(velocityX: Float, velocityY: Float, consumed: Boolean): Boolean {
|
||||||
|
return childHelper.dispatchNestedFling(velocityX, velocityY, consumed)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float): Boolean {
|
||||||
|
return childHelper.dispatchNestedPreFling(velocityX, velocityY)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.activity
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Intent
|
||||||
|
import mozilla.components.concept.engine.activity.ActivityDelegate
|
||||||
|
import mozilla.components.support.base.log.logger.Logger
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
import org.mozilla.geckoview.GeckoRuntime
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper for the [ActivityDelegate] to communicate with the Gecko-based delegate.
|
||||||
|
*/
|
||||||
|
internal class GeckoActivityDelegate(
|
||||||
|
private val delegateRef: WeakReference<ActivityDelegate>,
|
||||||
|
) : GeckoRuntime.ActivityDelegate {
|
||||||
|
|
||||||
|
private val logger = Logger(GeckoActivityDelegate::javaClass.name)
|
||||||
|
|
||||||
|
override fun onStartActivityForResult(intent: PendingIntent): GeckoResult<Intent> {
|
||||||
|
val result: GeckoResult<Intent> = GeckoResult()
|
||||||
|
val delegate = delegateRef.get()
|
||||||
|
|
||||||
|
if (delegate == null) {
|
||||||
|
logger.warn("No activity delegate attached. Cannot request FIDO auth.")
|
||||||
|
|
||||||
|
result.completeExceptionally(RuntimeException("Activity for result failed; no delegate attached."))
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate.startIntentSenderForResult(intent.intentSender) { data ->
|
||||||
|
if (data != null) {
|
||||||
|
result.complete(data)
|
||||||
|
} else {
|
||||||
|
result.completeExceptionally(RuntimeException("Activity for result failed."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.activity
|
||||||
|
|
||||||
|
import mozilla.components.concept.engine.activity.OrientationDelegate
|
||||||
|
import org.mozilla.geckoview.AllowOrDeny
|
||||||
|
import org.mozilla.geckoview.AllowOrDeny.ALLOW
|
||||||
|
import org.mozilla.geckoview.AllowOrDeny.DENY
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
import org.mozilla.geckoview.OrientationController
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default [OrientationController.OrientationDelegate] implementation that delegates both the behavior
|
||||||
|
* and the returned value to a [OrientationDelegate].
|
||||||
|
*/
|
||||||
|
internal class GeckoScreenOrientationDelegate(
|
||||||
|
private val delegate: OrientationDelegate,
|
||||||
|
) : OrientationController.OrientationDelegate {
|
||||||
|
override fun onOrientationLock(requestedOrientation: Int): GeckoResult<AllowOrDeny> {
|
||||||
|
val result = GeckoResult<AllowOrDeny>()
|
||||||
|
|
||||||
|
when (delegate.onOrientationLock(requestedOrientation)) {
|
||||||
|
true -> result.complete(ALLOW)
|
||||||
|
false -> result.complete(DENY)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOrientationUnlock() {
|
||||||
|
delegate.onOrientationUnlock()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.activity
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import mozilla.components.support.base.log.logger.Logger
|
||||||
|
import org.mozilla.geckoview.GeckoView
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GeckoViewActivityContextDelegate provides an active Activity to GeckoView or null. Not to be confused
|
||||||
|
* with the runtime delegate of [GeckoActivityDelegate], which is tightly coupled to webauthn.
|
||||||
|
* See bug 1806191 for more information on delegate differences.
|
||||||
|
*
|
||||||
|
* @param contextRef A reference to an active Activity context or null for GeckoView to use.
|
||||||
|
*/
|
||||||
|
class GeckoViewActivityContextDelegate(
|
||||||
|
private val contextRef: WeakReference<Context?>,
|
||||||
|
) : GeckoView.ActivityContextDelegate {
|
||||||
|
private val logger = Logger("GeckoViewActivityContextDelegate")
|
||||||
|
init {
|
||||||
|
if (contextRef.get() == null) {
|
||||||
|
logger.warn("Activity context is null.")
|
||||||
|
} else if (contextRef.get() !is Activity) {
|
||||||
|
logger.warn("A non-activity context was set.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by GeckoView to get an Activity context for operations such as printing.
|
||||||
|
* @return An active Activity context or null.
|
||||||
|
*/
|
||||||
|
override fun getActivityContext(): Context? {
|
||||||
|
val context = contextRef.get()
|
||||||
|
if ((context == null)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.autofill
|
||||||
|
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.toAutocompleteAddress
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.toCreditCardEntry
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.toLoginEntry
|
||||||
|
import mozilla.components.concept.storage.CreditCard
|
||||||
|
import mozilla.components.concept.storage.CreditCardsAddressesStorageDelegate
|
||||||
|
import mozilla.components.concept.storage.Login
|
||||||
|
import mozilla.components.concept.storage.LoginStorageDelegate
|
||||||
|
import org.mozilla.geckoview.Autocomplete
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gecko credit card and login storage delegate that handles runtime storage requests. This allows
|
||||||
|
* the Gecko runtime to call the underlying storage to handle requests for fetching, saving and
|
||||||
|
* updating of autocomplete items in the storage.
|
||||||
|
*
|
||||||
|
* @param creditCardsAddressesStorageDelegate An instance of [CreditCardsAddressesStorageDelegate].
|
||||||
|
* Provides methods for retrieving [CreditCard]s from the underlying storage.
|
||||||
|
* @param loginStorageDelegate An instance of [LoginStorageDelegate].
|
||||||
|
* Provides read/write methods for the [Login] storage.
|
||||||
|
*/
|
||||||
|
class GeckoAutocompleteStorageDelegate(
|
||||||
|
private val creditCardsAddressesStorageDelegate: CreditCardsAddressesStorageDelegate,
|
||||||
|
private val loginStorageDelegate: LoginStorageDelegate,
|
||||||
|
) : Autocomplete.StorageDelegate {
|
||||||
|
|
||||||
|
override fun onAddressFetch(): GeckoResult<Array<Autocomplete.Address>>? {
|
||||||
|
val result = GeckoResult<Array<Autocomplete.Address>>()
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
GlobalScope.launch(IO) {
|
||||||
|
val addresses = creditCardsAddressesStorageDelegate.onAddressesFetch()
|
||||||
|
.map { it.toAutocompleteAddress() }
|
||||||
|
.toTypedArray()
|
||||||
|
|
||||||
|
result.complete(addresses)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreditCardFetch(): GeckoResult<Array<Autocomplete.CreditCard>> {
|
||||||
|
val result = GeckoResult<Array<Autocomplete.CreditCard>>()
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
GlobalScope.launch(IO) {
|
||||||
|
val key = creditCardsAddressesStorageDelegate.getOrGenerateKey()
|
||||||
|
|
||||||
|
val creditCards = creditCardsAddressesStorageDelegate.onCreditCardsFetch()
|
||||||
|
.mapNotNull {
|
||||||
|
val plaintextCardNumber =
|
||||||
|
creditCardsAddressesStorageDelegate.decrypt(key, it.encryptedCardNumber)?.number
|
||||||
|
|
||||||
|
if (plaintextCardNumber == null) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
Autocomplete.CreditCard.Builder()
|
||||||
|
.guid(it.guid)
|
||||||
|
.name(it.billingName)
|
||||||
|
.number(plaintextCardNumber)
|
||||||
|
.expirationMonth(it.expiryMonth.toString())
|
||||||
|
.expirationYear(it.expiryYear.toString())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toTypedArray()
|
||||||
|
|
||||||
|
result.complete(creditCards)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreditCardSave(creditCard: Autocomplete.CreditCard) {
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
GlobalScope.launch(IO) {
|
||||||
|
creditCardsAddressesStorageDelegate.onCreditCardSave(creditCard.toCreditCardEntry())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoginSave(login: Autocomplete.LoginEntry) {
|
||||||
|
loginStorageDelegate.onLoginSave(login.toLoginEntry())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoginFetch(domain: String): GeckoResult<Array<Autocomplete.LoginEntry>> {
|
||||||
|
val result = GeckoResult<Array<Autocomplete.LoginEntry>>()
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
GlobalScope.launch(IO) {
|
||||||
|
val storedLogins = loginStorageDelegate.onLoginFetch(domain)
|
||||||
|
|
||||||
|
val logins = storedLogins.await()
|
||||||
|
.map { it.toLoginEntry() }
|
||||||
|
.toTypedArray()
|
||||||
|
|
||||||
|
result.complete(logins)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.content.blocking
|
||||||
|
|
||||||
|
import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a site that will be ignored by the tracking protection policies.
|
||||||
|
* @property url The url of the site to be ignored.
|
||||||
|
* @property privateMode Indicates if this exception should persisted in private mode.
|
||||||
|
* @property contentPermission The associated gecko content permission of this exception.
|
||||||
|
*/
|
||||||
|
data class GeckoTrackingProtectionException(
|
||||||
|
override val url: String,
|
||||||
|
val privateMode: Boolean = false,
|
||||||
|
val contentPermission: ContentPermission,
|
||||||
|
) : TrackingProtectionException
|
|
@ -0,0 +1,128 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.cookiebanners
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import mozilla.components.browser.engine.gecko.await
|
||||||
|
import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingMode
|
||||||
|
import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingMode.DISABLED
|
||||||
|
import mozilla.components.concept.engine.cookiehandling.CookieBannersStorage
|
||||||
|
import mozilla.components.support.base.log.logger.Logger
|
||||||
|
import org.mozilla.geckoview.GeckoRuntime
|
||||||
|
import org.mozilla.geckoview.StorageController
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A storage to store [CookieBannerHandlingMode] using GeckoView APIs.
|
||||||
|
*/
|
||||||
|
class GeckoCookieBannersStorage(
|
||||||
|
runtime: GeckoRuntime,
|
||||||
|
private val reportSiteDomainsRepository: ReportSiteDomainsRepository,
|
||||||
|
) : CookieBannersStorage {
|
||||||
|
|
||||||
|
private val geckoStorage: StorageController = runtime.storageController
|
||||||
|
private val mainScope = CoroutineScope(Dispatchers.Main)
|
||||||
|
|
||||||
|
override suspend fun addException(
|
||||||
|
uri: String,
|
||||||
|
privateBrowsing: Boolean,
|
||||||
|
) {
|
||||||
|
setGeckoException(uri, DISABLED, privateBrowsing)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun isSiteDomainReported(siteDomain: String): Boolean {
|
||||||
|
return reportSiteDomainsRepository.isSiteDomainReported(siteDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun saveSiteDomain(siteDomain: String) {
|
||||||
|
reportSiteDomainsRepository.saveSiteDomain(siteDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun addPersistentExceptionInPrivateMode(uri: String) {
|
||||||
|
setPersistentPrivateGeckoException(uri, DISABLED)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findExceptionFor(
|
||||||
|
uri: String,
|
||||||
|
privateBrowsing: Boolean,
|
||||||
|
): CookieBannerHandlingMode? {
|
||||||
|
return queryExceptionInGecko(uri, privateBrowsing)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun hasException(uri: String, privateBrowsing: Boolean): Boolean? {
|
||||||
|
val result = findExceptionFor(uri, privateBrowsing)
|
||||||
|
return if (result != null) {
|
||||||
|
result == DISABLED
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun removeException(uri: String, privateBrowsing: Boolean) {
|
||||||
|
removeGeckoException(uri, privateBrowsing)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun removeGeckoException(uri: String, privateBrowsing: Boolean) {
|
||||||
|
geckoStorage.removeCookieBannerModeForDomain(uri, privateBrowsing)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun setGeckoException(
|
||||||
|
uri: String,
|
||||||
|
mode: CookieBannerHandlingMode,
|
||||||
|
privateBrowsing: Boolean,
|
||||||
|
) {
|
||||||
|
geckoStorage.setCookieBannerModeForDomain(
|
||||||
|
uri,
|
||||||
|
mode.mode,
|
||||||
|
privateBrowsing,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun setPersistentPrivateGeckoException(
|
||||||
|
uri: String,
|
||||||
|
mode: CookieBannerHandlingMode,
|
||||||
|
) {
|
||||||
|
geckoStorage.setCookieBannerModeAndPersistInPrivateBrowsingForDomain(
|
||||||
|
uri,
|
||||||
|
mode.mode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
@Suppress("TooGenericExceptionCaught")
|
||||||
|
internal suspend fun queryExceptionInGecko(
|
||||||
|
uri: String,
|
||||||
|
privateBrowsing: Boolean,
|
||||||
|
): CookieBannerHandlingMode? {
|
||||||
|
return try {
|
||||||
|
withContext(mainScope.coroutineContext) {
|
||||||
|
geckoStorage.getCookieBannerModeForDomain(uri, privateBrowsing).await()
|
||||||
|
?.toCookieBannerHandlingMode() ?: throw IllegalArgumentException(
|
||||||
|
"An error happened trying to find cookie banners mode for the " +
|
||||||
|
"uri $uri and private browsing mode $privateBrowsing",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// This normally happen on internal sites like about:config or ip sites.
|
||||||
|
val disabledErrors = listOf("NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS", "NS_ERROR_HOST_IS_IP_ADDRESS")
|
||||||
|
if (disabledErrors.any { (e.message ?: "").contains(it) }) {
|
||||||
|
Logger("GeckoCookieBannersStorage").error("Unable to query cookie banners exception", e)
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun Int.toCookieBannerHandlingMode(): CookieBannerHandlingMode {
|
||||||
|
return CookieBannerHandlingMode.entries.first { it.mode == this }
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.cookiebanners
|
||||||
|
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.core.emptyPreferences
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import mozilla.components.browser.engine.gecko.cookiebanners.ReportSiteDomainsRepository.PreferencesKeys.REPORT_SITE_DOMAINS
|
||||||
|
import mozilla.components.support.base.log.logger.Logger
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A repository to save reported site domains with the datastore API.
|
||||||
|
*/
|
||||||
|
class ReportSiteDomainsRepository(
|
||||||
|
private val dataStore: DataStore<Preferences>,
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val SEPARATOR = "@<;>@"
|
||||||
|
const val REPORT_SITE_DOMAINS_REPOSITORY_NAME = "report_site_domains_preferences"
|
||||||
|
const val PREFERENCE_KEY_NAME = "report_site_domains"
|
||||||
|
}
|
||||||
|
|
||||||
|
private object PreferencesKeys {
|
||||||
|
val REPORT_SITE_DOMAINS = stringPreferencesKey(PREFERENCE_KEY_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given site's domain url is saved locally.
|
||||||
|
* @param siteDomain the [siteDomain] that will be checked.
|
||||||
|
*/
|
||||||
|
suspend fun isSiteDomainReported(siteDomain: String): Boolean {
|
||||||
|
return dataStore.data
|
||||||
|
.catch { exception ->
|
||||||
|
if (exception is IOException) {
|
||||||
|
Logger.error("Error reading preferences.", exception)
|
||||||
|
emit(emptyPreferences())
|
||||||
|
} else {
|
||||||
|
throw exception
|
||||||
|
}
|
||||||
|
}.map { preferences ->
|
||||||
|
val reportSiteDomainsString = preferences[REPORT_SITE_DOMAINS] ?: ""
|
||||||
|
val reportSiteDomainsList =
|
||||||
|
reportSiteDomainsString.split(SEPARATOR).filter { it.isNotEmpty() }
|
||||||
|
reportSiteDomainsList.contains(siteDomain)
|
||||||
|
}.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the given site's domain url in datastore to keep it persistent locally.
|
||||||
|
* This method gets called after the site domain was reported with Nimbus.
|
||||||
|
* @param siteDomain the [siteDomain] that will be saved.
|
||||||
|
*/
|
||||||
|
suspend fun saveSiteDomain(siteDomain: String) {
|
||||||
|
dataStore.edit { preferences ->
|
||||||
|
val siteDomainsPreferences = preferences[REPORT_SITE_DOMAINS] ?: ""
|
||||||
|
val siteDomainsList = siteDomainsPreferences.split(SEPARATOR).filter { it.isNotEmpty() }
|
||||||
|
if (siteDomainsList.contains(siteDomain)) {
|
||||||
|
return@edit
|
||||||
|
}
|
||||||
|
val domains = mutableListOf<String>()
|
||||||
|
domains.addAll(siteDomainsList)
|
||||||
|
domains.add(siteDomain)
|
||||||
|
preferences[REPORT_SITE_DOMAINS] = domains.joinToString(SEPARATOR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.ext
|
||||||
|
|
||||||
|
import mozilla.components.concept.storage.Address
|
||||||
|
import org.mozilla.geckoview.Autocomplete
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a GeckoView [Autocomplete.Address] to an Android Components [Address].
|
||||||
|
*/
|
||||||
|
fun Autocomplete.Address.toAddress() = Address(
|
||||||
|
guid = guid ?: "",
|
||||||
|
name = name,
|
||||||
|
organization = organization,
|
||||||
|
streetAddress = streetAddress,
|
||||||
|
addressLevel3 = addressLevel3,
|
||||||
|
addressLevel2 = addressLevel2,
|
||||||
|
addressLevel1 = addressLevel1,
|
||||||
|
postalCode = postalCode,
|
||||||
|
country = country,
|
||||||
|
tel = tel,
|
||||||
|
email = email,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an Android Components [Address] to a GeckoView [Autocomplete.Address].
|
||||||
|
*/
|
||||||
|
fun Address.toAutocompleteAddress() = Autocomplete.Address.Builder()
|
||||||
|
.guid(guid)
|
||||||
|
.name(name)
|
||||||
|
.organization(organization)
|
||||||
|
.streetAddress(streetAddress)
|
||||||
|
.addressLevel3(addressLevel3)
|
||||||
|
.addressLevel2(addressLevel2)
|
||||||
|
.addressLevel1(addressLevel1)
|
||||||
|
.postalCode(postalCode)
|
||||||
|
.country(country)
|
||||||
|
.tel(tel)
|
||||||
|
.email(email)
|
||||||
|
.build()
|
|
@ -0,0 +1,32 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.ext
|
||||||
|
|
||||||
|
import mozilla.components.concept.storage.CreditCardEntry
|
||||||
|
import mozilla.components.support.utils.creditCardIIN
|
||||||
|
import org.mozilla.geckoview.Autocomplete
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a GeckoView [Autocomplete.CreditCard] to an Android Components [CreditCardEntry].
|
||||||
|
*/
|
||||||
|
fun Autocomplete.CreditCard.toCreditCardEntry() = CreditCardEntry(
|
||||||
|
guid = guid,
|
||||||
|
name = name,
|
||||||
|
number = number,
|
||||||
|
expiryMonth = expirationMonth,
|
||||||
|
expiryYear = expirationYear,
|
||||||
|
cardType = number.creditCardIIN()?.creditCardIssuerNetwork?.name ?: "",
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an Android Components [CreditCardEntry] to a GeckoView [Autocomplete.CreditCard].
|
||||||
|
*/
|
||||||
|
fun CreditCardEntry.toAutocompleteCreditCard() = Autocomplete.CreditCard.Builder()
|
||||||
|
.guid(guid)
|
||||||
|
.name(name)
|
||||||
|
.number(number)
|
||||||
|
.expirationMonth(expiryMonth)
|
||||||
|
.expirationYear(expiryYear)
|
||||||
|
.build()
|
|
@ -0,0 +1,31 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.ext
|
||||||
|
|
||||||
|
import mozilla.components.browser.engine.gecko.prompt.GeckoChoice
|
||||||
|
import mozilla.components.concept.engine.prompt.Choice
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a GeckoView [GeckoChoice] to an Android Components [Choice].
|
||||||
|
*/
|
||||||
|
private fun GeckoChoice.toChoice(): Choice {
|
||||||
|
val choiceChildren = items?.map { it.toChoice() }?.toTypedArray()
|
||||||
|
// On the GeckoView docs states that label is a @NonNull, but on run-time
|
||||||
|
// we are getting null values
|
||||||
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1771149
|
||||||
|
@Suppress("USELESS_ELVIS")
|
||||||
|
return Choice(id, !disabled, label ?: "", selected, separator, choiceChildren)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an array of [GeckoChoice] to Choice array.
|
||||||
|
* @return array of Choice
|
||||||
|
*/
|
||||||
|
fun convertToChoices(
|
||||||
|
geckoChoices: Array<out GeckoChoice>,
|
||||||
|
): Array<Choice> = geckoChoices.map { geckoChoice ->
|
||||||
|
val choice = geckoChoice.toChoice()
|
||||||
|
choice
|
||||||
|
}.toTypedArray()
|
|
@ -0,0 +1,25 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.ext
|
||||||
|
|
||||||
|
import mozilla.components.browser.engine.gecko.GeckoEngineSession
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_ALLOW
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_TRACKING
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this Gecko permission is a tracking protection permission and it is excluded
|
||||||
|
* from the tracking protection policies.
|
||||||
|
*/
|
||||||
|
val ContentPermission.isExcludedForTrackingProtection: Boolean
|
||||||
|
get() = this.permission == PERMISSION_TRACKING &&
|
||||||
|
value == VALUE_ALLOW
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the tracking protection permission for the given [GeckoEngineSession].
|
||||||
|
* This is available after every onLocationChange call.
|
||||||
|
*/
|
||||||
|
val GeckoEngineSession.geckoTrackingProtectionPermission: ContentPermission?
|
||||||
|
get() = this.geckoPermissions.find { it.permission == PERMISSION_TRACKING }
|
|
@ -0,0 +1,43 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.ext
|
||||||
|
|
||||||
|
import mozilla.components.concept.storage.Login
|
||||||
|
import mozilla.components.concept.storage.LoginEntry
|
||||||
|
import org.mozilla.geckoview.Autocomplete
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a GeckoView [Autocomplete.LoginEntry] to an Android Components [LoginEntry].
|
||||||
|
*/
|
||||||
|
fun Autocomplete.LoginEntry.toLoginEntry() = LoginEntry(
|
||||||
|
origin = origin,
|
||||||
|
formActionOrigin = formActionOrigin,
|
||||||
|
httpRealm = httpRealm,
|
||||||
|
username = username,
|
||||||
|
password = password,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an Android Components [Login] to a GeckoView [Autocomplete.LoginEntry].
|
||||||
|
*/
|
||||||
|
fun Login.toLoginEntry() = Autocomplete.LoginEntry.Builder()
|
||||||
|
.guid(guid)
|
||||||
|
.origin(origin)
|
||||||
|
.formActionOrigin(formActionOrigin)
|
||||||
|
.httpRealm(httpRealm)
|
||||||
|
.username(username)
|
||||||
|
.password(password)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an Android Components [LoginEntry] to a GeckoView [Autocomplete.LoginEntry].
|
||||||
|
*/
|
||||||
|
fun LoginEntry.toLoginEntry() = Autocomplete.LoginEntry.Builder()
|
||||||
|
.origin(origin)
|
||||||
|
.formActionOrigin(formActionOrigin)
|
||||||
|
.httpRealm(httpRealm)
|
||||||
|
.username(username)
|
||||||
|
.password(password)
|
||||||
|
.build()
|
|
@ -0,0 +1,82 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.ext
|
||||||
|
|
||||||
|
import mozilla.components.concept.engine.EngineSession
|
||||||
|
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
|
||||||
|
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory
|
||||||
|
import org.mozilla.geckoview.ContentBlocking
|
||||||
|
import org.mozilla.geckoview.GeckoRuntimeSettings
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a [TrackingProtectionPolicy] into a GeckoView setting that can be used with [GeckoRuntimeSettings.Builder].
|
||||||
|
* Also contains the cookie banner handling settings for regular and private browsing.
|
||||||
|
*/
|
||||||
|
@Suppress("SpreadOperator")
|
||||||
|
fun TrackingProtectionPolicy.toContentBlockingSetting(
|
||||||
|
safeBrowsingPolicy: Array<EngineSession.SafeBrowsingPolicy> = arrayOf(EngineSession.SafeBrowsingPolicy.RECOMMENDED),
|
||||||
|
cookieBannerHandlingMode: EngineSession.CookieBannerHandlingMode = EngineSession.CookieBannerHandlingMode.DISABLED,
|
||||||
|
cookieBannerHandlingModePrivateBrowsing: EngineSession.CookieBannerHandlingMode =
|
||||||
|
EngineSession.CookieBannerHandlingMode.REJECT_ALL,
|
||||||
|
cookieBannerHandlingDetectOnlyMode: Boolean = false,
|
||||||
|
cookieBannerGlobalRulesEnabled: Boolean = false,
|
||||||
|
cookieBannerGlobalRulesSubFramesEnabled: Boolean = false,
|
||||||
|
queryParameterStripping: Boolean = false,
|
||||||
|
queryParameterStrippingPrivateBrowsing: Boolean = false,
|
||||||
|
queryParameterStrippingAllowList: String = "",
|
||||||
|
queryParameterStrippingStripList: String = "",
|
||||||
|
) = ContentBlocking.Settings.Builder().apply {
|
||||||
|
enhancedTrackingProtectionLevel(getEtpLevel())
|
||||||
|
antiTracking(getAntiTrackingPolicy())
|
||||||
|
cookieBehavior(cookiePolicy.id)
|
||||||
|
cookieBehaviorPrivateMode(cookiePolicyPrivateMode.id)
|
||||||
|
cookiePurging(cookiePurging)
|
||||||
|
safeBrowsing(safeBrowsingPolicy.sumOf { it.id })
|
||||||
|
strictSocialTrackingProtection(getStrictSocialTrackingProtection())
|
||||||
|
cookieBannerHandlingMode(cookieBannerHandlingMode.mode)
|
||||||
|
cookieBannerHandlingModePrivateBrowsing(cookieBannerHandlingModePrivateBrowsing.mode)
|
||||||
|
cookieBannerHandlingDetectOnlyMode(cookieBannerHandlingDetectOnlyMode)
|
||||||
|
cookieBannerGlobalRulesEnabled(cookieBannerGlobalRulesEnabled)
|
||||||
|
cookieBannerGlobalRulesSubFramesEnabled(cookieBannerGlobalRulesSubFramesEnabled)
|
||||||
|
queryParameterStrippingEnabled(queryParameterStripping)
|
||||||
|
queryParameterStrippingPrivateBrowsingEnabled(queryParameterStrippingPrivateBrowsing)
|
||||||
|
queryParameterStrippingAllowList(*queryParameterStrippingAllowList.split(",").toTypedArray())
|
||||||
|
queryParameterStrippingStripList(*queryParameterStrippingStripList.split(",").toTypedArray())
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether [TrackingCategory.STRICT] is enabled in the [TrackingProtectionPolicy].
|
||||||
|
*/
|
||||||
|
internal fun TrackingProtectionPolicy.getStrictSocialTrackingProtection(): Boolean {
|
||||||
|
return strictSocialTrackingProtection ?: trackingCategories.contains(TrackingCategory.STRICT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the [TrackingProtectionPolicy] categories as an Enhanced Tracking Protection level for GeckoView.
|
||||||
|
*/
|
||||||
|
internal fun TrackingProtectionPolicy.getEtpLevel(): Int {
|
||||||
|
return when {
|
||||||
|
trackingCategories.contains(TrackingCategory.NONE) -> ContentBlocking.EtpLevel.NONE
|
||||||
|
else -> ContentBlocking.EtpLevel.STRICT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the [TrackingProtectionPolicy] as a tracking policy for GeckoView.
|
||||||
|
*/
|
||||||
|
internal fun TrackingProtectionPolicy.getAntiTrackingPolicy(): Int {
|
||||||
|
/**
|
||||||
|
* The [TrackingProtectionPolicy.TrackingCategory.SCRIPTS_AND_SUB_RESOURCES] is an
|
||||||
|
* artificial category, created with the sole purpose of going around this bug
|
||||||
|
* https://bugzilla.mozilla.org/show_bug.cgi?id=1579264, for this reason we have to
|
||||||
|
* remove its value from the valid anti tracking categories, when is present.
|
||||||
|
*/
|
||||||
|
val total = trackingCategories.sumOf { it.id }
|
||||||
|
return if (contains(TrackingCategory.SCRIPTS_AND_SUB_RESOURCES)) {
|
||||||
|
total - TrackingCategory.SCRIPTS_AND_SUB_RESOURCES.id
|
||||||
|
} else {
|
||||||
|
total
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,139 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.fetch
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import mozilla.components.concept.fetch.Client
|
||||||
|
import mozilla.components.concept.fetch.Headers
|
||||||
|
import mozilla.components.concept.fetch.MutableHeaders
|
||||||
|
import mozilla.components.concept.fetch.Request
|
||||||
|
import mozilla.components.concept.fetch.Response
|
||||||
|
import mozilla.components.concept.fetch.Response.Companion.SUCCESS
|
||||||
|
import mozilla.components.concept.fetch.isBlobUri
|
||||||
|
import mozilla.components.concept.fetch.isDataUri
|
||||||
|
import org.mozilla.geckoview.GeckoRuntime
|
||||||
|
import org.mozilla.geckoview.GeckoWebExecutor
|
||||||
|
import org.mozilla.geckoview.WebRequest
|
||||||
|
import org.mozilla.geckoview.WebRequest.CACHE_MODE_DEFAULT
|
||||||
|
import org.mozilla.geckoview.WebRequest.CACHE_MODE_RELOAD
|
||||||
|
import org.mozilla.geckoview.WebRequestError
|
||||||
|
import org.mozilla.geckoview.WebResponse
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.SocketTimeoutException
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.TimeoutException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GeckoView ([GeckoWebExecutor]) based implementation of [Client].
|
||||||
|
*/
|
||||||
|
class GeckoViewFetchClient(
|
||||||
|
context: Context,
|
||||||
|
runtime: GeckoRuntime = GeckoRuntime.getDefault(context),
|
||||||
|
private val maxReadTimeOut: Pair<Long, TimeUnit> = Pair(MAX_READ_TIMEOUT_MINUTES, TimeUnit.MINUTES),
|
||||||
|
) : Client() {
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal var executor: GeckoWebExecutor = GeckoWebExecutor(runtime)
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun fetch(request: Request): Response {
|
||||||
|
if (request.isDataUri()) {
|
||||||
|
return fetchDataUri(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
val webRequest = request.toWebRequest()
|
||||||
|
|
||||||
|
val readTimeOut = request.readTimeout ?: maxReadTimeOut
|
||||||
|
val readTimeOutMillis = readTimeOut.let { (timeout, unit) ->
|
||||||
|
unit.toMillis(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val webResponse = executor.fetch(webRequest, request.fetchFlags).poll(readTimeOutMillis)
|
||||||
|
webResponse?.toResponse() ?: throw IOException("Fetch failed with null response")
|
||||||
|
} catch (e: TimeoutException) {
|
||||||
|
throw SocketTimeoutException()
|
||||||
|
} catch (e: WebRequestError) {
|
||||||
|
throw IOException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val Request.fetchFlags: Int
|
||||||
|
get() {
|
||||||
|
var fetchFlags = 0
|
||||||
|
if (cookiePolicy == Request.CookiePolicy.OMIT) {
|
||||||
|
fetchFlags += GeckoWebExecutor.FETCH_FLAGS_ANONYMOUS
|
||||||
|
}
|
||||||
|
if (private) {
|
||||||
|
fetchFlags += GeckoWebExecutor.FETCH_FLAGS_PRIVATE
|
||||||
|
}
|
||||||
|
if (redirect == Request.Redirect.MANUAL) {
|
||||||
|
fetchFlags += GeckoWebExecutor.FETCH_FLAGS_NO_REDIRECTS
|
||||||
|
}
|
||||||
|
return fetchFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val MAX_READ_TIMEOUT_MINUTES = 5L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Request.toWebRequest(): WebRequest = WebRequest.Builder(url)
|
||||||
|
.method(method.name)
|
||||||
|
.addHeadersFrom(this)
|
||||||
|
.addBodyFrom(this)
|
||||||
|
.referrer(referrerUrl)
|
||||||
|
.cacheMode(if (useCaches) CACHE_MODE_DEFAULT else CACHE_MODE_RELOAD)
|
||||||
|
.beConservative(conservative)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private fun WebRequest.Builder.addHeadersFrom(request: Request): WebRequest.Builder {
|
||||||
|
request.headers?.forEach { header ->
|
||||||
|
addHeader(header.name, header.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun WebRequest.Builder.addBodyFrom(request: Request): WebRequest.Builder {
|
||||||
|
request.body?.let { body ->
|
||||||
|
body.useStream { inStream ->
|
||||||
|
val bytes = inStream.readBytes()
|
||||||
|
val buffer = ByteBuffer.allocateDirect(bytes.size)
|
||||||
|
buffer.put(bytes)
|
||||||
|
this.body(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun WebResponse.toResponse(): Response {
|
||||||
|
val isDataUri = uri.startsWith("data:")
|
||||||
|
val isBlobUri = uri.startsWith("blob:")
|
||||||
|
val headers = translateHeaders(this)
|
||||||
|
// We use the same API for blobs, data URLs and HTTP requests, but blobs won't receive a status code.
|
||||||
|
// If no exception is thrown we assume success.
|
||||||
|
val status = if (isBlobUri || isDataUri) SUCCESS else statusCode
|
||||||
|
return Response(
|
||||||
|
uri,
|
||||||
|
status,
|
||||||
|
headers,
|
||||||
|
body?.let {
|
||||||
|
Response.Body(it, headers["Content-Type"])
|
||||||
|
} ?: Response.Body.empty(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun translateHeaders(webResponse: WebResponse): Headers {
|
||||||
|
val headers = MutableHeaders()
|
||||||
|
webResponse.headers.forEach { (k, v) ->
|
||||||
|
v.split(",").forEach { headers.append(k, it.trim()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.integration
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import mozilla.components.support.utils.ext.registerReceiverCompat
|
||||||
|
import org.mozilla.geckoview.GeckoRuntime
|
||||||
|
import androidx.core.os.LocaleListCompat as LocaleList
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to set the locales setting for geckoview, updating from the locale of the device.
|
||||||
|
*/
|
||||||
|
class LocaleSettingUpdater(
|
||||||
|
private val context: Context,
|
||||||
|
private val runtime: GeckoRuntime,
|
||||||
|
) : SettingUpdater<Array<String>>() {
|
||||||
|
|
||||||
|
override var value: Array<String> = findValue()
|
||||||
|
set(value) {
|
||||||
|
runtime.settings.locales = value
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
private val localeChangedReceiver by lazy {
|
||||||
|
object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
|
updateValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun registerForUpdates() {
|
||||||
|
context.registerReceiverCompat(
|
||||||
|
localeChangedReceiver,
|
||||||
|
IntentFilter(Intent.ACTION_LOCALE_CHANGED),
|
||||||
|
ContextCompat.RECEIVER_NOT_EXPORTED,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unregisterForUpdates() {
|
||||||
|
context.unregisterReceiver(localeChangedReceiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findValue(): Array<String> {
|
||||||
|
val localeList = LocaleList.getAdjustedDefault()
|
||||||
|
return arrayOfNulls<Unit>(localeList.size())
|
||||||
|
.mapIndexedNotNull { i, _ -> localeList.get(i)?.toLanguageTag() }
|
||||||
|
.toTypedArray()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.integration
|
||||||
|
|
||||||
|
abstract class SettingUpdater<T> {
|
||||||
|
/**
|
||||||
|
* Toggle the automatic tracking of a setting derived from the device state.
|
||||||
|
*/
|
||||||
|
var enabled: Boolean = false
|
||||||
|
set(value) {
|
||||||
|
if (value) {
|
||||||
|
updateValue()
|
||||||
|
registerForUpdates()
|
||||||
|
} else {
|
||||||
|
unregisterForUpdates()
|
||||||
|
}
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The setter for this property should change the GeckoView setting.
|
||||||
|
*/
|
||||||
|
abstract var value: T
|
||||||
|
|
||||||
|
internal fun updateValue() {
|
||||||
|
value = findValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register for updates from the device state. This is setting specific.
|
||||||
|
*/
|
||||||
|
abstract fun registerForUpdates()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister for updates from the device state.
|
||||||
|
*/
|
||||||
|
abstract fun unregisterForUpdates()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the value of the setting from the device state. This is setting specific.
|
||||||
|
*/
|
||||||
|
abstract fun findValue(): T
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
/* 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.media
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import mozilla.components.browser.engine.gecko.GeckoEngineSession
|
||||||
|
import mozilla.components.concept.engine.media.RecordingDevice
|
||||||
|
import org.mozilla.geckoview.GeckoSession
|
||||||
|
import java.security.InvalidParameterException
|
||||||
|
import org.mozilla.geckoview.GeckoSession.MediaDelegate.RecordingDevice as GeckoRecordingDevice
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gecko-based GeckoMediaDelegate implementation.
|
||||||
|
*/
|
||||||
|
internal class GeckoMediaDelegate(private val geckoEngineSession: GeckoEngineSession) :
|
||||||
|
GeckoSession.MediaDelegate {
|
||||||
|
|
||||||
|
override fun onRecordingStatusChanged(
|
||||||
|
session: GeckoSession,
|
||||||
|
geckoDevices: Array<out GeckoRecordingDevice>,
|
||||||
|
) {
|
||||||
|
val devices = geckoDevices.map { geckoRecording ->
|
||||||
|
val type = geckoRecording.toType()
|
||||||
|
val status = geckoRecording.toStatus()
|
||||||
|
RecordingDevice(type, status)
|
||||||
|
}
|
||||||
|
geckoEngineSession.notifyObservers { onRecordingStateChanged(devices) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal fun GeckoRecordingDevice.toType(): RecordingDevice.Type {
|
||||||
|
return when (type) {
|
||||||
|
GeckoRecordingDevice.Type.CAMERA -> RecordingDevice.Type.CAMERA
|
||||||
|
GeckoRecordingDevice.Type.MICROPHONE -> RecordingDevice.Type.MICROPHONE
|
||||||
|
else -> {
|
||||||
|
throw InvalidParameterException("Unexpected Gecko Media type $type status $status")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal fun GeckoRecordingDevice.toStatus(): RecordingDevice.Status {
|
||||||
|
return when (status) {
|
||||||
|
GeckoRecordingDevice.Status.RECORDING -> RecordingDevice.Status.RECORDING
|
||||||
|
GeckoRecordingDevice.Status.INACTIVE -> RecordingDevice.Status.INACTIVE
|
||||||
|
else -> {
|
||||||
|
throw InvalidParameterException("Unexpected Gecko Media type $type status $status")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
/* 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.mediaquery
|
||||||
|
|
||||||
|
import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
|
||||||
|
import org.mozilla.geckoview.GeckoRuntimeSettings
|
||||||
|
|
||||||
|
internal fun PreferredColorScheme.Companion.from(geckoValue: Int) =
|
||||||
|
when (geckoValue) {
|
||||||
|
GeckoRuntimeSettings.COLOR_SCHEME_DARK -> PreferredColorScheme.Dark
|
||||||
|
GeckoRuntimeSettings.COLOR_SCHEME_LIGHT -> PreferredColorScheme.Light
|
||||||
|
GeckoRuntimeSettings.COLOR_SCHEME_SYSTEM -> PreferredColorScheme.System
|
||||||
|
else -> PreferredColorScheme.System
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun PreferredColorScheme.toGeckoValue() =
|
||||||
|
when (this) {
|
||||||
|
is PreferredColorScheme.Dark -> GeckoRuntimeSettings.COLOR_SCHEME_DARK
|
||||||
|
is PreferredColorScheme.Light -> GeckoRuntimeSettings.COLOR_SCHEME_LIGHT
|
||||||
|
is PreferredColorScheme.System -> GeckoRuntimeSettings.COLOR_SCHEME_SYSTEM
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.mediasession
|
||||||
|
|
||||||
|
import mozilla.components.concept.engine.mediasession.MediaSession
|
||||||
|
import org.mozilla.geckoview.MediaSession as GeckoViewMediaSession
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [MediaSession.Controller] (`concept-engine`) implementation for GeckoView.
|
||||||
|
*/
|
||||||
|
internal class GeckoMediaSessionController(
|
||||||
|
private val mediaSession: GeckoViewMediaSession,
|
||||||
|
) : MediaSession.Controller {
|
||||||
|
|
||||||
|
override fun pause() {
|
||||||
|
mediaSession.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
mediaSession.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun play() {
|
||||||
|
mediaSession.play()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun seekTo(time: Double, fast: Boolean) {
|
||||||
|
mediaSession.seekTo(time, fast)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun seekForward() {
|
||||||
|
mediaSession.seekForward()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun seekBackward() {
|
||||||
|
mediaSession.seekBackward()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun nextTrack() {
|
||||||
|
mediaSession.nextTrack()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun previousTrack() {
|
||||||
|
mediaSession.previousTrack()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun skipAd() {
|
||||||
|
mediaSession.skipAd()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun muteAudio(mute: Boolean) {
|
||||||
|
mediaSession.muteAudio(mute)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,125 @@
|
||||||
|
/* 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.mediasession
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import mozilla.components.browser.engine.gecko.GeckoEngineSession
|
||||||
|
import mozilla.components.browser.engine.gecko.await
|
||||||
|
import mozilla.components.concept.engine.mediasession.MediaSession
|
||||||
|
import org.mozilla.geckoview.GeckoSession
|
||||||
|
import org.mozilla.geckoview.Image.ImageProcessingException
|
||||||
|
import org.mozilla.geckoview.MediaSession as GeckoViewMediaSession
|
||||||
|
|
||||||
|
private const val ARTWORK_RETRIEVE_TIMEOUT = 1000L
|
||||||
|
private const val ARTWORK_IMAGE_SIZE = 48
|
||||||
|
|
||||||
|
internal class GeckoMediaSessionDelegate(
|
||||||
|
private val engineSession: GeckoEngineSession,
|
||||||
|
) : GeckoViewMediaSession.Delegate {
|
||||||
|
|
||||||
|
override fun onActivated(geckoSession: GeckoSession, mediaSession: GeckoViewMediaSession) {
|
||||||
|
engineSession.notifyObservers {
|
||||||
|
onMediaActivated(GeckoMediaSessionController(mediaSession))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDeactivated(session: GeckoSession, mediaSession: GeckoViewMediaSession) {
|
||||||
|
engineSession.notifyObservers {
|
||||||
|
onMediaDeactivated()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMetadata(
|
||||||
|
session: GeckoSession,
|
||||||
|
mediaSession: GeckoViewMediaSession,
|
||||||
|
metaData: GeckoViewMediaSession.Metadata,
|
||||||
|
) {
|
||||||
|
val getArtwork: (suspend () -> Bitmap?)? = metaData.artwork?.let {
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
withTimeoutOrNull(ARTWORK_RETRIEVE_TIMEOUT) {
|
||||||
|
it.getBitmap(ARTWORK_IMAGE_SIZE).await()
|
||||||
|
}
|
||||||
|
} catch (e: ImageProcessingException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
engineSession.notifyObservers {
|
||||||
|
onMediaMetadataChanged(
|
||||||
|
MediaSession.Metadata(metaData.title, metaData.artist, metaData.album, getArtwork),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFeatures(
|
||||||
|
session: GeckoSession,
|
||||||
|
mediaSession: GeckoViewMediaSession,
|
||||||
|
features: Long,
|
||||||
|
) {
|
||||||
|
engineSession.notifyObservers {
|
||||||
|
onMediaFeatureChanged(MediaSession.Feature(features))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlay(session: GeckoSession, mediaSession: GeckoViewMediaSession) {
|
||||||
|
engineSession.notifyObservers {
|
||||||
|
onMediaPlaybackStateChanged(MediaSession.PlaybackState.PLAYING)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause(session: GeckoSession, mediaSession: GeckoViewMediaSession) {
|
||||||
|
engineSession.notifyObservers {
|
||||||
|
onMediaPlaybackStateChanged(MediaSession.PlaybackState.PAUSED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop(session: GeckoSession, mediaSession: GeckoViewMediaSession) {
|
||||||
|
engineSession.notifyObservers {
|
||||||
|
onMediaPlaybackStateChanged(MediaSession.PlaybackState.STOPPED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPositionState(
|
||||||
|
session: GeckoSession,
|
||||||
|
mediaSession: GeckoViewMediaSession,
|
||||||
|
positionState: GeckoViewMediaSession.PositionState,
|
||||||
|
) {
|
||||||
|
engineSession.notifyObservers {
|
||||||
|
onMediaPositionStateChanged(
|
||||||
|
MediaSession.PositionState(
|
||||||
|
positionState.duration,
|
||||||
|
positionState.position,
|
||||||
|
positionState.playbackRate,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFullscreen(
|
||||||
|
session: GeckoSession,
|
||||||
|
mediaSession: GeckoViewMediaSession,
|
||||||
|
enabled: Boolean,
|
||||||
|
elementMetaData: GeckoViewMediaSession.ElementMetadata?,
|
||||||
|
) {
|
||||||
|
val sessionElementMetaData =
|
||||||
|
elementMetaData?.let {
|
||||||
|
MediaSession.ElementMetadata(
|
||||||
|
elementMetaData.source,
|
||||||
|
elementMetaData.duration,
|
||||||
|
elementMetaData.width,
|
||||||
|
elementMetaData.height,
|
||||||
|
elementMetaData.audioTrackCount,
|
||||||
|
elementMetaData.videoTrackCount,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
engineSession.notifyObservers {
|
||||||
|
onMediaFullscreenChanged(enabled, sessionElementMetaData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,185 @@
|
||||||
|
/* 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.permission
|
||||||
|
|
||||||
|
import android.Manifest.permission.ACCESS_COARSE_LOCATION
|
||||||
|
import android.Manifest.permission.ACCESS_FINE_LOCATION
|
||||||
|
import android.Manifest.permission.CAMERA
|
||||||
|
import android.Manifest.permission.RECORD_AUDIO
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import mozilla.components.concept.engine.permission.Permission
|
||||||
|
import mozilla.components.concept.engine.permission.PermissionRequest
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_ALLOW
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_DENY
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource.SOURCE_AUDIOCAPTURE
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource.SOURCE_CAMERA
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource.SOURCE_MICROPHONE
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource.SOURCE_OTHER
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource.SOURCE_SCREEN
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_AUTOPLAY_AUDIBLE
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_AUTOPLAY_INAUDIBLE
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_DESKTOP_NOTIFICATION
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_GEOLOCATION
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_MEDIA_KEY_SYSTEM_ACCESS
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_PERSISTENT_STORAGE
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_STORAGE_ACCESS
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gecko-based implementation of [PermissionRequest].
|
||||||
|
*
|
||||||
|
* @property permissions the list of requested permissions.
|
||||||
|
* @property callback the callback to grant/reject the requested permissions.
|
||||||
|
* @property id a unique identifier for the request.
|
||||||
|
*/
|
||||||
|
sealed class GeckoPermissionRequest constructor(
|
||||||
|
override val permissions: List<Permission>,
|
||||||
|
private val callback: PermissionDelegate.Callback? = null,
|
||||||
|
override val id: String = UUID.randomUUID().toString(),
|
||||||
|
) : PermissionRequest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a gecko-based content permission request.
|
||||||
|
*
|
||||||
|
* @property uri the URI of the content requesting the permissions.
|
||||||
|
* @property type the type of the requested content permission (will be
|
||||||
|
* mapped to corresponding [Permission]).
|
||||||
|
* @property geckoPermission Indicates which gecko permissions is requested.
|
||||||
|
* @property geckoResult the gecko result that serves as a callback to grant/reject the requested permissions.
|
||||||
|
*/
|
||||||
|
data class Content(
|
||||||
|
override val uri: String,
|
||||||
|
private val type: Int,
|
||||||
|
internal val geckoPermission: PermissionDelegate.ContentPermission,
|
||||||
|
internal val geckoResult: GeckoResult<Int>,
|
||||||
|
) : GeckoPermissionRequest(
|
||||||
|
listOf(permissionsMap.getOrElse(type) { Permission.Generic("$type", "Gecko permission type = $type") }),
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
val permissionsMap = mapOf(
|
||||||
|
PERMISSION_DESKTOP_NOTIFICATION to Permission.ContentNotification(),
|
||||||
|
PERMISSION_GEOLOCATION to Permission.ContentGeoLocation(),
|
||||||
|
PERMISSION_AUTOPLAY_AUDIBLE to Permission.ContentAutoPlayAudible(),
|
||||||
|
PERMISSION_AUTOPLAY_INAUDIBLE to Permission.ContentAutoPlayInaudible(),
|
||||||
|
PERMISSION_PERSISTENT_STORAGE to Permission.ContentPersistentStorage(),
|
||||||
|
PERMISSION_MEDIA_KEY_SYSTEM_ACCESS to Permission.ContentMediaKeySystemAccess(),
|
||||||
|
PERMISSION_STORAGE_ACCESS to Permission.ContentCrossOriginStorageAccess(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal var isCompleted = false
|
||||||
|
|
||||||
|
override fun grant(permissions: List<Permission>) {
|
||||||
|
if (!isCompleted) {
|
||||||
|
geckoResult.complete(VALUE_ALLOW)
|
||||||
|
}
|
||||||
|
isCompleted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reject() {
|
||||||
|
if (!isCompleted) {
|
||||||
|
geckoResult.complete(VALUE_DENY)
|
||||||
|
}
|
||||||
|
isCompleted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a gecko-based application permission request.
|
||||||
|
*
|
||||||
|
* @property uri the URI of the content requesting the permissions.
|
||||||
|
* @property nativePermissions the list of requested app permissions (will be
|
||||||
|
* mapped to corresponding [Permission]s).
|
||||||
|
* @property callback the callback to grant/reject the requested permissions.
|
||||||
|
*/
|
||||||
|
data class App(
|
||||||
|
private val nativePermissions: List<String>,
|
||||||
|
private val callback: PermissionDelegate.Callback,
|
||||||
|
) : GeckoPermissionRequest(
|
||||||
|
nativePermissions.map { permissionsMap.getOrElse(it) { Permission.Generic(it) } },
|
||||||
|
callback,
|
||||||
|
) {
|
||||||
|
override val uri: String? = null
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val permissionsMap = mapOf(
|
||||||
|
ACCESS_COARSE_LOCATION to Permission.AppLocationCoarse(ACCESS_COARSE_LOCATION),
|
||||||
|
ACCESS_FINE_LOCATION to Permission.AppLocationFine(ACCESS_FINE_LOCATION),
|
||||||
|
CAMERA to Permission.AppCamera(CAMERA),
|
||||||
|
RECORD_AUDIO to Permission.AppAudio(RECORD_AUDIO),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a gecko-based media permission request.
|
||||||
|
*
|
||||||
|
* @property uri the URI of the content requesting the permissions.
|
||||||
|
* @property videoSources the list of requested video sources (will be
|
||||||
|
* mapped to the corresponding [Permission]).
|
||||||
|
* @property audioSources the list of requested audio sources (will be
|
||||||
|
* mapped to corresponding [Permission]).
|
||||||
|
* @property callback the callback to grant/reject the requested permissions.
|
||||||
|
*/
|
||||||
|
data class Media(
|
||||||
|
override val uri: String,
|
||||||
|
private val videoSources: List<MediaSource>,
|
||||||
|
private val audioSources: List<MediaSource>,
|
||||||
|
private val callback: PermissionDelegate.MediaCallback,
|
||||||
|
) : GeckoPermissionRequest(
|
||||||
|
videoSources.map { mapPermission(it) } + audioSources.map { mapPermission(it) },
|
||||||
|
) {
|
||||||
|
override fun grant(permissions: List<Permission>) {
|
||||||
|
val videos = permissions.mapNotNull { permission -> videoSources.find { it.id == permission.id } }
|
||||||
|
val audios = permissions.mapNotNull { permission -> audioSources.find { it.id == permission.id } }
|
||||||
|
callback.grant(videos.firstOrNull(), audios.firstOrNull())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun containsVideoAndAudioSources(): Boolean {
|
||||||
|
return videoSources.isNotEmpty() && audioSources.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reject() {
|
||||||
|
callback.reject()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun mapPermission(mediaSource: MediaSource): Permission =
|
||||||
|
if (mediaSource.type == MediaSource.TYPE_AUDIO) {
|
||||||
|
mapAudioPermission(mediaSource)
|
||||||
|
} else {
|
||||||
|
mapVideoPermission(mediaSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("SwitchIntDef")
|
||||||
|
private fun mapAudioPermission(mediaSource: MediaSource) = when (mediaSource.source) {
|
||||||
|
SOURCE_AUDIOCAPTURE -> Permission.ContentAudioCapture(mediaSource.id, mediaSource.name)
|
||||||
|
SOURCE_MICROPHONE -> Permission.ContentAudioMicrophone(mediaSource.id, mediaSource.name)
|
||||||
|
SOURCE_OTHER -> Permission.ContentAudioOther(mediaSource.id, mediaSource.name)
|
||||||
|
else -> Permission.Generic(mediaSource.id, mediaSource.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("ComplexMethod", "SwitchIntDef")
|
||||||
|
private fun mapVideoPermission(mediaSource: MediaSource) = when (mediaSource.source) {
|
||||||
|
SOURCE_CAMERA -> Permission.ContentVideoCamera(mediaSource.id, mediaSource.name)
|
||||||
|
SOURCE_SCREEN -> Permission.ContentVideoScreen(mediaSource.id, mediaSource.name)
|
||||||
|
SOURCE_OTHER -> Permission.ContentVideoOther(mediaSource.id, mediaSource.name)
|
||||||
|
else -> Permission.Generic(mediaSource.id, mediaSource.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun grant(permissions: List<Permission>) {
|
||||||
|
callback?.grant()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reject() {
|
||||||
|
callback?.reject()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,461 @@
|
||||||
|
/* 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.permission
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import androidx.paging.DataSource
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import mozilla.components.browser.engine.gecko.await
|
||||||
|
import mozilla.components.concept.engine.permission.PermissionRequest
|
||||||
|
import mozilla.components.concept.engine.permission.SitePermissions
|
||||||
|
import mozilla.components.concept.engine.permission.SitePermissions.AutoplayStatus
|
||||||
|
import mozilla.components.concept.engine.permission.SitePermissions.Status
|
||||||
|
import mozilla.components.concept.engine.permission.SitePermissions.Status.ALLOWED
|
||||||
|
import mozilla.components.concept.engine.permission.SitePermissions.Status.BLOCKED
|
||||||
|
import mozilla.components.concept.engine.permission.SitePermissions.Status.NO_DECISION
|
||||||
|
import mozilla.components.concept.engine.permission.SitePermissionsStorage
|
||||||
|
import mozilla.components.support.ktx.kotlin.stripDefaultPort
|
||||||
|
import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
|
||||||
|
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
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_PROMPT
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_AUTOPLAY_AUDIBLE
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_AUTOPLAY_INAUDIBLE
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_DESKTOP_NOTIFICATION
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_GEOLOCATION
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_MEDIA_KEY_SYSTEM_ACCESS
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_PERSISTENT_STORAGE
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_STORAGE_ACCESS
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_TRACKING
|
||||||
|
import org.mozilla.geckoview.StorageController
|
||||||
|
import org.mozilla.geckoview.StorageController.ClearFlags
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A storage to save [SitePermissions] using GeckoView APIs.
|
||||||
|
*/
|
||||||
|
@Suppress("LargeClass")
|
||||||
|
class GeckoSitePermissionsStorage(
|
||||||
|
runtime: GeckoRuntime,
|
||||||
|
private val onDiskStorage: SitePermissionsStorage,
|
||||||
|
) : SitePermissionsStorage {
|
||||||
|
|
||||||
|
private val geckoStorage: StorageController = runtime.storageController
|
||||||
|
private val mainScope = CoroutineScope(Dispatchers.Main)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Temporary permissions are created when users doesn't
|
||||||
|
* check the 'Remember my decision checkbox'. At the moment,
|
||||||
|
* gecko view doesn't handle temporary permission,
|
||||||
|
* we have to store them in memory, and clear them manually,
|
||||||
|
* until we have an API for it see:
|
||||||
|
* https://bugzilla.mozilla.org/show_bug.cgi?id=1710447
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
internal val geckoTemporaryPermissions = mutableListOf<ContentPermission>()
|
||||||
|
|
||||||
|
override suspend fun save(sitePermissions: SitePermissions, request: PermissionRequest?, private: Boolean) {
|
||||||
|
val geckoSavedPermissions = updateGeckoPermissionIfNeeded(sitePermissions, request, private)
|
||||||
|
onDiskStorage.save(geckoSavedPermissions, request, private)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveTemporary(request: PermissionRequest?) {
|
||||||
|
if (request is GeckoPermissionRequest.Content) {
|
||||||
|
geckoTemporaryPermissions.add(request.geckoPermission)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearTemporaryPermissions() {
|
||||||
|
geckoTemporaryPermissions.forEach {
|
||||||
|
geckoStorage.setPermission(it, VALUE_PROMPT)
|
||||||
|
}
|
||||||
|
geckoTemporaryPermissions.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun update(sitePermissions: SitePermissions, private: Boolean) {
|
||||||
|
val updatedPermission = updateGeckoPermissionIfNeeded(sitePermissions, private = private)
|
||||||
|
onDiskStorage.update(updatedPermission, private)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findSitePermissionsBy(
|
||||||
|
origin: String,
|
||||||
|
includeTemporary: Boolean,
|
||||||
|
private: Boolean,
|
||||||
|
): SitePermissions? {
|
||||||
|
/**
|
||||||
|
* GeckoView ony persists [GeckoPermissionRequest.Content] other permissions like
|
||||||
|
* [GeckoPermissionRequest.Media], we have to store them ourselves.
|
||||||
|
* For this reason, we query both storage ([geckoStorage] and [onDiskStorage]) and
|
||||||
|
* merge both results into one [SitePermissions] object.
|
||||||
|
*/
|
||||||
|
val onDiskPermission: SitePermissions? =
|
||||||
|
onDiskStorage.findSitePermissionsBy(origin, private = private)
|
||||||
|
val geckoPermissions = findGeckoContentPermissionBy(origin, includeTemporary, private).groupByType()
|
||||||
|
|
||||||
|
return mergePermissions(onDiskPermission, geckoPermissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getSitePermissionsPaged(): DataSource.Factory<Int, SitePermissions> {
|
||||||
|
val geckoPermissionsByHost = findAllGeckoContentPermissions().groupByDomain()
|
||||||
|
|
||||||
|
return onDiskStorage.getSitePermissionsPaged().map { onDiskPermission ->
|
||||||
|
val geckoPermissions = geckoPermissionsByHost[onDiskPermission.origin].groupByType()
|
||||||
|
mergePermissions(onDiskPermission, geckoPermissions) ?: onDiskPermission
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun remove(sitePermissions: SitePermissions, private: Boolean) {
|
||||||
|
onDiskStorage.remove(sitePermissions, private)
|
||||||
|
removeGeckoContentPermissionBy(sitePermissions.origin, private)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun removeAll() {
|
||||||
|
onDiskStorage.removeAll()
|
||||||
|
removeGeckoAllContentPermissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun all(): List<SitePermissions> {
|
||||||
|
val onDiskPermissions: List<SitePermissions> = onDiskStorage.all()
|
||||||
|
val geckoPermissionsByHost = findAllGeckoContentPermissions().groupByDomain()
|
||||||
|
|
||||||
|
return onDiskPermissions.mapNotNull { onDiskPermission ->
|
||||||
|
val map = geckoPermissionsByHost[onDiskPermission.origin].groupByType()
|
||||||
|
mergePermissions(onDiskPermission, map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal suspend fun findAllGeckoContentPermissions(): List<ContentPermission>? {
|
||||||
|
return withContext(mainScope.coroutineContext) {
|
||||||
|
geckoStorage.allPermissions.await()
|
||||||
|
.filterNotTemporaryPermissions(geckoTemporaryPermissions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the [geckoStorage] if the provided [userSitePermissions]
|
||||||
|
* exists on the [geckoStorage] or it's provided as a part of the [permissionRequest]
|
||||||
|
* otherwise nothing is updated.
|
||||||
|
* @param userSitePermissions the values provided by the user to be updated.
|
||||||
|
* @param permissionRequest the [PermissionRequest] from the web content.
|
||||||
|
* @return An updated [SitePermissions] with default values, if they were updated
|
||||||
|
* on the [geckoStorage] otherwise the same [SitePermissions].
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
@Suppress("LongMethod")
|
||||||
|
internal suspend fun updateGeckoPermissionIfNeeded(
|
||||||
|
userSitePermissions: SitePermissions,
|
||||||
|
permissionRequest: PermissionRequest? = null,
|
||||||
|
private: Boolean,
|
||||||
|
): SitePermissions {
|
||||||
|
var updatedPermission = userSitePermissions
|
||||||
|
val geckoPermissionsByType =
|
||||||
|
permissionRequest.extractGeckoPermissionsOrQueryTheStore(userSitePermissions.origin, private)
|
||||||
|
|
||||||
|
if (geckoPermissionsByType.isNotEmpty()) {
|
||||||
|
val geckoNotification = geckoPermissionsByType[PERMISSION_DESKTOP_NOTIFICATION]?.firstOrNull()
|
||||||
|
val geckoLocation = geckoPermissionsByType[PERMISSION_GEOLOCATION]?.firstOrNull()
|
||||||
|
val geckoMedia = geckoPermissionsByType[PERMISSION_MEDIA_KEY_SYSTEM_ACCESS]?.firstOrNull()
|
||||||
|
val geckoLocalStorage = geckoPermissionsByType[PERMISSION_PERSISTENT_STORAGE]?.firstOrNull()
|
||||||
|
val geckoCrossOriginStorageAccess = geckoPermissionsByType[PERMISSION_STORAGE_ACCESS]?.firstOrNull()
|
||||||
|
val geckoAudible = geckoPermissionsByType[PERMISSION_AUTOPLAY_AUDIBLE]?.firstOrNull()
|
||||||
|
val geckoInAudible = geckoPermissionsByType[PERMISSION_AUTOPLAY_INAUDIBLE]?.firstOrNull()
|
||||||
|
|
||||||
|
/*
|
||||||
|
* To avoid GeckoView caching previous request, we need to clear, previous data
|
||||||
|
* before updating. See: https://github.com/mozilla-mobile/android-components/issues/6322
|
||||||
|
*/
|
||||||
|
clearGeckoCacheFor(updatedPermission.origin)
|
||||||
|
|
||||||
|
if (geckoNotification != null) {
|
||||||
|
removeTemporaryPermissionIfAny(geckoNotification)
|
||||||
|
geckoStorage.setPermission(
|
||||||
|
geckoNotification,
|
||||||
|
userSitePermissions.notification.toGeckoStatus(),
|
||||||
|
)
|
||||||
|
updatedPermission = updatedPermission.copy(notification = NO_DECISION)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geckoLocation != null) {
|
||||||
|
removeTemporaryPermissionIfAny(geckoLocation)
|
||||||
|
geckoStorage.setPermission(
|
||||||
|
geckoLocation,
|
||||||
|
userSitePermissions.location.toGeckoStatus(),
|
||||||
|
)
|
||||||
|
updatedPermission = updatedPermission.copy(location = NO_DECISION)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geckoMedia != null) {
|
||||||
|
removeTemporaryPermissionIfAny(geckoMedia)
|
||||||
|
geckoStorage.setPermission(
|
||||||
|
geckoMedia,
|
||||||
|
userSitePermissions.mediaKeySystemAccess.toGeckoStatus(),
|
||||||
|
)
|
||||||
|
updatedPermission = updatedPermission.copy(mediaKeySystemAccess = NO_DECISION)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geckoLocalStorage != null) {
|
||||||
|
removeTemporaryPermissionIfAny(geckoLocalStorage)
|
||||||
|
geckoStorage.setPermission(
|
||||||
|
geckoLocalStorage,
|
||||||
|
userSitePermissions.localStorage.toGeckoStatus(),
|
||||||
|
)
|
||||||
|
updatedPermission = updatedPermission.copy(localStorage = NO_DECISION)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geckoCrossOriginStorageAccess != null) {
|
||||||
|
removeTemporaryPermissionIfAny(geckoCrossOriginStorageAccess)
|
||||||
|
geckoStorage.setPermission(
|
||||||
|
geckoCrossOriginStorageAccess,
|
||||||
|
userSitePermissions.crossOriginStorageAccess.toGeckoStatus(),
|
||||||
|
)
|
||||||
|
updatedPermission = updatedPermission.copy(crossOriginStorageAccess = NO_DECISION)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geckoAudible != null) {
|
||||||
|
removeTemporaryPermissionIfAny(geckoAudible)
|
||||||
|
geckoStorage.setPermission(
|
||||||
|
geckoAudible,
|
||||||
|
userSitePermissions.autoplayAudible.toGeckoStatus(),
|
||||||
|
)
|
||||||
|
updatedPermission = updatedPermission.copy(autoplayAudible = AutoplayStatus.BLOCKED)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geckoInAudible != null) {
|
||||||
|
removeTemporaryPermissionIfAny(geckoInAudible)
|
||||||
|
geckoStorage.setPermission(
|
||||||
|
geckoInAudible,
|
||||||
|
userSitePermissions.autoplayInaudible.toGeckoStatus(),
|
||||||
|
)
|
||||||
|
updatedPermission =
|
||||||
|
updatedPermission.copy(autoplayInaudible = AutoplayStatus.BLOCKED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updatedPermission
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines a permission that comes from our on disk storage with the gecko permissions,
|
||||||
|
* and combined both into a single a [SitePermissions].
|
||||||
|
* @param onDiskPermissions a permission from the on disk storage.
|
||||||
|
* @param geckoPermissionByType a list of all the gecko permissions mapped by permission type.
|
||||||
|
* @return a [SitePermissions] containing the values from the on disk and gecko permission.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
@Suppress("ComplexMethod")
|
||||||
|
internal fun mergePermissions(
|
||||||
|
onDiskPermissions: SitePermissions?,
|
||||||
|
geckoPermissionByType: Map<Int, List<ContentPermission>>,
|
||||||
|
): SitePermissions? {
|
||||||
|
var combinedPermissions = onDiskPermissions
|
||||||
|
|
||||||
|
if (geckoPermissionByType.isNotEmpty() && onDiskPermissions != null) {
|
||||||
|
val geckoNotification = geckoPermissionByType[PERMISSION_DESKTOP_NOTIFICATION]?.firstOrNull()
|
||||||
|
val geckoLocation = geckoPermissionByType[PERMISSION_GEOLOCATION]?.firstOrNull()
|
||||||
|
val geckoMedia = geckoPermissionByType[PERMISSION_MEDIA_KEY_SYSTEM_ACCESS]?.firstOrNull()
|
||||||
|
val geckoStorage = geckoPermissionByType[PERMISSION_PERSISTENT_STORAGE]?.firstOrNull()
|
||||||
|
// Currently we'll receive the "storage_access" permission for all iframes of the same parent
|
||||||
|
// so we need to ensure we are reporting the permission for the current iframe request.
|
||||||
|
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1746436 for more details.
|
||||||
|
val geckoCrossOriginStorageAccess = geckoPermissionByType[PERMISSION_STORAGE_ACCESS]?.firstOrNull {
|
||||||
|
it.thirdPartyOrigin == onDiskPermissions.origin.stripDefaultPort()
|
||||||
|
}
|
||||||
|
val geckoAudible = geckoPermissionByType[PERMISSION_AUTOPLAY_AUDIBLE]?.firstOrNull()
|
||||||
|
val geckoInAudible = geckoPermissionByType[PERMISSION_AUTOPLAY_INAUDIBLE]?.firstOrNull()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We only consider permissions from geckoView, when the values default value
|
||||||
|
* has been changed otherwise we favor the values [onDiskPermissions].
|
||||||
|
*/
|
||||||
|
if (geckoNotification != null && geckoNotification.value != VALUE_PROMPT) {
|
||||||
|
combinedPermissions = combinedPermissions?.copy(
|
||||||
|
notification = geckoNotification.value.toStatus(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geckoLocation != null && geckoLocation.value != VALUE_PROMPT) {
|
||||||
|
combinedPermissions = combinedPermissions?.copy(
|
||||||
|
location = geckoLocation.value.toStatus(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geckoMedia != null && geckoMedia.value != VALUE_PROMPT) {
|
||||||
|
combinedPermissions = combinedPermissions?.copy(
|
||||||
|
mediaKeySystemAccess = geckoMedia.value.toStatus(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geckoStorage != null && geckoStorage.value != VALUE_PROMPT) {
|
||||||
|
combinedPermissions = combinedPermissions?.copy(
|
||||||
|
localStorage = geckoStorage.value.toStatus(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geckoCrossOriginStorageAccess != null && geckoCrossOriginStorageAccess.value != VALUE_PROMPT) {
|
||||||
|
combinedPermissions = combinedPermissions?.copy(
|
||||||
|
crossOriginStorageAccess = geckoCrossOriginStorageAccess.value.toStatus(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Autoplay permissions don't have initial values, so when the value is changed on
|
||||||
|
* the gecko storage we trust it.
|
||||||
|
*/
|
||||||
|
if (geckoAudible != null && geckoAudible.value != VALUE_PROMPT) {
|
||||||
|
combinedPermissions = combinedPermissions?.copy(
|
||||||
|
autoplayAudible = geckoAudible.value.toAutoPlayStatus(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geckoInAudible != null && geckoInAudible.value != VALUE_PROMPT) {
|
||||||
|
combinedPermissions = combinedPermissions?.copy(
|
||||||
|
autoplayInaudible = geckoInAudible.value.toAutoPlayStatus(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return combinedPermissions
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal suspend fun findGeckoContentPermissionBy(
|
||||||
|
origin: String,
|
||||||
|
includeTemporary: Boolean = false,
|
||||||
|
private: Boolean,
|
||||||
|
): List<ContentPermission>? {
|
||||||
|
return withContext(mainScope.coroutineContext) {
|
||||||
|
val geckoPermissions = geckoStorage.getPermissions(origin, private).await()
|
||||||
|
if (includeTemporary) {
|
||||||
|
geckoPermissions
|
||||||
|
} else {
|
||||||
|
geckoPermissions.filterNotTemporaryPermissions(geckoTemporaryPermissions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal suspend fun clearGeckoCacheFor(origin: String) {
|
||||||
|
withContext(mainScope.coroutineContext) {
|
||||||
|
geckoStorage.clearDataFromHost(origin, ClearFlags.PERMISSIONS).await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun clearAllPermissionsGeckoCache() {
|
||||||
|
geckoStorage.clearData(ClearFlags.PERMISSIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun removeTemporaryPermissionIfAny(permission: ContentPermission) {
|
||||||
|
if (geckoTemporaryPermissions.any { permission.areSame(it) }) {
|
||||||
|
geckoTemporaryPermissions.removeAll { permission.areSame(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal suspend fun removeGeckoContentPermissionBy(origin: String, private: Boolean) {
|
||||||
|
findGeckoContentPermissionBy(
|
||||||
|
origin = origin,
|
||||||
|
private = private,
|
||||||
|
)?.forEach { geckoPermissions ->
|
||||||
|
removeGeckoContentPermission(geckoPermissions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun removeGeckoContentPermission(geckoPermissions: ContentPermission) {
|
||||||
|
val value = if (geckoPermissions.permission != PERMISSION_TRACKING) {
|
||||||
|
VALUE_PROMPT
|
||||||
|
} else {
|
||||||
|
VALUE_DENY
|
||||||
|
}
|
||||||
|
geckoStorage.setPermission(geckoPermissions, value)
|
||||||
|
removeTemporaryPermissionIfAny(geckoPermissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal suspend fun removeGeckoAllContentPermissions() {
|
||||||
|
findAllGeckoContentPermissions()?.forEach { geckoPermissions ->
|
||||||
|
removeGeckoContentPermission(geckoPermissions)
|
||||||
|
}
|
||||||
|
clearAllPermissionsGeckoCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun PermissionRequest?.extractGeckoPermissionsOrQueryTheStore(
|
||||||
|
origin: String,
|
||||||
|
private: Boolean,
|
||||||
|
): Map<Int, List<ContentPermission>> {
|
||||||
|
return if (this is GeckoPermissionRequest.Content) {
|
||||||
|
mapOf(geckoPermission.permission to listOf(geckoPermission))
|
||||||
|
} else {
|
||||||
|
findGeckoContentPermissionBy(origin, includeTemporary = true, private).groupByType()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun List<ContentPermission>?.groupByDomain(): Map<String, List<ContentPermission>> {
|
||||||
|
return this?.groupBy {
|
||||||
|
it.uri.tryGetHostFromUrl()
|
||||||
|
}.orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun List<ContentPermission>?.groupByType(): Map<Int, List<ContentPermission>> {
|
||||||
|
return this?.groupBy { it.permission }.orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun List<ContentPermission>?.filterNotTemporaryPermissions(
|
||||||
|
temporaryPermissions: List<ContentPermission>,
|
||||||
|
): List<ContentPermission>? {
|
||||||
|
return this?.filterNot { geckoPermission ->
|
||||||
|
temporaryPermissions.any { geckoPermission.areSame(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun ContentPermission.areSame(other: ContentPermission) =
|
||||||
|
other.uri.tryGetHostFromUrl() == this.uri.tryGetHostFromUrl() &&
|
||||||
|
other.permission == this.permission && other.privateMode == this.privateMode
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun Int.toStatus(): Status {
|
||||||
|
return when (this) {
|
||||||
|
VALUE_PROMPT -> NO_DECISION
|
||||||
|
VALUE_DENY -> BLOCKED
|
||||||
|
VALUE_ALLOW -> ALLOWED
|
||||||
|
else -> BLOCKED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun Int.toAutoPlayStatus(): AutoplayStatus {
|
||||||
|
return when (this) {
|
||||||
|
VALUE_PROMPT, VALUE_DENY -> AutoplayStatus.BLOCKED
|
||||||
|
VALUE_ALLOW -> AutoplayStatus.ALLOWED
|
||||||
|
else -> AutoplayStatus.BLOCKED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun Status.toGeckoStatus(): Int {
|
||||||
|
return when (this) {
|
||||||
|
NO_DECISION -> VALUE_PROMPT
|
||||||
|
BLOCKED -> VALUE_DENY
|
||||||
|
ALLOWED -> VALUE_ALLOW
|
||||||
|
else -> VALUE_ALLOW
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun AutoplayStatus.toGeckoStatus(): Int {
|
||||||
|
return when (this) {
|
||||||
|
AutoplayStatus.BLOCKED -> VALUE_DENY
|
||||||
|
AutoplayStatus.ALLOWED -> VALUE_ALLOW
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.profiler
|
||||||
|
|
||||||
|
import mozilla.components.concept.base.profiler.Profiler
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
import org.mozilla.geckoview.GeckoRuntime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gecko-based implementation of [Profiler], wrapping the
|
||||||
|
* ProfilerController object provided by GeckoView.
|
||||||
|
*/
|
||||||
|
class Profiler(
|
||||||
|
private val runtime: GeckoRuntime,
|
||||||
|
) : Profiler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [Profiler.isProfilerActive].
|
||||||
|
*/
|
||||||
|
override fun isProfilerActive(): Boolean {
|
||||||
|
return runtime.profilerController.isProfilerActive
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [Profiler.getProfilerTime].
|
||||||
|
*/
|
||||||
|
override fun getProfilerTime(): Double? {
|
||||||
|
return runtime.profilerController.profilerTime
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [Profiler.addMarker].
|
||||||
|
*/
|
||||||
|
override fun addMarker(markerName: String, startTime: Double?, endTime: Double?, text: String?) {
|
||||||
|
runtime.profilerController.addMarker(markerName, startTime, endTime, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [Profiler.addMarker].
|
||||||
|
*/
|
||||||
|
override fun addMarker(markerName: String, startTime: Double?, text: String?) {
|
||||||
|
runtime.profilerController.addMarker(markerName, startTime, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [Profiler.addMarker].
|
||||||
|
*/
|
||||||
|
override fun addMarker(markerName: String, startTime: Double?) {
|
||||||
|
runtime.profilerController.addMarker(markerName, startTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [Profiler.addMarker].
|
||||||
|
*/
|
||||||
|
override fun addMarker(markerName: String, text: String?) {
|
||||||
|
runtime.profilerController.addMarker(markerName, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [Profiler.addMarker].
|
||||||
|
*/
|
||||||
|
override fun addMarker(markerName: String) {
|
||||||
|
runtime.profilerController.addMarker(markerName)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startProfiler(filters: Array<String>, features: Array<String>) {
|
||||||
|
runtime.profilerController.startProfiler(filters, features)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stopProfiler(onSuccess: (ByteArray?) -> Unit, onError: (Throwable) -> Unit) {
|
||||||
|
runtime.profilerController.stopProfiler().then(
|
||||||
|
{ profileResult ->
|
||||||
|
onSuccess(profileResult)
|
||||||
|
GeckoResult<Void>()
|
||||||
|
},
|
||||||
|
{ throwable ->
|
||||||
|
onError(throwable)
|
||||||
|
GeckoResult<Void>()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
/* 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.prompt
|
||||||
|
|
||||||
|
import mozilla.components.browser.engine.gecko.GeckoEngineSession
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.convertToChoices
|
||||||
|
import mozilla.components.concept.engine.prompt.PromptRequest
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.BasePrompt
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.ChoicePrompt
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.PromptInstanceDelegate
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of [PromptInstanceDelegate] used to update a
|
||||||
|
* prompt request when onPromptUpdate is invoked.
|
||||||
|
*
|
||||||
|
* @param geckoSession [GeckoEngineSession] used to notify the engine observer
|
||||||
|
* with the onPromptUpdate callback.
|
||||||
|
* @param previousPrompt [PromptRequest] to be updated.
|
||||||
|
*/
|
||||||
|
internal class ChoicePromptDelegate(
|
||||||
|
private val geckoSession: GeckoEngineSession,
|
||||||
|
private var previousPrompt: PromptRequest,
|
||||||
|
) : PromptInstanceDelegate {
|
||||||
|
|
||||||
|
override fun onPromptDismiss(prompt: BasePrompt) {
|
||||||
|
geckoSession.notifyObservers {
|
||||||
|
onPromptDismissed(previousPrompt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPromptUpdate(prompt: BasePrompt) {
|
||||||
|
if (prompt is ChoicePrompt) {
|
||||||
|
val promptRequest = updatePromptChoices(prompt)
|
||||||
|
if (promptRequest != null) {
|
||||||
|
geckoSession.notifyObservers {
|
||||||
|
this.onPromptUpdate(previousPrompt.uid, promptRequest)
|
||||||
|
}
|
||||||
|
previousPrompt = promptRequest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the received prompt to create the updated [PromptRequest]
|
||||||
|
* @param updatedPrompt The [ChoicePrompt] with the updated choices.
|
||||||
|
*/
|
||||||
|
private fun updatePromptChoices(updatedPrompt: ChoicePrompt): PromptRequest? {
|
||||||
|
return when (previousPrompt) {
|
||||||
|
is PromptRequest.MenuChoice -> {
|
||||||
|
(previousPrompt as PromptRequest.MenuChoice)
|
||||||
|
.copy(choices = convertToChoices(updatedPrompt.choices))
|
||||||
|
}
|
||||||
|
is PromptRequest.SingleChoice -> {
|
||||||
|
(previousPrompt as PromptRequest.SingleChoice)
|
||||||
|
.copy(choices = convertToChoices(updatedPrompt.choices))
|
||||||
|
}
|
||||||
|
is PromptRequest.MultipleChoice -> {
|
||||||
|
(previousPrompt as PromptRequest.MultipleChoice)
|
||||||
|
.copy(choices = convertToChoices(updatedPrompt.choices))
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,914 @@
|
||||||
|
/* 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.prompt
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import mozilla.components.browser.engine.gecko.GeckoEngineSession
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.convertToChoices
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.toAddress
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.toAutocompleteAddress
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.toAutocompleteCreditCard
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.toCreditCardEntry
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.toLoginEntry
|
||||||
|
import mozilla.components.concept.engine.prompt.Choice
|
||||||
|
import mozilla.components.concept.engine.prompt.PromptRequest
|
||||||
|
import mozilla.components.concept.engine.prompt.PromptRequest.File.Companion.DEFAULT_UPLOADS_DIR_NAME
|
||||||
|
import mozilla.components.concept.engine.prompt.PromptRequest.MenuChoice
|
||||||
|
import mozilla.components.concept.engine.prompt.PromptRequest.MultipleChoice
|
||||||
|
import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice
|
||||||
|
import mozilla.components.concept.engine.prompt.ShareData
|
||||||
|
import mozilla.components.concept.identitycredential.Account
|
||||||
|
import mozilla.components.concept.identitycredential.Provider
|
||||||
|
import mozilla.components.concept.storage.Address
|
||||||
|
import mozilla.components.concept.storage.CreditCardEntry
|
||||||
|
import mozilla.components.concept.storage.Login
|
||||||
|
import mozilla.components.concept.storage.LoginEntry
|
||||||
|
import mozilla.components.support.ktx.android.net.toFileUri
|
||||||
|
import mozilla.components.support.ktx.kotlin.toDate
|
||||||
|
import mozilla.components.support.utils.TimePicker.shouldShowMillisecondsPicker
|
||||||
|
import mozilla.components.support.utils.TimePicker.shouldShowSecondsPicker
|
||||||
|
import org.mozilla.geckoview.AllowOrDeny
|
||||||
|
import org.mozilla.geckoview.Autocomplete
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
import org.mozilla.geckoview.GeckoSession
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.AutocompleteRequest
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.BeforeUnloadPrompt
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.DATE
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.DATETIME_LOCAL
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.MONTH
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.TIME
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.WEEK
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.IdentityCredential.AccountSelectorPrompt
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.IdentityCredential.PrivacyPolicyPrompt
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.IdentityCredential.ProviderSelectorPrompt
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.PromptResponse
|
||||||
|
import java.security.InvalidParameterException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
typealias GeckoAuthOptions = PromptDelegate.AuthPrompt.AuthOptions
|
||||||
|
typealias GeckoChoice = PromptDelegate.ChoicePrompt.Choice
|
||||||
|
typealias GECKO_AUTH_FLAGS = PromptDelegate.AuthPrompt.AuthOptions.Flags
|
||||||
|
typealias GECKO_AUTH_LEVEL = PromptDelegate.AuthPrompt.AuthOptions.Level
|
||||||
|
typealias GECKO_PROMPT_FILE_TYPE = PromptDelegate.FilePrompt.Type
|
||||||
|
typealias GECKO_PROMPT_PROVIDER_SELECTOR = ProviderSelectorPrompt.Provider
|
||||||
|
typealias GECKO_PROMPT_ACCOUNT_SELECTOR = AccountSelectorPrompt.Account
|
||||||
|
typealias GECKO_PROMPT_ACCOUNT_SELECTOR_PROVIDER = AccountSelectorPrompt.Provider
|
||||||
|
typealias GECKO_PROMPT_CHOICE_TYPE = PromptDelegate.ChoicePrompt.Type
|
||||||
|
typealias GECKO_PROMPT_FILE_CAPTURE = PromptDelegate.FilePrompt.Capture
|
||||||
|
typealias GECKO_PROMPT_SHARE_RESULT = PromptDelegate.SharePrompt.Result
|
||||||
|
typealias AC_AUTH_LEVEL = PromptRequest.Authentication.Level
|
||||||
|
typealias AC_AUTH_METHOD = PromptRequest.Authentication.Method
|
||||||
|
typealias AC_FILE_FACING_MODE = PromptRequest.File.FacingMode
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gecko-based PromptDelegate implementation.
|
||||||
|
*/
|
||||||
|
@Suppress("LargeClass")
|
||||||
|
internal class GeckoPromptDelegate(private val geckoEngineSession: GeckoEngineSession) :
|
||||||
|
PromptDelegate {
|
||||||
|
override fun onSelectIdentityCredentialProvider(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: ProviderSelectorPrompt,
|
||||||
|
): GeckoResult<PromptResponse> {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
|
||||||
|
val onConfirm: (Provider) -> Unit = { provider ->
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(
|
||||||
|
prompt.confirm(
|
||||||
|
provider.id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onDismiss: () -> Unit = {
|
||||||
|
prompt.dismissSafely(geckoResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.IdentityCredential.SelectProvider(
|
||||||
|
providers = prompt.providers.map { it.toProvider() },
|
||||||
|
onConfirm = onConfirm,
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSelectIdentityCredentialAccount(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: AccountSelectorPrompt,
|
||||||
|
): GeckoResult<PromptResponse> {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
|
||||||
|
val onConfirm: (Account) -> Unit = { account ->
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(
|
||||||
|
prompt.confirm(
|
||||||
|
account.id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onDismiss: () -> Unit = {
|
||||||
|
prompt.dismissSafely(geckoResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.IdentityCredential.SelectAccount(
|
||||||
|
accounts = prompt.accounts.map { it.toAccount() },
|
||||||
|
provider = prompt.provider.let { it.toProvider() },
|
||||||
|
onConfirm = onConfirm,
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShowPrivacyPolicyIdentityCredential(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: PrivacyPolicyPrompt,
|
||||||
|
): GeckoResult<PromptResponse> {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
|
||||||
|
val onConfirm: (Boolean) -> Unit = { confirmed ->
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(
|
||||||
|
prompt.confirm(confirmed),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onDismiss: () -> Unit = {
|
||||||
|
prompt.dismissSafely(geckoResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.IdentityCredential.PrivacyPolicy(
|
||||||
|
privacyPolicyUrl = prompt.privacyPolicyUrl,
|
||||||
|
termsOfServiceUrl = prompt.termsOfServiceUrl,
|
||||||
|
providerDomain = prompt.providerDomain,
|
||||||
|
host = prompt.host,
|
||||||
|
icon = prompt.icon,
|
||||||
|
onConfirm = onConfirm,
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreditCardSave(
|
||||||
|
session: GeckoSession,
|
||||||
|
request: AutocompleteRequest<Autocomplete.CreditCardSaveOption>,
|
||||||
|
): GeckoResult<PromptResponse> {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
|
||||||
|
val onConfirm: (CreditCardEntry) -> Unit = { creditCard ->
|
||||||
|
if (!request.isComplete) {
|
||||||
|
geckoResult.complete(
|
||||||
|
request.confirm(
|
||||||
|
Autocomplete.CreditCardSelectOption(creditCard.toAutocompleteCreditCard()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onDismiss: () -> Unit = {
|
||||||
|
request.dismissSafely(geckoResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.SaveCreditCard(
|
||||||
|
creditCard = request.options[0].value.toCreditCardEntry(),
|
||||||
|
onConfirm = onConfirm,
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
).also {
|
||||||
|
request.delegate = PromptInstanceDismissDelegate(
|
||||||
|
geckoEngineSession,
|
||||||
|
it,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a credit card selection prompt request. This is triggered by the user
|
||||||
|
* focusing on a credit card input field.
|
||||||
|
*
|
||||||
|
* @param session The [GeckoSession] that triggered the request.
|
||||||
|
* @param request The [AutocompleteRequest] containing the credit card selection request.
|
||||||
|
*/
|
||||||
|
override fun onCreditCardSelect(
|
||||||
|
session: GeckoSession,
|
||||||
|
request: AutocompleteRequest<Autocomplete.CreditCardSelectOption>,
|
||||||
|
): GeckoResult<PromptResponse>? {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
|
||||||
|
val onConfirm: (CreditCardEntry) -> Unit = { creditCard ->
|
||||||
|
if (!request.isComplete) {
|
||||||
|
geckoResult.complete(
|
||||||
|
request.confirm(
|
||||||
|
Autocomplete.CreditCardSelectOption(creditCard.toAutocompleteCreditCard()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onDismiss: () -> Unit = {
|
||||||
|
request.dismissSafely(geckoResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.SelectCreditCard(
|
||||||
|
creditCards = request.options.map { it.value.toCreditCardEntry() },
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
onConfirm = onConfirm,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoginSave(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: AutocompleteRequest<Autocomplete.LoginSaveOption>,
|
||||||
|
): GeckoResult<PromptResponse>? {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
val onConfirmSave: (LoginEntry) -> Unit = { entry ->
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(Autocomplete.LoginSelectOption(entry.toLoginEntry())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val onDismiss: () -> Unit = {
|
||||||
|
prompt.dismissSafely(geckoResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.SaveLoginPrompt(
|
||||||
|
hint = prompt.options[0].hint,
|
||||||
|
logins = prompt.options.map { it.value.toLoginEntry() },
|
||||||
|
onConfirm = onConfirmSave,
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
).also {
|
||||||
|
prompt.delegate = PromptInstanceDismissDelegate(
|
||||||
|
geckoEngineSession,
|
||||||
|
it,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
override fun onLoginSelect(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: AutocompleteRequest<Autocomplete.LoginSelectOption>,
|
||||||
|
): GeckoResult<PromptResponse>? {
|
||||||
|
val promptOptions = prompt.options
|
||||||
|
|
||||||
|
val generatedPassword =
|
||||||
|
promptOptions.firstOrNull { option -> option.hint == Autocomplete.SelectOption.Hint.GENERATED }?.value?.password
|
||||||
|
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
val onConfirmSelect: (Login) -> Unit = { login ->
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
var hint = Autocomplete.SelectOption.Hint.NONE
|
||||||
|
if (generatedPassword != null && login.password == generatedPassword) {
|
||||||
|
hint = Autocomplete.SelectOption.Hint.GENERATED
|
||||||
|
}
|
||||||
|
geckoResult.complete(prompt.confirm(Autocomplete.LoginSelectOption(login.toLoginEntry(), hint)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val onDismiss: () -> Unit = {
|
||||||
|
prompt.dismissSafely(geckoResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
// `guid` plus exactly one of `httpRealm` and `formSubmitURL` must be present to be a valid login entry.
|
||||||
|
val loginList = promptOptions.filter { option ->
|
||||||
|
option.value.guid != null && (option.value.formActionOrigin != null || option.value.httpRealm != null)
|
||||||
|
}.map { option ->
|
||||||
|
Login(
|
||||||
|
guid = option.value.guid!!,
|
||||||
|
origin = option.value.origin,
|
||||||
|
formActionOrigin = option.value.formActionOrigin,
|
||||||
|
httpRealm = option.value.httpRealm,
|
||||||
|
username = option.value.username,
|
||||||
|
password = option.value.password,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.SelectLoginPrompt(
|
||||||
|
logins = loginList,
|
||||||
|
generatedPassword = generatedPassword,
|
||||||
|
onConfirm = onConfirmSelect,
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onChoicePrompt(
|
||||||
|
session: GeckoSession,
|
||||||
|
geckoPrompt: PromptDelegate.ChoicePrompt,
|
||||||
|
): GeckoResult<PromptResponse>? {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
val choices = convertToChoices(geckoPrompt.choices)
|
||||||
|
|
||||||
|
val onDismiss: () -> Unit = {
|
||||||
|
geckoPrompt.dismissSafely(geckoResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
val onConfirmSingleChoice: (Choice) -> Unit = { selectedChoice ->
|
||||||
|
if (!geckoPrompt.isComplete) {
|
||||||
|
geckoResult.complete(geckoPrompt.confirm(selectedChoice.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val onConfirmMultipleSelection: (Array<Choice>) -> Unit = { selectedChoices ->
|
||||||
|
if (!geckoPrompt.isComplete) {
|
||||||
|
val ids = selectedChoices.toIdsArray()
|
||||||
|
geckoResult.complete(geckoPrompt.confirm(ids))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val promptRequest = when (geckoPrompt.type) {
|
||||||
|
GECKO_PROMPT_CHOICE_TYPE.SINGLE -> SingleChoice(
|
||||||
|
choices,
|
||||||
|
onConfirmSingleChoice,
|
||||||
|
onDismiss,
|
||||||
|
)
|
||||||
|
GECKO_PROMPT_CHOICE_TYPE.MENU -> MenuChoice(
|
||||||
|
choices,
|
||||||
|
onConfirmSingleChoice,
|
||||||
|
onDismiss,
|
||||||
|
)
|
||||||
|
GECKO_PROMPT_CHOICE_TYPE.MULTIPLE -> MultipleChoice(
|
||||||
|
choices,
|
||||||
|
onConfirmMultipleSelection,
|
||||||
|
onDismiss,
|
||||||
|
)
|
||||||
|
else -> throw InvalidParameterException("${geckoPrompt.type} is not a valid Gecko @Choice.ChoiceType")
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoPrompt.delegate = ChoicePromptDelegate(
|
||||||
|
geckoEngineSession,
|
||||||
|
promptRequest,
|
||||||
|
)
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(promptRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAddressSelect(
|
||||||
|
session: GeckoSession,
|
||||||
|
request: AutocompleteRequest<Autocomplete.AddressSelectOption>,
|
||||||
|
): GeckoResult<PromptResponse> {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
|
||||||
|
val onConfirm: (Address) -> Unit = { address ->
|
||||||
|
if (!request.isComplete) {
|
||||||
|
geckoResult.complete(
|
||||||
|
request.confirm(
|
||||||
|
Autocomplete.AddressSelectOption(address.toAutocompleteAddress()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onDismiss: () -> Unit = {
|
||||||
|
request.dismissSafely(geckoResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.SelectAddress(
|
||||||
|
addresses = request.options.map { it.value.toAddress() },
|
||||||
|
onConfirm = onConfirm,
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAlertPrompt(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: PromptDelegate.AlertPrompt,
|
||||||
|
): GeckoResult<PromptResponse> {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
val onDismiss: () -> Unit = { prompt.dismissSafely(geckoResult) }
|
||||||
|
val onConfirm: (Boolean) -> Unit = { _ -> onDismiss() }
|
||||||
|
val title = prompt.title ?: ""
|
||||||
|
val message = prompt.message ?: ""
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.Alert(
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
false,
|
||||||
|
onConfirm,
|
||||||
|
onDismiss,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFilePrompt(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: PromptDelegate.FilePrompt,
|
||||||
|
): GeckoResult<PromptResponse>? {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
val isMultipleFilesSelection = prompt.type == GECKO_PROMPT_FILE_TYPE.MULTIPLE
|
||||||
|
|
||||||
|
val captureMode = when (prompt.capture) {
|
||||||
|
GECKO_PROMPT_FILE_CAPTURE.ANY -> AC_FILE_FACING_MODE.ANY
|
||||||
|
GECKO_PROMPT_FILE_CAPTURE.USER -> AC_FILE_FACING_MODE.FRONT_CAMERA
|
||||||
|
GECKO_PROMPT_FILE_CAPTURE.ENVIRONMENT -> AC_FILE_FACING_MODE.BACK_CAMERA
|
||||||
|
else -> AC_FILE_FACING_MODE.NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
val onSelectMultiple: (Context, Array<Uri>) -> Unit = { context, uris ->
|
||||||
|
val filesUris = uris.map {
|
||||||
|
toFileUri(uri = it, context)
|
||||||
|
}.toTypedArray()
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(context, filesUris))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onSelectSingle: (Context, Uri) -> Unit = { context, uri ->
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(context, toFileUri(uri, context)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onDismiss: () -> Unit = {
|
||||||
|
prompt.dismissSafely(geckoResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.File(
|
||||||
|
prompt.mimeTypes ?: emptyArray(),
|
||||||
|
isMultipleFilesSelection,
|
||||||
|
captureMode,
|
||||||
|
onSelectSingle,
|
||||||
|
onSelectMultiple,
|
||||||
|
onDismiss,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("ComplexMethod")
|
||||||
|
override fun onDateTimePrompt(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: PromptDelegate.DateTimePrompt,
|
||||||
|
): GeckoResult<PromptResponse>? {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
val onConfirm: (String) -> Unit = {
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onDismiss: () -> Unit = {
|
||||||
|
prompt.dismissSafely(geckoResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
val onClear: () -> Unit = {
|
||||||
|
onConfirm("")
|
||||||
|
}
|
||||||
|
val initialDateString = prompt.defaultValue ?: ""
|
||||||
|
val stepValue = with(prompt.stepValue) {
|
||||||
|
if (this?.toDoubleOrNull() == null) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val format = when (prompt.type) {
|
||||||
|
DATE -> "yyyy-MM-dd"
|
||||||
|
MONTH -> "yyyy-MM"
|
||||||
|
WEEK -> "yyyy-'W'ww"
|
||||||
|
TIME -> {
|
||||||
|
if (shouldShowMillisecondsPicker(stepValue?.toFloat())) {
|
||||||
|
"HH:mm:ss.SSS"
|
||||||
|
} else if (shouldShowSecondsPicker(stepValue?.toFloat())) {
|
||||||
|
"HH:mm:ss"
|
||||||
|
} else {
|
||||||
|
"HH:mm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DATETIME_LOCAL -> "yyyy-MM-dd'T'HH:mm"
|
||||||
|
else -> {
|
||||||
|
throw InvalidParameterException("${prompt.type} is not a valid DatetimeType")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyDatePromptRequest(
|
||||||
|
prompt.title ?: "",
|
||||||
|
initialDateString,
|
||||||
|
prompt.minValue,
|
||||||
|
prompt.maxValue,
|
||||||
|
stepValue,
|
||||||
|
onClear,
|
||||||
|
format,
|
||||||
|
onConfirm,
|
||||||
|
onDismiss,
|
||||||
|
)
|
||||||
|
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthPrompt(
|
||||||
|
session: GeckoSession,
|
||||||
|
geckoPrompt: PromptDelegate.AuthPrompt,
|
||||||
|
): GeckoResult<PromptResponse>? {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
val title = geckoPrompt.title ?: ""
|
||||||
|
val message = geckoPrompt.message ?: ""
|
||||||
|
val uri = geckoPrompt.authOptions.uri
|
||||||
|
val flags = geckoPrompt.authOptions.flags
|
||||||
|
val userName = geckoPrompt.authOptions.username ?: ""
|
||||||
|
val password = geckoPrompt.authOptions.password ?: ""
|
||||||
|
val method =
|
||||||
|
if (flags in GECKO_AUTH_FLAGS.HOST) AC_AUTH_METHOD.HOST else AC_AUTH_METHOD.PROXY
|
||||||
|
val level = geckoPrompt.authOptions.toACLevel()
|
||||||
|
val onlyShowPassword = flags in GECKO_AUTH_FLAGS.ONLY_PASSWORD
|
||||||
|
val previousFailed = flags in GECKO_AUTH_FLAGS.PREVIOUS_FAILED
|
||||||
|
val isCrossOrigin = flags in GECKO_AUTH_FLAGS.CROSS_ORIGIN_SUB_RESOURCE
|
||||||
|
|
||||||
|
val onConfirm: (String, String) -> Unit =
|
||||||
|
{ user, pass ->
|
||||||
|
if (!geckoPrompt.isComplete) {
|
||||||
|
if (onlyShowPassword) {
|
||||||
|
geckoResult.complete(geckoPrompt.confirm(pass))
|
||||||
|
} else {
|
||||||
|
geckoResult.complete(geckoPrompt.confirm(user, pass))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onDismiss: () -> Unit = { geckoPrompt.dismissSafely(geckoResult) }
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.Authentication(
|
||||||
|
uri,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
userName,
|
||||||
|
password,
|
||||||
|
method,
|
||||||
|
level,
|
||||||
|
onlyShowPassword,
|
||||||
|
previousFailed,
|
||||||
|
isCrossOrigin,
|
||||||
|
onConfirm,
|
||||||
|
onDismiss,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTextPrompt(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: PromptDelegate.TextPrompt,
|
||||||
|
): GeckoResult<PromptResponse>? {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
val title = prompt.title ?: ""
|
||||||
|
val inputLabel = prompt.message ?: ""
|
||||||
|
val inputValue = prompt.defaultValue ?: ""
|
||||||
|
val onDismiss: () -> Unit = { prompt.dismissSafely(geckoResult) }
|
||||||
|
val onConfirm: (Boolean, String) -> Unit = { _, valueInput ->
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(valueInput))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.TextPrompt(
|
||||||
|
title,
|
||||||
|
inputLabel,
|
||||||
|
inputValue,
|
||||||
|
false,
|
||||||
|
onConfirm,
|
||||||
|
onDismiss,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onColorPrompt(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: PromptDelegate.ColorPrompt,
|
||||||
|
): GeckoResult<PromptResponse>? {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
val onConfirm: (String) -> Unit = {
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val onDismiss: () -> Unit = { prompt.dismissSafely(geckoResult) }
|
||||||
|
|
||||||
|
val defaultColor = prompt.defaultValue ?: ""
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.Color(defaultColor, onConfirm, onDismiss),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPopupPrompt(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: PromptDelegate.PopupPrompt,
|
||||||
|
): GeckoResult<PromptResponse> {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
val onAllow: () -> Unit = {
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(AllowOrDeny.ALLOW))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val onDeny: () -> Unit = {
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(AllowOrDeny.DENY))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.Popup(prompt.targetUri ?: "", onAllow, onDeny),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBeforeUnloadPrompt(
|
||||||
|
session: GeckoSession,
|
||||||
|
geckoPrompt: BeforeUnloadPrompt,
|
||||||
|
): GeckoResult<PromptResponse>? {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
val title = geckoPrompt.title ?: ""
|
||||||
|
val onAllow: () -> Unit = {
|
||||||
|
if (!geckoPrompt.isComplete) {
|
||||||
|
geckoResult.complete(geckoPrompt.confirm(AllowOrDeny.ALLOW))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val onDeny: () -> Unit = {
|
||||||
|
if (!geckoPrompt.isComplete) {
|
||||||
|
geckoResult.complete(geckoPrompt.confirm(AllowOrDeny.DENY))
|
||||||
|
geckoEngineSession.notifyObservers { onBeforeUnloadPromptDenied() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(PromptRequest.BeforeUnload(title, onAllow, onDeny))
|
||||||
|
}
|
||||||
|
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSharePrompt(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: PromptDelegate.SharePrompt,
|
||||||
|
): GeckoResult<PromptResponse> {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
val onSuccess = {
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(GECKO_PROMPT_SHARE_RESULT.SUCCESS))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val onFailure = {
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(GECKO_PROMPT_SHARE_RESULT.FAILURE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val onDismiss = { prompt.dismissSafely(geckoResult) }
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.Share(
|
||||||
|
ShareData(
|
||||||
|
title = prompt.title,
|
||||||
|
text = prompt.text,
|
||||||
|
url = prompt.uri,
|
||||||
|
),
|
||||||
|
onSuccess,
|
||||||
|
onFailure,
|
||||||
|
onDismiss,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onButtonPrompt(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: PromptDelegate.ButtonPrompt,
|
||||||
|
): GeckoResult<PromptResponse>? {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
val title = prompt.title ?: ""
|
||||||
|
val message = prompt.message ?: ""
|
||||||
|
|
||||||
|
val onConfirmPositiveButton: (Boolean) -> Unit = {
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(PromptDelegate.ButtonPrompt.Type.POSITIVE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val onConfirmNegativeButton: (Boolean) -> Unit = {
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(PromptDelegate.ButtonPrompt.Type.NEGATIVE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onDismiss: (Boolean) -> Unit = { prompt.dismissSafely(geckoResult) }
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.Confirm(
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
onConfirmPositiveButton,
|
||||||
|
onConfirmNegativeButton,
|
||||||
|
onDismiss,
|
||||||
|
) {
|
||||||
|
onDismiss(false)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRepostConfirmPrompt(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: PromptDelegate.RepostConfirmPrompt,
|
||||||
|
): GeckoResult<PromptResponse>? {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
|
||||||
|
val onConfirm: () -> Unit = {
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(AllowOrDeny.ALLOW))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val onCancel: () -> Unit = {
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(AllowOrDeny.DENY))
|
||||||
|
geckoEngineSession.notifyObservers { onRepostPromptCancelled() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.Repost(
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
private fun notifyDatePromptRequest(
|
||||||
|
title: String,
|
||||||
|
initialDateString: String,
|
||||||
|
minDateString: String?,
|
||||||
|
maxDateString: String?,
|
||||||
|
stepValue: String?,
|
||||||
|
onClear: () -> Unit,
|
||||||
|
format: String,
|
||||||
|
onConfirm: (String) -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
val initialDate = initialDateString.toDate(format)
|
||||||
|
val minDate = if (minDateString.isNullOrEmpty()) null else minDateString.toDate()
|
||||||
|
val maxDate = if (maxDateString.isNullOrEmpty()) null else maxDateString.toDate()
|
||||||
|
val onSelect: (Date) -> Unit = {
|
||||||
|
val stringDate = it.toString(format)
|
||||||
|
onConfirm(stringDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
val selectionType = when (format) {
|
||||||
|
"HH:mm", "HH:mm:ss", "HH:mm:ss.SSS" -> PromptRequest.TimeSelection.Type.TIME
|
||||||
|
"yyyy-MM" -> PromptRequest.TimeSelection.Type.MONTH
|
||||||
|
"yyyy-MM-dd'T'HH:mm" -> PromptRequest.TimeSelection.Type.DATE_AND_TIME
|
||||||
|
else -> PromptRequest.TimeSelection.Type.DATE
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.TimeSelection(
|
||||||
|
title,
|
||||||
|
initialDate,
|
||||||
|
minDate,
|
||||||
|
maxDate,
|
||||||
|
stepValue,
|
||||||
|
selectionType,
|
||||||
|
onSelect,
|
||||||
|
onClear,
|
||||||
|
onDismiss,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun GeckoAuthOptions.toACLevel(): AC_AUTH_LEVEL {
|
||||||
|
return when (level) {
|
||||||
|
GECKO_AUTH_LEVEL.NONE -> AC_AUTH_LEVEL.NONE
|
||||||
|
GECKO_AUTH_LEVEL.PW_ENCRYPTED -> AC_AUTH_LEVEL.PASSWORD_ENCRYPTED
|
||||||
|
GECKO_AUTH_LEVEL.SECURE -> AC_AUTH_LEVEL.SECURED
|
||||||
|
else -> {
|
||||||
|
AC_AUTH_LEVEL.NONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private operator fun Int.contains(mask: Int): Boolean {
|
||||||
|
return (this and mask) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun toFileUri(uri: Uri, context: Context): Uri {
|
||||||
|
return uri.toFileUri(context, dirToCopy = DEFAULT_UPLOADS_DIR_NAME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal fun Array<Choice>.toIdsArray(): Array<String> {
|
||||||
|
return this.map { it.id }.toTypedArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal fun Date.toString(format: String): String {
|
||||||
|
val formatter = SimpleDateFormat(format, Locale.ROOT)
|
||||||
|
return formatter.format(this) ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only dismiss if the prompt is not already dismissed.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal fun PromptDelegate.BasePrompt.dismissSafely(geckoResult: GeckoResult<PromptResponse>) {
|
||||||
|
if (!this.isComplete) {
|
||||||
|
geckoResult.complete(dismiss())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal fun GECKO_PROMPT_PROVIDER_SELECTOR.toProvider(): Provider {
|
||||||
|
return Provider(id, icon, name, domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal fun GECKO_PROMPT_ACCOUNT_SELECTOR.toAccount(): Account {
|
||||||
|
return Account(id, email, name, icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal fun GECKO_PROMPT_ACCOUNT_SELECTOR_PROVIDER.toProvider(): Provider {
|
||||||
|
return Provider(0, icon, name, domain)
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.prompt
|
||||||
|
|
||||||
|
import mozilla.components.browser.engine.gecko.GeckoEngineSession
|
||||||
|
import mozilla.components.concept.engine.prompt.PromptRequest
|
||||||
|
import org.mozilla.geckoview.GeckoSession
|
||||||
|
|
||||||
|
internal class PromptInstanceDismissDelegate(
|
||||||
|
private val geckoSession: GeckoEngineSession,
|
||||||
|
private val promptRequest: PromptRequest,
|
||||||
|
) : GeckoSession.PromptDelegate.PromptInstanceDelegate {
|
||||||
|
|
||||||
|
override fun onPromptDismiss(prompt: GeckoSession.PromptDelegate.BasePrompt) {
|
||||||
|
geckoSession.notifyObservers {
|
||||||
|
onPromptDismissed(promptRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.selection
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.MenuItem
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import mozilla.components.concept.engine.selection.SelectionActionDelegate
|
||||||
|
import org.mozilla.geckoview.BasicSelectionActionDelegate
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An adapter between the GV [BasicSelectionActionDelegate] and a generic [SelectionActionDelegate].
|
||||||
|
*
|
||||||
|
* @param customDelegate handles as much of this logic as possible.
|
||||||
|
*/
|
||||||
|
open class GeckoSelectionActionDelegate(
|
||||||
|
activity: Activity,
|
||||||
|
@get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal val customDelegate: SelectionActionDelegate,
|
||||||
|
) : BasicSelectionActionDelegate(activity) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* @returns a [GeckoSelectionActionDelegate] if [customDelegate] is non-null and [context]
|
||||||
|
* is an instance of [Activity]. Otherwise, returns null.
|
||||||
|
*/
|
||||||
|
fun maybeCreate(context: Context, customDelegate: SelectionActionDelegate?): GeckoSelectionActionDelegate? {
|
||||||
|
return if (context is Activity && customDelegate != null) {
|
||||||
|
GeckoSelectionActionDelegate(context, customDelegate)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAllActions(): Array<String> {
|
||||||
|
return customDelegate.sortedActions(super.getAllActions() + customDelegate.getAllActions())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isActionAvailable(id: String): Boolean {
|
||||||
|
val selectedText = mSelection?.text
|
||||||
|
|
||||||
|
val customActionIsAvailable = !selectedText.isNullOrEmpty() &&
|
||||||
|
customDelegate.isActionAvailable(id, selectedText)
|
||||||
|
|
||||||
|
return customActionIsAvailable ||
|
||||||
|
super.isActionAvailable(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun prepareAction(id: String, item: MenuItem) {
|
||||||
|
val title = customDelegate.getActionTitle(id)
|
||||||
|
?: return super.prepareAction(id, item)
|
||||||
|
|
||||||
|
item.title = title
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun performAction(id: String, item: MenuItem): Boolean {
|
||||||
|
/* Temporary, removed once https://bugzilla.mozilla.org/show_bug.cgi?id=1694983 is fixed */
|
||||||
|
try {
|
||||||
|
val selectedText = mSelection?.text ?: return super.performAction(id, item)
|
||||||
|
|
||||||
|
return customDelegate.performAction(id, selectedText) || super.performAction(id, item)
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.serviceworker
|
||||||
|
|
||||||
|
import mozilla.components.browser.engine.gecko.GeckoEngineSession
|
||||||
|
import mozilla.components.concept.engine.EngineSession
|
||||||
|
import mozilla.components.concept.engine.Settings
|
||||||
|
import mozilla.components.concept.engine.serviceworker.ServiceWorkerDelegate
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
import org.mozilla.geckoview.GeckoRuntime
|
||||||
|
import org.mozilla.geckoview.GeckoSession
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation for supporting Gecko service workers.
|
||||||
|
*
|
||||||
|
* @param delegate [ServiceWorkerDelegate] handling service workers requests.
|
||||||
|
* @param runtime [GeckoRuntime] current engine's runtime.
|
||||||
|
* @param engineSettings [Settings] default settings used when new [EngineSession]s are to be created.
|
||||||
|
*/
|
||||||
|
class GeckoServiceWorkerDelegate(
|
||||||
|
internal val delegate: ServiceWorkerDelegate,
|
||||||
|
internal val runtime: GeckoRuntime,
|
||||||
|
internal val engineSettings: Settings?,
|
||||||
|
) : GeckoRuntime.ServiceWorkerDelegate {
|
||||||
|
override fun onOpenWindow(url: String): GeckoResult<GeckoSession> {
|
||||||
|
val newEngineSession = GeckoEngineSession(runtime, false, engineSettings, openGeckoSession = false)
|
||||||
|
|
||||||
|
return when (delegate.addNewTab(newEngineSession)) {
|
||||||
|
true -> GeckoResult.fromValue(newEngineSession.geckoSession)
|
||||||
|
false -> GeckoResult.fromValue(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
/* 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.translate
|
||||||
|
|
||||||
|
import mozilla.components.browser.engine.gecko.GeckoEngineSession
|
||||||
|
import mozilla.components.concept.engine.translate.DetectedLanguages
|
||||||
|
import mozilla.components.concept.engine.translate.TranslationEngineState
|
||||||
|
import mozilla.components.concept.engine.translate.TranslationPair
|
||||||
|
import org.mozilla.geckoview.GeckoSession
|
||||||
|
import org.mozilla.geckoview.TranslationsController
|
||||||
|
|
||||||
|
internal class GeckoTranslateSessionDelegate(
|
||||||
|
private val engineSession: GeckoEngineSession,
|
||||||
|
) : TranslationsController.SessionTranslation.Delegate {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This delegate function is triggered when requesting a translation on the page is likely.
|
||||||
|
*
|
||||||
|
* The criteria is that the page is in a different language than the user's known languages and
|
||||||
|
* that the page is translatable (a model is available).
|
||||||
|
*
|
||||||
|
* @param session The session that this delegate event corresponds to.
|
||||||
|
*/
|
||||||
|
override fun onExpectedTranslate(session: GeckoSession) {
|
||||||
|
engineSession.notifyObservers {
|
||||||
|
onTranslateExpected()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This delegate function is triggered when the app should offer a translation.
|
||||||
|
*
|
||||||
|
* The criteria is that the translation is likely and it is the user's first visit to the host site.
|
||||||
|
*
|
||||||
|
* @param session The session that this delegate event corresponds to.
|
||||||
|
*/
|
||||||
|
override fun onOfferTranslate(session: GeckoSession) {
|
||||||
|
engineSession.notifyObservers {
|
||||||
|
onTranslateOffer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This delegate function is triggered when the state of the translation or translation options
|
||||||
|
* for the page has changed. State changes usually occur on navigation or if a translation
|
||||||
|
* action was requested, such as translating or restoring to the original page.
|
||||||
|
*
|
||||||
|
* This provides the translations engine state and information for the page.
|
||||||
|
*
|
||||||
|
* @param session The session that this delegate event corresponds to.
|
||||||
|
* @param state The reported translations state. Not to be confused
|
||||||
|
* with the browser translation state.
|
||||||
|
*/
|
||||||
|
override fun onTranslationStateChange(
|
||||||
|
session: GeckoSession,
|
||||||
|
state: TranslationsController.SessionTranslation.TranslationState?,
|
||||||
|
) {
|
||||||
|
val detectedLanguages = DetectedLanguages(
|
||||||
|
state?.detectedLanguages?.docLangTag,
|
||||||
|
state?.detectedLanguages?.isDocLangTagSupported,
|
||||||
|
state?.detectedLanguages?.userLangTag,
|
||||||
|
)
|
||||||
|
val pair = TranslationPair(
|
||||||
|
state?.requestedTranslationPair?.fromLanguage,
|
||||||
|
state?.requestedTranslationPair?.toLanguage,
|
||||||
|
)
|
||||||
|
val translationsState = TranslationEngineState(
|
||||||
|
detectedLanguages = detectedLanguages,
|
||||||
|
error = state?.error,
|
||||||
|
isEngineReady = state?.isEngineReady,
|
||||||
|
hasVisibleChange = state?.hasVisibleChange,
|
||||||
|
requestedTranslationPair = pair,
|
||||||
|
)
|
||||||
|
|
||||||
|
engineSession.notifyObservers {
|
||||||
|
onTranslateStateChange(translationsState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
/* 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.translate
|
||||||
|
|
||||||
|
import mozilla.components.concept.engine.translate.TranslationError
|
||||||
|
import org.mozilla.geckoview.TranslationsController.TranslationsException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility file for translations functions related to the Gecko implementation.
|
||||||
|
*/
|
||||||
|
object GeckoTranslationUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method for mapping a [TranslationsException] to the Android Components defined
|
||||||
|
* error type of [TranslationError].
|
||||||
|
*
|
||||||
|
* Throwable is the engine throwable that occurred during translating. Ordinarily should be
|
||||||
|
* a [TranslationsException].
|
||||||
|
*/
|
||||||
|
fun Throwable.intoTranslationError(): TranslationError {
|
||||||
|
return if (this is TranslationsException) {
|
||||||
|
when ((this).code) {
|
||||||
|
TranslationsException.ERROR_UNKNOWN ->
|
||||||
|
TranslationError.UnknownError(this)
|
||||||
|
|
||||||
|
TranslationsException.ERROR_ENGINE_NOT_SUPPORTED ->
|
||||||
|
TranslationError.EngineNotSupportedError(this)
|
||||||
|
|
||||||
|
TranslationsException.ERROR_COULD_NOT_TRANSLATE ->
|
||||||
|
TranslationError.CouldNotTranslateError(this)
|
||||||
|
|
||||||
|
TranslationsException.ERROR_COULD_NOT_RESTORE ->
|
||||||
|
TranslationError.CouldNotRestoreError(this)
|
||||||
|
|
||||||
|
TranslationsException.ERROR_COULD_NOT_LOAD_LANGUAGES ->
|
||||||
|
TranslationError.CouldNotLoadLanguagesError(this)
|
||||||
|
|
||||||
|
TranslationsException.ERROR_LANGUAGE_NOT_SUPPORTED ->
|
||||||
|
TranslationError.LanguageNotSupportedError(this)
|
||||||
|
|
||||||
|
TranslationsException.ERROR_MODEL_COULD_NOT_RETRIEVE ->
|
||||||
|
TranslationError.ModelCouldNotRetrieveError(this)
|
||||||
|
|
||||||
|
TranslationsException.ERROR_MODEL_COULD_NOT_DELETE ->
|
||||||
|
TranslationError.ModelCouldNotDeleteError(this)
|
||||||
|
|
||||||
|
TranslationsException.ERROR_MODEL_COULD_NOT_DOWNLOAD ->
|
||||||
|
TranslationError.ModelCouldNotDownloadError(this)
|
||||||
|
|
||||||
|
TranslationsException.ERROR_MODEL_LANGUAGE_REQUIRED ->
|
||||||
|
TranslationError.ModelLanguageRequiredError(this)
|
||||||
|
|
||||||
|
TranslationsException.ERROR_MODEL_DOWNLOAD_REQUIRED ->
|
||||||
|
TranslationError.ModelDownloadRequiredError(this)
|
||||||
|
|
||||||
|
else -> TranslationError.UnknownError(this)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TranslationError.UnknownError(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,154 @@
|
||||||
|
/* 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.util
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import mozilla.components.browser.engine.gecko.GeckoEngineSession
|
||||||
|
import mozilla.components.concept.engine.EngineSession
|
||||||
|
import mozilla.components.concept.engine.Settings
|
||||||
|
import org.mozilla.geckoview.GeckoRuntime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper factory for creating and maintaining a speculative [EngineSession].
|
||||||
|
*/
|
||||||
|
internal class SpeculativeSessionFactory {
|
||||||
|
@VisibleForTesting
|
||||||
|
internal var speculativeEngineSession: SpeculativeEngineSession? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a speculative [EngineSession] using the provided [contextId] and [defaultSettings].
|
||||||
|
* Creates a private session if [private] is set to true.
|
||||||
|
*
|
||||||
|
* The speculative [EngineSession] is kept internally until explicitly needed and access via [get].
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun create(
|
||||||
|
runtime: GeckoRuntime,
|
||||||
|
private: Boolean,
|
||||||
|
contextId: String?,
|
||||||
|
defaultSettings: Settings?,
|
||||||
|
) {
|
||||||
|
if (speculativeEngineSession?.matches(private, contextId) == true) {
|
||||||
|
// We already have a speculative engine session for this configuration. Nothing to do here.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any potentially non-matching engine session
|
||||||
|
clear()
|
||||||
|
|
||||||
|
speculativeEngineSession = SpeculativeEngineSession.create(
|
||||||
|
this,
|
||||||
|
runtime,
|
||||||
|
private,
|
||||||
|
contextId,
|
||||||
|
defaultSettings,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the internal speculative [EngineSession].
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun clear() {
|
||||||
|
speculativeEngineSession?.cleanUp()
|
||||||
|
speculativeEngineSession = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns and consumes a previously created [private] speculative [EngineSession] if it uses
|
||||||
|
* the same [contextId]. Returns `null` if no speculative [EngineSession] for that
|
||||||
|
* configuration is available.
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun get(
|
||||||
|
private: Boolean,
|
||||||
|
contextId: String?,
|
||||||
|
): GeckoEngineSession? {
|
||||||
|
val speculativeEngineSession = speculativeEngineSession ?: return null
|
||||||
|
|
||||||
|
return if (speculativeEngineSession.matches(private, contextId)) {
|
||||||
|
this.speculativeEngineSession = null
|
||||||
|
speculativeEngineSession.unwrap()
|
||||||
|
} else {
|
||||||
|
clear()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun hasSpeculativeSession(): Boolean {
|
||||||
|
return speculativeEngineSession != null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal wrapper for [GeckoEngineSession] that takes care of registering and unregistering an
|
||||||
|
* observer for handling content process crashes/kills.
|
||||||
|
*/
|
||||||
|
internal class SpeculativeEngineSession constructor(
|
||||||
|
@get:VisibleForTesting internal val engineSession: GeckoEngineSession,
|
||||||
|
@get:VisibleForTesting internal val observer: SpeculativeSessionObserver,
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Checks whether the [SpeculativeEngineSession] matches the given configuration.
|
||||||
|
*/
|
||||||
|
fun matches(private: Boolean, contextId: String?): Boolean {
|
||||||
|
return engineSession.geckoSession.settings.usePrivateMode == private &&
|
||||||
|
engineSession.geckoSession.settings.contextId == contextId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unwraps the internal [GeckoEngineSession].
|
||||||
|
*
|
||||||
|
* After calling [unwrap] the wrapper will no longer observe the [GeckoEngineSession] and further
|
||||||
|
* crash handling is left to the application.
|
||||||
|
*/
|
||||||
|
fun unwrap(): GeckoEngineSession {
|
||||||
|
engineSession.unregister(observer)
|
||||||
|
return engineSession
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up the internal state of this [SpeculativeEngineSession]. After calling this method
|
||||||
|
* his [SpeculativeEngineSession] cannot be used anymore.
|
||||||
|
*/
|
||||||
|
fun cleanUp() {
|
||||||
|
engineSession.unregister(observer)
|
||||||
|
engineSession.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun create(
|
||||||
|
factory: SpeculativeSessionFactory,
|
||||||
|
runtime: GeckoRuntime,
|
||||||
|
private: Boolean,
|
||||||
|
contextId: String?,
|
||||||
|
defaultSettings: Settings?,
|
||||||
|
): SpeculativeEngineSession {
|
||||||
|
val engineSession = GeckoEngineSession(runtime, private, defaultSettings, contextId)
|
||||||
|
val observer = SpeculativeSessionObserver(factory)
|
||||||
|
engineSession.register(observer)
|
||||||
|
|
||||||
|
return SpeculativeEngineSession(engineSession, observer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [EngineSession.Observer] implementation that will notify the [SpeculativeSessionFactory] if an
|
||||||
|
* [GeckoEngineSession] can no longer be used after a crash.
|
||||||
|
*/
|
||||||
|
internal class SpeculativeSessionObserver(
|
||||||
|
private val factory: SpeculativeSessionFactory,
|
||||||
|
|
||||||
|
) : EngineSession.Observer {
|
||||||
|
override fun onCrash() {
|
||||||
|
factory.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onProcessKilled() {
|
||||||
|
factory.clear()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,449 @@
|
||||||
|
/* 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.webextension
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import mozilla.components.browser.engine.gecko.GeckoEngineSession
|
||||||
|
import mozilla.components.browser.engine.gecko.await
|
||||||
|
import mozilla.components.concept.engine.EngineSession
|
||||||
|
import mozilla.components.concept.engine.Settings
|
||||||
|
import mozilla.components.concept.engine.webextension.Action
|
||||||
|
import mozilla.components.concept.engine.webextension.ActionHandler
|
||||||
|
import mozilla.components.concept.engine.webextension.DisabledFlags
|
||||||
|
import mozilla.components.concept.engine.webextension.Incognito
|
||||||
|
import mozilla.components.concept.engine.webextension.MessageHandler
|
||||||
|
import mozilla.components.concept.engine.webextension.Metadata
|
||||||
|
import mozilla.components.concept.engine.webextension.Port
|
||||||
|
import mozilla.components.concept.engine.webextension.TabHandler
|
||||||
|
import mozilla.components.concept.engine.webextension.WebExtension
|
||||||
|
import mozilla.components.support.base.log.logger.Logger
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.mozilla.geckoview.AllowOrDeny
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
import org.mozilla.geckoview.GeckoRuntime
|
||||||
|
import org.mozilla.geckoview.GeckoSession
|
||||||
|
import org.mozilla.geckoview.WebExtension as GeckoNativeWebExtension
|
||||||
|
import org.mozilla.geckoview.WebExtension.Action as GeckoNativeWebExtensionAction
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gecko-based implementation of [WebExtension], wrapping the native web
|
||||||
|
* extension object provided by GeckoView.
|
||||||
|
*/
|
||||||
|
class GeckoWebExtension(
|
||||||
|
val nativeExtension: GeckoNativeWebExtension,
|
||||||
|
val runtime: GeckoRuntime,
|
||||||
|
) : WebExtension(nativeExtension.id, nativeExtension.location, true) {
|
||||||
|
|
||||||
|
private val connectedPorts: MutableMap<PortId, GeckoPort> = mutableMapOf()
|
||||||
|
private val logger = Logger("GeckoWebExtension")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uniquely identifies a port using its name and the session it
|
||||||
|
* was opened for. Ports connected from background scripts will
|
||||||
|
* have a null session.
|
||||||
|
*/
|
||||||
|
data class PortId(val name: String, val session: EngineSession? = null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [WebExtension.registerBackgroundMessageHandler].
|
||||||
|
*/
|
||||||
|
override fun registerBackgroundMessageHandler(name: String, messageHandler: MessageHandler) {
|
||||||
|
val portDelegate = object : GeckoNativeWebExtension.PortDelegate {
|
||||||
|
|
||||||
|
override fun onPortMessage(message: Any, port: GeckoNativeWebExtension.Port) {
|
||||||
|
messageHandler.onPortMessage(message, GeckoPort(port))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDisconnect(port: GeckoNativeWebExtension.Port) {
|
||||||
|
val connectedPort = connectedPorts[PortId(name)]
|
||||||
|
if (connectedPort != null && connectedPort.nativePort == port) {
|
||||||
|
connectedPorts.remove(PortId(name))
|
||||||
|
messageHandler.onPortDisconnected(GeckoPort(port))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedPorts[PortId(name)]?.nativePort?.setDelegate(portDelegate)
|
||||||
|
|
||||||
|
val messageDelegate = object : GeckoNativeWebExtension.MessageDelegate {
|
||||||
|
|
||||||
|
override fun onConnect(port: GeckoNativeWebExtension.Port) {
|
||||||
|
port.setDelegate(portDelegate)
|
||||||
|
val geckoPort = GeckoPort(port)
|
||||||
|
connectedPorts[PortId(name)] = geckoPort
|
||||||
|
messageHandler.onPortConnected(geckoPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMessage(
|
||||||
|
// We don't use the same delegate instance for multiple apps so we don't need to verify the name.
|
||||||
|
name: String,
|
||||||
|
message: Any,
|
||||||
|
sender: GeckoNativeWebExtension.MessageSender,
|
||||||
|
): GeckoResult<Any>? {
|
||||||
|
val response = messageHandler.onMessage(message, null)
|
||||||
|
return response?.let { GeckoResult.fromValue(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nativeExtension.setMessageDelegate(messageDelegate, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [WebExtension.registerContentMessageHandler].
|
||||||
|
*/
|
||||||
|
override fun registerContentMessageHandler(session: EngineSession, name: String, messageHandler: MessageHandler) {
|
||||||
|
val portDelegate = object : GeckoNativeWebExtension.PortDelegate {
|
||||||
|
|
||||||
|
override fun onPortMessage(message: Any, port: GeckoNativeWebExtension.Port) {
|
||||||
|
messageHandler.onPortMessage(message, GeckoPort(port, session))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDisconnect(port: GeckoNativeWebExtension.Port) {
|
||||||
|
val connectedPort = connectedPorts[PortId(name, session)]
|
||||||
|
if (connectedPort != null && connectedPort.nativePort == port) {
|
||||||
|
connectedPorts.remove(PortId(name, session))
|
||||||
|
messageHandler.onPortDisconnected(connectedPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedPorts[PortId(name, session)]?.nativePort?.setDelegate(portDelegate)
|
||||||
|
|
||||||
|
val messageDelegate = object : GeckoNativeWebExtension.MessageDelegate {
|
||||||
|
|
||||||
|
override fun onConnect(port: GeckoNativeWebExtension.Port) {
|
||||||
|
port.setDelegate(portDelegate)
|
||||||
|
val geckoPort = GeckoPort(port, session)
|
||||||
|
connectedPorts[PortId(name, session)] = geckoPort
|
||||||
|
messageHandler.onPortConnected(geckoPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMessage(
|
||||||
|
// We don't use the same delegate instance for multiple apps so we don't need to verify the name.
|
||||||
|
name: String,
|
||||||
|
message: Any,
|
||||||
|
sender: GeckoNativeWebExtension.MessageSender,
|
||||||
|
): GeckoResult<Any>? {
|
||||||
|
val response = messageHandler.onMessage(message, session)
|
||||||
|
return response?.let { GeckoResult.fromValue(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val geckoSession = (session as GeckoEngineSession).geckoSession
|
||||||
|
geckoSession.webExtensionController.setMessageDelegate(nativeExtension, messageDelegate, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [WebExtension.hasContentMessageHandler].
|
||||||
|
*/
|
||||||
|
override fun hasContentMessageHandler(session: EngineSession, name: String): Boolean {
|
||||||
|
val geckoSession = (session as GeckoEngineSession).geckoSession
|
||||||
|
return geckoSession.webExtensionController.getMessageDelegate(nativeExtension, name) != null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [WebExtension.getConnectedPort].
|
||||||
|
*/
|
||||||
|
override fun getConnectedPort(name: String, session: EngineSession?): Port? {
|
||||||
|
return connectedPorts[PortId(name, session)]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [WebExtension.disconnectPort].
|
||||||
|
*/
|
||||||
|
override fun disconnectPort(name: String, session: EngineSession?) {
|
||||||
|
val portId = PortId(name, session)
|
||||||
|
val port = connectedPorts[portId]
|
||||||
|
port?.let {
|
||||||
|
it.disconnect()
|
||||||
|
connectedPorts.remove(portId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [WebExtension.registerActionHandler].
|
||||||
|
*/
|
||||||
|
override fun registerActionHandler(actionHandler: ActionHandler) {
|
||||||
|
if (!supportActions) {
|
||||||
|
logger.error(
|
||||||
|
"Attempt to register default action handler but browser and page " +
|
||||||
|
"action support is turned off for this extension: $id",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val actionDelegate = object : GeckoNativeWebExtension.ActionDelegate {
|
||||||
|
|
||||||
|
override fun onBrowserAction(
|
||||||
|
ext: GeckoNativeWebExtension,
|
||||||
|
// Session will always be null here for the global default delegate
|
||||||
|
session: GeckoSession?,
|
||||||
|
action: GeckoNativeWebExtensionAction,
|
||||||
|
) {
|
||||||
|
actionHandler.onBrowserAction(this@GeckoWebExtension, null, action.convert())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageAction(
|
||||||
|
ext: GeckoNativeWebExtension,
|
||||||
|
// Session will always be null here for the global default delegate
|
||||||
|
session: GeckoSession?,
|
||||||
|
action: GeckoNativeWebExtensionAction,
|
||||||
|
) {
|
||||||
|
actionHandler.onPageAction(this@GeckoWebExtension, null, action.convert())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTogglePopup(
|
||||||
|
ext: GeckoNativeWebExtension,
|
||||||
|
action: GeckoNativeWebExtensionAction,
|
||||||
|
): GeckoResult<GeckoSession>? {
|
||||||
|
val session = actionHandler.onToggleActionPopup(this@GeckoWebExtension, action.convert())
|
||||||
|
return session?.let { GeckoResult.fromValue((session as GeckoEngineSession).geckoSession) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nativeExtension.setActionDelegate(actionDelegate)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [WebExtension.registerActionHandler].
|
||||||
|
*/
|
||||||
|
override fun registerActionHandler(session: EngineSession, actionHandler: ActionHandler) {
|
||||||
|
if (!supportActions) {
|
||||||
|
logger.error(
|
||||||
|
"Attempt to register action handler on session but browser and page " +
|
||||||
|
"action support is turned off for this extension: $id",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val actionDelegate = object : GeckoNativeWebExtension.ActionDelegate {
|
||||||
|
|
||||||
|
override fun onBrowserAction(
|
||||||
|
ext: GeckoNativeWebExtension,
|
||||||
|
geckoSession: GeckoSession?,
|
||||||
|
action: GeckoNativeWebExtensionAction,
|
||||||
|
) {
|
||||||
|
actionHandler.onBrowserAction(this@GeckoWebExtension, session, action.convert())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageAction(
|
||||||
|
ext: GeckoNativeWebExtension,
|
||||||
|
geckoSession: GeckoSession?,
|
||||||
|
action: GeckoNativeWebExtensionAction,
|
||||||
|
) {
|
||||||
|
actionHandler.onPageAction(this@GeckoWebExtension, session, action.convert())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val geckoSession = (session as GeckoEngineSession).geckoSession
|
||||||
|
geckoSession.webExtensionController.setActionDelegate(nativeExtension, actionDelegate)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [WebExtension.hasActionHandler].
|
||||||
|
*/
|
||||||
|
override fun hasActionHandler(session: EngineSession): Boolean {
|
||||||
|
val geckoSession = (session as GeckoEngineSession).geckoSession
|
||||||
|
return geckoSession.webExtensionController.getActionDelegate(nativeExtension) != null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [WebExtension.registerTabHandler].
|
||||||
|
*/
|
||||||
|
override fun registerTabHandler(tabHandler: TabHandler, defaultSettings: Settings?) {
|
||||||
|
val tabDelegate = object : GeckoNativeWebExtension.TabDelegate {
|
||||||
|
|
||||||
|
override fun onNewTab(
|
||||||
|
ext: GeckoNativeWebExtension,
|
||||||
|
tabDetails: GeckoNativeWebExtension.CreateTabDetails,
|
||||||
|
): GeckoResult<GeckoSession>? {
|
||||||
|
val geckoEngineSession = GeckoEngineSession(
|
||||||
|
runtime,
|
||||||
|
defaultSettings = defaultSettings,
|
||||||
|
openGeckoSession = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
tabHandler.onNewTab(
|
||||||
|
this@GeckoWebExtension,
|
||||||
|
geckoEngineSession,
|
||||||
|
tabDetails.active == true,
|
||||||
|
tabDetails.url ?: "",
|
||||||
|
)
|
||||||
|
return GeckoResult.fromValue(geckoEngineSession.geckoSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOpenOptionsPage(ext: GeckoNativeWebExtension) {
|
||||||
|
ext.metaData.optionsPageUrl?.let { optionsPageUrl ->
|
||||||
|
tabHandler.onNewTab(
|
||||||
|
this@GeckoWebExtension,
|
||||||
|
GeckoEngineSession(
|
||||||
|
runtime,
|
||||||
|
defaultSettings = defaultSettings,
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
optionsPageUrl,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nativeExtension.tabDelegate = tabDelegate
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [WebExtension.registerTabHandler].
|
||||||
|
*/
|
||||||
|
override fun registerTabHandler(session: EngineSession, tabHandler: TabHandler) {
|
||||||
|
val tabDelegate = object : GeckoNativeWebExtension.SessionTabDelegate {
|
||||||
|
|
||||||
|
override fun onUpdateTab(
|
||||||
|
ext: GeckoNativeWebExtension,
|
||||||
|
geckoSession: GeckoSession,
|
||||||
|
tabDetails: GeckoNativeWebExtension.UpdateTabDetails,
|
||||||
|
): GeckoResult<AllowOrDeny> {
|
||||||
|
return if (tabHandler.onUpdateTab(
|
||||||
|
this@GeckoWebExtension,
|
||||||
|
session,
|
||||||
|
tabDetails.active == true,
|
||||||
|
tabDetails.url,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
GeckoResult.allow()
|
||||||
|
} else {
|
||||||
|
GeckoResult.deny()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCloseTab(
|
||||||
|
ext: GeckoNativeWebExtension?,
|
||||||
|
geckoSession: GeckoSession,
|
||||||
|
): GeckoResult<AllowOrDeny> {
|
||||||
|
return if (ext != null) {
|
||||||
|
if (tabHandler.onCloseTab(this@GeckoWebExtension, session)) {
|
||||||
|
GeckoResult.allow()
|
||||||
|
} else {
|
||||||
|
GeckoResult.deny()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
GeckoResult.deny()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val geckoSession = (session as GeckoEngineSession).geckoSession
|
||||||
|
geckoSession.webExtensionController.setTabDelegate(nativeExtension, tabDelegate)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [WebExtension.hasTabHandler].
|
||||||
|
*/
|
||||||
|
override fun hasTabHandler(session: EngineSession): Boolean {
|
||||||
|
val geckoSession = (session as GeckoEngineSession).geckoSession
|
||||||
|
return geckoSession.webExtensionController.getTabDelegate(nativeExtension) != null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [WebExtension.getMetadata].
|
||||||
|
*/
|
||||||
|
override fun getMetadata(): Metadata {
|
||||||
|
return nativeExtension.metaData.let {
|
||||||
|
Metadata(
|
||||||
|
name = it.name,
|
||||||
|
fullDescription = it.fullDescription,
|
||||||
|
downloadUrl = it.downloadUrl,
|
||||||
|
updateDate = it.updateDate,
|
||||||
|
averageRating = it.averageRating.toFloat(),
|
||||||
|
reviewCount = it.reviewCount,
|
||||||
|
description = it.description,
|
||||||
|
developerName = it.creatorName,
|
||||||
|
developerUrl = it.creatorUrl,
|
||||||
|
homepageUrl = it.homepageUrl,
|
||||||
|
creatorName = it.creatorName,
|
||||||
|
creatorUrl = it.creatorUrl,
|
||||||
|
reviewUrl = it.reviewUrl,
|
||||||
|
version = it.version,
|
||||||
|
requiredPermissions = it.requiredPermissions.toList(),
|
||||||
|
// Origins is marked as @NonNull but may be null: https://bugzilla.mozilla.org/show_bug.cgi?id=1629957
|
||||||
|
requiredOrigins = it.requiredOrigins.orEmpty().toList(),
|
||||||
|
optionalPermissions = it.optionalPermissions.toList(),
|
||||||
|
grantedOptionalPermissions = it.grantedOptionalPermissions.toList(),
|
||||||
|
grantedOptionalOrigins = it.grantedOptionalOrigins.toList(),
|
||||||
|
optionalOrigins = it.optionalOrigins.toList(),
|
||||||
|
disabledFlags = DisabledFlags.select(it.disabledFlags),
|
||||||
|
optionsPageUrl = it.optionsPageUrl,
|
||||||
|
openOptionsPageInTab = it.openOptionsPageInTab,
|
||||||
|
baseUrl = it.baseUrl,
|
||||||
|
temporary = it.temporary,
|
||||||
|
detailUrl = it.amoListingUrl,
|
||||||
|
incognito = Incognito.fromString(it.incognito),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isBuiltIn(): Boolean {
|
||||||
|
return nativeExtension.isBuiltIn
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isEnabled(): Boolean {
|
||||||
|
return nativeExtension.metaData.enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isAllowedInPrivateBrowsing(): Boolean {
|
||||||
|
return isBuiltIn() || nativeExtension.metaData.allowedInPrivateBrowsing
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun loadIcon(size: Int): Bitmap? {
|
||||||
|
return getIcon(size).await()
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun getIcon(size: Int): GeckoResult<Bitmap> {
|
||||||
|
return nativeExtension.metaData.icon.getBitmap(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gecko-based implementation of [Port], wrapping the native port provided by GeckoView.
|
||||||
|
*/
|
||||||
|
class GeckoPort(
|
||||||
|
internal val nativePort: GeckoNativeWebExtension.Port,
|
||||||
|
engineSession: EngineSession? = null,
|
||||||
|
) : Port(engineSession) {
|
||||||
|
|
||||||
|
override fun postMessage(message: JSONObject) {
|
||||||
|
nativePort.postMessage(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun name(): String {
|
||||||
|
return nativePort.name
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun senderUrl(): String {
|
||||||
|
return nativePort.sender.url
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun disconnect() {
|
||||||
|
nativePort.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun GeckoNativeWebExtensionAction.convert(): Action {
|
||||||
|
val loadIcon: (suspend (Int) -> Bitmap?)? = icon?.let {
|
||||||
|
{ size -> icon?.getBitmap(size)?.await() }
|
||||||
|
}
|
||||||
|
|
||||||
|
val onClick = { click() }
|
||||||
|
|
||||||
|
return Action(
|
||||||
|
title,
|
||||||
|
enabled,
|
||||||
|
loadIcon,
|
||||||
|
badgeText,
|
||||||
|
badgeTextColor,
|
||||||
|
badgeBackgroundColor,
|
||||||
|
onClick,
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
/* 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.webextension
|
||||||
|
|
||||||
|
import mozilla.components.concept.engine.webextension.WebExtensionException
|
||||||
|
import mozilla.components.concept.engine.webextension.WebExtensionInstallException
|
||||||
|
import org.mozilla.geckoview.WebExtension.InstallException
|
||||||
|
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_ADMIN_INSTALL_ONLY
|
||||||
|
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_BLOCKLISTED
|
||||||
|
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_CORRUPT_FILE
|
||||||
|
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_INCOMPATIBLE
|
||||||
|
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_NETWORK_FAILURE
|
||||||
|
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_SIGNEDSTATE_REQUIRED
|
||||||
|
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_UNSUPPORTED_ADDON_TYPE
|
||||||
|
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_USER_CANCELED
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An unexpected gecko exception that occurs when trying to perform an action on the extension like
|
||||||
|
* (but not exclusively) installing/uninstalling, removing or updating..
|
||||||
|
*/
|
||||||
|
class GeckoWebExtensionException(throwable: Throwable) : WebExtensionException(throwable) {
|
||||||
|
override val isRecoverable: Boolean = throwable is InstallException &&
|
||||||
|
throwable.code == ERROR_USER_CANCELED
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
internal fun createWebExtensionException(throwable: Throwable): WebExtensionException {
|
||||||
|
if (throwable is InstallException) {
|
||||||
|
return when (throwable.code) {
|
||||||
|
ERROR_USER_CANCELED -> WebExtensionInstallException.UserCancelled(
|
||||||
|
extensionName = throwable.extensionName,
|
||||||
|
throwable,
|
||||||
|
)
|
||||||
|
|
||||||
|
ERROR_BLOCKLISTED -> WebExtensionInstallException.Blocklisted(
|
||||||
|
extensionName = throwable.extensionName,
|
||||||
|
throwable,
|
||||||
|
)
|
||||||
|
|
||||||
|
ERROR_CORRUPT_FILE -> WebExtensionInstallException.CorruptFile(
|
||||||
|
throwable = throwable,
|
||||||
|
)
|
||||||
|
|
||||||
|
ERROR_NETWORK_FAILURE -> WebExtensionInstallException.NetworkFailure(
|
||||||
|
throwable = throwable,
|
||||||
|
)
|
||||||
|
|
||||||
|
ERROR_SIGNEDSTATE_REQUIRED -> WebExtensionInstallException.NotSigned(
|
||||||
|
throwable = throwable,
|
||||||
|
)
|
||||||
|
|
||||||
|
ERROR_INCOMPATIBLE -> WebExtensionInstallException.Incompatible(
|
||||||
|
extensionName = throwable.extensionName,
|
||||||
|
throwable,
|
||||||
|
)
|
||||||
|
|
||||||
|
ERROR_UNSUPPORTED_ADDON_TYPE -> WebExtensionInstallException.UnsupportedAddonType(
|
||||||
|
extensionName = throwable.extensionName,
|
||||||
|
throwable,
|
||||||
|
)
|
||||||
|
|
||||||
|
ERROR_ADMIN_INSTALL_ONLY -> WebExtensionInstallException.AdminInstallOnly(
|
||||||
|
extensionName = throwable.extensionName,
|
||||||
|
throwable,
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> WebExtensionInstallException.Unknown(
|
||||||
|
extensionName = throwable.extensionName,
|
||||||
|
throwable,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return GeckoWebExtensionException(throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.webnotifications
|
||||||
|
|
||||||
|
import mozilla.components.concept.engine.webnotifications.WebNotification
|
||||||
|
import mozilla.components.concept.engine.webnotifications.WebNotificationDelegate
|
||||||
|
import org.mozilla.geckoview.WebNotification as GeckoViewWebNotification
|
||||||
|
import org.mozilla.geckoview.WebNotificationDelegate as GeckoViewWebNotificationDelegate
|
||||||
|
|
||||||
|
internal class GeckoWebNotificationDelegate(
|
||||||
|
private val webNotificationDelegate: WebNotificationDelegate,
|
||||||
|
) : GeckoViewWebNotificationDelegate {
|
||||||
|
override fun onShowNotification(webNotification: GeckoViewWebNotification) {
|
||||||
|
webNotificationDelegate.onShowNotification(webNotification.toWebNotification())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCloseNotification(webNotification: GeckoViewWebNotification) {
|
||||||
|
webNotificationDelegate.onCloseNotification(webNotification.toWebNotification())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun GeckoViewWebNotification.toWebNotification(): WebNotification {
|
||||||
|
return WebNotification(
|
||||||
|
title = title,
|
||||||
|
tag = tag,
|
||||||
|
body = text,
|
||||||
|
sourceUrl = source,
|
||||||
|
iconUrl = imageUrl,
|
||||||
|
direction = textDirection,
|
||||||
|
lang = lang,
|
||||||
|
requireInteraction = requireInteraction,
|
||||||
|
triggeredByWebExtension = source == null,
|
||||||
|
privateBrowsing = privateBrowsing,
|
||||||
|
engineNotification = this@toWebNotification,
|
||||||
|
silent = silent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.webpush
|
||||||
|
|
||||||
|
import mozilla.components.concept.engine.webpush.WebPushDelegate
|
||||||
|
import mozilla.components.concept.engine.webpush.WebPushSubscription
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
import org.mozilla.geckoview.WebPushDelegate as GeckoViewWebPushDelegate
|
||||||
|
import org.mozilla.geckoview.WebPushSubscription as GeckoWebPushSubscription
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper for the [WebPushDelegate] to communicate with the Gecko-based delegate.
|
||||||
|
*/
|
||||||
|
internal class GeckoWebPushDelegate(private val delegate: WebPushDelegate) : GeckoViewWebPushDelegate {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [GeckoViewWebPushDelegate.onGetSubscription].
|
||||||
|
*/
|
||||||
|
override fun onGetSubscription(scope: String): GeckoResult<GeckoWebPushSubscription>? {
|
||||||
|
val result: GeckoResult<GeckoWebPushSubscription> = GeckoResult()
|
||||||
|
|
||||||
|
delegate.onGetSubscription(scope) { subscription ->
|
||||||
|
result.complete(subscription?.toGeckoSubscription())
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [GeckoViewWebPushDelegate.onSubscribe].
|
||||||
|
*/
|
||||||
|
override fun onSubscribe(scope: String, appServerKey: ByteArray?): GeckoResult<GeckoWebPushSubscription>? {
|
||||||
|
val result: GeckoResult<GeckoWebPushSubscription> = GeckoResult()
|
||||||
|
|
||||||
|
delegate.onSubscribe(scope, appServerKey) { subscription ->
|
||||||
|
result.complete(subscription?.toGeckoSubscription())
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [GeckoViewWebPushDelegate.onUnsubscribe].
|
||||||
|
*/
|
||||||
|
override fun onUnsubscribe(scope: String): GeckoResult<Void>? {
|
||||||
|
val result: GeckoResult<Void> = GeckoResult()
|
||||||
|
|
||||||
|
delegate.onUnsubscribe(scope) { success ->
|
||||||
|
if (success) {
|
||||||
|
result.complete(null)
|
||||||
|
} else {
|
||||||
|
result.completeExceptionally(WebPushException("Un-subscribing from subscription failed."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper extension to convert the subscription data class to the Gecko-based implementation.
|
||||||
|
*/
|
||||||
|
internal fun WebPushSubscription.toGeckoSubscription() = GeckoWebPushSubscription(
|
||||||
|
scope,
|
||||||
|
endpoint,
|
||||||
|
appServerKey,
|
||||||
|
publicKey,
|
||||||
|
authSecret,
|
||||||
|
)
|
||||||
|
|
||||||
|
internal class WebPushException(message: String) : IllegalStateException(message)
|
|
@ -0,0 +1,31 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.webpush
|
||||||
|
|
||||||
|
import mozilla.components.concept.engine.webpush.WebPushHandler
|
||||||
|
import org.mozilla.geckoview.GeckoRuntime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gecko-based implementation of [WebPushHandler], wrapping the
|
||||||
|
* controller object provided by GeckoView.
|
||||||
|
*/
|
||||||
|
internal class GeckoWebPushHandler(
|
||||||
|
private val runtime: GeckoRuntime,
|
||||||
|
) : WebPushHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [WebPushHandler].
|
||||||
|
*/
|
||||||
|
override fun onPushMessage(scope: String, message: ByteArray?) {
|
||||||
|
runtime.webPushController.onPushEvent(scope, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [WebPushHandler].
|
||||||
|
*/
|
||||||
|
override fun onSubscriptionChanged(scope: String) {
|
||||||
|
runtime.webPushController.onSubscriptionChanged(scope)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
/* 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.window
|
||||||
|
|
||||||
|
import mozilla.components.browser.engine.gecko.GeckoEngineSession
|
||||||
|
import mozilla.components.concept.engine.EngineSession
|
||||||
|
import mozilla.components.concept.engine.window.WindowRequest
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gecko-based implementation of [WindowRequest].
|
||||||
|
*/
|
||||||
|
class GeckoWindowRequest(
|
||||||
|
override val url: String = "",
|
||||||
|
private val engineSession: GeckoEngineSession,
|
||||||
|
override val type: WindowRequest.Type = WindowRequest.Type.OPEN,
|
||||||
|
) : WindowRequest {
|
||||||
|
|
||||||
|
override fun prepare(): EngineSession {
|
||||||
|
return this.engineSession
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
/* 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.experiment
|
||||||
|
|
||||||
|
import mozilla.components.browser.engine.gecko.GeckoNimbus
|
||||||
|
import mozilla.components.support.base.log.logger.Logger
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.mozilla.experiments.nimbus.internal.FeatureHolder
|
||||||
|
import org.mozilla.geckoview.ExperimentDelegate
|
||||||
|
import org.mozilla.geckoview.ExperimentDelegate.ExperimentException
|
||||||
|
import org.mozilla.geckoview.ExperimentDelegate.ExperimentException.ERROR_FEATURE_NOT_FOUND
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default Nimbus [ExperimentDelegate] implementation to communicate with mobile Gecko and GeckoView.
|
||||||
|
*/
|
||||||
|
class NimbusExperimentDelegate : ExperimentDelegate {
|
||||||
|
|
||||||
|
private val logger = Logger(NimbusExperimentDelegate::javaClass.name)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves experiment information on the feature for use in GeckoView.
|
||||||
|
*
|
||||||
|
* @param feature Nimbus feature to retrieve information about
|
||||||
|
* @return a [GeckoResult] with a JSON object containing experiment information or completes exceptionally.
|
||||||
|
*/
|
||||||
|
override fun onGetExperimentFeature(feature: String): GeckoResult<JSONObject> {
|
||||||
|
val result = GeckoResult<JSONObject>()
|
||||||
|
val nimbusFeature = GeckoNimbus.getFeature(feature)
|
||||||
|
if (nimbusFeature != null) {
|
||||||
|
result.complete(nimbusFeature.toJSONObject())
|
||||||
|
} else {
|
||||||
|
logger.warn("Could not find Nimbus feature '$feature' to retrieve experiment information.")
|
||||||
|
result.completeExceptionally(ExperimentException(ERROR_FEATURE_NOT_FOUND))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records that an exposure event occurred with the feature.
|
||||||
|
*
|
||||||
|
* @param feature Nimbus feature to record information about
|
||||||
|
* @return a [GeckoResult] that completes if the feature was found and recorded or completes exceptionally.
|
||||||
|
*/
|
||||||
|
override fun onRecordExposureEvent(feature: String): GeckoResult<Void> {
|
||||||
|
return recordWithFeature(feature) { it.recordExposure() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records that an exposure event occurred with the feature, in a given experiment.
|
||||||
|
* Note: See [onRecordExposureEvent] if no slug is known or needed
|
||||||
|
*
|
||||||
|
* @param feature Nimbus feature to record information about
|
||||||
|
* @param slug Nimbus experiment slug to record information about
|
||||||
|
* @return a [GeckoResult] that completes if the feature was found and recorded or completes exceptionally.
|
||||||
|
*/
|
||||||
|
override fun onRecordExperimentExposureEvent(feature: String, slug: String): GeckoResult<Void> {
|
||||||
|
return recordWithFeature(feature) { it.recordExperimentExposure(slug) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records a malformed exposure event for the feature.
|
||||||
|
*
|
||||||
|
* @param feature Nimbus feature to record information about
|
||||||
|
* @param part an optional detail or part identifier for then event. May be an empty string.
|
||||||
|
* @return a [GeckoResult] that completes if the feature was found and recorded or completes exceptionally.
|
||||||
|
*/
|
||||||
|
override fun onRecordMalformedConfigurationEvent(feature: String, part: String): GeckoResult<Void> {
|
||||||
|
return recordWithFeature(feature) { it.recordMalformedConfiguration(part) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method to record experiment events and return the correct errors.
|
||||||
|
*
|
||||||
|
* @param featureId Nimbus feature to record information on
|
||||||
|
* @param closure Nimbus record function to use
|
||||||
|
* @return a [GeckoResult] that completes if successful or else with an exception
|
||||||
|
*/
|
||||||
|
private fun recordWithFeature(featureId: String, closure: (FeatureHolder<*>) -> Unit): GeckoResult<Void> {
|
||||||
|
val result = GeckoResult<Void>()
|
||||||
|
val nimbusFeature = GeckoNimbus.getFeature(featureId)
|
||||||
|
if (nimbusFeature != null) {
|
||||||
|
closure(nimbusFeature)
|
||||||
|
result.complete(null)
|
||||||
|
} else {
|
||||||
|
logger.warn("Could not find Nimbus feature '$featureId' to record an exposure event.")
|
||||||
|
result.completeExceptionally(ExperimentException(ERROR_FEATURE_NOT_FOUND))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue