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