Compare commits
7 Commits
Author | SHA1 | Date |
---|---|---|
harvey186 | ff6bef97c7 | |
harvey186 | 7a6de5f3e6 | |
harvey186 | c64e459308 | |
harvey186 | 331c06960a | |
harvey186 | d46b64ddc0 | |
harvey186 | 91005272fc | |
harvey186 | 2ed87e4857 |
|
@ -83,9 +83,11 @@ projects:
|
||||||
- support-rusthttp
|
- support-rusthttp
|
||||||
- support-rustlog
|
- support-rustlog
|
||||||
- support-test
|
- support-test
|
||||||
|
- support-test-fakes
|
||||||
- support-test-libstate
|
- support-test-libstate
|
||||||
- support-utils
|
- support-utils
|
||||||
- support-webextensions
|
- support-webextensions
|
||||||
|
- tooling-lint
|
||||||
- ui-autocomplete
|
- ui-autocomplete
|
||||||
- ui-colors
|
- ui-colors
|
||||||
- ui-icons
|
- ui-icons
|
||||||
|
|
|
@ -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
|
|
@ -54,6 +54,12 @@ jobs:
|
||||||
gradle-executable: /usr/bin/time
|
gradle-executable: /usr/bin/time
|
||||||
arguments: -v ./gradlew app:assemblefenixForkRelease -x lintVitalFenixForkRelease -PversionName=${{ env.VERSION_NAME }} --stacktrace
|
arguments: -v ./gradlew app:assemblefenixForkRelease -x lintVitalFenixForkRelease -PversionName=${{ env.VERSION_NAME }} --stacktrace
|
||||||
|
|
||||||
|
- name: Setup build tool version variable
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1)
|
||||||
|
echo "BUILD_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Create signed APKs
|
- name: Create signed APKs
|
||||||
uses: abhijitvalluri/sign-apks@v0.8
|
uses: abhijitvalluri/sign-apks@v0.8
|
||||||
with:
|
with:
|
||||||
|
@ -62,6 +68,8 @@ jobs:
|
||||||
alias: ${{ secrets.DEBUG_ALIAS }}
|
alias: ${{ secrets.DEBUG_ALIAS }}
|
||||||
keyStorePassword: ${{ secrets.DEBUG_KEY_STORE_PASSWORD }}
|
keyStorePassword: ${{ secrets.DEBUG_KEY_STORE_PASSWORD }}
|
||||||
keyPassword: ${{ secrets.DEBUG_KEY_PASSWORD }}
|
keyPassword: ${{ secrets.DEBUG_KEY_PASSWORD }}
|
||||||
|
env:
|
||||||
|
BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
|
||||||
|
|
||||||
- name: Upload arm64 apk
|
- name: Upload arm64 apk
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
|
|
|
@ -52,6 +52,12 @@ jobs:
|
||||||
gradle-executable: /usr/bin/time
|
gradle-executable: /usr/bin/time
|
||||||
arguments: -v ./gradlew app:assemblefenixForkRelease -x lintVitalFenixForkRelease -PversionName=${{ env.VERSION_NAME }} --stacktrace
|
arguments: -v ./gradlew app:assemblefenixForkRelease -x lintVitalFenixForkRelease -PversionName=${{ env.VERSION_NAME }} --stacktrace
|
||||||
|
|
||||||
|
- name: Setup build tool version variable
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1)
|
||||||
|
echo "BUILD_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Create signed APKs
|
- name: Create signed APKs
|
||||||
if: "contains(toJSON(github.event.ref_type), 'tag') && contains(toJSON(github.event.ref), 'iceraven')"
|
if: "contains(toJSON(github.event.ref_type), 'tag') && contains(toJSON(github.event.ref), 'iceraven')"
|
||||||
uses: abhijitvalluri/sign-apks@v0.8
|
uses: abhijitvalluri/sign-apks@v0.8
|
||||||
|
@ -61,6 +67,8 @@ jobs:
|
||||||
alias: ${{ secrets.DEBUG_ALIAS }}
|
alias: ${{ secrets.DEBUG_ALIAS }}
|
||||||
keyStorePassword: ${{ secrets.DEBUG_KEY_STORE_PASSWORD }}
|
keyStorePassword: ${{ secrets.DEBUG_KEY_STORE_PASSWORD }}
|
||||||
keyPassword: ${{ secrets.DEBUG_KEY_PASSWORD }}
|
keyPassword: ${{ secrets.DEBUG_KEY_PASSWORD }}
|
||||||
|
env:
|
||||||
|
BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
|
||||||
|
|
||||||
- name: Create changelog
|
- name: Create changelog
|
||||||
if: "contains(toJSON(github.event.ref_type), 'tag') && contains(toJSON(github.event.ref), 'iceraven')"
|
if: "contains(toJSON(github.event.ref_type), 'tag') && contains(toJSON(github.event.ref), 'iceraven')"
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
Definitely not brought to you by Mozilla!
|
Definitely not brought to you by Mozilla!
|
||||||
|
|
||||||
|
|
||||||
Iceraven Browser is a web browser for Android, based on [Mozilla's Fenix version of Firefox](https://github.com/mozilla-mobile/fenix/), [GeckoView](https://mozilla.github.io/geckoview/) and [Mozilla Android Components](https://mozac.org/).
|
Iceraven Browser is a web browser for Android, based on [Mozilla's Fenix version of Firefox](https://github.com/mozilla-mobile/fenix/), [GeckoView](https://mozilla.github.io/geckoview/) and [Mozilla Android Components](https://mozac.org/).
|
||||||
|
|
||||||
Our goal is to be a close fork of the new Firefox for Android that seeks to provide users with more options, more opportunities to customize (including a broad extension library), and more information about the pages they visit and how their browsers are interacting with those pages.
|
Our goal is to be a close fork of the new Firefox for Android that seeks to provide users with more options, more opportunities to customize (including a broad extension library), and more information about the pages they visit and how their browsers are interacting with those pages.
|
||||||
|
|
|
@ -1,473 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
@file:Suppress("TooManyFunctions")
|
|
||||||
|
|
||||||
package org.mozilla.fenix.ui.robots
|
|
||||||
|
|
||||||
import androidx.compose.ui.test.SemanticsMatcher
|
|
||||||
import androidx.compose.ui.test.assert
|
|
||||||
import androidx.compose.ui.test.assertIsDisplayed
|
|
||||||
import androidx.compose.ui.test.hasAnySibling
|
|
||||||
import androidx.compose.ui.test.hasContentDescription
|
|
||||||
import androidx.compose.ui.test.hasText
|
|
||||||
import androidx.compose.ui.test.junit4.ComposeTestRule
|
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
|
||||||
import androidx.compose.ui.test.performClick
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import androidx.test.espresso.Espresso.closeSoftKeyboard
|
|
||||||
import androidx.test.espresso.Espresso.onView
|
|
||||||
import androidx.test.espresso.ViewInteraction
|
|
||||||
import androidx.test.espresso.action.ViewActions.clearText
|
|
||||||
import androidx.test.espresso.action.ViewActions.click
|
|
||||||
import androidx.test.espresso.action.ViewActions.typeText
|
|
||||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
|
||||||
import androidx.test.espresso.contrib.RecyclerViewActions
|
|
||||||
import androidx.test.espresso.matcher.ViewMatchers
|
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
|
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
|
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.withChild
|
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.withClassName
|
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
|
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
|
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
|
||||||
import androidx.test.uiautomator.By
|
|
||||||
import androidx.test.uiautomator.UiSelector
|
|
||||||
import org.hamcrest.CoreMatchers
|
|
||||||
import org.hamcrest.Matchers.allOf
|
|
||||||
import org.hamcrest.Matchers.endsWith
|
|
||||||
import org.junit.Assert.assertFalse
|
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
import org.mozilla.fenix.R
|
|
||||||
import org.mozilla.fenix.helpers.DataGenerationHelper.getAvailableSearchEngines
|
|
||||||
import org.mozilla.fenix.helpers.DataGenerationHelper.getRegionSearchEnginesList
|
|
||||||
import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
|
|
||||||
import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
|
|
||||||
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
|
|
||||||
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
|
|
||||||
import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
|
|
||||||
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
|
|
||||||
import org.mozilla.fenix.helpers.TestHelper.hasCousin
|
|
||||||
import org.mozilla.fenix.helpers.TestHelper.mDevice
|
|
||||||
import org.mozilla.fenix.helpers.TestHelper.packageName
|
|
||||||
import org.mozilla.fenix.helpers.click
|
|
||||||
import org.mozilla.fenix.helpers.isChecked
|
|
||||||
import org.mozilla.fenix.helpers.isEnabled
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of Robot Pattern for the settings search sub menu.
|
|
||||||
*/
|
|
||||||
class SettingsSubMenuSearchRobot {
|
|
||||||
fun verifyToolbarText(title: String) {
|
|
||||||
onView(
|
|
||||||
allOf(
|
|
||||||
withId(R.id.navigationToolbar),
|
|
||||||
hasDescendant(withContentDescription(R.string.action_bar_up_description)),
|
|
||||||
hasDescendant(withText(title)),
|
|
||||||
),
|
|
||||||
).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun verifySearchEnginesSectionHeader() {
|
|
||||||
onView(withText("Search engines")).check(matches(isDisplayed()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun verifyDefaultSearchEngineHeader() {
|
|
||||||
defaultSearchEngineHeader
|
|
||||||
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun verifyDefaultSearchEngineSummary(engineName: String) {
|
|
||||||
defaultSearchEngineHeader.check(matches(hasSibling(withText(engineName))))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun verifyManageSearchShortcutsHeader() {
|
|
||||||
manageSearchShortcutsHeader.check(matches(isDisplayed()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun verifyManageShortcutsSummary() {
|
|
||||||
manageSearchShortcutsHeader
|
|
||||||
.check(matches(hasSibling(withText("Edit engines visible in the search menu"))))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun verifyEnginesShortcutsListHeader() =
|
|
||||||
assertUIObjectExists(itemWithText("Engines visible on the search menu"))
|
|
||||||
|
|
||||||
fun verifyAddressBarSectionHeader() {
|
|
||||||
onView(withText("Address bar - Firefox Suggest")).check(matches(isDisplayed()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun verifyDefaultSearchEngineList() {
|
|
||||||
defaultSearchEngineOption("LeOSearch")
|
|
||||||
.check(matches(hasSibling(withId(R.id.engine_icon))))
|
|
||||||
.check(matches(isDisplayed()))
|
|
||||||
defaultSearchEngineOption("DuckDuckGo")
|
|
||||||
.check(matches(hasSibling(withId(R.id.engine_icon))))
|
|
||||||
.check(matches(isDisplayed()))
|
|
||||||
assertUIObjectExists(addSearchEngineButton)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun verifyManageShortcutsList(testRule: ComposeTestRule) {
|
|
||||||
val availableShortcutsEngines = getRegionSearchEnginesList() + getAvailableSearchEngines()
|
|
||||||
|
|
||||||
availableShortcutsEngines.forEach {
|
|
||||||
testRule.onNodeWithText(it.name)
|
|
||||||
.assert(hasAnySibling(hasContentDescription("${it.name} search engine")))
|
|
||||||
.assertIsDisplayed()
|
|
||||||
}
|
|
||||||
|
|
||||||
assertUIObjectExists(addSearchEngineButton)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method that verifies the selected engines inside the Manage search shortcuts list.
|
|
||||||
*/
|
|
||||||
fun verifySearchShortcutChecked(vararg engineShortcut: EngineShortcut) {
|
|
||||||
engineShortcut.forEach {
|
|
||||||
val shortcutIsChecked = mDevice.findObject(UiSelector().text(it.name))
|
|
||||||
.getFromParent(
|
|
||||||
UiSelector().index(it.checkboxIndex),
|
|
||||||
).isChecked
|
|
||||||
|
|
||||||
if (it.isChecked) {
|
|
||||||
assertTrue(shortcutIsChecked)
|
|
||||||
} else {
|
|
||||||
assertFalse(shortcutIsChecked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun verifyAutocompleteURlsIsEnabled(enabled: Boolean) {
|
|
||||||
autocompleteSwitchButton()
|
|
||||||
.check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun verifyShowSearchSuggestionsEnabled(enabled: Boolean) {
|
|
||||||
showSearchSuggestionSwitchButton()
|
|
||||||
.check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun verifyShowSearchSuggestionsInPrivateEnabled(enabled: Boolean) {
|
|
||||||
showSuggestionsInPrivateModeSwitch()
|
|
||||||
.check(
|
|
||||||
matches(
|
|
||||||
hasSibling(
|
|
||||||
withChild(
|
|
||||||
allOf(
|
|
||||||
withClassName(endsWith("CheckBox")),
|
|
||||||
isChecked(enabled),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun verifyShowClipboardSuggestionsEnabled(enabled: Boolean) {
|
|
||||||
showClipboardSuggestionSwitch()
|
|
||||||
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
|
|
||||||
.check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun verifySearchBrowsingHistoryEnabled(enabled: Boolean) {
|
|
||||||
searchHistorySwitchButton()
|
|
||||||
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
|
|
||||||
.check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun verifySearchBookmarksEnabled(enabled: Boolean) {
|
|
||||||
searchBookmarksSwitchButton()
|
|
||||||
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
|
|
||||||
.check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun verifySearchSyncedTabsEnabled(enabled: Boolean) {
|
|
||||||
searchSyncedTabsSwitchButton()
|
|
||||||
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
|
|
||||||
.check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun verifyVoiceSearchEnabled(enabled: Boolean) {
|
|
||||||
voiceSearchSwitchButton()
|
|
||||||
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
|
|
||||||
.check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openDefaultSearchEngineMenu() {
|
|
||||||
defaultSearchEngineHeader.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openManageShortcutsMenu() {
|
|
||||||
manageSearchShortcutsHeader.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun changeDefaultSearchEngine(searchEngineName: String) {
|
|
||||||
onView(withText(searchEngineName))
|
|
||||||
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
|
|
||||||
.perform(click())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun selectSearchShortcut(shortcut: EngineShortcut) {
|
|
||||||
mDevice.findObject(UiSelector().text(shortcut.name))
|
|
||||||
.getFromParent(UiSelector().index(shortcut.checkboxIndex))
|
|
||||||
.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toggleAutocomplete() = autocompleteSwitchButton().click()
|
|
||||||
|
|
||||||
fun toggleShowSearchSuggestions() = showSearchSuggestionSwitchButton().click()
|
|
||||||
|
|
||||||
fun toggleVoiceSearch() {
|
|
||||||
voiceSearchSwitchButton().perform(click())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toggleClipboardSuggestion() {
|
|
||||||
showClipboardSuggestionSwitch().click()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun switchSearchHistoryToggle() = searchHistorySwitchButton().click()
|
|
||||||
|
|
||||||
fun switchSearchBookmarksToggle() = searchBookmarksSwitchButton().click()
|
|
||||||
|
|
||||||
fun switchShowSuggestionsInPrivateSessionsToggle() =
|
|
||||||
showSuggestionsInPrivateModeSwitch().click()
|
|
||||||
|
|
||||||
fun openAddSearchEngineMenu() = addSearchEngineButton.click()
|
|
||||||
|
|
||||||
fun verifyEngineListContains(searchEngineName: String, shouldExist: Boolean) =
|
|
||||||
assertUIObjectExists(itemWithText(searchEngineName), exists = shouldExist)
|
|
||||||
|
|
||||||
fun verifyDefaultSearchEngineSelected(searchEngineName: String) {
|
|
||||||
defaultSearchEngineOption(searchEngineName).check(matches(isChecked(true)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun verifySaveSearchEngineButtonEnabled(enabled: Boolean) {
|
|
||||||
addSearchEngineSaveButton().check(matches(isEnabled(enabled)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveNewSearchEngine() {
|
|
||||||
closeSoftKeyboard()
|
|
||||||
addSearchEngineSaveButton().click()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun typeCustomEngineDetails(engineName: String, engineURL: String) {
|
|
||||||
try {
|
|
||||||
mDevice.findObject(By.res("$packageName:id/edit_engine_name")).clear()
|
|
||||||
mDevice.findObject(By.res("$packageName:id/edit_engine_name")).text = engineName
|
|
||||||
assertUIObjectExists(
|
|
||||||
itemWithResIdAndText("$packageName:id/edit_engine_name", engineName),
|
|
||||||
)
|
|
||||||
|
|
||||||
mDevice.findObject(By.res("$packageName:id/edit_search_string")).clear()
|
|
||||||
mDevice.findObject(By.res("$packageName:id/edit_search_string")).text = engineURL
|
|
||||||
assertUIObjectExists(
|
|
||||||
itemWithResIdAndText("$packageName:id/edit_search_string", engineURL),
|
|
||||||
)
|
|
||||||
} catch (e: AssertionError) {
|
|
||||||
println("The name or the search string were not set properly")
|
|
||||||
|
|
||||||
mDevice.findObject(By.res("$packageName:id/edit_engine_name")).clear()
|
|
||||||
mDevice.findObject(By.res("$packageName:id/edit_engine_name")).setText(engineName)
|
|
||||||
assertUIObjectExists(
|
|
||||||
itemWithResIdAndText("$packageName:id/edit_engine_name", engineName),
|
|
||||||
)
|
|
||||||
mDevice.findObject(By.res("$packageName:id/edit_search_string")).clear()
|
|
||||||
mDevice.findObject(By.res("$packageName:id/edit_search_string")).setText(engineURL)
|
|
||||||
assertUIObjectExists(
|
|
||||||
itemWithResIdAndText("$packageName:id/edit_search_string", engineURL),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun typeSearchEngineSuggestionString(searchSuggestionString: String) {
|
|
||||||
onView(withId(R.id.edit_suggest_string))
|
|
||||||
.click()
|
|
||||||
.perform(clearText())
|
|
||||||
.perform(typeText(searchSuggestionString))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used in the non-Compose Default search engines menu
|
|
||||||
fun openEngineOverflowMenu(searchEngineName: String) {
|
|
||||||
threeDotMenu(searchEngineName).waitForExists(waitingTimeShort)
|
|
||||||
threeDotMenu(searchEngineName).click()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used in the composable Manage shortcuts menu, otherwise the overflow menu is not visible
|
|
||||||
fun openCustomShortcutOverflowMenu(testRule: ComposeTestRule, searchEngineName: String) {
|
|
||||||
testRule.onNode(overflowMenuWithSiblingText(searchEngineName)).performClick()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clickEdit() = onView(withText("Edit")).click()
|
|
||||||
|
|
||||||
// Used in the Default search engine menu
|
|
||||||
fun clickDeleteSearchEngine() =
|
|
||||||
mDevice.findObject(
|
|
||||||
UiSelector().textContains(getStringResource(R.string.search_engine_delete)),
|
|
||||||
).click()
|
|
||||||
|
|
||||||
// Used in the composable Manage search shortcuts menu, otherwise the overflow menu is not visible
|
|
||||||
fun clickDeleteSearchEngine(testRule: ComposeTestRule) =
|
|
||||||
testRule.onNodeWithText("Delete").performClick()
|
|
||||||
|
|
||||||
fun clickUndoSnackBarButton() =
|
|
||||||
mDevice.findObject(
|
|
||||||
UiSelector()
|
|
||||||
.resourceId("$packageName:id/snackbar_btn"),
|
|
||||||
).click()
|
|
||||||
|
|
||||||
fun saveEditSearchEngine() {
|
|
||||||
onView(withId(R.id.save_button)).click()
|
|
||||||
assertUIObjectExists(itemContainingText("Saved"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun verifyInvalidTemplateSearchStringFormatError() {
|
|
||||||
closeSoftKeyboard()
|
|
||||||
onView(withText(getStringResource(R.string.search_add_custom_engine_error_missing_template)))
|
|
||||||
.check(matches(isDisplayed()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun verifyErrorConnectingToSearchString(searchEngineName: String) {
|
|
||||||
closeSoftKeyboard()
|
|
||||||
onView(withText(getStringResource(R.string.search_add_custom_engine_error_cannot_reach, searchEngineName)))
|
|
||||||
.check(matches(isDisplayed()))
|
|
||||||
}
|
|
||||||
|
|
||||||
class Transition {
|
|
||||||
fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
|
|
||||||
mDevice.waitForIdle()
|
|
||||||
goBackButton().perform(click())
|
|
||||||
|
|
||||||
SettingsRobot().interact()
|
|
||||||
return SettingsRobot.Transition()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clickCustomSearchStringLearnMoreLink(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
|
|
||||||
onView(withId(R.id.custom_search_engines_learn_more)).click()
|
|
||||||
|
|
||||||
BrowserRobot().interact()
|
|
||||||
return BrowserRobot.Transition()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clickCustomSearchSuggestionsLearnMoreLink(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
|
|
||||||
onView(withId(R.id.custom_search_suggestions_learn_more)).click()
|
|
||||||
|
|
||||||
BrowserRobot().interact()
|
|
||||||
return BrowserRobot.Transition()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Matches search shortcut items inside the 'Manage search shortcuts' menu
|
|
||||||
* @param name, of type String, should be the name of the search engine.
|
|
||||||
* @param checkboxIndex, of type Int, is the checkbox' index afferent to the search engine.
|
|
||||||
* @param isChecked, of type Boolean, should show if the checkbox is expected to be checked.
|
|
||||||
*/
|
|
||||||
class EngineShortcut(
|
|
||||||
val name: String,
|
|
||||||
val checkboxIndex: Int,
|
|
||||||
val isChecked: Boolean = true,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val defaultSearchEngineHeader = onView(withText("Default search engine"))
|
|
||||||
|
|
||||||
private val manageSearchShortcutsHeader = onView(withText("Manage alternative search engines"))
|
|
||||||
|
|
||||||
private fun searchHistorySwitchButton(): ViewInteraction {
|
|
||||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(
|
|
||||||
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
|
|
||||||
hasDescendant(withText("Search browsing history")),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return onView(withText("Search browsing history"))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun searchBookmarksSwitchButton(): ViewInteraction {
|
|
||||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(
|
|
||||||
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
|
|
||||||
hasDescendant(withText("Search bookmarks")),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return onView(withText("Search bookmarks"))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun searchSyncedTabsSwitchButton(): ViewInteraction {
|
|
||||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(
|
|
||||||
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
|
|
||||||
hasDescendant(withText("Search synced tabs")),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return onView(withText("Search synced tabs"))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun voiceSearchSwitchButton(): ViewInteraction {
|
|
||||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(
|
|
||||||
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
|
|
||||||
hasDescendant(withText("Show voice search")),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return onView(withText("Show voice search"))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun autocompleteSwitchButton(): ViewInteraction {
|
|
||||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(
|
|
||||||
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
|
|
||||||
hasDescendant(withText(getStringResource(R.string.preferences_enable_autocomplete_urls))),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return onView(withText(getStringResource(R.string.preferences_enable_autocomplete_urls)))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showSearchSuggestionSwitchButton(): ViewInteraction {
|
|
||||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(
|
|
||||||
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
|
|
||||||
hasDescendant(withText("Show search suggestions")),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return onView(withText("Show search suggestions"))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showClipboardSuggestionSwitch(): ViewInteraction {
|
|
||||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(
|
|
||||||
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
|
|
||||||
hasDescendant(withText(getStringResource(R.string.preferences_show_clipboard_suggestions))),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return onView(withText(getStringResource(R.string.preferences_show_clipboard_suggestions)))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showSuggestionsInPrivateModeSwitch(): ViewInteraction {
|
|
||||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(
|
|
||||||
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
|
|
||||||
hasDescendant(withText(getStringResource(R.string.preferences_show_search_suggestions_in_private))),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return onView(withText(getStringResource(R.string.preferences_show_search_suggestions_in_private)))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun goBackButton() =
|
|
||||||
onView(CoreMatchers.allOf(withContentDescription("Navigate up")))
|
|
||||||
|
|
||||||
private val addSearchEngineButton = mDevice.findObject(UiSelector().text("Add search engine"))
|
|
||||||
|
|
||||||
private fun addSearchEngineSaveButton() = onView(withId(R.id.save_button))
|
|
||||||
|
|
||||||
private fun threeDotMenu(searchEngineName: String) =
|
|
||||||
mDevice.findObject(UiSelector().text(searchEngineName))
|
|
||||||
.getFromParent(UiSelector().description("More options"))
|
|
||||||
|
|
||||||
private fun defaultSearchEngineOption(searchEngineName: String) =
|
|
||||||
onView(
|
|
||||||
allOf(
|
|
||||||
withId(R.id.radio_button),
|
|
||||||
hasSibling(withText(searchEngineName)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun overflowMenuWithSiblingText(text: String): SemanticsMatcher =
|
|
||||||
hasAnySibling(hasText(text)) and hasContentDescription("More options")
|
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit 2f135938b7842257f108e0b4274a6f5bd0f86189
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,8 @@
|
||||||
|
# Maven group ID used for all components
|
||||||
|
componentsGroupId: "org.mozilla.components"
|
||||||
|
|
||||||
|
# Synchronized build configuration for all modules
|
||||||
|
jvmTargetCompatibility: 17
|
||||||
|
compileSdkVersion: 35
|
||||||
|
minSdkVersion: 21
|
||||||
|
targetSdkVersion: 34
|
|
@ -0,0 +1,15 @@
|
||||||
|
# This is an .editorconfig that ktlint will "stop" at.
|
||||||
|
|
||||||
|
# If an .editorconfig file exists in any parent directory of the checkout
|
||||||
|
# directory, ktlint will fail because it uses those settings to determine what
|
||||||
|
# the indentation should be.
|
||||||
|
|
||||||
|
root = True
|
||||||
|
|
||||||
|
[*.kt]
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
|
||||||
|
ij_kotlin_allow_trailing_comma_on_call_site=true
|
||||||
|
ij_kotlin_allow_trailing_comma=true
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
# Treat generated API docs as binary. We do not want them to pollute
|
||||||
|
# our diffs.
|
||||||
|
docs/api/**/* -text -diff linguist-generated=true
|
||||||
|
|
||||||
|
# Treat the public suffix list file as text
|
||||||
|
**/publicsuffixes diff
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
,ich,pop-os,12.10.2024 11:30,file:///home/ich/.config/libreoffice/4;
|
|
@ -0,0 +1,2 @@
|
||||||
|
The changelog is now hosted here:
|
||||||
|
https://mozilla-mobile.github.io/android-components/changelog/
|
|
@ -0,0 +1,325 @@
|
||||||
|
# Android components
|
||||||
|
|
||||||
|
[![Task Status](https://firefox-ci-tc.services.mozilla.com/api/github/v1/repository/mozilla-mobile/android-components/main/badge.svg)](https://firefox-ci-tc.services.mozilla.com/api/github/v1/repository/mozilla-mobile/android-components/main/latest)
|
||||||
|
[![Mergify Status](https://img.shields.io/endpoint.svg?url=https://gh.mergify.io/badges/mozilla-mobile/android-components&style=flat)](https://mergify.io)
|
||||||
|
[![chat.mozilla.org](https://img.shields.io/badge/chat-on%20matrix-51bb9c)](https://chat.mozilla.org/#/room/#android-components:mozilla.org)
|
||||||
|
|
||||||
|
_A collection of Android libraries to build browsers or browser-like applications._
|
||||||
|
|
||||||
|
ℹ️ For more information **[see the website](https://mozilla-mobile.github.io/android-components/)**.
|
||||||
|
|
||||||
|
A fully-featured reference browser implementation based on the components can be found in the [reference-browser repository](https://github.com/mozilla-mobile/reference-browser).
|
||||||
|
|
||||||
|
# Getting Involved
|
||||||
|
|
||||||
|
We encourage you to participate in this open source project. We love pull requests, bug reports, ideas, (security) code reviews or any kind of positive contribution.
|
||||||
|
|
||||||
|
Before you attempt to make a contribution please read the [Community Participation Guidelines](https://www.mozilla.org/en-US/about/governance/policies/participation/).
|
||||||
|
|
||||||
|
* Matrix: [android-components:mozilla.org chat room](https://chat.mozilla.org/#/room/#android-components:mozilla.org) ([How to connect](https://wiki.mozilla.org/Matrix#Connect_to_Matrix)).
|
||||||
|
|
||||||
|
* Localization happens on [Pontoon](https://pontoon.mozilla.org/projects/android-l10n/). Please get in touch with delphine (at) mozilla (dot) com directly for more information.
|
||||||
|
|
||||||
|
# Maven repository
|
||||||
|
|
||||||
|
All components are getting published on [maven.mozilla.org](https://maven.mozilla.org/).
|
||||||
|
To use them, you need to add the following to your project's top-level build file, in the `allprojects` block (see e.g. the [reference-browser](https://github.com/mozilla-mobile/reference-browser/blob/main/build.gradle)):
|
||||||
|
|
||||||
|
```groovy
|
||||||
|
repositories {
|
||||||
|
maven {
|
||||||
|
url "https://maven.mozilla.org/maven2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each module that uses a component needs to specify it in its build file, in the `dependencies` block. For example, to use the `Base` component (in the `support`) collection, you need:
|
||||||
|
|
||||||
|
```groovy
|
||||||
|
dependencies {
|
||||||
|
implementation 'org.mozilla.components:support-base:+'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nightly builds
|
||||||
|
|
||||||
|
Nightly builds are created every day from the `main` branch and published on [nightly.maven.mozilla.org](https://nightly.maven.mozilla.org).
|
||||||
|
|
||||||
|
# Components
|
||||||
|
|
||||||
|
* 🔴 **In Development** - Not ready to be used in shipping products.
|
||||||
|
* ⚪ **Preview** - This component is almost/partially ready and can be tested in products.
|
||||||
|
* 🔵 **Production ready** - Used by shipping products.
|
||||||
|
|
||||||
|
## Browser
|
||||||
|
|
||||||
|
High-level components for building browser(-like) apps.
|
||||||
|
|
||||||
|
* 🔵 [**Awesomebar**](components/browser/awesomebar/README.md) - A customizable [Awesome Bar](https://support.mozilla.org/en-US/kb/awesome-bar-search-firefox-bookmarks-history-tabs) implementation for browsers.
|
||||||
|
|
||||||
|
* 🔵 [**Domains**](components/browser/domains/README.md) Localized and customizable domain lists for auto-completion in browsers.
|
||||||
|
|
||||||
|
* 🔵 [**Engine-Gecko**](components/browser/engine-gecko/README.md) - *Engine* implementation based on [GeckoView](https://wiki.mozilla.org/Mobile/GeckoView).
|
||||||
|
|
||||||
|
* 🔵 [**Engine-System**](components/browser/engine-system/README.md) - *Engine* implementation based on the system's WebView.
|
||||||
|
|
||||||
|
* 🔵 [**Errorpages**](components/browser/errorpages/README.md) - Responsive browser error pages for Android apps.
|
||||||
|
|
||||||
|
* 🔵 [**Icons**](components/browser/icons/README.md) - A component for loading and storing website icons (like [Favicons](https://en.wikipedia.org/wiki/Favicon)).
|
||||||
|
|
||||||
|
* 🔵 [**Menu**](components/browser/menu/README.md) - A generic menu with customizable items primarily for browser toolbars.
|
||||||
|
|
||||||
|
* ⚪ [**Menu 2**](components/browser/menu2/README.md) - A generic menu with customizable items primarily for browser toolbars.
|
||||||
|
|
||||||
|
* 🔵 [**Session-Storage**](components/browser/session-storage/README.md) - Component for saving and restoring the browser state.
|
||||||
|
|
||||||
|
* 🔵 [**State**](components/browser/state/README.md) - Component for maintaining the centralized state of the browser and its components.
|
||||||
|
|
||||||
|
* 🔵 [**Storage-Sync**](components/browser/storage-sync/README.md) - A syncable implementation of browser storage backed by [application-services' Places lib](https://github.com/mozilla/application-services).
|
||||||
|
|
||||||
|
* 🔵 [**Tabstray**](components/browser/tabstray/README.md) - A customizable tabs tray for browsers.
|
||||||
|
|
||||||
|
* 🔵 [**Thumbnails**](components/browser/thumbnails/README.md) - A component for loading and storing website thumbnails (screenshot of the website).
|
||||||
|
|
||||||
|
* 🔵 [**Toolbar**](components/browser/toolbar/README.md) - A customizable toolbar for browsers.
|
||||||
|
|
||||||
|
## Concept
|
||||||
|
|
||||||
|
_API contracts and abstraction layers for browser components._
|
||||||
|
|
||||||
|
* 🔵 [**Awesomebar**](components/concept/awesomebar/README.md) - An abstract definition of an awesome bar component.
|
||||||
|
|
||||||
|
* 🔵 [**Engine**](components/concept/engine/README.md) - Abstraction layer that allows hiding the actual browser engine implementation.
|
||||||
|
|
||||||
|
* 🔵 [**Fetch**](components/concept/fetch/README.md) - An abstract definition of an HTTP client for fetching resources.
|
||||||
|
|
||||||
|
* 🔵 [**Push**](components/concept/push/README.md) - An abstract definition of a push service component.
|
||||||
|
|
||||||
|
* 🔵 [**Storage**](components/concept/storage/README.md) - Abstract definition of a browser storage component.
|
||||||
|
|
||||||
|
* 🔵 [**Tabstray**](components/concept/tabstray/README.md) - Abstract definition of a tabs tray component.
|
||||||
|
|
||||||
|
* 🔵 [**Toolbar**](components/concept/toolbar/README.md) - Abstract definition of a browser toolbar component.
|
||||||
|
|
||||||
|
## Feature
|
||||||
|
|
||||||
|
_Combined components to implement feature-specific use cases._
|
||||||
|
|
||||||
|
* 🔵 [**Accounts**](components/feature/accounts/README.md) - A component that connects an FxaAccountManager from [service-firefox-accounts](components/service/firefox-accounts/README.md) with [feature-tabs](components/feature/tabs/README.md) in order to facilitate authentication flows.
|
||||||
|
|
||||||
|
* 🔵 [**Accounts Push**](components/feature/accounts-push/README.md) - Feature of use cases for FxA Account that work with push support.
|
||||||
|
|
||||||
|
* 🔵 [**Autofill**](components/feature/autofill/README.md) - A component that provides support for Android's Autofill framework.
|
||||||
|
|
||||||
|
* 🔵 [**Awesomebar**](components/feature/awesomebar/README.md) - A component that connects a [concept-awesomebar](components/concept/awesomebar/README.md) implementation to a [concept-toolbar](components/concept/toolbar/README.md) implementation and provides implementations of various suggestion providers.
|
||||||
|
|
||||||
|
* 🔴 [**Containers**](components/feature/containers/README.md) - A component for working with contextual identities also known as containers.
|
||||||
|
|
||||||
|
* 🔵 [**Context Menu**](components/feature/contextmenu/README.md) - A component for displaying context menus when *long-pressing* web content.
|
||||||
|
|
||||||
|
* 🔵 [**Custom Tabs**](components/feature/customtabs/README.md) - A component for providing [Custom Tabs](https://developer.chrome.com/multidevice/android/customtabs) functionality in browsers.
|
||||||
|
|
||||||
|
* 🔵 [**Downloads**](components/feature/downloads/README.md) - A component to perform downloads using the [Android downloads manager](https://developer.android.com/reference/android/app/DownloadManager).
|
||||||
|
|
||||||
|
* 🔵 [**Intent**](components/feature/intent/README.md) - A component that provides intent processing functionality by combining various other feature modules.
|
||||||
|
|
||||||
|
* ⚪ [**Progressive Web Apps (PWA)**](components/feature/pwa/README.md) - A component that provides functionality for supporting Progressive Web Apps (PWA).
|
||||||
|
|
||||||
|
* 🔵 [**Reader View**](components/feature/readerview/README.md) - A component that provides Reader View functionality.
|
||||||
|
|
||||||
|
* 🔵 [**QR**](components/feature/qr/README.md) - A component that provides functionality for scanning QR codes.
|
||||||
|
|
||||||
|
* 🔵 [**Search**](components/feature/search/README.md) - A component that connects an (concept) engine implementation with the browser search module.
|
||||||
|
|
||||||
|
* 🔵 [**Session**](components/feature/session/README.md) - A component that connects an (concept) engine implementation with the browser session and storage modules.
|
||||||
|
|
||||||
|
* 🔵 [**Share**](components/feature/share/README.md) - Feature implementation for saving and sorting recent apps used for sharing.
|
||||||
|
|
||||||
|
* 🔵 [**Sync**](components/feature/sync/README.md) -A component that provides synchronization orchestration for groups of (concept) SyncableStore objects.
|
||||||
|
|
||||||
|
* 🔵 [**Tabs**](components/feature/tabs/README.md) - A component that connects a tabs tray implementation with the session and toolbar modules.
|
||||||
|
|
||||||
|
* 🔵 [**Tab Collections**](components/feature/tab-collections/README.md) - Feature implementation for saving, restoring and organizing collections of tabs.
|
||||||
|
|
||||||
|
* 🔵 [**Toolbar**](components/feature/toolbar/README.md) - A component that connects a (concept) toolbar implementation with the browser session module.
|
||||||
|
|
||||||
|
* 🔵 [**Top Sites**](components/feature/top-sites/README.md) - Feature implementation for saving and removing top sites.
|
||||||
|
|
||||||
|
* 🔵 [**Prompts**](components/feature/prompts/README.md) - A component that will handle all the common prompt dialogs from web content.
|
||||||
|
|
||||||
|
* 🔵 [**Push**](components/feature/push/README.md) - A component that provides Autopush messages with help from a supported push service.
|
||||||
|
|
||||||
|
* 🔵 [**Find In Page**](components/feature/findinpage/README.md) - A component that provides an UI widget for [find in page functionality](https://support.mozilla.org/en-US/kb/search-contents-current-page-text-or-links).
|
||||||
|
|
||||||
|
* 🔵 [**Remote Tabs**](components/feature/remotetabs/README.md) - Feature that provides access to other device's tabs in the same account.
|
||||||
|
|
||||||
|
* 🔵 [**Site Permissions**](components/feature/sitepermissions/README.md) - A feature for showing site permission request prompts.
|
||||||
|
|
||||||
|
* 🔵 [**WebAuthn**](components/feature/webauthn/README.md) - A feature that provides WebAuthn functionality for supported engines.
|
||||||
|
|
||||||
|
* 🔵 [**Web Notifications**](components/feature/webnotifications/README.md) - A component for displaying web notifications.
|
||||||
|
|
||||||
|
* 🔵 [**WebCompat**](components/feature/webcompat/README.md) - A feature to enable website-hotfixing via the Web Compatibility System-Addon.
|
||||||
|
|
||||||
|
* 🔵 [**WebCompat Reporter**](components/feature/webcompat-reporter/README.md) - A feature that enables users to report site issues to Mozilla's Web Compatibility team for further diagnosis.
|
||||||
|
|
||||||
|
* 🔵 [**Web Add-ons**](components/feature/addons/README.md) - A feature that provides functionality for managing add-ons.
|
||||||
|
|
||||||
|
## UI
|
||||||
|
|
||||||
|
_Generic low-level UI components for building apps._
|
||||||
|
|
||||||
|
* 🔵 [**Autocomplete**](components/ui/autocomplete/README.md) - A set of components to provide autocomplete functionality.
|
||||||
|
|
||||||
|
* 🔵 [**Colors**](components/ui/colors/README.md) - The standard set of [Photon](https://design.firefox.com/photon/) colors.
|
||||||
|
|
||||||
|
* 🔵 [**Fonts**](components/ui/fonts/README.md) - The standard set of fonts used by Mozilla Android products.
|
||||||
|
|
||||||
|
* 🔵 [**Icons**](components/ui/icons/README.md) - A collection of often used browser icons.
|
||||||
|
|
||||||
|
* 🔵 [**Tabcounter**](components/ui/tabcounter/README.md) - A button that shows the current tab count and can animate state changes.
|
||||||
|
|
||||||
|
## Service
|
||||||
|
|
||||||
|
_Components and libraries to interact with backend services._
|
||||||
|
|
||||||
|
* 🔵 [**Firefox Accounts (FxA)**](components/service/firefox-accounts/README.md) - A library for integrating with Firefox Accounts.
|
||||||
|
|
||||||
|
* 🔵 [**Firefox Sync - Logins**](components/service/sync-logins/README.md) - A library for integrating with Firefox Sync - Logins.
|
||||||
|
|
||||||
|
* 🔵 [**Firefox Sync - Autofill**](components/service/sync-autofill/README.md) - A library for integrating with Firefox Sync - Autofill.
|
||||||
|
|
||||||
|
* 🔵 [**Glean**](components/service/glean/README.md) - A client-side telemetry SDK for collecting metrics and sending them to Mozilla's telemetry service (eventually replacing [service-telemetry](components/service/telemetry/README.md)).
|
||||||
|
|
||||||
|
* 🔵 [**Location**](components/service/location/README.md) - A library for accessing Mozilla's and other location services.
|
||||||
|
|
||||||
|
* 🔴 [**Nimbus**](components/service/nimbus/README.md) - A wrapper for the Nimbus SDK.
|
||||||
|
|
||||||
|
* 🔵 [**Pocket**](components/service/pocket/README.md) - A library for communicating with the Pocket API.
|
||||||
|
|
||||||
|
* 🔵 [**Contile**](components/service/contile/README.md) - A library for communicating with the Contile services API.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
_Supporting components with generic helper code._
|
||||||
|
|
||||||
|
* 🔵 [**Android Test**](components/support/android-test/README.md) - A collection of helpers for testing components in instrumented (on device) tests (`src/androidTest`).
|
||||||
|
|
||||||
|
* 🔵 [**Base**](components/support/base/README.md) - Base component containing building blocks for components.
|
||||||
|
|
||||||
|
* 🔵 [**Ktx**](components/support/ktx/README.md) - A set of Kotlin extensions on top of the Android framework and Kotlin standard library.
|
||||||
|
|
||||||
|
* 🔵 [**Test**](components/support/test/README.md) - A collection of helpers for testing components in local unit tests (`src/test`).
|
||||||
|
|
||||||
|
* 🔵 [**Test Appservices**](components/support/test-appservices/README.md) - A component for synchronizing Application Services' unit testing dependencies used in Android Components.
|
||||||
|
|
||||||
|
* 🔵 [**Test LibState**](components/support/test-libstate/README.md) - A collection of helpers for testing functionality that relies on the lib-state component in local unit tests (`src/test`).
|
||||||
|
|
||||||
|
* 🔵 [**Utils**](components/support/utils/README.md) - Generic utility classes to be shared between projects.
|
||||||
|
|
||||||
|
* 🔵 [**Webextensions**](components/support/webextensions/README.md) - A component containing building blocks for features implemented as web extensions.
|
||||||
|
|
||||||
|
## Standalone libraries
|
||||||
|
|
||||||
|
* 🔵 [**Crash**](components/lib/crash/README.md) - A generic crash reporter component that can report crashes to multiple services.
|
||||||
|
|
||||||
|
* 🔵 [**Dataprotect**](components/lib/dataprotect/README.md) - A component using AndroidKeyStore to protect user data.
|
||||||
|
|
||||||
|
* 🔵 [**Fetch-HttpURLConnection**](components/lib/fetch-httpurlconnection/README.md) - A [concept-fetch](concept/fetch/README.md) implementation using [HttpURLConnection](https://developer.android.com/reference/java/net/HttpURLConnection.html).
|
||||||
|
|
||||||
|
* 🔵 [**Fetch-OkHttp**](components/lib/fetch-okhttp/README.md) - A [concept-fetch](concept/fetch/README.md) implementation using [OkHttp](https://github.com/square/okhttp).
|
||||||
|
|
||||||
|
* ⚪ [**JEXL**](components/lib/jexl/README.md) - Javascript Expression Language: Context-based expression parser and evaluator.
|
||||||
|
|
||||||
|
* 🔵 [**Public Suffix List**](components/lib/publicsuffixlist/README.md) - A library for reading and using the [public suffix list](https://publicsuffix.org/).
|
||||||
|
|
||||||
|
* 🔵 [**Push-Firebase**](components/lib/push-firebase/README.md) - A [concept-push](concept/push/README.md) implementation using [Firebase Cloud Messaging](https://firebase.google.com/products/cloud-messaging/).
|
||||||
|
|
||||||
|
* 🔵 [**State**](components/lib/state/README.md) - A library for maintaining application state.
|
||||||
|
|
||||||
|
## Tooling
|
||||||
|
|
||||||
|
* 🔵 [**Fetch-Tests**](components/tooling/fetch-tests/README.md) - A generic test suite for components that implement [concept-fetch](concept/fetch/README.md).
|
||||||
|
|
||||||
|
* 🔵 [**Lint**](components/tooling/lint/README.md) - Custom Lint rules for the components repository.
|
||||||
|
|
||||||
|
# Sample apps
|
||||||
|
|
||||||
|
_Sample apps using various components._
|
||||||
|
|
||||||
|
* [**Browser**](samples/browser) - A simple browser composed from browser components. This sample application is only a very basic browser. For a full-featured reference browser implementation see the **[reference-browser repository](https://github.com/mozilla-mobile/reference-browser)**.
|
||||||
|
|
||||||
|
* [**Crash**](samples/crash) - An app showing the integration of the `lib-crash` component.
|
||||||
|
|
||||||
|
* [**Firefox Accounts (FxA)**](samples/firefox-accounts) - A simple app demoing Firefox Accounts integration.
|
||||||
|
|
||||||
|
* [**Firefox Sync**](samples/sync) - A simple app demoing general Firefox Sync integration, with bookmarks and history.
|
||||||
|
|
||||||
|
* [**Firefox Sync - Logins**](samples/sync-logins) - A simple app demoing Firefox Sync (Logins) integration.
|
||||||
|
|
||||||
|
* [**DataProtect**](samples/dataprotect) - An app demoing how to use the [**Dataprotect**](components/lib/dataprotect/README.md) component to load and store encrypted data in `SharedPreferences`.
|
||||||
|
|
||||||
|
* [**Glean**](samples/glean) - An app demoing how to use the [**Glean**](components/service/glean/README.md) library to collect and send telemetry data.
|
||||||
|
|
||||||
|
* [**Toolbar**](samples/toolbar) - An app demoing multiple customized toolbars using the [**browser-toolbar**](components/browser/toolbar/README.md) component.
|
||||||
|
|
||||||
|
# Building #
|
||||||
|
|
||||||
|
## Command line ##
|
||||||
|
|
||||||
|
```
|
||||||
|
$ git clone https://github.com/mozilla-mobile/android-components.git
|
||||||
|
$ cd android-components
|
||||||
|
$ ./gradlew assemble
|
||||||
|
```
|
||||||
|
|
||||||
|
## Android Studio ##
|
||||||
|
|
||||||
|
If the environment variable `JAVA_HOME` is not defined, you will need to set it. If you would like to use the JDK installed by Android Studio, here's how to find it:
|
||||||
|
|
||||||
|
1. Open Android Studio.
|
||||||
|
2. Select "Configure".
|
||||||
|
3. Select "Default Project Structure". You should now see the Android JDK location.
|
||||||
|
4. Set the environment variable `JAVA_HOME` to the location. (How you set an environment variable depends on your OS.)
|
||||||
|
5. Restart Android Studio.
|
||||||
|
|
||||||
|
Once the environment variable is set, you can import the project into Android Studio with the default wizard options.
|
||||||
|
|
||||||
|
If your build fails, you may find you get more instructive error messages by attempting the build at the command line.
|
||||||
|
|
||||||
|
# Coding Standards #
|
||||||
|
|
||||||
|
## Style ##
|
||||||
|
We follow the style enforced by [ktlint](https://ktlint.github.io/) and [detekt](https://github.com/detekt/detekt). See [how to configure Android Studio appropriately](https://github.com/pinterest/ktlint#option-1-recommended).
|
||||||
|
|
||||||
|
To check your style, run:
|
||||||
|
|
||||||
|
```
|
||||||
|
./gradlew ktlint
|
||||||
|
./gradlew detekt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation ##
|
||||||
|
We use `README.md` files for each component.
|
||||||
|
|
||||||
|
If you fix a bug or change an API, you should update [docs/changelog.md](https://github.com/mozilla-mobile/android-components/blob/main/docs/changelog.md).
|
||||||
|
|
||||||
|
## Testing ##
|
||||||
|
You are expected to both add tests for code that you write and make sure that your changes do not
|
||||||
|
cause existing tests to fail. You may find these command lines helpful:
|
||||||
|
|
||||||
|
```
|
||||||
|
./gradlew test # Run all tests
|
||||||
|
./gradlew :support-ktx:testdebugunittest # Run unit tests for a specified module
|
||||||
|
```
|
||||||
|
|
||||||
|
See also [how to measure code coverage](https://mozac.org/contributing/code-coverage).
|
||||||
|
|
||||||
|
## Accessibility ##
|
||||||
|
If your code has user-facing changes, follow [Android accessibility best practices](https://github.com/mozilla-mobile/shared-docs/blob/main/android/accessibility_guide.md).
|
||||||
|
|
||||||
|
# License
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/
|
|
@ -0,0 +1,19 @@
|
||||||
|
android {
|
||||||
|
lint {
|
||||||
|
warningsAsErrors true
|
||||||
|
abortOnError (project.name != "support-test")
|
||||||
|
|
||||||
|
// With our L10N process its totally possible to have missing or (temporarily) extra translations.
|
||||||
|
disable 'MissingTranslation',
|
||||||
|
'ExtraTranslation',
|
||||||
|
// We do not want to enforce this as a generic rule for all languages (see #6117, #6056, #6118)
|
||||||
|
'TypographyEllipsis',
|
||||||
|
// https://github.com/mozilla-mobile/android-components/issues/10641
|
||||||
|
'UnspecifiedImmutableFlag',
|
||||||
|
// https://github.com/mozilla-mobile/android-components/issues/10643
|
||||||
|
'UnusedResources',
|
||||||
|
// "We do not impose rules on locales"
|
||||||
|
// https://github.com/mozilla-mobile/android-components/pull/11069
|
||||||
|
'TypographyDashes'
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
|
||||||
|
# Purpose: Publish android packages to local maven repo, but only if changed since last publish.
|
||||||
|
# Dependencies: None
|
||||||
|
# Usage: ./automation/publish_to_maven_local_if_modified.py
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def fatal_err(msg):
|
||||||
|
print(f"\033[31mError: {msg}\033[0m")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def run_cmd_checked(*args, **kwargs):
|
||||||
|
"""Run a command, throwing an exception if it exits with non-zero status."""
|
||||||
|
kwargs["check"] = True
|
||||||
|
return subprocess.run(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def find_project_root():
|
||||||
|
"""Find the absolute path of the project repository root."""
|
||||||
|
# As a convention, we expect this file in [project-root]/automation/.
|
||||||
|
automation_dir = Path(__file__).parent
|
||||||
|
|
||||||
|
# Therefore the automation dir's parent is the project root we're looking for.
|
||||||
|
return automation_dir.parent
|
||||||
|
|
||||||
|
|
||||||
|
LAST_CONTENTS_HASH_FILE = ".lastAutoPublishContentsHash"
|
||||||
|
|
||||||
|
GITIGNORED_FILES_THAT_AFFECT_THE_BUILD = ["local.properties"]
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Publish android packages to local maven repo, but only if changed since last publish"
|
||||||
|
)
|
||||||
|
parser.parse_args()
|
||||||
|
|
||||||
|
root_dir = find_project_root()
|
||||||
|
if str(root_dir) != os.path.abspath(os.curdir):
|
||||||
|
fatal_err(
|
||||||
|
f"This only works if run from the repo root ({root_dir!r} != {os.path.abspath(os.curdir)!r})"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Calculate a hash reflecting the current state of the repo.
|
||||||
|
|
||||||
|
contents_hash = hashlib.sha256()
|
||||||
|
|
||||||
|
contents_hash.update(
|
||||||
|
run_cmd_checked(["git", "rev-parse", "HEAD"], capture_output=True).stdout
|
||||||
|
)
|
||||||
|
contents_hash.update(b"\x00")
|
||||||
|
|
||||||
|
# Get a diff of all tracked (staged and unstaged) files.
|
||||||
|
|
||||||
|
changes = run_cmd_checked(["git", "diff", "HEAD", "."], capture_output=True).stdout
|
||||||
|
contents_hash.update(changes)
|
||||||
|
contents_hash.update(b"\x00")
|
||||||
|
|
||||||
|
# But unfortunately it can only tell us the names of untracked
|
||||||
|
# files, and it won't tell us anything about files that are in
|
||||||
|
# .gitignore but can still affect the build.
|
||||||
|
|
||||||
|
untracked_files = []
|
||||||
|
|
||||||
|
# Get a list of all untracked files sans standard exclusions.
|
||||||
|
|
||||||
|
# -o is for getting other (i.e. untracked) files
|
||||||
|
# --exclude-standard is to handle standard Git exclusions: .git/info/exclude, .gitignore in each directory,
|
||||||
|
# and the user's global exclusion file.
|
||||||
|
changes_others = run_cmd_checked(
|
||||||
|
["git", "ls-files", "-o", "--exclude-standard"], capture_output=True
|
||||||
|
).stdout
|
||||||
|
changes_lines = iter(ln.strip() for ln in changes_others.split(b"\n"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
ln = next(changes_lines)
|
||||||
|
while ln:
|
||||||
|
untracked_files.append(ln)
|
||||||
|
ln = next(changes_lines)
|
||||||
|
except StopIteration:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Then, account for some excluded files that we care about.
|
||||||
|
untracked_files.extend(GITIGNORED_FILES_THAT_AFFECT_THE_BUILD)
|
||||||
|
|
||||||
|
# Finally, get hashes of everything.
|
||||||
|
# Skip files that don't exist, e.g. missing GITIGNORED_FILES_THAT_AFFECT_THE_BUILD. `hash-object` errors out if it gets
|
||||||
|
# a non-existent file, so we hope that disk won't change between this filter and the cmd run just below.
|
||||||
|
filtered_untracked = [nm for nm in untracked_files if os.path.isfile(nm)]
|
||||||
|
# Reading contents of the files is quite slow when there are lots of them, so delegate to `git hash-object`.
|
||||||
|
git_hash_object_cmd = ["git", "hash-object"]
|
||||||
|
git_hash_object_cmd.extend(filtered_untracked)
|
||||||
|
changes_untracked = run_cmd_checked(git_hash_object_cmd, capture_output=True).stdout
|
||||||
|
contents_hash.update(changes_untracked)
|
||||||
|
contents_hash.update(b"\x00")
|
||||||
|
|
||||||
|
contents_hash = contents_hash.hexdigest()
|
||||||
|
|
||||||
|
# If the contents hash has changed since last publish, re-publish.
|
||||||
|
last_contents_hash = ""
|
||||||
|
try:
|
||||||
|
with open(LAST_CONTENTS_HASH_FILE) as f:
|
||||||
|
last_contents_hash = f.read().strip()
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if contents_hash == last_contents_hash:
|
||||||
|
print("Contents have not changed, no need to publish")
|
||||||
|
else:
|
||||||
|
print("Contents have changed, publishing")
|
||||||
|
if sys.platform.startswith("win"):
|
||||||
|
run_cmd_checked(
|
||||||
|
["gradlew.bat", "publishToMavenLocal", f"-Plocal={time.time_ns()}"],
|
||||||
|
shell=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
run_cmd_checked(
|
||||||
|
["./gradlew", "publishToMavenLocal", f"-Plocal={time.time_ns()}"]
|
||||||
|
)
|
||||||
|
with open(LAST_CONTENTS_HASH_FILE, "w") as f:
|
||||||
|
f.write(contents_hash)
|
||||||
|
f.write("\n")
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Google Cloud Documentation: https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run
|
||||||
|
# Flank Documentation: https://flank.github.io/flank/
|
||||||
|
gcloud:
|
||||||
|
results-bucket: android-components_test_artifacts
|
||||||
|
record-video: true
|
||||||
|
timeout: 30m
|
||||||
|
async: false
|
||||||
|
num-flaky-test-attempts: 2
|
||||||
|
|
||||||
|
app: /APP/PATH
|
||||||
|
test: /TEST/PATH
|
||||||
|
|
||||||
|
auto-google-login: false
|
||||||
|
use-orchestrator: true
|
||||||
|
environment-variables:
|
||||||
|
clearPackageData: true
|
||||||
|
directories-to-pull:
|
||||||
|
- /sdcard/screenshots
|
||||||
|
performance-metrics: true
|
||||||
|
|
||||||
|
device:
|
||||||
|
- model: Pixel2.arm
|
||||||
|
version: 28
|
||||||
|
locale: en_US
|
||||||
|
|
||||||
|
flank:
|
||||||
|
project: GOOGLE_PROJECT
|
||||||
|
max-test-shards: -1
|
||||||
|
num-test-runs: 1
|
||||||
|
output-style: compact
|
||||||
|
full-junit-result: true
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Google Cloud Documentation: https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run
|
||||||
|
# Flank Documentation: https://flank.github.io/flank/
|
||||||
|
gcloud:
|
||||||
|
results-bucket: android-components_test_artifacts
|
||||||
|
record-video: true
|
||||||
|
timeout: 30m
|
||||||
|
async: false
|
||||||
|
num-flaky-test-attempts: 1
|
||||||
|
|
||||||
|
app: /app/path
|
||||||
|
test: /test/path
|
||||||
|
|
||||||
|
auto-google-login: false
|
||||||
|
use-orchestrator: true
|
||||||
|
environment-variables:
|
||||||
|
clearPackageData: true
|
||||||
|
directories-to-pull:
|
||||||
|
- /sdcard/screenshots
|
||||||
|
performance-metrics: true
|
||||||
|
|
||||||
|
device:
|
||||||
|
- model: Pixel2
|
||||||
|
version: 28
|
||||||
|
|
||||||
|
flank:
|
||||||
|
project: GOOGLE_PROJECT
|
||||||
|
max-test-shards: -1
|
||||||
|
num-test-runs: 1
|
||||||
|
output-style: compact
|
||||||
|
full-junit-result: true
|
||||||
|
repeat-tests: 1
|
|
@ -0,0 +1,144 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import xml
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from beautifultable import BeautifulTable
|
||||||
|
from junitparser import Attr, Failure, JUnitXml, TestCase, TestSuite
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(cmdln_args):
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Parse and print UI test JUnit results"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--results",
|
||||||
|
type=Path,
|
||||||
|
help="Directory containing task artifact results",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
return parser.parse_args(args=cmdln_args)
|
||||||
|
|
||||||
|
|
||||||
|
class test_suite(TestSuite):
|
||||||
|
flakes = Attr()
|
||||||
|
|
||||||
|
|
||||||
|
class test_case(TestCase):
|
||||||
|
flaky = Attr()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_print_failure_results(results):
|
||||||
|
"""
|
||||||
|
Parses the given JUnit test results and prints a formatted table of failures and flaky tests.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
results (JUnitXml): Parsed JUnit XML results.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The number of test failures.
|
||||||
|
|
||||||
|
The function processes each test suite and each test case within the suite.
|
||||||
|
If a test case has a result that is an instance of Failure, it is added to the table.
|
||||||
|
The test case is marked as 'Flaky' if the flaky attribute is set to "true", otherwise it is marked as 'Failure'.
|
||||||
|
|
||||||
|
Example of possible JUnit XML (FullJUnitReport.xml):
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="ExampleSuite" tests="2" failures="1" flakes="1" time="0.003">
|
||||||
|
<testcase classname="example.TestClass" name="testSuccess" flaky="true" time="0.001">
|
||||||
|
<failure message="Assertion failed">Expected true but was false</failure>
|
||||||
|
</testcase>
|
||||||
|
<testcase classname="example.TestClass" name="testFailure" time="0.002">
|
||||||
|
<failure message="Assertion failed">Expected true but was false</failure>
|
||||||
|
<failure message="Assertion failed">Expected true but was false</failure>
|
||||||
|
</testcase>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
"""
|
||||||
|
|
||||||
|
table = BeautifulTable(maxwidth=256)
|
||||||
|
table.columns.header = ["UI Test", "Outcome", "Details"]
|
||||||
|
table.columns.alignment = BeautifulTable.ALIGN_LEFT
|
||||||
|
table.set_style(BeautifulTable.STYLE_GRID)
|
||||||
|
|
||||||
|
failure_count = 0
|
||||||
|
|
||||||
|
# Dictionary to store the last seen failure details for each test case
|
||||||
|
last_seen_failures = {}
|
||||||
|
|
||||||
|
for suite in results:
|
||||||
|
cur_suite = test_suite.fromelem(suite)
|
||||||
|
for case in cur_suite:
|
||||||
|
cur_case = test_case.fromelem(case)
|
||||||
|
if cur_case.result:
|
||||||
|
for entry in case.result:
|
||||||
|
if isinstance(entry, Failure):
|
||||||
|
flaky_status = getattr(cur_case, "flaky", "false") == "true"
|
||||||
|
if flaky_status:
|
||||||
|
test_id = "%s#%s" % (case.classname, case.name)
|
||||||
|
details = (
|
||||||
|
entry.text.replace("\t", " ") if entry.text else ""
|
||||||
|
)
|
||||||
|
# Check if the current failure details are different from the last seen ones
|
||||||
|
if details != last_seen_failures.get(test_id, ""):
|
||||||
|
table.rows.append(
|
||||||
|
[
|
||||||
|
test_id,
|
||||||
|
"Flaky",
|
||||||
|
details,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
last_seen_failures[test_id] = details
|
||||||
|
else:
|
||||||
|
test_id = "%s#%s" % (case.classname, case.name)
|
||||||
|
details = (
|
||||||
|
entry.text.replace("\t", " ") if entry.text else ""
|
||||||
|
)
|
||||||
|
# Check if the current failure details are different from the last seen ones
|
||||||
|
if details != last_seen_failures.get(test_id, ""):
|
||||||
|
table.rows.append(
|
||||||
|
[
|
||||||
|
test_id,
|
||||||
|
"Failure",
|
||||||
|
details,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
print(f"TEST-UNEXPECTED-FAIL | {test_id} | {details}")
|
||||||
|
failure_count += 1
|
||||||
|
# Update the last seen failure details for this test case
|
||||||
|
last_seen_failures[test_id] = details
|
||||||
|
|
||||||
|
print(table)
|
||||||
|
return failure_count
|
||||||
|
|
||||||
|
|
||||||
|
def load_results_file(filename):
|
||||||
|
ret = None
|
||||||
|
try:
|
||||||
|
f = open(filename, "r")
|
||||||
|
try:
|
||||||
|
ret = JUnitXml.fromfile(f)
|
||||||
|
except xml.etree.ElementTree.ParseError as e:
|
||||||
|
print(f"Error parsing {filename} file: {e}")
|
||||||
|
finally:
|
||||||
|
f.close()
|
||||||
|
except IOError as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = parse_args(sys.argv[1:])
|
||||||
|
|
||||||
|
failure_count = 0
|
||||||
|
junitxml = load_results_file(args.results.joinpath("FullJUnitReport.xml"))
|
||||||
|
if junitxml:
|
||||||
|
failure_count = parse_print_failure_results(junitxml)
|
||||||
|
return failure_count
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
|
@ -0,0 +1,93 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(cmdln_args):
|
||||||
|
parser = argparse.ArgumentParser(description="Parse UI test logs an results")
|
||||||
|
parser.add_argument(
|
||||||
|
"--output-md",
|
||||||
|
type=argparse.FileType("w", encoding="utf-8"),
|
||||||
|
help="Output markdown file.",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--log",
|
||||||
|
type=argparse.FileType("r", encoding="utf-8"),
|
||||||
|
help="Log output of flank.",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--results", type=Path, help="Directory containing flank results", required=True
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--exit-code", type=int, help="Exit code of flank.", required=True
|
||||||
|
)
|
||||||
|
parser.add_argument("--device-type", help="Type of device ", required=True)
|
||||||
|
parser.add_argument(
|
||||||
|
"--report-treeherder-failures",
|
||||||
|
help="Report failures in treeherder format.",
|
||||||
|
required=False,
|
||||||
|
action="store_true",
|
||||||
|
)
|
||||||
|
|
||||||
|
return parser.parse_args(args=cmdln_args)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_android_args(log):
|
||||||
|
return yaml.safe_load(log.split("AndroidArgs\n")[1].split("RunTests\n")[0])
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = parse_args(sys.argv[1:])
|
||||||
|
|
||||||
|
log = args.log.read()
|
||||||
|
matrix_ids = json.loads(args.results.joinpath("matrix_ids.json").read_text())
|
||||||
|
# with args.results.joinpath("flank.yml") as f:
|
||||||
|
# flank_config = yaml.safe_load(f)
|
||||||
|
|
||||||
|
android_args = extract_android_args(log)
|
||||||
|
|
||||||
|
print = args.output_md.write
|
||||||
|
|
||||||
|
print("# Devices\n")
|
||||||
|
print(yaml.safe_dump(android_args["gcloud"]["device"]))
|
||||||
|
|
||||||
|
print("# Results\n")
|
||||||
|
print("| matrix | result | logs | details \n")
|
||||||
|
print("| --- | --- | --- | --- |\n")
|
||||||
|
for matrix, matrix_result in matrix_ids.items():
|
||||||
|
print(
|
||||||
|
"| {matrixId} | {outcome} | [logs]({webLink}) | {axes[0][details]}\n".format(
|
||||||
|
**matrix_result
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
args.report_treeherder_failures
|
||||||
|
and matrix_result["outcome"] != "success"
|
||||||
|
and matrix_result["outcome"] != "flaky"
|
||||||
|
):
|
||||||
|
# write failures to test log in format known to treeherder logviewer
|
||||||
|
sys.stdout.write(
|
||||||
|
f"TEST-UNEXPECTED-FAIL | {matrix_result['outcome']} | {matrix_result['webLink']} | {matrix_result['axes'][0]['details']}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("---\n")
|
||||||
|
print("# References & Documentation\n")
|
||||||
|
print(
|
||||||
|
"* [Automated UI Testing Documentation](https://github.com/mozilla-mobile/shared-docs/blob/main/android/ui-testing.md)\n"
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
"* Mobile Test Engineering on [Confluence](https://mozilla-hub.atlassian.net/wiki/spaces/MTE/overview) | [Slack](https://mozilla.slack.com/archives/C02KDDS9QM9) | [Alerts](https://mozilla.slack.com/archives/C0134KJ4JHL)\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
|
@ -0,0 +1,164 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
# This script does the following:
|
||||||
|
# 1. Retrieves glcoud service account token
|
||||||
|
# 2. Activates gcloud service account
|
||||||
|
# 3. Connects to google Firebase (using TestArmada's Flank tool)
|
||||||
|
# 4. Executes UI tests
|
||||||
|
# 5. Puts test artifacts into the test_artifacts folder
|
||||||
|
|
||||||
|
# NOTE:
|
||||||
|
# Flank supports sharding across multiple devices at a time, but gcloud API
|
||||||
|
# only supports 1 defined APK per test run.
|
||||||
|
|
||||||
|
|
||||||
|
# If a command fails then do not proceed and fail this script too.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
#########################
|
||||||
|
# The command line help #
|
||||||
|
#########################
|
||||||
|
display_help() {
|
||||||
|
echo "Usage: $0 Component_Name Build_Variant [Number_Shards...]"
|
||||||
|
echo
|
||||||
|
echo "Examples:"
|
||||||
|
echo "To run component/browser tests on ARM device shard (1 test / shard)"
|
||||||
|
echo "$ execute-firebase-test.sh component arm"
|
||||||
|
echo
|
||||||
|
echo "To run component/feature tests on X86 device (on 3 shards)"
|
||||||
|
echo "$ execute-firebase-test.sh feature x86 3"
|
||||||
|
echo
|
||||||
|
echo "To run UI samples/sampleName tests"
|
||||||
|
echo "$ execute-firebase-test.sh sample-sampleName arm 1"
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
# Basic parameter check
|
||||||
|
if [[ $# -lt 2 ]]; then
|
||||||
|
echo "Your command line contains $# arguments"
|
||||||
|
display_help
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
component="$1" # browser, concept, feature
|
||||||
|
device_type="$2" # arm | x86
|
||||||
|
if [[ ! -z "$3" ]]; then
|
||||||
|
num_shards=$3
|
||||||
|
fi
|
||||||
|
|
||||||
|
JAVA_BIN="/usr/bin/java"
|
||||||
|
PATH_TEST="./automation/taskcluster/androidTest"
|
||||||
|
FLANK_BIN="/builds/worker/test-tools/flank.jar"
|
||||||
|
FLANK_CONF_ARM="${PATH_TEST}/flank-arm.yml"
|
||||||
|
FLANK_CONF_X86="${PATH_TEST}/flank-x86.yml"
|
||||||
|
ARTIFACT_DIR="/builds/worker/artifacts"
|
||||||
|
RESULTS_DIR="${ARTIFACT_DIR}/results"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "ACTIVATE SERVICE ACCT"
|
||||||
|
echo
|
||||||
|
# this is where the Google Testcloud project ID is set
|
||||||
|
gcloud config set project "$GOOGLE_PROJECT"
|
||||||
|
echo
|
||||||
|
|
||||||
|
gcloud auth activate-service-account --key-file "$GOOGLE_APPLICATION_CREDENTIALS"
|
||||||
|
echo
|
||||||
|
echo
|
||||||
|
|
||||||
|
# From now on disable exiting on error. If the tests fail we want to continue
|
||||||
|
# and try to download the artifacts. We will exit with the actual error code later.
|
||||||
|
set +e
|
||||||
|
|
||||||
|
if [[ "${device_type,,}" == "x86" ]]
|
||||||
|
then
|
||||||
|
flank_template="$FLANK_CONF_X86"
|
||||||
|
else
|
||||||
|
flank_template="$FLANK_CONF_ARM"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove samples- from the component for each APK path
|
||||||
|
samples=${component//samples-}
|
||||||
|
|
||||||
|
# If tests are for components, the path is different than for samples
|
||||||
|
if [[ "${component}" != samples-* ]]
|
||||||
|
then
|
||||||
|
# Case 1: tests for any component (but NOT samples, NOT real UI tests)
|
||||||
|
APK_APP="./samples/browser/build/outputs/apk/gecko/debug/samples-browser-gecko-debug.apk"
|
||||||
|
if [[ "${component}" == *"-"* ]]
|
||||||
|
then
|
||||||
|
regex='([a-z]*)-(.*)'
|
||||||
|
[[ "$component" =~ $regex ]]
|
||||||
|
APK_TEST="./components/${BASH_REMATCH[1]}/${BASH_REMATCH[2]}/build/outputs/apk/androidTest/debug/${component}-debug-androidTest.apk"
|
||||||
|
else
|
||||||
|
APK_TEST="./components/${component}/engine-gecko/build/outputs/apk/androidTest/debug/browser-engine-gecko-debug-androidTest.apk"
|
||||||
|
fi
|
||||||
|
elif [[ "${component}" == "samples-browser" ]]
|
||||||
|
then
|
||||||
|
# Case 2: tests for browser sample (gecko sample only)
|
||||||
|
APK_APP="./samples/${samples}/build/outputs/apk/gecko/debug/samples-${samples}-gecko-debug.apk"
|
||||||
|
APK_TEST="./samples/${samples}/build/outputs/apk/androidTest/gecko/debug/samples-{$samples}-gecko-debug-androidTest.apk"
|
||||||
|
else
|
||||||
|
# Case 3: tests for non-browser samples (i.e. samples-glean)
|
||||||
|
APK_APP="./samples/${samples}/build/outputs/apk/debug/samples-${samples}-debug.apk"
|
||||||
|
APK_TEST="./samples/${samples}/build/outputs/apk/androidTest/debug/samples-${samples}-debug-androidTest.apk"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# function to exit script with exit code from test run.
|
||||||
|
# (Only 0 if all test executions passed)
|
||||||
|
function failure_check() {
|
||||||
|
echo
|
||||||
|
echo
|
||||||
|
if [[ $exitcode -ne 0 ]]; then
|
||||||
|
echo "ERROR: UI test run failed, please check above URL"
|
||||||
|
else
|
||||||
|
echo "All UI test(s) have passed!"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "RESULTS"
|
||||||
|
echo
|
||||||
|
mkdir -p ${ARTIFACT_DIR}/github
|
||||||
|
chmod +x ${PATH_TEST}/parse-ui-test.py
|
||||||
|
chmod +x ${PATH_TEST}/parse-ui-test-fromfile.py
|
||||||
|
${PATH_TEST}/parse-ui-test-fromfile.py \
|
||||||
|
--results "${RESULTS_DIR}"
|
||||||
|
if [[ $? -ne 0 ]]; then
|
||||||
|
${PATH_TEST}/parse-ui-test.py \
|
||||||
|
--exit-code "${exitcode}" \
|
||||||
|
--log flank.log \
|
||||||
|
--results "${RESULTS_DIR}" \
|
||||||
|
--output-md "${ARTIFACT_DIR}/github/customCheckRunText.md" \
|
||||||
|
--device-type "${device_type}"
|
||||||
|
else
|
||||||
|
${PATH_TEST}/parse-ui-test.py \
|
||||||
|
--exit-code "${exitcode}" \
|
||||||
|
--log flank.log \
|
||||||
|
--results "${RESULTS_DIR}" \
|
||||||
|
--output-md "${ARTIFACT_DIR}/github/customCheckRunText.md" \
|
||||||
|
--device-type "${device_type}" \
|
||||||
|
--report-treeherder-failures
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "EXECUTE TEST(S)"
|
||||||
|
echo
|
||||||
|
|
||||||
|
set -o pipefail && $JAVA_BIN -jar $FLANK_BIN android run \
|
||||||
|
--config=$flank_template \
|
||||||
|
--max-test-shards=$num_shards \
|
||||||
|
--app=$APK_APP --test=$APK_TEST \
|
||||||
|
--local-result-dir="${RESULTS_DIR}" \
|
||||||
|
--project=$GOOGLE_PROJECT \
|
||||||
|
--client-details=commit=${MOBILE_HEAD_REV:-None},pullRequest=${PULL_REQUEST_NUMBER:-None} \
|
||||||
|
| tee flank.log
|
||||||
|
|
||||||
|
exitcode=$?
|
||||||
|
failure_check
|
||||||
|
|
||||||
|
exit $exitcode
|
|
@ -0,0 +1,356 @@
|
||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
|
import io.gitlab.arturbosch.detekt.Detekt
|
||||||
|
import io.gitlab.arturbosch.detekt.DetektCreateBaselineTask
|
||||||
|
import org.gradle.internal.logging.text.StyledTextOutput.Style
|
||||||
|
import org.gradle.internal.logging.text.StyledTextOutputFactory
|
||||||
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
import static org.gradle.api.tasks.testing.TestResult.ResultType
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORIES.each { repository ->
|
||||||
|
maven {
|
||||||
|
url repository
|
||||||
|
if (gradle.mozconfig.substs.ALLOW_INSECURE_GRADLE_REPOSITORIES) {
|
||||||
|
allowInsecureProtocol = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath ComponentsDependencies.tools_androidgradle
|
||||||
|
classpath ComponentsDependencies.tools_kotlingradle
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variables in plugins {} aren't directly supported. Hack around it by setting an
|
||||||
|
// intermediate variable which can pull from FenixDependencies.kt and be used later.
|
||||||
|
ext {
|
||||||
|
detekt_plugin = Versions.detekt
|
||||||
|
python_envs_plugin = Versions.python_envs_plugin
|
||||||
|
ksp_plugin = Versions.ksp_plugin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("io.gitlab.arturbosch.detekt").version("$detekt_plugin")
|
||||||
|
id("com.google.devtools.ksp").version("$ksp_plugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORIES.each { repository ->
|
||||||
|
maven {
|
||||||
|
url repository
|
||||||
|
if (gradle.mozconfig.substs.ALLOW_INSECURE_GRADLE_REPOSITORIES) {
|
||||||
|
allowInsecureProtocol = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
maven {
|
||||||
|
url "${gradle.mozconfig.topobjdir}/gradle/maven"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
apply plugin: 'jacoco'
|
||||||
|
|
||||||
|
// Enable Kotlin warnings as errors for all modules
|
||||||
|
tasks.withType(KotlinCompile).configureEach {
|
||||||
|
kotlinOptions.allWarningsAsErrors = true
|
||||||
|
}
|
||||||
|
|
||||||
|
project.configurations.configureEach {
|
||||||
|
// Dependencies can't depend on a different major version of Glean than A-C itself.
|
||||||
|
resolutionStrategy.eachDependency { details ->
|
||||||
|
if (details.requested.group == 'org.mozilla.telemetry'
|
||||||
|
&& details.requested.name.contains('glean') ) {
|
||||||
|
def requested = details.requested.version.tokenize(".")
|
||||||
|
def defined = Versions.mozilla_glean.tokenize(".")
|
||||||
|
// Check the major version
|
||||||
|
if (requested[0] != defined[0]) {
|
||||||
|
throw new AssertionError("Cannot resolve to a single Glean version. Requested: ${details.requested.version}, A-C uses: ${Versions.mozilla_glean}")
|
||||||
|
} else {
|
||||||
|
// Enforce that all (transitive) dependencies are using the defined Glean version
|
||||||
|
details.useVersion Versions.mozilla_glean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolutionStrategy.capabilitiesResolution.withCapability("org.mozilla.telemetry:glean-native") {
|
||||||
|
def toBeSelected = candidates.find { it.id instanceof ModuleComponentIdentifier && it.id.module.contains('geckoview') }
|
||||||
|
if (toBeSelected != null) {
|
||||||
|
select(toBeSelected)
|
||||||
|
}
|
||||||
|
because 'use GeckoView Glean instead of standalone Glean'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gradle.hasProperty('localProperties.dependencySubstitutions.geckoviewTopsrcdir')) {
|
||||||
|
if (gradle.hasProperty('localProperties.dependencySubstitutions.geckoviewTopobjdir')) {
|
||||||
|
ext.topobjdir = gradle."localProperties.dependencySubstitutions.geckoviewTopobjdir"
|
||||||
|
}
|
||||||
|
ext.topsrcdir = gradle."localProperties.dependencySubstitutions.geckoviewTopsrcdir"
|
||||||
|
apply from: "${topsrcdir}/substitute-local-geckoview.gradle"
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEvaluate {
|
||||||
|
if (it.hasProperty('android')) {
|
||||||
|
jacoco {
|
||||||
|
toolVersion = Versions.jacoco
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format test output
|
||||||
|
tasks.matching {it instanceof Test}.configureEach() {
|
||||||
|
systemProperty "robolectric.logging", "stdout"
|
||||||
|
systemProperty "logging.test-mode", "true"
|
||||||
|
systemProperty "javax.net.ssl.trustStoreType", "JKS"
|
||||||
|
|
||||||
|
testLogging.events = []
|
||||||
|
|
||||||
|
def out = services.get(StyledTextOutputFactory).create("an-ouput")
|
||||||
|
|
||||||
|
beforeSuite { descriptor ->
|
||||||
|
if (descriptor.getClassName() != null) {
|
||||||
|
out.style(Style.Header).println("\nSUITE: " + descriptor.getClassName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeTest { descriptor ->
|
||||||
|
out.style(Style.Description).println(" TEST: " + descriptor.getName())
|
||||||
|
}
|
||||||
|
|
||||||
|
onOutput { descriptor, event ->
|
||||||
|
logger.lifecycle(" " + event.message.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
afterTest { descriptor, result ->
|
||||||
|
switch (result.getResultType()) {
|
||||||
|
case ResultType.SUCCESS:
|
||||||
|
out.style(Style.Success).println(" SUCCESS")
|
||||||
|
break
|
||||||
|
|
||||||
|
case ResultType.FAILURE:
|
||||||
|
def testId = descriptor.getClassName() + "." + descriptor.getName()
|
||||||
|
out.style(Style.Failure).println(" TEST-UNEXPECTED-FAIL | " + testId + " | " + result.getException())
|
||||||
|
break
|
||||||
|
|
||||||
|
case ResultType.SKIPPED:
|
||||||
|
out.style(Style.Info).println(" SKIPPED")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
logger.lifecycle("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
lintChecks project(':tooling-lint')
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(config.jvmTargetCompatibility)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
includeAndroidResources = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
resources {
|
||||||
|
excludes += ['META-INF/atomicfu.kotlin_module', 'META-INF/AL2.0', 'META-INF/LGPL2.1']
|
||||||
|
// Required dependencies using byte-buddy; remove after this is
|
||||||
|
// fixed by: https://issuetracker.google.com/issues/170131605
|
||||||
|
excludes.add("META-INF/licenses/ASM")
|
||||||
|
|
||||||
|
pickFirsts += ['win32-x86-64/attach_hotspot_windows.dll', 'win32-x86/attach_hotspot_windows.dll']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
androidResources {
|
||||||
|
ignoreAssetsPattern "manifest.template.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType(KotlinCompile).configureEach {
|
||||||
|
kotlinOptions.freeCompilerArgs += ["-opt-in=kotlin.RequiresOptIn"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (project.hasProperty("coverage") && project.name != "support-test") {
|
||||||
|
android.buildTypes.all { buildType ->
|
||||||
|
tasks.withType(Test).configureEach() {
|
||||||
|
jacoco {
|
||||||
|
includeNoLocationClasses = true
|
||||||
|
excludes = ['jdk.internal.*']
|
||||||
|
}
|
||||||
|
|
||||||
|
finalizedBy { "jacoco${buildType.name.capitalize()}TestReport" }
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("jacoco${buildType.name.capitalize()}TestReport", JacocoReport) {
|
||||||
|
reports {
|
||||||
|
xml.required = true
|
||||||
|
html.required = true
|
||||||
|
}
|
||||||
|
|
||||||
|
def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*',
|
||||||
|
'**/*Test*.*', 'android/**/*.*', '**/*$[0-9].*']
|
||||||
|
def kotlinDebugTree = fileTree(dir: "$project.layout.buildDirectory/tmp/kotlin-classes/${buildType.name}", excludes: fileFilter)
|
||||||
|
def javaDebugTree = fileTree(dir: "$project.layout.buildDirectory/intermediates/classes/${buildType.name}", excludes: fileFilter)
|
||||||
|
def mainSrc = "$project.projectDir/src/main/java"
|
||||||
|
|
||||||
|
sourceDirectories.setFrom(files([mainSrc]))
|
||||||
|
classDirectories.setFrom(files([kotlinDebugTree, javaDebugTree]))
|
||||||
|
getExecutionData().setFrom(fileTree(project.layout.buildDirectory).include([
|
||||||
|
"jacoco/test${buildType.name.capitalize()}UnitTest.exec"
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
testCoverageEnabled true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType(KotlinCompile).configureEach {
|
||||||
|
// Translate Kotlin messages like "w: ..." and "e: ..." into
|
||||||
|
// "...: warning: ..." and "...: error: ...", to make Treeherder understand.
|
||||||
|
def listener = {
|
||||||
|
if (it.startsWith("e: warnings found")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it.startsWith('w: ') || it.startsWith('e: ')) {
|
||||||
|
def matches = (it =~ /([ew]): (.+):(\d+):(\d+) (.*)/)
|
||||||
|
if (!matches) {
|
||||||
|
logger.quiet "kotlinc message format has changed!"
|
||||||
|
if (it.startsWith('w: ')) {
|
||||||
|
// For warnings, don't continue because we don't want to throw an
|
||||||
|
// exception. For errors, we want the exception so that the new error
|
||||||
|
// message format gets translated properly.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def (_, type, file, line, column, message) = matches[0]
|
||||||
|
type = (type == 'w') ? 'warning' : 'error'
|
||||||
|
// Use logger.lifecycle, which does not go through stderr again.
|
||||||
|
logger.lifecycle "$file:$line:$column: $type: $message"
|
||||||
|
}
|
||||||
|
} as StandardOutputListener
|
||||||
|
|
||||||
|
doFirst {
|
||||||
|
logging.addStandardErrorListener(listener)
|
||||||
|
}
|
||||||
|
doLast {
|
||||||
|
logging.removeStandardErrorListener(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (findProject(":geckoview") == null) {
|
||||||
|
// Avoid adding this task if it already exists in a different root project.
|
||||||
|
tasks.register("clean", Delete) {
|
||||||
|
delete rootProject.layout.buildDirectory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
detekt {
|
||||||
|
input = files("$projectDir/components", "$projectDir/buildSrc", "$projectDir/samples")
|
||||||
|
config = files("$projectDir/config/detekt.yml")
|
||||||
|
baseline = file("$projectDir/config/detekt-baseline.xml")
|
||||||
|
|
||||||
|
reports {
|
||||||
|
html {
|
||||||
|
enabled = true
|
||||||
|
destination = file("$projectDir/build/reports/detekt.html")
|
||||||
|
}
|
||||||
|
xml {
|
||||||
|
enabled = false
|
||||||
|
}
|
||||||
|
txt {
|
||||||
|
enabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType(Detekt).configureEach() {
|
||||||
|
// Custom detekt rules should be build before
|
||||||
|
// See https://arturbosch.github.io/detekt/extensions.html#pitfalls
|
||||||
|
dependsOn(":tooling-detekt:assemble")
|
||||||
|
|
||||||
|
autoCorrect = true
|
||||||
|
|
||||||
|
exclude "**/build.gradle.kts"
|
||||||
|
exclude "**/src/androidTest/**"
|
||||||
|
exclude "**/src/iosTest/**"
|
||||||
|
exclude "**/src/test/**"
|
||||||
|
exclude "**/test/src/**"
|
||||||
|
exclude "**/build/**"
|
||||||
|
exclude "**/resources/**"
|
||||||
|
exclude "**/tmp/**"
|
||||||
|
exclude "**/tooling/fetch/tests/**"
|
||||||
|
exclude "**/tooling/fetch-tests/**"
|
||||||
|
exclude "**/src/main/assets/extensions/**"
|
||||||
|
exclude "**/docs/**"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply same path exclusions as for the main task
|
||||||
|
tasks.withType(DetektCreateBaselineTask).configureEach() {
|
||||||
|
exclude "**/src/androidTest/**"
|
||||||
|
exclude "**/src/test/**"
|
||||||
|
exclude "**/test/src/**"
|
||||||
|
exclude "**/build/**"
|
||||||
|
exclude "**/resources/**"
|
||||||
|
exclude "**/tmp/**"
|
||||||
|
exclude "**/tooling/fetch/tests/**"
|
||||||
|
exclude "**/tooling/fetch-tests/**"
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations {
|
||||||
|
ktlint
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
ktlint("com.pinterest:ktlint:${Versions.ktlint}") {
|
||||||
|
attributes {
|
||||||
|
attribute(Bundling.BUNDLING_ATTRIBUTE, getObjects().named(Bundling, Bundling.EXTERNAL))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
detektPlugins project(":tooling-detekt")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("ktlint", JavaExec) {
|
||||||
|
group = "verification"
|
||||||
|
description = "Check Kotlin code style."
|
||||||
|
classpath = configurations.ktlint
|
||||||
|
mainClass.set("com.pinterest.ktlint.Main")
|
||||||
|
args "components/**/*.kt" , "samples/**/*.kt", "!**/build/**/*.kt", "buildSrc/**/*.kt", "--baseline=ktlint-baseline.xml",
|
||||||
|
"--reporter=json,output=build/reports/ktlint/ktlint.json", "--reporter=plain"
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("ktlintFormat", JavaExec) {
|
||||||
|
group = "formatting"
|
||||||
|
description = "Fix Kotlin code style deviations."
|
||||||
|
classpath = configurations.ktlint
|
||||||
|
mainClass.set("com.pinterest.ktlint.Main")
|
||||||
|
args "-F", "components/**/*.kt" , "samples/**/*.kt", "!**/build/**/*.kt", "buildSrc/**/*.kt", "--baseline=ktlint-baseline.xml"
|
||||||
|
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("listRepositories") {
|
||||||
|
doLast {
|
||||||
|
println "Repositories:"
|
||||||
|
project.repositories.each { println "Name: " + it.name + "; url: " + it.url }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
# [Android Components](../../../README.md) > Browser > Domains
|
||||||
|
|
||||||
|
This component provides APIs for managing localized and customizable domain lists (see [Domains](#domains) and [CustomDomains](#customdomains)). It also contains auto-complete functionality for these lists (see [DomainAutoCompleteProvider](#domainautocompleteprovider)) which can be used in conjuction with our [UI autocomplete component](../../ui/autocomplete/README.md).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Setting up the dependency
|
||||||
|
|
||||||
|
Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)):
|
||||||
|
|
||||||
|
```Groovy
|
||||||
|
implementation "org.mozilla.components:browser-domains:{latest-version}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Domains
|
||||||
|
|
||||||
|
The `Domains` object is used to load the built-in localized domain lists which are shipped as part of this component. These lists are grouped by country and can be found [in our repository](src/main/assets/domains).
|
||||||
|
|
||||||
|
```Kotlin
|
||||||
|
// Load the domain lists for all countries in the default locale (fallback is US)
|
||||||
|
val domains = Domains.load(context)
|
||||||
|
```
|
||||||
|
|
||||||
|
### CustomDomains
|
||||||
|
|
||||||
|
The `CustomDomains` object can be used to manage a custom domain list which will be stored in `SharedPreferences`.
|
||||||
|
|
||||||
|
```Kotlin
|
||||||
|
// Load the custom domain list
|
||||||
|
val domains = CustomDomains.load(context)
|
||||||
|
|
||||||
|
// Save custom domains
|
||||||
|
CustomDomains.save(context, listOf("mozilla.org", "getpocket.com"))
|
||||||
|
|
||||||
|
// Remove custom domains
|
||||||
|
CustomDomains.remove(context, listOf("nolongerexists.org"))
|
||||||
|
```
|
||||||
|
|
||||||
|
### DomainAutoCompleteProvider
|
||||||
|
|
||||||
|
The class provides auto-complete functionality for both `Domains` and `CustomDomains`.
|
||||||
|
|
||||||
|
```Kotlin
|
||||||
|
// Initialize the provider
|
||||||
|
val provider = DomainAutocompleteProvider()
|
||||||
|
provider.initialize(
|
||||||
|
context,
|
||||||
|
useShippedDomains = true,
|
||||||
|
useCustomDomains = true,
|
||||||
|
loadDomainsFromDisk = true
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that when `loadDomainsFromDisk` is set to true there is no need to manually call `load` on either `Domains` or `CustomDomains`.
|
||||||
|
|
||||||
|
```Kotlin
|
||||||
|
// Autocomplete domain lists
|
||||||
|
val result = provider.autocomplete("moz")
|
||||||
|
```
|
||||||
|
|
||||||
|
The result will contain the autocompleted text (`result.text`), the URL (`result.url`), and the source of the match (`result.source`), which is either `DEFAULT_LIST` if a result was found in the shipped domain list or `CUSTOM_LIST` otherwise. The custom domain list takes precendece over the built-in shipped domain list and the API will only return the first match.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/
|
|
@ -0,0 +1,40 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
apply plugin: 'com.android.library'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion config.minSdkVersion
|
||||||
|
compileSdk config.compileSdkVersion
|
||||||
|
targetSdkVersion config.targetSdkVersion
|
||||||
|
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace 'mozilla.components.browser.domains'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(':concept-toolbar')
|
||||||
|
implementation ComponentsDependencies.kotlin_coroutines
|
||||||
|
|
||||||
|
testImplementation project(':support-test')
|
||||||
|
|
||||||
|
testImplementation ComponentsDependencies.androidx_test_junit
|
||||||
|
testImplementation ComponentsDependencies.testing_robolectric
|
||||||
|
testImplementation ComponentsDependencies.testing_coroutines
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: '../../../android-lint.gradle'
|
||||||
|
apply from: '../../../publish.gradle'
|
||||||
|
ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
|
@ -0,0 +1,4 @@
|
||||||
|
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
<manifest />
|
|
@ -0,0 +1,50 @@
|
||||||
|
google.com.br
|
||||||
|
youtube.com
|
||||||
|
google.com
|
||||||
|
facebook.com
|
||||||
|
globo.com
|
||||||
|
uol.com.br
|
||||||
|
blastingnews.com
|
||||||
|
live.com
|
||||||
|
mercadolivre.com.br
|
||||||
|
yahoo.com
|
||||||
|
blogspot.com.br
|
||||||
|
wikipedia.org
|
||||||
|
whatsapp.com
|
||||||
|
netflix.com
|
||||||
|
olx.com.br
|
||||||
|
instagram.com
|
||||||
|
msn.com
|
||||||
|
metropoles.com
|
||||||
|
fatosdesconhecidos.com.br
|
||||||
|
twitter.com
|
||||||
|
caixa.gov.br
|
||||||
|
uptodown.com
|
||||||
|
aliexpress.com
|
||||||
|
curapelanatureza.com.br
|
||||||
|
wordpress.com
|
||||||
|
abril.com.br
|
||||||
|
americanas.com.br
|
||||||
|
correios.com.br
|
||||||
|
reclameaqui.com.br
|
||||||
|
bet365.com
|
||||||
|
onclkds.com
|
||||||
|
bol.uol.com.br
|
||||||
|
techtudo.com.br
|
||||||
|
fazenda.gov.br
|
||||||
|
microsoft.com
|
||||||
|
folha.uol.com.br
|
||||||
|
linkedin.com
|
||||||
|
tumblr.com
|
||||||
|
sp.gov.br
|
||||||
|
reddit.com
|
||||||
|
bb.com.br
|
||||||
|
pinterest.com
|
||||||
|
itau.com.br
|
||||||
|
letras.mus.br
|
||||||
|
otvfoco.com.br
|
||||||
|
vagalume.com.br
|
||||||
|
portalinteressante.com
|
||||||
|
myappolicious.com.br
|
||||||
|
thewhizmarketing.com
|
||||||
|
twitch.tv
|
|
@ -0,0 +1,49 @@
|
||||||
|
google.com
|
||||||
|
youtube.com
|
||||||
|
facebook.com
|
||||||
|
reddit.com
|
||||||
|
amazon.com
|
||||||
|
wikipedia.org
|
||||||
|
yahoo.com
|
||||||
|
twitter.com
|
||||||
|
netflix.com
|
||||||
|
ebay.com
|
||||||
|
imgur.com
|
||||||
|
linkedin.com
|
||||||
|
instagram.com
|
||||||
|
diply.com
|
||||||
|
craigslist.org
|
||||||
|
live.com
|
||||||
|
office.com
|
||||||
|
twitch.tv
|
||||||
|
tumblr.com
|
||||||
|
pinterest.com
|
||||||
|
espn.com
|
||||||
|
cnn.com
|
||||||
|
bing.com
|
||||||
|
wikia.com
|
||||||
|
chase.com
|
||||||
|
imdb.com
|
||||||
|
nytimes.com
|
||||||
|
paypal.com
|
||||||
|
blogspot.com
|
||||||
|
apple.com
|
||||||
|
yelp.com
|
||||||
|
stackoverflow.com
|
||||||
|
bankofamerica.com
|
||||||
|
wordpress.com
|
||||||
|
github.com
|
||||||
|
microsoft.com
|
||||||
|
wellsfargo.com
|
||||||
|
zillow.com
|
||||||
|
salesforce.com
|
||||||
|
msn.com
|
||||||
|
walmart.com
|
||||||
|
weather.com
|
||||||
|
dropbox.com
|
||||||
|
buzzfeed.com
|
||||||
|
intuit.com
|
||||||
|
washingtonpost.com
|
||||||
|
soundcloud.com
|
||||||
|
huffingtonpost.com
|
||||||
|
indeed.com
|
|
@ -0,0 +1,50 @@
|
||||||
|
google.de
|
||||||
|
youtube.com
|
||||||
|
google.com
|
||||||
|
facebook.com
|
||||||
|
amazon.de
|
||||||
|
ebay.de
|
||||||
|
wikipedia.org
|
||||||
|
ebay-kleinanzeigen.de
|
||||||
|
web.de
|
||||||
|
yahoo.com
|
||||||
|
ok.ru
|
||||||
|
gmx.net
|
||||||
|
reddit.com
|
||||||
|
vk.com
|
||||||
|
t-online.de
|
||||||
|
twitter.com
|
||||||
|
spiegel.de
|
||||||
|
mail.ru
|
||||||
|
instagram.com
|
||||||
|
live.com
|
||||||
|
chip.de
|
||||||
|
bild.de
|
||||||
|
paypal.com
|
||||||
|
bing.com
|
||||||
|
twitch.tv
|
||||||
|
whatsapp.com
|
||||||
|
yandex.ru
|
||||||
|
gutefrage.net
|
||||||
|
mobile.de
|
||||||
|
google.ru
|
||||||
|
blogspot.de
|
||||||
|
tumblr.com
|
||||||
|
bs.to
|
||||||
|
focus.de
|
||||||
|
linkedin.com
|
||||||
|
netflix.com
|
||||||
|
wordpress.com
|
||||||
|
imgur.com
|
||||||
|
postbank.de
|
||||||
|
welt.de
|
||||||
|
streamcloud.eu
|
||||||
|
microsoft.com
|
||||||
|
immobilienscout24.de
|
||||||
|
msn.com
|
||||||
|
dict.cc
|
||||||
|
otto.de
|
||||||
|
xing.com
|
||||||
|
amazon.com
|
||||||
|
heise.de
|
||||||
|
github.com
|
|
@ -0,0 +1,50 @@
|
||||||
|
google.fr
|
||||||
|
youtube.com
|
||||||
|
google.com
|
||||||
|
facebook.com
|
||||||
|
wikipedia.org
|
||||||
|
amazon.fr
|
||||||
|
leboncoin.fr
|
||||||
|
yahoo.com
|
||||||
|
live.com
|
||||||
|
twitter.com
|
||||||
|
orange.fr
|
||||||
|
free.fr
|
||||||
|
linkedin.com
|
||||||
|
lemonde.fr
|
||||||
|
instagram.com
|
||||||
|
reddit.com
|
||||||
|
lefigaro.fr
|
||||||
|
ebay.fr
|
||||||
|
cdiscount.com
|
||||||
|
jeuxvideo.com
|
||||||
|
zone-telechargement.ws
|
||||||
|
labanquepostale.fr
|
||||||
|
blogspot.fr
|
||||||
|
allocine.fr
|
||||||
|
msn.com
|
||||||
|
commentcamarche.net
|
||||||
|
pole-emploi.fr
|
||||||
|
vk.com
|
||||||
|
sfr.fr
|
||||||
|
lequipe.fr
|
||||||
|
twitch.tv
|
||||||
|
francetvinfo.fr
|
||||||
|
20minutes.fr
|
||||||
|
pinterest.com
|
||||||
|
netflix.com
|
||||||
|
programme-tv.net
|
||||||
|
credit-agricole.fr
|
||||||
|
linternaute.com
|
||||||
|
github.com
|
||||||
|
wordpress.com
|
||||||
|
caf.fr
|
||||||
|
aliexpress.com
|
||||||
|
dailymotion.com
|
||||||
|
tumblr.com
|
||||||
|
t411.ai
|
||||||
|
stackoverflow.com
|
||||||
|
microsoft.com
|
||||||
|
meteofrance.com
|
||||||
|
onclkds.com
|
||||||
|
bfmtv.com
|
|
@ -0,0 +1,49 @@
|
||||||
|
google.co.uk
|
||||||
|
youtube.com
|
||||||
|
google.com
|
||||||
|
facebook.com
|
||||||
|
reddit.com
|
||||||
|
bbc.co.uk
|
||||||
|
amazon.co.uk
|
||||||
|
wikipedia.org
|
||||||
|
ebay.co.uk
|
||||||
|
twitter.com
|
||||||
|
live.com
|
||||||
|
yahoo.com
|
||||||
|
instagram.com
|
||||||
|
diply.com
|
||||||
|
linkedin.com
|
||||||
|
imgur.com
|
||||||
|
netflix.com
|
||||||
|
theguardian.com
|
||||||
|
dailymail.co.uk
|
||||||
|
twitch.tv
|
||||||
|
imdb.com
|
||||||
|
paypal.com
|
||||||
|
office.com
|
||||||
|
tumblr.com
|
||||||
|
www.gov.uk
|
||||||
|
wikia.com
|
||||||
|
givemesport.com
|
||||||
|
amazon.com
|
||||||
|
bing.com
|
||||||
|
wordpress.com
|
||||||
|
telegraph.co.uk
|
||||||
|
rightmove.co.uk
|
||||||
|
pinterest.com
|
||||||
|
gumtree.com
|
||||||
|
msn.com
|
||||||
|
microsoft.com
|
||||||
|
stackoverflow.com
|
||||||
|
booking.com
|
||||||
|
vk.com
|
||||||
|
tripadvisor.co.uk
|
||||||
|
lloydsbank.co.uk
|
||||||
|
apple.com
|
||||||
|
service.gov.uk
|
||||||
|
onclkds.com
|
||||||
|
github.com
|
||||||
|
independent.co.uk
|
||||||
|
bt.com
|
||||||
|
vice.com
|
||||||
|
hsbc.co.uk
|
|
@ -0,0 +1,444 @@
|
||||||
|
google.com
|
||||||
|
facebook.com
|
||||||
|
amazon.com
|
||||||
|
youtube.com
|
||||||
|
yahoo.com
|
||||||
|
ebay.com
|
||||||
|
wikipedia.org
|
||||||
|
twitter.com
|
||||||
|
reddit.com
|
||||||
|
go.com
|
||||||
|
craigslist.org
|
||||||
|
live.com
|
||||||
|
netflix.com
|
||||||
|
pinterest.com
|
||||||
|
bing.com
|
||||||
|
linkedin.com
|
||||||
|
imgur.com
|
||||||
|
espn.go.com
|
||||||
|
walmart.com
|
||||||
|
tumblr.com
|
||||||
|
target.com
|
||||||
|
paypal.com
|
||||||
|
cnn.com
|
||||||
|
chase.com
|
||||||
|
instagram.com
|
||||||
|
bestbuy.com
|
||||||
|
blogspot.com
|
||||||
|
nytimes.com
|
||||||
|
msn.com
|
||||||
|
imdb.com
|
||||||
|
apple.com
|
||||||
|
bankofamerica.com
|
||||||
|
diply.com
|
||||||
|
huffingtonpost.com
|
||||||
|
yelp.com
|
||||||
|
wellsfargo.com
|
||||||
|
etsy.com
|
||||||
|
weather.com
|
||||||
|
wordpress.com
|
||||||
|
buzzfeed.com
|
||||||
|
zillow.com
|
||||||
|
kohls.com
|
||||||
|
aol.com
|
||||||
|
homedepot.com
|
||||||
|
foxnews.com
|
||||||
|
microsoft.com
|
||||||
|
comcast.net
|
||||||
|
wikia.com
|
||||||
|
groupon.com
|
||||||
|
macys.com
|
||||||
|
washingtonpost.com
|
||||||
|
outbrain.com
|
||||||
|
xfinity.com
|
||||||
|
usps.com
|
||||||
|
hulu.com
|
||||||
|
americanexpress.com
|
||||||
|
slickdeals.net
|
||||||
|
pandora.com
|
||||||
|
office.com
|
||||||
|
cnet.com
|
||||||
|
indeed.com
|
||||||
|
capitalone.com
|
||||||
|
nfl.com
|
||||||
|
ups.com
|
||||||
|
ask.com
|
||||||
|
verizonwireless.com
|
||||||
|
newegg.com
|
||||||
|
usatoday.com
|
||||||
|
forbes.com
|
||||||
|
dailymail.co.uk
|
||||||
|
dropbox.com
|
||||||
|
att.com
|
||||||
|
costco.com
|
||||||
|
gfycat.com
|
||||||
|
lowes.com
|
||||||
|
gap.com
|
||||||
|
about.com
|
||||||
|
tripadvisor.com
|
||||||
|
fedex.com
|
||||||
|
baidu.com
|
||||||
|
vice.com
|
||||||
|
nordstrom.com
|
||||||
|
adobe.com
|
||||||
|
bbc.com
|
||||||
|
twitch.tv
|
||||||
|
allrecipes.com
|
||||||
|
retailmenot.com
|
||||||
|
stackoverflow.com
|
||||||
|
citi.com
|
||||||
|
sears.com
|
||||||
|
jcpenney.com
|
||||||
|
webmd.com
|
||||||
|
nih.gov
|
||||||
|
answers.com
|
||||||
|
foodnetwork.com
|
||||||
|
discovercard.com
|
||||||
|
cbssports.com
|
||||||
|
overstock.com
|
||||||
|
businessinsider.com
|
||||||
|
office365.com
|
||||||
|
theguardian.com
|
||||||
|
staples.com
|
||||||
|
bleacherreport.com
|
||||||
|
verizon.com
|
||||||
|
github.com
|
||||||
|
wayfair.com
|
||||||
|
salesforce.com
|
||||||
|
zulily.com
|
||||||
|
wsj.com
|
||||||
|
flickr.com
|
||||||
|
goodreads.com
|
||||||
|
realtor.com
|
||||||
|
nbcnews.com
|
||||||
|
ebates.com
|
||||||
|
ancestry.com
|
||||||
|
wunderground.com
|
||||||
|
instructure.com
|
||||||
|
people.com
|
||||||
|
stackexchange.com
|
||||||
|
drudgereport.com
|
||||||
|
fidelity.com
|
||||||
|
southwest.com
|
||||||
|
deviantart.com
|
||||||
|
thesaurus.com
|
||||||
|
intuit.com
|
||||||
|
woot.com
|
||||||
|
pch.com
|
||||||
|
soundcloud.com
|
||||||
|
force.com
|
||||||
|
samsclub.com
|
||||||
|
ign.com
|
||||||
|
qvc.com
|
||||||
|
npr.org
|
||||||
|
patch.com
|
||||||
|
dell.com
|
||||||
|
accuweather.com
|
||||||
|
vimeo.com
|
||||||
|
expedia.com
|
||||||
|
trulia.com
|
||||||
|
ca.gov
|
||||||
|
swagbucks.com
|
||||||
|
spotify.com
|
||||||
|
bedbathandbeyond.com
|
||||||
|
nypost.com
|
||||||
|
aliexpress.com
|
||||||
|
blackboard.com
|
||||||
|
ticketmaster.com
|
||||||
|
ikea.com
|
||||||
|
feedly.com
|
||||||
|
usaa.com
|
||||||
|
tmz.com
|
||||||
|
quora.com
|
||||||
|
lifehacker.com
|
||||||
|
kayak.com
|
||||||
|
reference.com
|
||||||
|
zappos.com
|
||||||
|
gizmodo.com
|
||||||
|
slate.com
|
||||||
|
faithtap.com
|
||||||
|
adp.com
|
||||||
|
abcnews.go.com
|
||||||
|
sephora.com
|
||||||
|
cbs.com
|
||||||
|
latimes.com
|
||||||
|
shutterfly.com
|
||||||
|
t-mobile.com
|
||||||
|
littlethings.com
|
||||||
|
glassdoor.com
|
||||||
|
bloomberg.com
|
||||||
|
cbsnews.com
|
||||||
|
wikihow.com
|
||||||
|
walgreens.com
|
||||||
|
usbank.com
|
||||||
|
blogger.com
|
||||||
|
weebly.com
|
||||||
|
gamestop.com
|
||||||
|
food.com
|
||||||
|
time.com
|
||||||
|
kickstarter.com
|
||||||
|
okcupid.com
|
||||||
|
aa.com
|
||||||
|
weather.gov
|
||||||
|
nametests.com
|
||||||
|
fandango.com
|
||||||
|
engadget.com
|
||||||
|
steamcommunity.com
|
||||||
|
thekitchn.com
|
||||||
|
nba.com
|
||||||
|
mashable.com
|
||||||
|
hp.com
|
||||||
|
gamefaqs.com
|
||||||
|
delta.com
|
||||||
|
coupons.com
|
||||||
|
eonline.com
|
||||||
|
surveymonkey.com
|
||||||
|
kmart.com
|
||||||
|
barnesandnoble.com
|
||||||
|
meetup.com
|
||||||
|
bhphotovideo.com
|
||||||
|
fanduel.com
|
||||||
|
quizlet.com
|
||||||
|
nydailynews.com
|
||||||
|
sbnation.com
|
||||||
|
nbcsports.com
|
||||||
|
bbc.co.uk
|
||||||
|
ew.com
|
||||||
|
nike.com
|
||||||
|
rottentomatoes.com
|
||||||
|
steampowered.com
|
||||||
|
reuters.com
|
||||||
|
qq.com
|
||||||
|
today.com
|
||||||
|
mapquest.com
|
||||||
|
audible.com
|
||||||
|
priceline.com
|
||||||
|
whitepages.com
|
||||||
|
united.com
|
||||||
|
myfitnesspal.com
|
||||||
|
icloud.com
|
||||||
|
forever21.com
|
||||||
|
theatlantic.com
|
||||||
|
microsoftstore.com
|
||||||
|
theverge.com
|
||||||
|
gawker.com
|
||||||
|
houzz.com
|
||||||
|
mayoclinic.org
|
||||||
|
rei.com
|
||||||
|
sfgate.com
|
||||||
|
lifebuzz.com
|
||||||
|
discover.com
|
||||||
|
pnc.com
|
||||||
|
pof.com
|
||||||
|
iflscience.com
|
||||||
|
popsugar.com
|
||||||
|
creditkarma.com
|
||||||
|
telegraph.co.uk
|
||||||
|
airbnb.com
|
||||||
|
buzzlie.com
|
||||||
|
cnbc.com
|
||||||
|
deadspin.com
|
||||||
|
sina.com.cn
|
||||||
|
legacy.com
|
||||||
|
thedailybeast.com
|
||||||
|
samsung.com
|
||||||
|
nextdoor.com
|
||||||
|
evite.com
|
||||||
|
shopify.com
|
||||||
|
yellowpages.com
|
||||||
|
pcmag.com
|
||||||
|
redfin.com
|
||||||
|
weibo.com
|
||||||
|
alibaba.com
|
||||||
|
cabelas.com
|
||||||
|
battle.net
|
||||||
|
foxsports.com
|
||||||
|
taobao.com
|
||||||
|
eventbrite.com
|
||||||
|
victoriassecret.com
|
||||||
|
theblaze.com
|
||||||
|
dealnews.com
|
||||||
|
cbslocal.com
|
||||||
|
cvs.com
|
||||||
|
dailymotion.com
|
||||||
|
ecollege.com
|
||||||
|
gofundme.com
|
||||||
|
fitbit.com
|
||||||
|
instructables.com
|
||||||
|
godaddy.com
|
||||||
|
babycenter.com
|
||||||
|
squarespace.com
|
||||||
|
llbean.com
|
||||||
|
dickssportinggoods.com
|
||||||
|
6pm.com
|
||||||
|
myway.com
|
||||||
|
hsn.com
|
||||||
|
wired.com
|
||||||
|
officedepot.com
|
||||||
|
ozztube.com
|
||||||
|
usmagazine.com
|
||||||
|
match.com
|
||||||
|
cracked.com
|
||||||
|
evernote.com
|
||||||
|
box.com
|
||||||
|
starbucks.com
|
||||||
|
kbb.com
|
||||||
|
mlb.com
|
||||||
|
marriott.com
|
||||||
|
si.com
|
||||||
|
jezebel.com
|
||||||
|
pbs.org
|
||||||
|
consumerreports.org
|
||||||
|
roblox.com
|
||||||
|
urbandictionary.com
|
||||||
|
kotaku.com
|
||||||
|
xbox.com
|
||||||
|
marketwatch.com
|
||||||
|
refinery29.com
|
||||||
|
wikimedia.org
|
||||||
|
tvguide.com
|
||||||
|
politico.com
|
||||||
|
barclaycardus.com
|
||||||
|
abc.go.com
|
||||||
|
mint.com
|
||||||
|
topix.com
|
||||||
|
theblackfriday.com
|
||||||
|
aarp.org
|
||||||
|
hotnewhiphop.com
|
||||||
|
yourdailydish.com
|
||||||
|
sprint.com
|
||||||
|
vox.com
|
||||||
|
cafemom.com
|
||||||
|
nbc.com
|
||||||
|
dailykos.com
|
||||||
|
azlyrics.com
|
||||||
|
autotrader.com
|
||||||
|
hilton.com
|
||||||
|
irs.gov
|
||||||
|
monster.com
|
||||||
|
mailchimp.com
|
||||||
|
webex.com
|
||||||
|
landsend.com
|
||||||
|
wix.com
|
||||||
|
usnews.com
|
||||||
|
jcrew.com
|
||||||
|
jet.com
|
||||||
|
capitalone360.com
|
||||||
|
sharepoint.com
|
||||||
|
schwab.com
|
||||||
|
ulta.com
|
||||||
|
vistaprint.com
|
||||||
|
rollingstone.com
|
||||||
|
biblegateway.com
|
||||||
|
gamespot.com
|
||||||
|
io9.com
|
||||||
|
opentable.com
|
||||||
|
hm.com
|
||||||
|
duckduckgo.com
|
||||||
|
chron.com
|
||||||
|
photobucket.com
|
||||||
|
shareasale.com
|
||||||
|
directv.com
|
||||||
|
avg.com
|
||||||
|
oracle.com
|
||||||
|
hotels.com
|
||||||
|
timewarnercable.com
|
||||||
|
chicagotribune.com
|
||||||
|
ehow.com
|
||||||
|
primewire.ag
|
||||||
|
abs-cbnnews.com
|
||||||
|
salon.com
|
||||||
|
greatergood.com
|
||||||
|
epicurious.com
|
||||||
|
fool.com
|
||||||
|
patheos.com
|
||||||
|
custhelp.com
|
||||||
|
purdue.edu
|
||||||
|
tickld.com
|
||||||
|
frys.com
|
||||||
|
indiatimes.com
|
||||||
|
amazon.co.uk
|
||||||
|
zendesk.com
|
||||||
|
tigerdirect.com
|
||||||
|
stubhub.com
|
||||||
|
healthcare.gov
|
||||||
|
archive.org
|
||||||
|
qualtrics.com
|
||||||
|
ravelry.com
|
||||||
|
cars.com
|
||||||
|
redbox.com
|
||||||
|
jalopnik.com
|
||||||
|
speedtest.net
|
||||||
|
harvard.edu
|
||||||
|
slideshare.net
|
||||||
|
kinja.com
|
||||||
|
nesn.com
|
||||||
|
michaels.com
|
||||||
|
mit.edu
|
||||||
|
bodybuilding.com
|
||||||
|
edmunds.com
|
||||||
|
nhl.com
|
||||||
|
zergnet.com
|
||||||
|
techcrunch.com
|
||||||
|
pogo.com
|
||||||
|
mozilla.org
|
||||||
|
naver.com
|
||||||
|
giphy.com
|
||||||
|
bankrate.com
|
||||||
|
msnbc.com
|
||||||
|
digitaltrends.com
|
||||||
|
fanfiction.net
|
||||||
|
skype.com
|
||||||
|
disney.go.com
|
||||||
|
norton.com
|
||||||
|
androidcentral.com
|
||||||
|
tomshardware.com
|
||||||
|
thefreedictionary.com
|
||||||
|
liveleak.com
|
||||||
|
247sports.com
|
||||||
|
merriam-webster.com
|
||||||
|
wnd.com
|
||||||
|
earthlink.net
|
||||||
|
independent.co.uk
|
||||||
|
drugs.com
|
||||||
|
rotoworld.com
|
||||||
|
nationalgeographic.com
|
||||||
|
ae.com
|
||||||
|
noaa.gov
|
||||||
|
arstechnica.com
|
||||||
|
thinkgeek.com
|
||||||
|
stanford.edu
|
||||||
|
bizjournals.com
|
||||||
|
hootsuite.com
|
||||||
|
genius.com
|
||||||
|
goodhousekeeping.com
|
||||||
|
vanguard.com
|
||||||
|
ny.gov
|
||||||
|
citibankonline.com
|
||||||
|
booking.com
|
||||||
|
mic.com
|
||||||
|
orbitz.com
|
||||||
|
dominos.com
|
||||||
|
medium.com
|
||||||
|
wow.com
|
||||||
|
urbanoutfitters.com
|
||||||
|
douban.com
|
||||||
|
timeanddate.com
|
||||||
|
draftkings.com
|
||||||
|
livestrong.com
|
||||||
|
livingsocial.com
|
||||||
|
cox.net
|
||||||
|
theonion.com
|
||||||
|
marthastewart.com
|
||||||
|
comenity.net
|
||||||
|
worldlifestyle.com
|
||||||
|
disney.com
|
||||||
|
realsimple.com
|
||||||
|
vrbo.com
|
||||||
|
playstation.com
|
||||||
|
potterybarn.com
|
||||||
|
zazzle.com
|
||||||
|
ksl.com
|
||||||
|
tdbank.com
|
||||||
|
sourceforge.net
|
||||||
|
careerbuilder.com
|
|
@ -0,0 +1,50 @@
|
||||||
|
google.com.hk
|
||||||
|
youtube.com
|
||||||
|
google.com
|
||||||
|
facebook.com
|
||||||
|
yahoo.com
|
||||||
|
discuss.com.hk
|
||||||
|
aastocks.com
|
||||||
|
wikipedia.org
|
||||||
|
baidu.com
|
||||||
|
taobao.com
|
||||||
|
pixnet.net
|
||||||
|
bastillepost.com
|
||||||
|
nextmedia.com
|
||||||
|
whatsapp.com
|
||||||
|
instagram.com
|
||||||
|
price.com.hk
|
||||||
|
ettoday.net
|
||||||
|
qq.com
|
||||||
|
hsbc.com.hk
|
||||||
|
tmall.com
|
||||||
|
live.com
|
||||||
|
hkgolden.com
|
||||||
|
reddit.com
|
||||||
|
beautyexchange.com.hk
|
||||||
|
etnet.com.hk
|
||||||
|
on.cc
|
||||||
|
amazon.com
|
||||||
|
twitter.com
|
||||||
|
uwants.com
|
||||||
|
presslogic.com
|
||||||
|
unwire.hk
|
||||||
|
gamer.com.tw
|
||||||
|
hangseng.com
|
||||||
|
hk01.com
|
||||||
|
twitch.tv
|
||||||
|
linkedin.com
|
||||||
|
teepr.com
|
||||||
|
hkjc.com
|
||||||
|
apple.com
|
||||||
|
bomb01.com
|
||||||
|
sina.com.cn
|
||||||
|
weibo.com
|
||||||
|
dcfever.com
|
||||||
|
thestandnews.com
|
||||||
|
office.com
|
||||||
|
openrice.com
|
||||||
|
tumblr.com
|
||||||
|
tvb.com
|
||||||
|
alipay.com
|
||||||
|
stackoverflow.com
|
|
@ -0,0 +1,50 @@
|
||||||
|
google.com
|
||||||
|
google.co.id
|
||||||
|
youtube.com
|
||||||
|
detik.com
|
||||||
|
tribunnews.com
|
||||||
|
facebook.com
|
||||||
|
yahoo.com
|
||||||
|
tokopedia.com
|
||||||
|
liputan6.com
|
||||||
|
kompas.com
|
||||||
|
bukalapak.com
|
||||||
|
kaskus.co.id
|
||||||
|
kapanlagi.com
|
||||||
|
wordpress.com
|
||||||
|
merdeka.com
|
||||||
|
okezone.com
|
||||||
|
elevenia.co.id
|
||||||
|
lazada.co.id
|
||||||
|
uzone.id
|
||||||
|
bintang.com
|
||||||
|
brilio.net
|
||||||
|
popads.net
|
||||||
|
instagram.com
|
||||||
|
bola.net
|
||||||
|
wikipedia.org
|
||||||
|
blogspot.com
|
||||||
|
onclkds.com
|
||||||
|
dream.co.id
|
||||||
|
viva.co.id
|
||||||
|
alodokter.com
|
||||||
|
tempo.co
|
||||||
|
suara.com
|
||||||
|
wowkeren.com
|
||||||
|
idntimes.com
|
||||||
|
bola.com
|
||||||
|
sindonews.com
|
||||||
|
republika.co.id
|
||||||
|
kompasiana.com
|
||||||
|
vemale.com
|
||||||
|
blanja.com
|
||||||
|
cnnindonesia.com
|
||||||
|
olx.co.id
|
||||||
|
lk21.org
|
||||||
|
popcash.net
|
||||||
|
blibli.com
|
||||||
|
poptm.com
|
||||||
|
nonton.movie
|
||||||
|
indexmovie.me
|
||||||
|
adexchangeprediction.com
|
||||||
|
subscene.com
|
|
@ -0,0 +1,50 @@
|
||||||
|
google.pl
|
||||||
|
youtube.com
|
||||||
|
facebook.com
|
||||||
|
google.com
|
||||||
|
allegro.pl
|
||||||
|
onet.pl
|
||||||
|
wp.pl
|
||||||
|
wikipedia.org
|
||||||
|
olx.pl
|
||||||
|
vk.com
|
||||||
|
interia.pl
|
||||||
|
wykop.pl
|
||||||
|
gazeta.pl
|
||||||
|
filmweb.pl
|
||||||
|
instagram.com
|
||||||
|
wiocha.pl
|
||||||
|
cda.pl
|
||||||
|
aliexpress.com
|
||||||
|
otomoto.pl
|
||||||
|
mbank.pl
|
||||||
|
reddit.com
|
||||||
|
ceneo.pl
|
||||||
|
tvn24.pl
|
||||||
|
twitter.com
|
||||||
|
gumtree.pl
|
||||||
|
blogspot.com
|
||||||
|
kwejk.pl
|
||||||
|
wyborcza.pl
|
||||||
|
joemonster.org
|
||||||
|
stackoverflow.com
|
||||||
|
twitch.tv
|
||||||
|
o2.pl
|
||||||
|
ipko.pl
|
||||||
|
steamcommunity.com
|
||||||
|
github.com
|
||||||
|
chomikuj.pl
|
||||||
|
centrum24.pl
|
||||||
|
linkedin.com
|
||||||
|
money.pl
|
||||||
|
librus.pl
|
||||||
|
demotywatory.pl
|
||||||
|
sport.pl
|
||||||
|
microsoft.com
|
||||||
|
zalukaj.com
|
||||||
|
wikia.com
|
||||||
|
jbzdy.pl
|
||||||
|
imgur.com
|
||||||
|
flashscore.pl
|
||||||
|
gry-online.pl
|
||||||
|
pudelek.pl
|
|
@ -0,0 +1,50 @@
|
||||||
|
vk.com
|
||||||
|
google.ru
|
||||||
|
yandex.ru
|
||||||
|
youtube.com
|
||||||
|
mail.ru
|
||||||
|
ok.ru
|
||||||
|
google.com
|
||||||
|
avito.ru
|
||||||
|
aliexpress.com
|
||||||
|
wikipedia.org
|
||||||
|
instagram.com
|
||||||
|
sberbank.ru
|
||||||
|
gismeteo.ru
|
||||||
|
rambler.ru
|
||||||
|
kinogo.club
|
||||||
|
kinopoisk.ru
|
||||||
|
drom.ru
|
||||||
|
facebook.com
|
||||||
|
pikabu.ru
|
||||||
|
drive2.ru
|
||||||
|
rutracker.org
|
||||||
|
twitch.tv
|
||||||
|
rbc.ru
|
||||||
|
hh.ru
|
||||||
|
gosuslugi.ru
|
||||||
|
lenta.ru
|
||||||
|
pochta.ru
|
||||||
|
wildberries.ru
|
||||||
|
wikia.com
|
||||||
|
4pda.ru
|
||||||
|
fb.ru
|
||||||
|
seasonvar.ru
|
||||||
|
kp.ru
|
||||||
|
znanija.com
|
||||||
|
ucoz.ru
|
||||||
|
narod.ru
|
||||||
|
mts.ru
|
||||||
|
infourok.ru
|
||||||
|
ebay.com
|
||||||
|
ozon.ru
|
||||||
|
worldoftanks.ru
|
||||||
|
mos.ru
|
||||||
|
vesti.ru
|
||||||
|
nnmclub.to
|
||||||
|
microsoft.com
|
||||||
|
rp5.ru
|
||||||
|
2gis.ru
|
||||||
|
consultant.ru
|
||||||
|
fotostrana.ru
|
||||||
|
dnevnik.ru
|
|
@ -0,0 +1,49 @@
|
||||||
|
google.com.sg
|
||||||
|
youtube.com
|
||||||
|
google.com
|
||||||
|
facebook.com
|
||||||
|
yahoo.com
|
||||||
|
wikipedia.org
|
||||||
|
reddit.com
|
||||||
|
blogspot.sg
|
||||||
|
live.com
|
||||||
|
instagram.com
|
||||||
|
qoo10.sg
|
||||||
|
whatsapp.com
|
||||||
|
linkedin.com
|
||||||
|
dbs.com.sg
|
||||||
|
amazon.com
|
||||||
|
twitter.com
|
||||||
|
wordpress.com
|
||||||
|
onclkds.com
|
||||||
|
office.com
|
||||||
|
allsingaporestuff.com
|
||||||
|
baidu.com
|
||||||
|
lazada.sg
|
||||||
|
straitstimes.com
|
||||||
|
singpass.gov.sg
|
||||||
|
google.co.id
|
||||||
|
taobao.com
|
||||||
|
tumblr.com
|
||||||
|
gomovies.to
|
||||||
|
wikia.com
|
||||||
|
hardwarezone.com.sg
|
||||||
|
nus.edu.sg
|
||||||
|
msn.com
|
||||||
|
microsoft.com
|
||||||
|
carousell.com
|
||||||
|
kissanime.ru
|
||||||
|
ocbc.com
|
||||||
|
stackoverflow.com
|
||||||
|
ntu.edu.sg
|
||||||
|
thepiratebay.org
|
||||||
|
aliexpress.com
|
||||||
|
imgur.com
|
||||||
|
dropbox.com
|
||||||
|
apple.com
|
||||||
|
channelnewsasia.com
|
||||||
|
imdb.com
|
||||||
|
twitch.tv
|
||||||
|
abs-cbn.com
|
||||||
|
jobstreet.com.sg
|
||||||
|
uob.com.sg
|
|
@ -0,0 +1,50 @@
|
||||||
|
google.com.tw
|
||||||
|
pixnet.net
|
||||||
|
youtube.com
|
||||||
|
facebook.com
|
||||||
|
ettoday.net
|
||||||
|
google.com
|
||||||
|
yahoo.com
|
||||||
|
ltn.com.tw
|
||||||
|
nownews.com
|
||||||
|
setn.com
|
||||||
|
momoshop.com.tw
|
||||||
|
wikipedia.org
|
||||||
|
ck101.com
|
||||||
|
ptt.cc
|
||||||
|
tvbs.com.tw
|
||||||
|
104.com.tw
|
||||||
|
gamer.com.tw
|
||||||
|
appledaily.com.tw
|
||||||
|
pchome.com.tw
|
||||||
|
ruten.com.tw
|
||||||
|
ctitv.com.tw
|
||||||
|
teepr.com
|
||||||
|
life.tw
|
||||||
|
blogspot.tw
|
||||||
|
dcard.tw
|
||||||
|
baidu.com
|
||||||
|
udn.com
|
||||||
|
mobile01.com
|
||||||
|
eyny.com
|
||||||
|
epochtimes.com
|
||||||
|
qoolquiz.com
|
||||||
|
bomb01.com
|
||||||
|
talk.tw
|
||||||
|
ipetgroup.com
|
||||||
|
storm.mg
|
||||||
|
123kubo.com
|
||||||
|
cmoney.tw
|
||||||
|
taobao.com
|
||||||
|
twitch.tv
|
||||||
|
instagram.com
|
||||||
|
xuite.net
|
||||||
|
sina.com.tw
|
||||||
|
1111.com.tw
|
||||||
|
businessweekly.com.tw
|
||||||
|
elle.com.tw
|
||||||
|
twitter.com
|
||||||
|
books.com.tw
|
||||||
|
591.com.tw
|
||||||
|
everydayhealth.com.tw
|
||||||
|
techbang.com
|
|
@ -0,0 +1,49 @@
|
||||||
|
google.com
|
||||||
|
youtube.com
|
||||||
|
facebook.com
|
||||||
|
reddit.com
|
||||||
|
amazon.com
|
||||||
|
wikipedia.org
|
||||||
|
yahoo.com
|
||||||
|
twitter.com
|
||||||
|
netflix.com
|
||||||
|
ebay.com
|
||||||
|
imgur.com
|
||||||
|
linkedin.com
|
||||||
|
instagram.com
|
||||||
|
diply.com
|
||||||
|
craigslist.org
|
||||||
|
live.com
|
||||||
|
office.com
|
||||||
|
twitch.tv
|
||||||
|
tumblr.com
|
||||||
|
pinterest.com
|
||||||
|
espn.com
|
||||||
|
cnn.com
|
||||||
|
bing.com
|
||||||
|
wikia.com
|
||||||
|
chase.com
|
||||||
|
imdb.com
|
||||||
|
nytimes.com
|
||||||
|
paypal.com
|
||||||
|
blogspot.com
|
||||||
|
apple.com
|
||||||
|
yelp.com
|
||||||
|
stackoverflow.com
|
||||||
|
bankofamerica.com
|
||||||
|
wordpress.com
|
||||||
|
github.com
|
||||||
|
microsoft.com
|
||||||
|
wellsfargo.com
|
||||||
|
zillow.com
|
||||||
|
salesforce.com
|
||||||
|
msn.com
|
||||||
|
walmart.com
|
||||||
|
weather.com
|
||||||
|
dropbox.com
|
||||||
|
buzzfeed.com
|
||||||
|
intuit.com
|
||||||
|
washingtonpost.com
|
||||||
|
soundcloud.com
|
||||||
|
huffingtonpost.com
|
||||||
|
indeed.com
|
|
@ -0,0 +1,68 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains functionality to manage custom domains for auto-completion.
|
||||||
|
*/
|
||||||
|
object CustomDomains {
|
||||||
|
private const val PREFERENCE_NAME = "custom_autocomplete"
|
||||||
|
private const val KEY_DOMAINS = "custom_domains"
|
||||||
|
private const val SEPARATOR = "@<;>@"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the previously added/saved custom domains from preferences.
|
||||||
|
*
|
||||||
|
* @param context the application context
|
||||||
|
* @return list of custom domains
|
||||||
|
*/
|
||||||
|
fun load(context: Context): List<String> =
|
||||||
|
preferences(context).getString(KEY_DOMAINS, "")!!
|
||||||
|
.split(SEPARATOR)
|
||||||
|
.filter { !it.isEmpty() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the provided domains to preferences.
|
||||||
|
*
|
||||||
|
* @param context the application context
|
||||||
|
* @param domains list of domains
|
||||||
|
*/
|
||||||
|
fun save(context: Context, domains: List<String>) {
|
||||||
|
preferences(context)
|
||||||
|
.edit()
|
||||||
|
.putString(KEY_DOMAINS, domains.joinToString(separator = SEPARATOR))
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the provided domain to preferences.
|
||||||
|
*
|
||||||
|
* @param context the application context
|
||||||
|
* @param domain the domain to add
|
||||||
|
*/
|
||||||
|
fun add(context: Context, domain: String) {
|
||||||
|
val domains = mutableListOf<String>()
|
||||||
|
domains.addAll(load(context))
|
||||||
|
domains.add(domain)
|
||||||
|
|
||||||
|
save(context, domains)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the provided domain from preferences.
|
||||||
|
*
|
||||||
|
* @param context the application context
|
||||||
|
* @param domains the domain to remove
|
||||||
|
*/
|
||||||
|
fun remove(context: Context, domains: List<String>) {
|
||||||
|
save(context, load(context) - domains)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun preferences(context: Context): SharedPreferences =
|
||||||
|
context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class intended for internal use which encapsulates meta data about a domain.
|
||||||
|
*/
|
||||||
|
data class Domain(val protocol: String, val hasWww: Boolean, val host: String) {
|
||||||
|
internal val url: String
|
||||||
|
get() = "$protocol${if (hasWww) "www." else "" }$host"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PROTOCOL_INDEX = 1
|
||||||
|
private const val WWW_INDEX = 2
|
||||||
|
private const val HOST_INDEX = 3
|
||||||
|
|
||||||
|
private const val DEFAULT_PROTOCOL = "http://"
|
||||||
|
|
||||||
|
private val urlMatcher = Regex("""(https?://)?(www.)?(.+)?""")
|
||||||
|
|
||||||
|
fun create(url: String): Domain {
|
||||||
|
val result = urlMatcher.find(url)
|
||||||
|
|
||||||
|
return result?.let {
|
||||||
|
val protocol = it.groups[PROTOCOL_INDEX]?.value ?: DEFAULT_PROTOCOL
|
||||||
|
val hasWww = it.groups[WWW_INDEX]?.value == "www."
|
||||||
|
val host = it.groups[HOST_INDEX]?.value ?: throw IllegalStateException()
|
||||||
|
|
||||||
|
return Domain(protocol, hasWww, host)
|
||||||
|
} ?: throw IllegalStateException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun Iterable<String>.into(): List<Domain> {
|
||||||
|
return this.map { Domain.create(it) }
|
||||||
|
}
|
|
@ -0,0 +1,151 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides autocomplete functionality for domains, based on a provided list
|
||||||
|
* of assets (see [Domains]) and/or a custom domain list managed by
|
||||||
|
* [CustomDomains].
|
||||||
|
*/
|
||||||
|
// FIXME delete this https://github.com/mozilla-mobile/android-components/issues/1358
|
||||||
|
@Deprecated(
|
||||||
|
"Use `ShippedDomainsProvider` or `CustomDomainsProvider`",
|
||||||
|
ReplaceWith(
|
||||||
|
"ShippedDomainsProvider()/CustomDomainsProvider()",
|
||||||
|
"mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider",
|
||||||
|
"mozilla.components.browser.domains.autocomplete.CustomDomainsProvider",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
class DomainAutoCompleteProvider {
|
||||||
|
|
||||||
|
object AutocompleteSource {
|
||||||
|
const val DEFAULT_LIST = "default"
|
||||||
|
const val CUSTOM_LIST = "custom"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a result of auto-completion.
|
||||||
|
*
|
||||||
|
* @property text the result text starting with the raw search text as passed
|
||||||
|
* to [autocomplete] followed by the completion text (e.g. moz => mozilla.org)
|
||||||
|
* @property url the complete url (containing the protocol) as provided
|
||||||
|
* when the domain was saved. (e.g. https://mozilla.org)
|
||||||
|
* @property source the source identifier of the autocomplete source
|
||||||
|
* @property size total number of available autocomplete domains
|
||||||
|
* in this source
|
||||||
|
*/
|
||||||
|
data class Result(val text: String, val url: String, val source: String, val size: Int)
|
||||||
|
|
||||||
|
// We compute these on worker threads; make sure results are immediately visible on the UI thread.
|
||||||
|
@Volatile
|
||||||
|
internal var customDomains = emptyList<Domain>()
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
internal var shippedDomains = emptyList<Domain>()
|
||||||
|
private var useCustomDomains = false
|
||||||
|
private var useShippedDomains = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes an autocomplete suggestion for the given text, and invokes the
|
||||||
|
* provided callback, passing the result.
|
||||||
|
*
|
||||||
|
* @param rawText text to be auto completed
|
||||||
|
* @return the result of auto-completion. If no match is found an empty
|
||||||
|
* result object is returned.
|
||||||
|
*/
|
||||||
|
@Suppress("ReturnCount")
|
||||||
|
fun autocomplete(rawText: String): Result {
|
||||||
|
if (useCustomDomains) {
|
||||||
|
val result = tryToAutocomplete(rawText, customDomains, AutocompleteSource.CUSTOM_LIST)
|
||||||
|
if (result != null) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useShippedDomains) {
|
||||||
|
val result = tryToAutocomplete(rawText, shippedDomains, AutocompleteSource.DEFAULT_LIST)
|
||||||
|
if (result != null) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result("", "", "", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes this provider instance by making sure the shipped and/or custom
|
||||||
|
* domains are loaded.
|
||||||
|
*
|
||||||
|
* @param context the application context
|
||||||
|
* @param useShippedDomains true (default) if the domains provided by this
|
||||||
|
* module should be used, otherwise false.
|
||||||
|
* @param useCustomDomains true if the custom domains provided by
|
||||||
|
* [CustomDomains] should be used, otherwise false (default).
|
||||||
|
* @param loadDomainsFromDisk true (default) if domains should be loaded,
|
||||||
|
* otherwise false. This parameter is for testing purposes only.
|
||||||
|
*/
|
||||||
|
fun initialize(
|
||||||
|
context: Context,
|
||||||
|
useShippedDomains: Boolean = true,
|
||||||
|
useCustomDomains: Boolean = false,
|
||||||
|
loadDomainsFromDisk: Boolean = true,
|
||||||
|
) {
|
||||||
|
this.useCustomDomains = useCustomDomains
|
||||||
|
this.useShippedDomains = useShippedDomains
|
||||||
|
|
||||||
|
if (!loadDomainsFromDisk) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!useCustomDomains && !useShippedDomains) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
if (useCustomDomains) {
|
||||||
|
customDomains = async { CustomDomains.load(context).into() }.await()
|
||||||
|
}
|
||||||
|
if (useShippedDomains) {
|
||||||
|
shippedDomains = async { Domains.load(context).into() }.await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("ReturnCount")
|
||||||
|
private fun tryToAutocomplete(rawText: String, domains: List<Domain>, source: String): Result? {
|
||||||
|
// Search terms are all lowercase already, we just need to lowercase the search text
|
||||||
|
val searchText = rawText.lowercase(Locale.US)
|
||||||
|
|
||||||
|
domains.forEach {
|
||||||
|
val wwwDomain = "www.${it.host}"
|
||||||
|
if (wwwDomain.startsWith(searchText)) {
|
||||||
|
return Result(getResultText(rawText, wwwDomain), it.url, source, domains.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it.host.startsWith(searchText)) {
|
||||||
|
return Result(getResultText(rawText, it.host), it.url, source, domains.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Our autocomplete list is all lower case, however the search text might
|
||||||
|
* be mixed case. Our autocomplete EditText code does more string comparison,
|
||||||
|
* which fails if the suggestion doesn't exactly match searchText (ie.
|
||||||
|
* if casing differs). It's simplest to just build a suggestion
|
||||||
|
* that exactly matches the search text - which is what this method is for:
|
||||||
|
*/
|
||||||
|
private fun getResultText(rawSearchText: String, autocomplete: String) =
|
||||||
|
rawSearchText + autocomplete.substring(rawSearchText.length)
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build.VERSION.SDK_INT
|
||||||
|
import android.os.Build.VERSION_CODES
|
||||||
|
import android.os.LocaleList
|
||||||
|
import android.text.TextUtils
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains functionality to access domain lists shipped as part of this
|
||||||
|
* module's assets.
|
||||||
|
*/
|
||||||
|
object Domains {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the domains applicable to the app's locale, plus the domains
|
||||||
|
* in the 'global' list.
|
||||||
|
*
|
||||||
|
* @param context the application context
|
||||||
|
* @return list of domains
|
||||||
|
*/
|
||||||
|
fun load(context: Context): List<String> {
|
||||||
|
return load(context, getCountriesInDefaultLocaleList())
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun load(context: Context, countries: Set<String>): List<String> {
|
||||||
|
val domains = LinkedHashSet<String>()
|
||||||
|
val availableLists = getAvailableDomainLists(context)
|
||||||
|
|
||||||
|
// First initialize the country specific lists following the default locale order
|
||||||
|
countries
|
||||||
|
.filter { availableLists.contains(it) }
|
||||||
|
.forEach { loadDomainsForLanguage(context, domains, it) }
|
||||||
|
|
||||||
|
// And then add domains from the global list
|
||||||
|
loadDomainsForLanguage(context, domains, "global")
|
||||||
|
|
||||||
|
return domains.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAvailableDomainLists(context: Context): Set<String> {
|
||||||
|
val availableDomains = LinkedHashSet<String>()
|
||||||
|
val assetManager = context.assets
|
||||||
|
val domains = try {
|
||||||
|
assetManager.list("domains") ?: emptyArray<String>()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
emptyArray<String>()
|
||||||
|
}
|
||||||
|
availableDomains.addAll(domains)
|
||||||
|
return availableDomains
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadDomainsForLanguage(context: Context, domains: MutableSet<String>, country: String) {
|
||||||
|
val assetManager = context.assets
|
||||||
|
val languageDomains = try {
|
||||||
|
assetManager.open("domains/$country").bufferedReader().readLines()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
emptyList<String>()
|
||||||
|
}
|
||||||
|
domains.addAll(languageDomains)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCountriesInDefaultLocaleList(): Set<String> {
|
||||||
|
val countries = java.util.LinkedHashSet<String>()
|
||||||
|
val addIfNotEmpty = { c: String -> if (!TextUtils.isEmpty(c)) countries.add(c.lowercase(Locale.US)) }
|
||||||
|
|
||||||
|
if (SDK_INT >= VERSION_CODES.N) {
|
||||||
|
val list = LocaleList.getDefault()
|
||||||
|
for (i in 0 until list.size()) {
|
||||||
|
addIfNotEmpty(list.get(i).country)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addIfNotEmpty(Locale.getDefault().country)
|
||||||
|
}
|
||||||
|
|
||||||
|
return countries
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains.autocomplete
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import mozilla.components.browser.domains.CustomDomains
|
||||||
|
import mozilla.components.browser.domains.Domain
|
||||||
|
import mozilla.components.browser.domains.Domains
|
||||||
|
import mozilla.components.browser.domains.into
|
||||||
|
import mozilla.components.concept.toolbar.AutocompleteProvider
|
||||||
|
import mozilla.components.concept.toolbar.AutocompleteResult
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
enum class DomainList(val listName: String) {
|
||||||
|
DEFAULT("default"),
|
||||||
|
CUSTOM("custom"),
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides autocomplete functionality for domains based on provided list of assets (see [Domains]).
|
||||||
|
*/
|
||||||
|
class ShippedDomainsProvider(override val autocompletePriority: Int = 0) :
|
||||||
|
BaseDomainAutocompleteProvider(DomainList.DEFAULT, Domains.asLoader())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides autocomplete functionality for domains based on a list managed by [CustomDomains].
|
||||||
|
*/
|
||||||
|
class CustomDomainsProvider(override val autocompletePriority: Int = 0) :
|
||||||
|
BaseDomainAutocompleteProvider(DomainList.CUSTOM, CustomDomains.asLoader())
|
||||||
|
|
||||||
|
typealias DomainsLoader = (Context) -> List<Domain>
|
||||||
|
|
||||||
|
private fun Domains.asLoader(): DomainsLoader = { context: Context -> load(context).into() }
|
||||||
|
private fun CustomDomains.asLoader(): DomainsLoader = { context: Context -> load(context).into() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides common autocomplete functionality powered by domain lists.
|
||||||
|
*
|
||||||
|
* @param list source of domains
|
||||||
|
* @param domainsLoader provider for all available domains
|
||||||
|
*/
|
||||||
|
open class BaseDomainAutocompleteProvider(
|
||||||
|
private val list: DomainList,
|
||||||
|
private val domainsLoader: DomainsLoader,
|
||||||
|
override val autocompletePriority: Int = 0,
|
||||||
|
) : AutocompleteProvider, CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||||
|
|
||||||
|
// We compute 'domains' on the worker thread; make sure it's immediately visible on the UI thread.
|
||||||
|
@Volatile
|
||||||
|
var domains: List<Domain> = emptyList()
|
||||||
|
|
||||||
|
fun initialize(context: Context) {
|
||||||
|
launch {
|
||||||
|
domains = async { domainsLoader(context) }.await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes an autocomplete suggestion for the given text, and invokes the
|
||||||
|
* provided callback, passing the result.
|
||||||
|
*
|
||||||
|
* @param query text to be auto completed
|
||||||
|
* @return the result of auto-completion, or null if no match is found.
|
||||||
|
*/
|
||||||
|
override suspend fun getAutocompleteSuggestion(query: String): AutocompleteResult? {
|
||||||
|
// Search terms are all lowercase already, we just need to lowercase the search text
|
||||||
|
val searchText = query.lowercase(Locale.US)
|
||||||
|
|
||||||
|
domains.forEach {
|
||||||
|
val wwwDomain = "www.${it.host}"
|
||||||
|
if (wwwDomain.startsWith(searchText)) {
|
||||||
|
return AutocompleteResult(
|
||||||
|
input = searchText,
|
||||||
|
text = getResultText(query, wwwDomain),
|
||||||
|
url = it.url,
|
||||||
|
source = list.listName,
|
||||||
|
totalItems = domains.size,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it.host.startsWith(searchText)) {
|
||||||
|
return AutocompleteResult(
|
||||||
|
input = searchText,
|
||||||
|
text = getResultText(query, it.host),
|
||||||
|
url = it.url,
|
||||||
|
source = list.listName,
|
||||||
|
totalItems = domains.size,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Our autocomplete list is all lower case, however the search text might
|
||||||
|
* be mixed case. Our autocomplete EditText code does more string comparison,
|
||||||
|
* which fails if the suggestion doesn't exactly match searchText (ie.
|
||||||
|
* if casing differs). It's simplest to just build a suggestion
|
||||||
|
* that exactly matches the search text - which is what this method is for:
|
||||||
|
*/
|
||||||
|
private fun getResultText(rawSearchText: String, autocomplete: String) =
|
||||||
|
rawSearchText + autocomplete.substring(rawSearchText.length)
|
||||||
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Looper.getMainLooper
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import mozilla.components.browser.domains.autocomplete.BaseDomainAutocompleteProvider
|
||||||
|
import mozilla.components.browser.domains.autocomplete.DomainList
|
||||||
|
import mozilla.components.browser.domains.autocomplete.DomainsLoader
|
||||||
|
import mozilla.components.concept.toolbar.AutocompleteProvider
|
||||||
|
import mozilla.components.support.test.robolectric.testContext
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.Shadows.shadowOf
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class BaseDomainAutocompleteProviderTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `empty provider with DEFAULT list returns nothing`() {
|
||||||
|
val provider = createAndInitProvider(testContext, DomainList.DEFAULT) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNoCompletion(provider, "m")
|
||||||
|
assertNoCompletion(provider, "mo")
|
||||||
|
assertNoCompletion(provider, "moz")
|
||||||
|
assertNoCompletion(provider, "g")
|
||||||
|
assertNoCompletion(provider, "go")
|
||||||
|
assertNoCompletion(provider, "goo")
|
||||||
|
assertNoCompletion(provider, "w")
|
||||||
|
assertNoCompletion(provider, "www")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `empty provider with CUSTOM list returns nothing`() {
|
||||||
|
val provider = createAndInitProvider(testContext, DomainList.CUSTOM) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNoCompletion(provider, "m")
|
||||||
|
assertNoCompletion(provider, "mo")
|
||||||
|
assertNoCompletion(provider, "moz")
|
||||||
|
assertNoCompletion(provider, "g")
|
||||||
|
assertNoCompletion(provider, "go")
|
||||||
|
assertNoCompletion(provider, "goo")
|
||||||
|
assertNoCompletion(provider, "w")
|
||||||
|
assertNoCompletion(provider, "www")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `non-empty provider with DEFAULT list returns completion`() {
|
||||||
|
val domains = listOf("mozilla.org", "google.com", "facebook.com").into()
|
||||||
|
val list = DomainList.DEFAULT
|
||||||
|
val domainsCount = domains.size
|
||||||
|
|
||||||
|
val provider = createAndInitProvider(testContext, list) { domains }
|
||||||
|
shadowOf(getMainLooper()).idle()
|
||||||
|
|
||||||
|
assertCompletion(provider, list, domainsCount, "m", "m", "mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "moz", "moz", "mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "www", "www", "www.mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "www.face", "www.face", "www.facebook.com", "http://facebook.com")
|
||||||
|
assertCompletion(provider, list, domainsCount, "M", "m", "Mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "MOZ", "moz", "MOZilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "www.GOO", "www.goo", "www.GOOgle.com", "http://google.com")
|
||||||
|
assertCompletion(provider, list, domainsCount, "WWW.GOOGLE.", "www.google.", "WWW.GOOGLE.com", "http://google.com")
|
||||||
|
assertCompletion(provider, list, domainsCount, "www.facebook.com", "www.facebook.com", "www.facebook.com", "http://facebook.com")
|
||||||
|
assertCompletion(provider, list, domainsCount, "facebook.com", "facebook.com", "facebook.com", "http://facebook.com")
|
||||||
|
|
||||||
|
assertNoCompletion(provider, "wwww")
|
||||||
|
assertNoCompletion(provider, "yahoo")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `non-empty provider with CUSTOM list returns completion`() {
|
||||||
|
val domains = listOf("mozilla.org", "google.com", "facebook.com").into()
|
||||||
|
val list = DomainList.CUSTOM
|
||||||
|
val domainsCount = domains.size
|
||||||
|
|
||||||
|
val provider = createAndInitProvider(testContext, list) { domains }
|
||||||
|
shadowOf(getMainLooper()).idle()
|
||||||
|
|
||||||
|
assertCompletion(provider, list, domainsCount, "m", "m", "mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "moz", "moz", "mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "www", "www", "www.mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "www.face", "www.face", "www.facebook.com", "http://facebook.com")
|
||||||
|
assertCompletion(provider, list, domainsCount, "M", "m", "Mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "MOZ", "moz", "MOZilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, list, domainsCount, "www.GOO", "www.goo", "www.GOOgle.com", "http://google.com")
|
||||||
|
assertCompletion(provider, list, domainsCount, "WWW.GOOGLE.", "www.google.", "WWW.GOOGLE.com", "http://google.com")
|
||||||
|
assertCompletion(provider, list, domainsCount, "www.facebook.com", "www.facebook.com", "www.facebook.com", "http://facebook.com")
|
||||||
|
assertCompletion(provider, list, domainsCount, "facebook.com", "facebook.com", "facebook.com", "http://facebook.com")
|
||||||
|
|
||||||
|
assertNoCompletion(provider, "wwww")
|
||||||
|
assertNoCompletion(provider, "yahoo")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private fun assertCompletion(
|
||||||
|
provider: AutocompleteProvider,
|
||||||
|
domainSource: DomainList,
|
||||||
|
sourceSize: Int,
|
||||||
|
input: String,
|
||||||
|
expectedInput: String,
|
||||||
|
completion: String,
|
||||||
|
expectedUrl: String,
|
||||||
|
) = runTest {
|
||||||
|
val result = provider.getAutocompleteSuggestion(input)!!
|
||||||
|
|
||||||
|
assertTrue("Autocompletion shouldn't be empty", result.text.isNotEmpty())
|
||||||
|
|
||||||
|
assertEquals("Autocompletion input", expectedInput, result.input)
|
||||||
|
assertEquals("Autocompletion completion", completion, result.text)
|
||||||
|
assertEquals("Autocompletion source list", domainSource.listName, result.source)
|
||||||
|
assertEquals("Autocompletion url", expectedUrl, result.url)
|
||||||
|
assertEquals("Autocompletion source list size", sourceSize, result.totalItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private fun assertNoCompletion(provider: AutocompleteProvider, input: String) = runTest {
|
||||||
|
val result = provider.getAutocompleteSuggestion(input)
|
||||||
|
|
||||||
|
assertNull("Result should be null", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createAndInitProvider(context: Context, list: DomainList, loader: DomainsLoader): AutocompleteProvider =
|
||||||
|
object : BaseDomainAutocompleteProvider(list, loader) {
|
||||||
|
override val coroutineContext = super.coroutineContext + Dispatchers.Main
|
||||||
|
}.apply { initialize(context) }
|
|
@ -0,0 +1,81 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import mozilla.components.support.test.robolectric.testContext
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class CustomDomainsTest {
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
testContext.getSharedPreferences("custom_autocomplete", Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.clear()
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
@Test
|
||||||
|
fun customListIsEmptyByDefault() {
|
||||||
|
val domains = CustomDomains.load(testContext)
|
||||||
|
|
||||||
|
assertEquals(0, domains.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun saveAndRemoveDomains() {
|
||||||
|
CustomDomains.save(
|
||||||
|
testContext,
|
||||||
|
listOf(
|
||||||
|
"mozilla.org",
|
||||||
|
"example.org",
|
||||||
|
"example.com",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
var domains = CustomDomains.load(testContext)
|
||||||
|
assertEquals(3, domains.size)
|
||||||
|
|
||||||
|
CustomDomains.remove(testContext, listOf("example.org", "example.com"))
|
||||||
|
domains = CustomDomains.load(testContext)
|
||||||
|
assertEquals(1, domains.size)
|
||||||
|
assertEquals("mozilla.org", domains.elementAt(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun addAndLoadDomains() {
|
||||||
|
CustomDomains.add(testContext, "mozilla.org")
|
||||||
|
val domains = CustomDomains.load(testContext)
|
||||||
|
assertEquals(1, domains.size)
|
||||||
|
assertEquals("mozilla.org", domains.elementAt(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun saveAndLoadDomains() {
|
||||||
|
CustomDomains.save(
|
||||||
|
testContext,
|
||||||
|
listOf(
|
||||||
|
"mozilla.org",
|
||||||
|
"example.org",
|
||||||
|
"example.com",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val domains = CustomDomains.load(testContext)
|
||||||
|
|
||||||
|
assertEquals(3, domains.size)
|
||||||
|
assertEquals("mozilla.org", domains.elementAt(0))
|
||||||
|
assertEquals("example.org", domains.elementAt(1))
|
||||||
|
assertEquals("example.com", domains.elementAt(2))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
@file:Suppress("DEPRECATION")
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import mozilla.components.browser.domains.DomainAutoCompleteProvider.AutocompleteSource.CUSTOM_LIST
|
||||||
|
import mozilla.components.browser.domains.DomainAutoCompleteProvider.AutocompleteSource.DEFAULT_LIST
|
||||||
|
import mozilla.components.support.test.robolectric.testContext
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
/**
|
||||||
|
* While [DomainAutoCompleteProvider] exists (even if it's deprecated) we need to test it.
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class DomainAutoCompleteProviderTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun autocompletionWithShippedDomains() {
|
||||||
|
val provider = DomainAutoCompleteProvider().also {
|
||||||
|
it.initialize(
|
||||||
|
testContext,
|
||||||
|
useShippedDomains = true,
|
||||||
|
useCustomDomains = false,
|
||||||
|
loadDomainsFromDisk = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
it.shippedDomains = listOf("mozilla.org", "google.com", "facebook.com").into()
|
||||||
|
it.customDomains = emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val size = provider.shippedDomains.size
|
||||||
|
|
||||||
|
assertCompletion(provider, "m", DEFAULT_LIST, size, "mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, "www", DEFAULT_LIST, size, "www.mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, "www.face", DEFAULT_LIST, size, "www.facebook.com", "http://facebook.com")
|
||||||
|
assertCompletion(provider, "MOZ", DEFAULT_LIST, size, "MOZilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, "www.GOO", DEFAULT_LIST, size, "www.GOOgle.com", "http://google.com")
|
||||||
|
assertCompletion(provider, "WWW.GOOGLE.", DEFAULT_LIST, size, "WWW.GOOGLE.com", "http://google.com")
|
||||||
|
assertCompletion(provider, "www.facebook.com", DEFAULT_LIST, size, "www.facebook.com", "http://facebook.com")
|
||||||
|
assertCompletion(provider, "facebook.com", DEFAULT_LIST, size, "facebook.com", "http://facebook.com")
|
||||||
|
|
||||||
|
assertNoCompletion(provider, "wwww")
|
||||||
|
assertNoCompletion(provider, "yahoo")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun autocompletionWithCustomDomains() {
|
||||||
|
val domains = listOf("facebook.com", "google.com", "mozilla.org")
|
||||||
|
val customDomains = listOf("gap.com", "www.fanfiction.com", "https://mobile.de")
|
||||||
|
|
||||||
|
val provider = DomainAutoCompleteProvider().also {
|
||||||
|
it.initialize(
|
||||||
|
testContext,
|
||||||
|
useShippedDomains = true,
|
||||||
|
useCustomDomains = true,
|
||||||
|
loadDomainsFromDisk = false,
|
||||||
|
)
|
||||||
|
it.shippedDomains = domains.into()
|
||||||
|
it.customDomains = customDomains.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertCompletion(provider, "f", CUSTOM_LIST, customDomains.size, "fanfiction.com", "http://www.fanfiction.com")
|
||||||
|
assertCompletion(provider, "fa", CUSTOM_LIST, customDomains.size, "fanfiction.com", "http://www.fanfiction.com")
|
||||||
|
assertCompletion(provider, "fac", DEFAULT_LIST, domains.size, "facebook.com", "http://facebook.com")
|
||||||
|
|
||||||
|
assertCompletion(provider, "g", CUSTOM_LIST, customDomains.size, "gap.com", "http://gap.com")
|
||||||
|
assertCompletion(provider, "go", DEFAULT_LIST, domains.size, "google.com", "http://google.com")
|
||||||
|
assertCompletion(provider, "ga", CUSTOM_LIST, customDomains.size, "gap.com", "http://gap.com")
|
||||||
|
|
||||||
|
assertCompletion(provider, "m", CUSTOM_LIST, customDomains.size, "mobile.de", "https://mobile.de")
|
||||||
|
assertCompletion(provider, "mo", CUSTOM_LIST, customDomains.size, "mobile.de", "https://mobile.de")
|
||||||
|
assertCompletion(provider, "mob", CUSTOM_LIST, customDomains.size, "mobile.de", "https://mobile.de")
|
||||||
|
assertCompletion(provider, "moz", DEFAULT_LIST, domains.size, "mozilla.org", "http://mozilla.org")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun autocompletionWithoutDomains() {
|
||||||
|
val filter = DomainAutoCompleteProvider()
|
||||||
|
assertNoCompletion(filter, "mozilla")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertCompletion(
|
||||||
|
provider: DomainAutoCompleteProvider,
|
||||||
|
text: String,
|
||||||
|
domainSource: String,
|
||||||
|
sourceSize: Int,
|
||||||
|
completion: String,
|
||||||
|
expectedUrl: String,
|
||||||
|
) {
|
||||||
|
val result = provider.autocomplete(text)
|
||||||
|
|
||||||
|
assertFalse(result.text.isEmpty())
|
||||||
|
|
||||||
|
assertEquals(completion, result.text)
|
||||||
|
assertEquals(domainSource, result.source)
|
||||||
|
assertEquals(expectedUrl, result.url)
|
||||||
|
assertEquals(sourceSize, result.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertNoCompletion(provider: DomainAutoCompleteProvider, text: String) {
|
||||||
|
val result = provider.autocomplete(text)
|
||||||
|
|
||||||
|
assertTrue(result.text.isEmpty())
|
||||||
|
assertTrue(result.url.isEmpty())
|
||||||
|
assertTrue(result.source.isEmpty())
|
||||||
|
assertEquals(0, result.size)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class DomainTest {
|
||||||
|
@Test
|
||||||
|
fun domainCreation() {
|
||||||
|
val firstItem = Domain.create("https://mozilla.com")
|
||||||
|
|
||||||
|
Assert.assertTrue(firstItem.protocol == "https://")
|
||||||
|
Assert.assertFalse(firstItem.hasWww)
|
||||||
|
Assert.assertTrue(firstItem.host == "mozilla.com")
|
||||||
|
|
||||||
|
val secondItem = Domain.create("www.mozilla.com")
|
||||||
|
|
||||||
|
Assert.assertTrue(secondItem.protocol == "http://")
|
||||||
|
Assert.assertTrue(secondItem.hasWww)
|
||||||
|
Assert.assertTrue(secondItem.host == "mozilla.com")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun domainCanCreateUrl() {
|
||||||
|
val firstItem = Domain.create("https://mozilla.com")
|
||||||
|
Assert.assertEquals("https://mozilla.com", firstItem.url)
|
||||||
|
|
||||||
|
val secondItem = Domain.create("www.mozilla.com")
|
||||||
|
Assert.assertEquals("http://www.mozilla.com", secondItem.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalStateException::class)
|
||||||
|
fun domainCreationWithBadURLThrowsException() {
|
||||||
|
Domain.create("http://www.")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import mozilla.components.support.test.robolectric.testContext
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class DomainsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun loadDomains() {
|
||||||
|
val domains = Domains.load(testContext, setOf("us"))
|
||||||
|
Assert.assertFalse(domains.isEmpty())
|
||||||
|
Assert.assertTrue(domains.contains("reddit.com"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun loadDomainsWithDefaultCountries() {
|
||||||
|
val domains = Domains.load(testContext)
|
||||||
|
Assert.assertFalse(domains.isEmpty())
|
||||||
|
// From the global list
|
||||||
|
Assert.assertTrue(domains.contains("mozilla.org"))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.domains
|
||||||
|
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import mozilla.components.browser.domains.autocomplete.BaseDomainAutocompleteProvider
|
||||||
|
import mozilla.components.browser.domains.autocomplete.CustomDomainsProvider
|
||||||
|
import mozilla.components.browser.domains.autocomplete.DomainList
|
||||||
|
import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ProvidersTest {
|
||||||
|
@Test
|
||||||
|
fun autocompletionWithShippedDomains() {
|
||||||
|
val provider = ShippedDomainsProvider()
|
||||||
|
provider.domains = listOf("mozilla.org", "google.com", "facebook.com").into()
|
||||||
|
|
||||||
|
val size = provider.domains.size
|
||||||
|
|
||||||
|
assertCompletion(provider, "m", DomainList.DEFAULT, size, "mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, "www", DomainList.DEFAULT, size, "www.mozilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, "www.face", DomainList.DEFAULT, size, "www.facebook.com", "http://facebook.com")
|
||||||
|
assertCompletion(provider, "MOZ", DomainList.DEFAULT, size, "MOZilla.org", "http://mozilla.org")
|
||||||
|
assertCompletion(provider, "www.GOO", DomainList.DEFAULT, size, "www.GOOgle.com", "http://google.com")
|
||||||
|
assertCompletion(provider, "WWW.GOOGLE.", DomainList.DEFAULT, size, "WWW.GOOGLE.com", "http://google.com")
|
||||||
|
assertCompletion(provider, "www.facebook.com", DomainList.DEFAULT, size, "www.facebook.com", "http://facebook.com")
|
||||||
|
assertCompletion(provider, "facebook.com", DomainList.DEFAULT, size, "facebook.com", "http://facebook.com")
|
||||||
|
|
||||||
|
assertNoCompletion(provider, "wwww")
|
||||||
|
assertNoCompletion(provider, "yahoo")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun autocompletionWithCustomDomains() {
|
||||||
|
val customDomains = listOf("gap.com", "www.fanfiction.com", "https://mobile.de")
|
||||||
|
|
||||||
|
val provider = CustomDomainsProvider()
|
||||||
|
provider.domains = customDomains.into()
|
||||||
|
|
||||||
|
assertCompletion(provider, "f", DomainList.CUSTOM, customDomains.size, "fanfiction.com", "http://www.fanfiction.com")
|
||||||
|
assertCompletion(provider, "fa", DomainList.CUSTOM, customDomains.size, "fanfiction.com", "http://www.fanfiction.com")
|
||||||
|
|
||||||
|
assertCompletion(provider, "g", DomainList.CUSTOM, customDomains.size, "gap.com", "http://gap.com")
|
||||||
|
assertCompletion(provider, "ga", DomainList.CUSTOM, customDomains.size, "gap.com", "http://gap.com")
|
||||||
|
|
||||||
|
assertCompletion(provider, "m", DomainList.CUSTOM, customDomains.size, "mobile.de", "https://mobile.de")
|
||||||
|
assertCompletion(provider, "mo", DomainList.CUSTOM, customDomains.size, "mobile.de", "https://mobile.de")
|
||||||
|
assertCompletion(provider, "mob", DomainList.CUSTOM, customDomains.size, "mobile.de", "https://mobile.de")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun autocompletionWithoutDomains() {
|
||||||
|
val filter = CustomDomainsProvider()
|
||||||
|
assertNoCompletion(filter, "mozilla")
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private fun assertCompletion(
|
||||||
|
provider: BaseDomainAutocompleteProvider,
|
||||||
|
text: String,
|
||||||
|
domainSource: DomainList,
|
||||||
|
sourceSize: Int,
|
||||||
|
completion: String,
|
||||||
|
expectedUrl: String,
|
||||||
|
) = runTest {
|
||||||
|
val result = provider.getAutocompleteSuggestion(text)!!
|
||||||
|
assertFalse(result.text.isEmpty())
|
||||||
|
|
||||||
|
assertEquals(completion, result.text)
|
||||||
|
assertEquals(domainSource.listName, result.source)
|
||||||
|
assertEquals(expectedUrl, result.url)
|
||||||
|
assertEquals(sourceSize, result.totalItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private fun assertNoCompletion(provider: BaseDomainAutocompleteProvider, text: String) = runTest {
|
||||||
|
assertNull(provider.getAutocompleteSuggestion(text))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
sdk=34
|
|
@ -0,0 +1,41 @@
|
||||||
|
# [Android Components](../../../README.md) > Browser > Engine-Gecko
|
||||||
|
|
||||||
|
[*Engine*](../../concept/engine/README.md) implementation based on [GeckoView](https://wiki.mozilla.org/Mobile/GeckoView).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Setting up the dependency
|
||||||
|
|
||||||
|
Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)):
|
||||||
|
|
||||||
|
```Groovy
|
||||||
|
implementation "org.mozilla.components:browser-engine-gecko:{latest-version}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration with the Glean SDK
|
||||||
|
|
||||||
|
#### Before using this component
|
||||||
|
Products sending telemetry and using this component *must request* a data-review following [this process](https://wiki.mozilla.org/Firefox/Data_Collection).
|
||||||
|
|
||||||
|
The [Glean SDK](../../../components/service/glean/README.md) can be used to collect [Gecko Telemetry](https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/index.html).
|
||||||
|
Applications using both this component and the Glean SDK should setup the Gecko Telemetry delegate
|
||||||
|
as shown below:
|
||||||
|
|
||||||
|
```Kotlin
|
||||||
|
val builder = GeckoRuntimeSettings.Builder()
|
||||||
|
val runtimeSettings = builder
|
||||||
|
.telemetryDelegate(GeckoGleanAdapter()) // Sets up the delegate!
|
||||||
|
.build()
|
||||||
|
// Create the Gecko runtime.
|
||||||
|
GeckoRuntime.create(context, runtimeSettings)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Adding new metrics
|
||||||
|
|
||||||
|
New Gecko metrics can be added as described [in the Firefox Telemetry docs](https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/start/adding-a-new-probe.html).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/
|
|
@ -0,0 +1,195 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORIES.each { repository ->
|
||||||
|
maven {
|
||||||
|
url repository
|
||||||
|
if (gradle.mozconfig.substs.ALLOW_INSECURE_GRADLE_REPOSITORIES) {
|
||||||
|
allowInsecureProtocol = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath "${ApplicationServicesConfig.groupId}:tooling-nimbus-gradle:${ApplicationServicesConfig.version}"
|
||||||
|
classpath "org.mozilla.telemetry:glean-gradle-plugin:${Versions.mozilla_glean}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id "com.jetbrains.python.envs" version "$python_envs_plugin"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'com.android.library'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion config.minSdkVersion
|
||||||
|
compileSdk config.compileSdkVersion
|
||||||
|
targetSdkVersion config.targetSdkVersion
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
resources {
|
||||||
|
excludes += ['META-INF/proguard/androidx-annotations.pro']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig true
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace 'mozilla.components.browser.engine.gecko'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set configuration for the Glean parser to extract metrics.yaml
|
||||||
|
// file from AAR dependencies of this project rather than look
|
||||||
|
// for it into the project directory.
|
||||||
|
ext.allowMetricsFromAAR = true
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(':concept-engine')
|
||||||
|
implementation project(':concept-fetch')
|
||||||
|
implementation project(':support-ktx')
|
||||||
|
implementation project(':support-utils')
|
||||||
|
implementation(project(':service-nimbus')) {
|
||||||
|
exclude group: 'org.mozilla.telemetry', module: 'glean-native'
|
||||||
|
}
|
||||||
|
implementation ComponentsDependencies.kotlin_coroutines
|
||||||
|
|
||||||
|
if (findProject(":geckoview") != null) {
|
||||||
|
api project(':geckoview')
|
||||||
|
} else {
|
||||||
|
api getGeckoViewDependency()
|
||||||
|
}
|
||||||
|
|
||||||
|
implementation ComponentsDependencies.androidx_core_ktx
|
||||||
|
implementation ComponentsDependencies.androidx_paging
|
||||||
|
implementation ComponentsDependencies.androidx_datastore_preferences
|
||||||
|
implementation ComponentsDependencies.androidx_lifecycle_livedata
|
||||||
|
|
||||||
|
testImplementation ComponentsDependencies.androidx_test_core
|
||||||
|
testImplementation ComponentsDependencies.androidx_test_junit
|
||||||
|
testImplementation ComponentsDependencies.testing_robolectric
|
||||||
|
testImplementation ComponentsDependencies.testing_coroutines
|
||||||
|
testImplementation ComponentsDependencies.testing_mockwebserver
|
||||||
|
testImplementation project(':support-test')
|
||||||
|
testImplementation project(':tooling-fetch-tests')
|
||||||
|
|
||||||
|
// We only compile against Glean. It's up to the app to add those dependencies
|
||||||
|
// if it wants to collect GeckoView telemetry through the Glean SDK.
|
||||||
|
compileOnly ComponentsDependencies.mozilla_glean
|
||||||
|
testImplementation ComponentsDependencies.mozilla_glean
|
||||||
|
testImplementation ComponentsDependencies.androidx_work_testing
|
||||||
|
|
||||||
|
androidTestImplementation ComponentsDependencies.androidx_test_core
|
||||||
|
androidTestImplementation ComponentsDependencies.androidx_test_runner
|
||||||
|
androidTestImplementation ComponentsDependencies.androidx_test_rules
|
||||||
|
androidTestImplementation project(':tooling-fetch-tests')
|
||||||
|
}
|
||||||
|
|
||||||
|
ext.gleanNamespace = "mozilla.telemetry.glean"
|
||||||
|
apply plugin: "org.mozilla.telemetry.glean-gradle-plugin"
|
||||||
|
apply from: '../../../android-lint.gradle'
|
||||||
|
apply from: '../../../publish.gradle'
|
||||||
|
apply plugin: "org.mozilla.appservices.nimbus-gradle-plugin"
|
||||||
|
nimbus {
|
||||||
|
// The path to the Nimbus feature manifest file
|
||||||
|
manifestFile = "geckoview.fml.yaml"
|
||||||
|
|
||||||
|
channels = [
|
||||||
|
debug: "debug",
|
||||||
|
release: "release",
|
||||||
|
]
|
||||||
|
|
||||||
|
// This is an optional value, and updates the plugin to use a copy of application
|
||||||
|
// services. The path should be relative to the root project directory.
|
||||||
|
// *NOTE*: This example will not work for all projects, but should work for Fenix, Focus, and Android Components
|
||||||
|
applicationServicesDir = gradle.hasProperty('localProperties.autoPublish.application-services.dir')
|
||||||
|
? gradle.getProperty('localProperties.autoPublish.application-services.dir') : null
|
||||||
|
}
|
||||||
|
ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
|
||||||
|
|
||||||
|
// Non-official versions are like "61.0a1", where "a1" is the milestone.
|
||||||
|
// This simply strips that off, leaving "61.0" in this example.
|
||||||
|
def getAppVersionWithoutMilestone() {
|
||||||
|
return gradle.mozconfig.substs.MOZ_APP_VERSION.replaceFirst(/a[0-9]/, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mimic Python: open(os.path.join(buildconfig.topobjdir, 'buildid.h')).readline().split()[2]
|
||||||
|
def getBuildId() {
|
||||||
|
if (System.env.MOZ_BUILD_DATE) {
|
||||||
|
if (System.env.MOZ_BUILD_DATE.length() == 14) {
|
||||||
|
return System.env.MOZ_BUILD_DATE
|
||||||
|
}
|
||||||
|
logger.warn("Ignoring invalid MOZ_BUILD_DATE: ${System.env.MOZ_BUILD_DATE}")
|
||||||
|
}
|
||||||
|
return file("${gradle.mozconfig.topobjdir}/buildid.h").getText('utf-8').split()[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
def getVersionNumber() {
|
||||||
|
def appVersion = getAppVersionWithoutMilestone()
|
||||||
|
def parts = appVersion.split('\\.')
|
||||||
|
def version = parts[0] + "." + parts[1] + "." + getBuildId()
|
||||||
|
|
||||||
|
if (!gradle.mozconfig.substs.MOZILLA_OFFICIAL && !gradle.mozconfig.substs.MOZ_ANDROID_FAT_AAR_ARCHITECTURES) {
|
||||||
|
// Use -SNAPSHOT versions locally to enable the local GeckoView substitution flow.
|
||||||
|
version += "-SNAPSHOT"
|
||||||
|
}
|
||||||
|
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
def getArtifactSuffix() {
|
||||||
|
def suffix = ""
|
||||||
|
|
||||||
|
// Release artifacts don't specify the channel, for the sake of simplicity.
|
||||||
|
if (gradle.mozconfig.substs.MOZ_UPDATE_CHANNEL != 'release') {
|
||||||
|
suffix += "-${gradle.mozconfig.substs.MOZ_UPDATE_CHANNEL}"
|
||||||
|
}
|
||||||
|
|
||||||
|
return suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
def getArtifactId() {
|
||||||
|
def id = "geckoview" + getArtifactSuffix()
|
||||||
|
|
||||||
|
if (!gradle.mozconfig.substs.MOZ_ANDROID_GECKOVIEW_LITE) {
|
||||||
|
id += "-omni"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gradle.mozconfig.substs.MOZILLA_OFFICIAL && !gradle.mozconfig.substs.MOZ_ANDROID_FAT_AAR_ARCHITECTURES) {
|
||||||
|
// In automation, per-architecture artifacts identify
|
||||||
|
// the architecture; multi-architecture artifacts don't.
|
||||||
|
// When building locally, we produce a "skinny AAR" with
|
||||||
|
// one target architecture masquerading as a "fat AAR"
|
||||||
|
// to enable Gradle composite builds to substitute this
|
||||||
|
// project into consumers easily.
|
||||||
|
id += "-${gradle.mozconfig.substs.ANDROID_CPU_ARCH}"
|
||||||
|
}
|
||||||
|
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
def getGeckoViewDependency() {
|
||||||
|
// on try, relax geckoview version pin to allow for --use-existing-task
|
||||||
|
if ('https://hg.mozilla.org/try' == System.env.GECKO_HEAD_REPOSITORY) {
|
||||||
|
rootProject.logger.lifecycle("Getting geckoview on try: ${getArtifactId()}:+")
|
||||||
|
return "org.mozilla.geckoview:${getArtifactId()}:+"
|
||||||
|
}
|
||||||
|
rootProject.logger.lifecycle("Getting geckoview: ${getArtifactId()}:${getVersionNumber()}")
|
||||||
|
return "org.mozilla.geckoview:${getArtifactId()}:${getVersionNumber()}"
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Metrics definitions have moved
|
||||||
|
|
||||||
|
Metrics definitions for projects using `engine-gecko` moved to the Glean Dictionary.
|
||||||
|
|
||||||
|
For Firefox for Android those definitions can be found at:
|
||||||
|
[https://dictionary.telemetry.mozilla.org/apps/fenix](https://dictionary.telemetry.mozilla.org/apps/fenix)
|
||||||
|
|
||||||
|
This file is kept only for historical reference.
|
|
@ -0,0 +1,24 @@
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
about:
|
||||||
|
description: GeckoView features configurable via Nimbus
|
||||||
|
android:
|
||||||
|
package: mozilla.components.browser.engine.gecko
|
||||||
|
class: .GeckoNimbus
|
||||||
|
channels:
|
||||||
|
- debug
|
||||||
|
- release
|
||||||
|
features:
|
||||||
|
pdfjs:
|
||||||
|
description: "PDF.js features"
|
||||||
|
variables:
|
||||||
|
download-button:
|
||||||
|
description: "Download button"
|
||||||
|
type: Boolean
|
||||||
|
default: true
|
||||||
|
|
||||||
|
open-in-app-button:
|
||||||
|
description: "Open in app button"
|
||||||
|
type: Boolean
|
||||||
|
default: true
|
|
@ -0,0 +1,30 @@
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
# IMPORTANT NOTE: this file is here only as a safety measure, to make
|
||||||
|
# sure the correct code is generated even though the GeckoView AAR file
|
||||||
|
# reports an empty metrics.yaml file. The metric in this file is currently
|
||||||
|
# disabled and not supposed to collect any data.
|
||||||
|
---
|
||||||
|
|
||||||
|
$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
|
||||||
|
|
||||||
|
test.glean.geckoview:
|
||||||
|
streaming:
|
||||||
|
type: timing_distribution
|
||||||
|
gecko_datapoint: TELEMETRY_TEST_STREAMING
|
||||||
|
disabled: true
|
||||||
|
description: |
|
||||||
|
A test-only, disabled metric. This is required to guarantee
|
||||||
|
that a `GleanGeckoHistogramMapping` is always generated, even
|
||||||
|
though the GeckoView AAR exports no metric. Please note that
|
||||||
|
the data-review field below contains no review, since this
|
||||||
|
metric is disabled and not allowed to collect any data.
|
||||||
|
bugs:
|
||||||
|
- https://bugzilla.mozilla.org/1566374
|
||||||
|
data_reviews:
|
||||||
|
- https://bugzilla.mozilla.org/1566374
|
||||||
|
notification_emails:
|
||||||
|
- glean-team@mozilla.com
|
||||||
|
expires: never
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
|
@ -0,0 +1,126 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.fetch.geckoview
|
||||||
|
|
||||||
|
import androidx.test.annotation.UiThreadTest
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.filters.MediumTest
|
||||||
|
import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient
|
||||||
|
import mozilla.components.concept.fetch.Client
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@MediumTest
|
||||||
|
class GeckoViewFetchTestCases : mozilla.components.tooling.fetch.tests.FetchTestCases() {
|
||||||
|
override fun createNewClient(): Client = GeckoViewFetchClient(ApplicationProvider.getApplicationContext())
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
fun clientInstance() {
|
||||||
|
assertTrue(createNewClient() is GeckoViewFetchClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200WithGzippedBody() {
|
||||||
|
super.get200WithGzippedBody()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200OverridingDefaultHeaders() {
|
||||||
|
super.get200OverridingDefaultHeaders()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200WithDuplicatedCacheControlRequestHeaders() {
|
||||||
|
super.get200WithDuplicatedCacheControlRequestHeaders()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200WithDuplicatedCacheControlResponseHeaders() {
|
||||||
|
super.get200WithDuplicatedCacheControlResponseHeaders()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200WithHeaders() {
|
||||||
|
super.get200WithHeaders()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200WithReadTimeout() {
|
||||||
|
super.get200WithReadTimeout()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200WithStringBody() {
|
||||||
|
super.get200WithStringBody()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get302FollowRedirects() {
|
||||||
|
super.get302FollowRedirects()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get302FollowRedirectsDisabled() {
|
||||||
|
super.get302FollowRedirectsDisabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get404WithBody() {
|
||||||
|
super.get404WithBody()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun post200WithBody() {
|
||||||
|
super.post200WithBody()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun put201FileUpload() {
|
||||||
|
super.put201FileUpload()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200WithCookiePolicy() {
|
||||||
|
super.get200WithCookiePolicy()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200WithContentTypeCharset() {
|
||||||
|
super.get200WithContentTypeCharset()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun get200WithCacheControl() {
|
||||||
|
super.get200WithCacheControl()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun getThrowsIOExceptionWhenHostNotReachable() {
|
||||||
|
super.getThrowsIOExceptionWhenHostNotReachable()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UiThreadTest
|
||||||
|
override fun getDataUri() {
|
||||||
|
super.getDataUri()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
<manifest />
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,73 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko
|
||||||
|
|
||||||
|
import android.util.JsonReader
|
||||||
|
import android.util.JsonWriter
|
||||||
|
import mozilla.components.concept.engine.EngineSessionState
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.mozilla.geckoview.GeckoSession
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
private const val GECKO_STATE_KEY = "GECKO_STATE"
|
||||||
|
|
||||||
|
class GeckoEngineSessionState internal constructor(
|
||||||
|
internal val actualState: GeckoSession.SessionState?,
|
||||||
|
) : EngineSessionState {
|
||||||
|
override fun writeTo(writer: JsonWriter) {
|
||||||
|
with(writer) {
|
||||||
|
beginObject()
|
||||||
|
|
||||||
|
name(GECKO_STATE_KEY)
|
||||||
|
value(actualState.toString())
|
||||||
|
|
||||||
|
endObject()
|
||||||
|
flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromJSON(json: JSONObject): GeckoEngineSessionState = try {
|
||||||
|
val state = json.getString(GECKO_STATE_KEY)
|
||||||
|
|
||||||
|
GeckoEngineSessionState(
|
||||||
|
GeckoSession.SessionState.fromString(state),
|
||||||
|
)
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
GeckoEngineSessionState(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a [GeckoEngineSessionState] from the given [JsonReader].
|
||||||
|
*/
|
||||||
|
fun from(reader: JsonReader): GeckoEngineSessionState = try {
|
||||||
|
reader.beginObject()
|
||||||
|
|
||||||
|
val rawState = if (reader.hasNext()) {
|
||||||
|
val key = reader.nextName()
|
||||||
|
if (key != GECKO_STATE_KEY) {
|
||||||
|
throw AssertionError("Unknown state key: $key")
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.nextString()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.endObject()
|
||||||
|
|
||||||
|
GeckoEngineSessionState(
|
||||||
|
rawState?.let { GeckoSession.SessionState.fromString(it) },
|
||||||
|
)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
GeckoEngineSessionState(null)
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
// Internally GeckoView uses org.json and currently may throw JSONException in certain cases
|
||||||
|
// https://github.com/mozilla-mobile/android-components/issues/9332
|
||||||
|
GeckoEngineSessionState(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,247 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import mozilla.components.browser.engine.gecko.activity.GeckoViewActivityContextDelegate
|
||||||
|
import mozilla.components.browser.engine.gecko.selection.GeckoSelectionActionDelegate
|
||||||
|
import mozilla.components.concept.engine.EngineSession
|
||||||
|
import mozilla.components.concept.engine.EngineView
|
||||||
|
import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
|
||||||
|
import mozilla.components.concept.engine.selection.SelectionActionDelegate
|
||||||
|
import org.mozilla.geckoview.BasicSelectionActionDelegate
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
import org.mozilla.geckoview.GeckoSession
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
import androidx.core.view.OnApplyWindowInsetsListener as AndroidxOnApplyWindowInsetsListener
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gecko-based EngineView implementation.
|
||||||
|
*/
|
||||||
|
class GeckoEngineView @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0,
|
||||||
|
) : FrameLayout(context, attrs, defStyleAttr), EngineView {
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal var geckoView = object : NestedGeckoView(context) {
|
||||||
|
|
||||||
|
override fun onAttachedToWindow() {
|
||||||
|
try {
|
||||||
|
super.onAttachedToWindow()
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
// This is to debug "display already acquired" crashes
|
||||||
|
val otherActivityClassName =
|
||||||
|
this.session?.accessibility?.view?.context?.javaClass?.simpleName
|
||||||
|
val otherActivityClassHashcode =
|
||||||
|
this.session?.accessibility?.view?.context?.hashCode()
|
||||||
|
val activityClassName = context.javaClass.simpleName
|
||||||
|
val activityClassHashCode = context.hashCode()
|
||||||
|
val msg = "ATTACH VIEW: Current activity: $activityClassName hashcode " +
|
||||||
|
"$activityClassHashCode Other activity: $otherActivityClassName " +
|
||||||
|
"hashcode $otherActivityClassHashcode"
|
||||||
|
throw IllegalStateException(msg, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromWindow() {
|
||||||
|
// We are releasing the session before GeckoView gets detached from the window. Otherwise
|
||||||
|
// GeckoView will close the session automatically and we do not want that.
|
||||||
|
releaseSession()
|
||||||
|
|
||||||
|
super.onDetachedFromWindow()
|
||||||
|
}
|
||||||
|
}.apply {
|
||||||
|
// Explicitly mark this view as important for autofill. The default "auto" doesn't seem to trigger any
|
||||||
|
// autofill behavior for us here.
|
||||||
|
@Suppress("WrongConstant")
|
||||||
|
ViewCompat.setImportantForAutofill(this, View.IMPORTANT_FOR_ACCESSIBILITY_YES)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun setColorScheme(preferredColorScheme: PreferredColorScheme) {
|
||||||
|
var colorScheme = preferredColorScheme
|
||||||
|
if (preferredColorScheme == PreferredColorScheme.System) {
|
||||||
|
colorScheme =
|
||||||
|
if (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
||||||
|
== Configuration.UI_MODE_NIGHT_YES
|
||||||
|
) {
|
||||||
|
PreferredColorScheme.Dark
|
||||||
|
} else {
|
||||||
|
PreferredColorScheme.Light
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colorScheme == PreferredColorScheme.Dark) {
|
||||||
|
geckoView.coverUntilFirstPaint(DARK_COVER)
|
||||||
|
} else {
|
||||||
|
geckoView.coverUntilFirstPaint(Color.WHITE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal var currentSession: GeckoEngineSession? = null
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal var currentSelection: BasicSelectionActionDelegate? = null
|
||||||
|
|
||||||
|
override var selectionActionDelegate: SelectionActionDelegate? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
addView(geckoView)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* With the current design, we have a [NestedGeckoView] inside this
|
||||||
|
* [GeckoEngineView]. In our supported embedders, we wrap this with the
|
||||||
|
* AndroidX `SwipeRefreshLayout` to enable features like Pull-To-Refresh:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* SwipeRefreshLayout
|
||||||
|
* └── GeckoEngineView
|
||||||
|
* └── NestedGeckoView
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* `SwipeRefreshLayout` only looks at the direct child to see if it has nested scrolling
|
||||||
|
* enabled. As we embed [NestedGeckoView] inside [GeckoEngineView], we change the hierarchy
|
||||||
|
* so that [NestedGeckoView] is no longer the direct child of `SwipeRefreshLayout`.
|
||||||
|
*
|
||||||
|
* To fix this we enable nested scrolling on the GeckoEngineView to emulate this
|
||||||
|
* information. This is required information for `View.requestDisallowInterceptTouchEvent`
|
||||||
|
* to work correctly in the [NestedGeckoView].
|
||||||
|
*/
|
||||||
|
isNestedScrollingEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the content of the given session.
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
override fun render(session: EngineSession) {
|
||||||
|
val internalSession = session as GeckoEngineSession
|
||||||
|
currentSession = session
|
||||||
|
|
||||||
|
if (geckoView.session != internalSession.geckoSession) {
|
||||||
|
geckoView.session?.let {
|
||||||
|
// Release a previously assigned session. Otherwise GeckoView will close it
|
||||||
|
// automatically.
|
||||||
|
detachSelectionActionDelegate(it)
|
||||||
|
geckoView.releaseSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
geckoView.setSession(internalSession.geckoSession)
|
||||||
|
attachSelectionActionDelegate(internalSession.geckoSession)
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
// This is to debug "display already acquired" crashes
|
||||||
|
val otherActivityClassName =
|
||||||
|
internalSession.geckoSession.accessibility.view?.context?.javaClass?.simpleName
|
||||||
|
val otherActivityClassHashcode =
|
||||||
|
internalSession.geckoSession.accessibility.view?.context?.hashCode()
|
||||||
|
val activityClassName = context.javaClass.simpleName
|
||||||
|
val activityClassHashCode = context.hashCode()
|
||||||
|
val msg = "SET SESSION: Current activity: $activityClassName hashcode " +
|
||||||
|
"$activityClassHashCode Other activity: $otherActivityClassName " +
|
||||||
|
"hashcode $otherActivityClassHashcode"
|
||||||
|
throw IllegalStateException(msg, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun attachSelectionActionDelegate(session: GeckoSession) {
|
||||||
|
val delegate = GeckoSelectionActionDelegate.maybeCreate(context, selectionActionDelegate)
|
||||||
|
if (delegate != null) {
|
||||||
|
session.selectionActionDelegate = delegate
|
||||||
|
currentSelection = delegate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun detachSelectionActionDelegate(session: GeckoSession?) {
|
||||||
|
if (currentSelection != null) {
|
||||||
|
session?.selectionActionDelegate = null
|
||||||
|
currentSelection = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun release() {
|
||||||
|
detachSelectionActionDelegate(currentSession?.geckoSession)
|
||||||
|
|
||||||
|
currentSession = null
|
||||||
|
|
||||||
|
geckoView.releaseSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromWindow() {
|
||||||
|
super.onDetachedFromWindow()
|
||||||
|
|
||||||
|
release()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun canClearSelection() = !currentSelection?.selection?.text.isNullOrEmpty()
|
||||||
|
|
||||||
|
override fun canScrollVerticallyUp() = currentSession?.let { it.scrollY > 0 } != false
|
||||||
|
|
||||||
|
override fun canScrollVerticallyDown() =
|
||||||
|
true // waiting for this issue https://bugzilla.mozilla.org/show_bug.cgi?id=1507569
|
||||||
|
|
||||||
|
override fun getInputResultDetail() = geckoView.inputResultDetail
|
||||||
|
|
||||||
|
override fun setVerticalClipping(clippingHeight: Int) {
|
||||||
|
geckoView.setVerticalClipping(clippingHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setDynamicToolbarMaxHeight(height: Int) {
|
||||||
|
geckoView.setDynamicToolbarMaxHeight(height)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setActivityContext(context: Context?) {
|
||||||
|
geckoView.activityContextDelegate = GeckoViewActivityContextDelegate(WeakReference(context))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun captureThumbnail(onFinish: (Bitmap?) -> Unit) {
|
||||||
|
val geckoResult = geckoView.capturePixels()
|
||||||
|
geckoResult.then(
|
||||||
|
{ bitmap ->
|
||||||
|
onFinish(bitmap)
|
||||||
|
GeckoResult()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onFinish(null)
|
||||||
|
GeckoResult<Void>()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearSelection() {
|
||||||
|
currentSelection?.clearSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setVisibility(visibility: Int) {
|
||||||
|
// GeckoView doesn't react to onVisibilityChanged so we need to propagate ourselves for now:
|
||||||
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1630775
|
||||||
|
// We do this to prevent the content from resizing when the view is not visible:
|
||||||
|
// https://github.com/mozilla-mobile/android-components/issues/6664
|
||||||
|
geckoView.visibility = visibility
|
||||||
|
super.setVisibility(visibility)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addWindowInsetsListener(
|
||||||
|
key: String,
|
||||||
|
listener: AndroidxOnApplyWindowInsetsListener?,
|
||||||
|
) = geckoView.addWindowInsetsListener(key, listener)
|
||||||
|
|
||||||
|
override fun removeWindowInsetsListener(key: String) = geckoView.removeWindowInsetsListener(key)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
internal const val DARK_COVER = 0xFF2A2A2E.toInt()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.CoroutineStart
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import mozilla.components.concept.engine.CancellableOperation
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for a GeckoResult to be complete in a co-routine.
|
||||||
|
*/
|
||||||
|
suspend fun <T> GeckoResult<T>.await() = suspendCoroutine<T?> { continuation ->
|
||||||
|
then(
|
||||||
|
{
|
||||||
|
continuation.resume(it)
|
||||||
|
GeckoResult<Void>()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
continuation.resumeWithException(it)
|
||||||
|
GeckoResult<Void>()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a [GeckoResult] to a [CancellableOperation].
|
||||||
|
*/
|
||||||
|
fun <T> GeckoResult<T>.asCancellableOperation(): CancellableOperation {
|
||||||
|
val geckoResult = this
|
||||||
|
return object : CancellableOperation {
|
||||||
|
override fun cancel(): Deferred<Boolean> {
|
||||||
|
val result = CompletableDeferred<Boolean>()
|
||||||
|
geckoResult.cancel().then(
|
||||||
|
{
|
||||||
|
result.complete(it ?: false)
|
||||||
|
GeckoResult<Void>()
|
||||||
|
},
|
||||||
|
{ throwable ->
|
||||||
|
result.completeExceptionally(throwable)
|
||||||
|
GeckoResult<Void>()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a GeckoResult from a co-routine.
|
||||||
|
*/
|
||||||
|
@Suppress("TooGenericExceptionCaught")
|
||||||
|
fun <T> CoroutineScope.launchGeckoResult(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
block: suspend CoroutineScope.() -> T,
|
||||||
|
) = GeckoResult<T>().apply {
|
||||||
|
launch(context, start) {
|
||||||
|
try {
|
||||||
|
val value = block()
|
||||||
|
complete(value)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
completeExceptionally(exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import mozilla.components.browser.engine.gecko.content.blocking.GeckoTrackingProtectionException
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.geckoTrackingProtectionPermission
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.isExcludedForTrackingProtection
|
||||||
|
import mozilla.components.concept.engine.EngineSession
|
||||||
|
import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
|
||||||
|
import mozilla.components.concept.engine.content.blocking.TrackingProtectionExceptionStorage
|
||||||
|
import mozilla.components.support.ktx.kotlin.getOrigin
|
||||||
|
import mozilla.components.support.ktx.kotlin.stripDefaultPort
|
||||||
|
import org.mozilla.geckoview.GeckoRuntime
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_ALLOW
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_DENY
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [TrackingProtectionExceptionStorage] implementation to store tracking protection exceptions.
|
||||||
|
*/
|
||||||
|
internal class GeckoTrackingProtectionExceptionStorage(
|
||||||
|
private val runtime: GeckoRuntime,
|
||||||
|
) : TrackingProtectionExceptionStorage {
|
||||||
|
internal var scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
|
override fun contains(session: EngineSession, onResult: (Boolean) -> Unit) {
|
||||||
|
val url = (session as GeckoEngineSession).currentUrl
|
||||||
|
if (!url.isNullOrEmpty()) {
|
||||||
|
getPermissions(url) { permissions ->
|
||||||
|
val contains = permissions.isNotEmpty()
|
||||||
|
onResult(contains)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onResult(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchAll(onResult: (List<TrackingProtectionException>) -> Unit) {
|
||||||
|
runtime.storageController.allPermissions.accept { permissions ->
|
||||||
|
val trackingExceptions = permissions.filterTrackingProtectionExceptions()
|
||||||
|
onResult(trackingExceptions.map { exceptions -> exceptions.toTrackingProtectionException() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<ContentPermission>?.filterTrackingProtectionExceptions() =
|
||||||
|
this.orEmpty().filter { it.isExcludedForTrackingProtection }
|
||||||
|
|
||||||
|
private fun List<ContentPermission>?.filterTrackingProtectionExceptions(url: String) =
|
||||||
|
this.orEmpty()
|
||||||
|
.filter {
|
||||||
|
it.isExcludedForTrackingProtection && it.uri.getOrigin().orEmpty()
|
||||||
|
.stripDefaultPort() == url
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun add(session: EngineSession, persistInPrivateMode: Boolean) {
|
||||||
|
val geckoEngineSession = (session as GeckoEngineSession)
|
||||||
|
if (persistInPrivateMode) {
|
||||||
|
addPersistentPrivateException(geckoEngineSession)
|
||||||
|
} else {
|
||||||
|
geckoEngineSession.geckoTrackingProtectionPermission?.let {
|
||||||
|
runtime.storageController.setPermission(it, VALUE_ALLOW)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onExcludedOnTrackingProtectionChange(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun addPersistentPrivateException(geckoEngineSession: GeckoEngineSession) {
|
||||||
|
val permission = geckoEngineSession.geckoTrackingProtectionPermission
|
||||||
|
permission?.let {
|
||||||
|
runtime.storageController.setPrivateBrowsingPermanentPermission(it, VALUE_ALLOW)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun remove(session: EngineSession) {
|
||||||
|
val geckoEngineSession = (session as GeckoEngineSession)
|
||||||
|
val url = geckoEngineSession.currentUrl ?: return
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onExcludedOnTrackingProtectionChange(false)
|
||||||
|
}
|
||||||
|
remove(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun remove(exception: TrackingProtectionException) {
|
||||||
|
if (exception is GeckoTrackingProtectionException) {
|
||||||
|
remove(exception.contentPermission)
|
||||||
|
} else {
|
||||||
|
remove(exception.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun remove(url: String) {
|
||||||
|
val storage = runtime.storageController
|
||||||
|
getPermissions(url) { permissions ->
|
||||||
|
permissions.forEach { geckoPermissions ->
|
||||||
|
storage.setPermission(geckoPermissions, VALUE_DENY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a workaround until https://bugzilla.mozilla.org/show_bug.cgi?id=1723280 gets addressed
|
||||||
|
private fun getPermissions(url: String, onFinish: (List<ContentPermission>) -> Unit) {
|
||||||
|
val localUrl = url.getOrigin().orEmpty().stripDefaultPort()
|
||||||
|
val storage = runtime.storageController
|
||||||
|
if (localUrl.isNotEmpty()) {
|
||||||
|
storage.allPermissions.accept { permissions ->
|
||||||
|
onFinish(permissions.filterTrackingProtectionExceptions(localUrl))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onFinish(emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun remove(contentPermission: ContentPermission) {
|
||||||
|
runtime.storageController.setPermission(contentPermission, VALUE_DENY)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeAll(activeSessions: List<EngineSession>?, onRemove: () -> Unit) {
|
||||||
|
val storage = runtime.storageController
|
||||||
|
activeSessions?.forEach { engineSession ->
|
||||||
|
engineSession.notifyObservers {
|
||||||
|
onExcludedOnTrackingProtectionChange(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
storage.allPermissions.accept { permissions ->
|
||||||
|
val trackingExceptions = permissions.filterTrackingProtectionExceptions()
|
||||||
|
trackingExceptions.forEach {
|
||||||
|
storage.setPermission(it, VALUE_DENY)
|
||||||
|
}
|
||||||
|
onRemove.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ContentPermission.toTrackingProtectionException(): GeckoTrackingProtectionException {
|
||||||
|
return GeckoTrackingProtectionException(uri, privateMode, this)
|
||||||
|
}
|
|
@ -0,0 +1,257 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import androidx.core.view.NestedScrollingChild
|
||||||
|
import androidx.core.view.NestedScrollingChildHelper
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import mozilla.components.concept.engine.InputResultDetail
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
import org.mozilla.geckoview.GeckoView
|
||||||
|
import org.mozilla.geckoview.PanZoomController
|
||||||
|
|
||||||
|
/**
|
||||||
|
* geckoView that supports nested scrolls (for using in a CoordinatorLayout).
|
||||||
|
*
|
||||||
|
* This code is a simplified version of the NestedScrollView implementation
|
||||||
|
* which can be found in the support library:
|
||||||
|
* [android.support.v4.widget.NestedScrollView]
|
||||||
|
*
|
||||||
|
* Based on:
|
||||||
|
* https://github.com/takahirom/webview-in-coordinatorlayout
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Suppress("ClickableViewAccessibility")
|
||||||
|
open class NestedGeckoView(context: Context) : GeckoView(context), NestedScrollingChild {
|
||||||
|
@VisibleForTesting
|
||||||
|
internal var lastY: Int = 0
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal val scrollOffset = IntArray(2)
|
||||||
|
|
||||||
|
private val scrollConsumed = IntArray(2)
|
||||||
|
|
||||||
|
private var gestureCanReachParent = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the initial scroll.
|
||||||
|
*/
|
||||||
|
enum class InitialScrollDirection {
|
||||||
|
NOT_YET,
|
||||||
|
DOWN,
|
||||||
|
UP,
|
||||||
|
}
|
||||||
|
|
||||||
|
private var initialScrollDirection = InitialScrollDirection.NOT_YET
|
||||||
|
|
||||||
|
private var initialDownY: Float = 0f
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal var nestedOffsetY: Int = 0
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal var childHelper: NestedScrollingChildHelper = NestedScrollingChildHelper(this)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How user's MotionEvent will be handled.
|
||||||
|
*
|
||||||
|
* @see InputResultDetail
|
||||||
|
*/
|
||||||
|
internal var inputResultDetail = InputResultDetail.newInstance(true)
|
||||||
|
|
||||||
|
init {
|
||||||
|
isNestedScrollingEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("ComplexMethod")
|
||||||
|
override fun onTouchEvent(ev: MotionEvent): Boolean {
|
||||||
|
val event = MotionEvent.obtain(ev)
|
||||||
|
val action = ev.actionMasked
|
||||||
|
val eventY = event.y.toInt()
|
||||||
|
|
||||||
|
when (action) {
|
||||||
|
MotionEvent.ACTION_MOVE -> {
|
||||||
|
val allowScroll = !shouldPinOnScreen() && inputResultDetail.isTouchHandledByBrowser()
|
||||||
|
|
||||||
|
var deltaY = lastY - eventY
|
||||||
|
|
||||||
|
if (allowScroll && dispatchNestedPreScroll(0, deltaY, scrollConsumed, scrollOffset)) {
|
||||||
|
deltaY -= scrollConsumed[1]
|
||||||
|
event.offsetLocation(0f, (-scrollOffset[1]).toFloat())
|
||||||
|
nestedOffsetY += scrollOffset[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
lastY = eventY - scrollOffset[1]
|
||||||
|
|
||||||
|
if (allowScroll && dispatchNestedScroll(0, scrollOffset[1], 0, deltaY, scrollOffset)) {
|
||||||
|
lastY -= scrollOffset[1]
|
||||||
|
event.offsetLocation(0f, scrollOffset[1].toFloat())
|
||||||
|
nestedOffsetY += scrollOffset[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initialScrollDirection == InitialScrollDirection.NOT_YET) {
|
||||||
|
if (event.y > initialDownY) {
|
||||||
|
initialScrollDirection = InitialScrollDirection.UP
|
||||||
|
} else if (event.y < initialDownY) {
|
||||||
|
initialScrollDirection = InitialScrollDirection.DOWN
|
||||||
|
} else {
|
||||||
|
// We may want to disallow pull-to-refresh in the case of
|
||||||
|
// scrolling left or right initially?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MotionEvent.ACTION_DOWN -> {
|
||||||
|
// A new gesture started. Ask GV if it can handle this.
|
||||||
|
parent?.requestDisallowInterceptTouchEvent(true)
|
||||||
|
updateInputResult(event)
|
||||||
|
|
||||||
|
initialScrollDirection = InitialScrollDirection.NOT_YET
|
||||||
|
nestedOffsetY = 0
|
||||||
|
lastY = eventY
|
||||||
|
initialDownY = event.y
|
||||||
|
|
||||||
|
// The event should be handled either by onTouchEvent,
|
||||||
|
// either by onTouchEventForResult, never by both.
|
||||||
|
// Early return if we sent it to updateInputResult(..) which calls onTouchEventForResult.
|
||||||
|
event.recycle()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't care about other touch events
|
||||||
|
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||||
|
// inputResultDetail needs to be reset here and not in the next ACTION_DOWN, because
|
||||||
|
// its value is used by other features that poll for the value via
|
||||||
|
// `EngineView.getInputResultDetail`. Not resetting this in ACTION_CANCEL/ACTION_UP
|
||||||
|
// would then mean we send stale information to those features from a previous
|
||||||
|
// gesture's result.
|
||||||
|
inputResultDetail = InputResultDetail.newInstance(true)
|
||||||
|
stopNestedScroll()
|
||||||
|
|
||||||
|
// Allow touch event interception here so that the next ACTION_DOWN event can be properly
|
||||||
|
// intercepted by the parent.
|
||||||
|
parent?.requestDisallowInterceptTouchEvent(false)
|
||||||
|
gestureCanReachParent = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute event handler from parent class in all cases
|
||||||
|
val eventHandled = callSuperOnTouchEvent(event)
|
||||||
|
|
||||||
|
// Recycle previously obtained event
|
||||||
|
event.recycle()
|
||||||
|
|
||||||
|
return eventHandled
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun callSuperOnTouchEvent(event: MotionEvent): Boolean {
|
||||||
|
return super.onTouchEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("WrongThread") // Lint complains startNestedScroll() needs to be called on the main thread
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun updateInputResult(event: MotionEvent) {
|
||||||
|
val eventAction = event.actionMasked
|
||||||
|
superOnTouchEventForDetailResult(event)
|
||||||
|
.accept {
|
||||||
|
// Since the response from APZ is async, we could theoretically have a response
|
||||||
|
// which is out of time when we get the ACTION_MOVE events, and we do not want
|
||||||
|
// to forward this to the parent pre-emptively.
|
||||||
|
if (!gestureCanReachParent) {
|
||||||
|
return@accept
|
||||||
|
}
|
||||||
|
|
||||||
|
inputResultDetail = inputResultDetail.copy(
|
||||||
|
it?.handledResult(),
|
||||||
|
it?.scrollableDirections(),
|
||||||
|
it?.overscrollDirections(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (eventAction == MotionEvent.ACTION_DOWN) {
|
||||||
|
// Gesture can reach the parent only if the content is already at the top
|
||||||
|
gestureCanReachParent = inputResultDetail.canOverscrollTop()
|
||||||
|
|
||||||
|
when (initialScrollDirection) {
|
||||||
|
InitialScrollDirection.NOT_YET -> {
|
||||||
|
// If the event wasn't used in GeckoView, allow touch event interception.
|
||||||
|
if (gestureCanReachParent && !inputResultDetail.isTouchHandledByWebsite()) {
|
||||||
|
parent?.requestDisallowInterceptTouchEvent(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InitialScrollDirection.UP -> {
|
||||||
|
// If we received the InputResultDetail from Gecko after we've sent the
|
||||||
|
// first touch move event to Gecko, that means there's at least a touch
|
||||||
|
// event listener whether to prevent the event or not, or CSS touch-action
|
||||||
|
// properties in the contents, thus if the result isn't
|
||||||
|
// `INPUT_HANDLED_CONTENT`, we can allow pull-to-refresh.
|
||||||
|
if (gestureCanReachParent && !inputResultDetail.isTouchHandledByWebsite()) {
|
||||||
|
parent?.requestDisallowInterceptTouchEvent(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InitialScrollDirection.DOWN -> {
|
||||||
|
parent?.requestDisallowInterceptTouchEvent(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal open fun superOnTouchEventForDetailResult(
|
||||||
|
event: MotionEvent,
|
||||||
|
): GeckoResult<PanZoomController.InputResultDetail> =
|
||||||
|
super.onTouchEventForDetailResult(event)
|
||||||
|
|
||||||
|
override fun setNestedScrollingEnabled(enabled: Boolean) {
|
||||||
|
childHelper.isNestedScrollingEnabled = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isNestedScrollingEnabled(): Boolean {
|
||||||
|
return childHelper.isNestedScrollingEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startNestedScroll(axes: Int): Boolean {
|
||||||
|
return childHelper.startNestedScroll(axes)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stopNestedScroll() {
|
||||||
|
childHelper.stopNestedScroll()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hasNestedScrollingParent(): Boolean {
|
||||||
|
return childHelper.hasNestedScrollingParent()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispatchNestedScroll(
|
||||||
|
dxConsumed: Int,
|
||||||
|
dyConsumed: Int,
|
||||||
|
dxUnconsumed: Int,
|
||||||
|
dyUnconsumed: Int,
|
||||||
|
offsetInWindow: IntArray?,
|
||||||
|
): Boolean {
|
||||||
|
return childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?): Boolean {
|
||||||
|
return childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispatchNestedFling(velocityX: Float, velocityY: Float, consumed: Boolean): Boolean {
|
||||||
|
return childHelper.dispatchNestedFling(velocityX, velocityY, consumed)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float): Boolean {
|
||||||
|
return childHelper.dispatchNestedPreFling(velocityX, velocityY)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.activity
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Intent
|
||||||
|
import mozilla.components.concept.engine.activity.ActivityDelegate
|
||||||
|
import mozilla.components.support.base.log.logger.Logger
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
import org.mozilla.geckoview.GeckoRuntime
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper for the [ActivityDelegate] to communicate with the Gecko-based delegate.
|
||||||
|
*/
|
||||||
|
internal class GeckoActivityDelegate(
|
||||||
|
private val delegateRef: WeakReference<ActivityDelegate>,
|
||||||
|
) : GeckoRuntime.ActivityDelegate {
|
||||||
|
|
||||||
|
private val logger = Logger(GeckoActivityDelegate::javaClass.name)
|
||||||
|
|
||||||
|
override fun onStartActivityForResult(intent: PendingIntent): GeckoResult<Intent> {
|
||||||
|
val result: GeckoResult<Intent> = GeckoResult()
|
||||||
|
val delegate = delegateRef.get()
|
||||||
|
|
||||||
|
if (delegate == null) {
|
||||||
|
logger.warn("No activity delegate attached. Cannot request FIDO auth.")
|
||||||
|
|
||||||
|
result.completeExceptionally(RuntimeException("Activity for result failed; no delegate attached."))
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate.startIntentSenderForResult(intent.intentSender) { data ->
|
||||||
|
if (data != null) {
|
||||||
|
result.complete(data)
|
||||||
|
} else {
|
||||||
|
result.completeExceptionally(RuntimeException("Activity for result failed."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.activity
|
||||||
|
|
||||||
|
import mozilla.components.concept.engine.activity.OrientationDelegate
|
||||||
|
import org.mozilla.geckoview.AllowOrDeny
|
||||||
|
import org.mozilla.geckoview.AllowOrDeny.ALLOW
|
||||||
|
import org.mozilla.geckoview.AllowOrDeny.DENY
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
import org.mozilla.geckoview.OrientationController
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default [OrientationController.OrientationDelegate] implementation that delegates both the behavior
|
||||||
|
* and the returned value to a [OrientationDelegate].
|
||||||
|
*/
|
||||||
|
internal class GeckoScreenOrientationDelegate(
|
||||||
|
private val delegate: OrientationDelegate,
|
||||||
|
) : OrientationController.OrientationDelegate {
|
||||||
|
override fun onOrientationLock(requestedOrientation: Int): GeckoResult<AllowOrDeny> {
|
||||||
|
val result = GeckoResult<AllowOrDeny>()
|
||||||
|
|
||||||
|
when (delegate.onOrientationLock(requestedOrientation)) {
|
||||||
|
true -> result.complete(ALLOW)
|
||||||
|
false -> result.complete(DENY)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOrientationUnlock() {
|
||||||
|
delegate.onOrientationUnlock()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.activity
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import mozilla.components.support.base.log.logger.Logger
|
||||||
|
import org.mozilla.geckoview.GeckoView
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GeckoViewActivityContextDelegate provides an active Activity to GeckoView or null. Not to be confused
|
||||||
|
* with the runtime delegate of [GeckoActivityDelegate], which is tightly coupled to webauthn.
|
||||||
|
* See bug 1806191 for more information on delegate differences.
|
||||||
|
*
|
||||||
|
* @param contextRef A reference to an active Activity context or null for GeckoView to use.
|
||||||
|
*/
|
||||||
|
class GeckoViewActivityContextDelegate(
|
||||||
|
private val contextRef: WeakReference<Context?>,
|
||||||
|
) : GeckoView.ActivityContextDelegate {
|
||||||
|
private val logger = Logger("GeckoViewActivityContextDelegate")
|
||||||
|
init {
|
||||||
|
if (contextRef.get() == null) {
|
||||||
|
logger.warn("Activity context is null.")
|
||||||
|
} else if (contextRef.get() !is Activity) {
|
||||||
|
logger.warn("A non-activity context was set.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by GeckoView to get an Activity context for operations such as printing.
|
||||||
|
* @return An active Activity context or null.
|
||||||
|
*/
|
||||||
|
override fun getActivityContext(): Context? {
|
||||||
|
val context = contextRef.get()
|
||||||
|
if ((context == null)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.autofill
|
||||||
|
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.toAutocompleteAddress
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.toCreditCardEntry
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.toLoginEntry
|
||||||
|
import mozilla.components.concept.storage.CreditCard
|
||||||
|
import mozilla.components.concept.storage.CreditCardsAddressesStorageDelegate
|
||||||
|
import mozilla.components.concept.storage.Login
|
||||||
|
import mozilla.components.concept.storage.LoginStorageDelegate
|
||||||
|
import org.mozilla.geckoview.Autocomplete
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gecko credit card and login storage delegate that handles runtime storage requests. This allows
|
||||||
|
* the Gecko runtime to call the underlying storage to handle requests for fetching, saving and
|
||||||
|
* updating of autocomplete items in the storage.
|
||||||
|
*
|
||||||
|
* @param creditCardsAddressesStorageDelegate An instance of [CreditCardsAddressesStorageDelegate].
|
||||||
|
* Provides methods for retrieving [CreditCard]s from the underlying storage.
|
||||||
|
* @param loginStorageDelegate An instance of [LoginStorageDelegate].
|
||||||
|
* Provides read/write methods for the [Login] storage.
|
||||||
|
*/
|
||||||
|
class GeckoAutocompleteStorageDelegate(
|
||||||
|
private val creditCardsAddressesStorageDelegate: CreditCardsAddressesStorageDelegate,
|
||||||
|
private val loginStorageDelegate: LoginStorageDelegate,
|
||||||
|
) : Autocomplete.StorageDelegate {
|
||||||
|
|
||||||
|
override fun onAddressFetch(): GeckoResult<Array<Autocomplete.Address>>? {
|
||||||
|
val result = GeckoResult<Array<Autocomplete.Address>>()
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
GlobalScope.launch(IO) {
|
||||||
|
val addresses = creditCardsAddressesStorageDelegate.onAddressesFetch()
|
||||||
|
.map { it.toAutocompleteAddress() }
|
||||||
|
.toTypedArray()
|
||||||
|
|
||||||
|
result.complete(addresses)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreditCardFetch(): GeckoResult<Array<Autocomplete.CreditCard>> {
|
||||||
|
val result = GeckoResult<Array<Autocomplete.CreditCard>>()
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
GlobalScope.launch(IO) {
|
||||||
|
val key = creditCardsAddressesStorageDelegate.getOrGenerateKey()
|
||||||
|
|
||||||
|
val creditCards = creditCardsAddressesStorageDelegate.onCreditCardsFetch()
|
||||||
|
.mapNotNull {
|
||||||
|
val plaintextCardNumber =
|
||||||
|
creditCardsAddressesStorageDelegate.decrypt(key, it.encryptedCardNumber)?.number
|
||||||
|
|
||||||
|
if (plaintextCardNumber == null) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
Autocomplete.CreditCard.Builder()
|
||||||
|
.guid(it.guid)
|
||||||
|
.name(it.billingName)
|
||||||
|
.number(plaintextCardNumber)
|
||||||
|
.expirationMonth(it.expiryMonth.toString())
|
||||||
|
.expirationYear(it.expiryYear.toString())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toTypedArray()
|
||||||
|
|
||||||
|
result.complete(creditCards)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreditCardSave(creditCard: Autocomplete.CreditCard) {
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
GlobalScope.launch(IO) {
|
||||||
|
creditCardsAddressesStorageDelegate.onCreditCardSave(creditCard.toCreditCardEntry())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoginSave(login: Autocomplete.LoginEntry) {
|
||||||
|
loginStorageDelegate.onLoginSave(login.toLoginEntry())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoginFetch(domain: String): GeckoResult<Array<Autocomplete.LoginEntry>> {
|
||||||
|
val result = GeckoResult<Array<Autocomplete.LoginEntry>>()
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
GlobalScope.launch(IO) {
|
||||||
|
val storedLogins = loginStorageDelegate.onLoginFetch(domain)
|
||||||
|
|
||||||
|
val logins = storedLogins.await()
|
||||||
|
.map { it.toLoginEntry() }
|
||||||
|
.toTypedArray()
|
||||||
|
|
||||||
|
result.complete(logins)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.content.blocking
|
||||||
|
|
||||||
|
import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a site that will be ignored by the tracking protection policies.
|
||||||
|
* @property url The url of the site to be ignored.
|
||||||
|
* @property privateMode Indicates if this exception should persisted in private mode.
|
||||||
|
* @property contentPermission The associated gecko content permission of this exception.
|
||||||
|
*/
|
||||||
|
data class GeckoTrackingProtectionException(
|
||||||
|
override val url: String,
|
||||||
|
val privateMode: Boolean = false,
|
||||||
|
val contentPermission: ContentPermission,
|
||||||
|
) : TrackingProtectionException
|
|
@ -0,0 +1,128 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.cookiebanners
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import mozilla.components.browser.engine.gecko.await
|
||||||
|
import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingMode
|
||||||
|
import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingMode.DISABLED
|
||||||
|
import mozilla.components.concept.engine.cookiehandling.CookieBannersStorage
|
||||||
|
import mozilla.components.support.base.log.logger.Logger
|
||||||
|
import org.mozilla.geckoview.GeckoRuntime
|
||||||
|
import org.mozilla.geckoview.StorageController
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A storage to store [CookieBannerHandlingMode] using GeckoView APIs.
|
||||||
|
*/
|
||||||
|
class GeckoCookieBannersStorage(
|
||||||
|
runtime: GeckoRuntime,
|
||||||
|
private val reportSiteDomainsRepository: ReportSiteDomainsRepository,
|
||||||
|
) : CookieBannersStorage {
|
||||||
|
|
||||||
|
private val geckoStorage: StorageController = runtime.storageController
|
||||||
|
private val mainScope = CoroutineScope(Dispatchers.Main)
|
||||||
|
|
||||||
|
override suspend fun addException(
|
||||||
|
uri: String,
|
||||||
|
privateBrowsing: Boolean,
|
||||||
|
) {
|
||||||
|
setGeckoException(uri, DISABLED, privateBrowsing)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun isSiteDomainReported(siteDomain: String): Boolean {
|
||||||
|
return reportSiteDomainsRepository.isSiteDomainReported(siteDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun saveSiteDomain(siteDomain: String) {
|
||||||
|
reportSiteDomainsRepository.saveSiteDomain(siteDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun addPersistentExceptionInPrivateMode(uri: String) {
|
||||||
|
setPersistentPrivateGeckoException(uri, DISABLED)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findExceptionFor(
|
||||||
|
uri: String,
|
||||||
|
privateBrowsing: Boolean,
|
||||||
|
): CookieBannerHandlingMode? {
|
||||||
|
return queryExceptionInGecko(uri, privateBrowsing)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun hasException(uri: String, privateBrowsing: Boolean): Boolean? {
|
||||||
|
val result = findExceptionFor(uri, privateBrowsing)
|
||||||
|
return if (result != null) {
|
||||||
|
result == DISABLED
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun removeException(uri: String, privateBrowsing: Boolean) {
|
||||||
|
removeGeckoException(uri, privateBrowsing)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun removeGeckoException(uri: String, privateBrowsing: Boolean) {
|
||||||
|
geckoStorage.removeCookieBannerModeForDomain(uri, privateBrowsing)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun setGeckoException(
|
||||||
|
uri: String,
|
||||||
|
mode: CookieBannerHandlingMode,
|
||||||
|
privateBrowsing: Boolean,
|
||||||
|
) {
|
||||||
|
geckoStorage.setCookieBannerModeForDomain(
|
||||||
|
uri,
|
||||||
|
mode.mode,
|
||||||
|
privateBrowsing,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun setPersistentPrivateGeckoException(
|
||||||
|
uri: String,
|
||||||
|
mode: CookieBannerHandlingMode,
|
||||||
|
) {
|
||||||
|
geckoStorage.setCookieBannerModeAndPersistInPrivateBrowsingForDomain(
|
||||||
|
uri,
|
||||||
|
mode.mode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
@Suppress("TooGenericExceptionCaught")
|
||||||
|
internal suspend fun queryExceptionInGecko(
|
||||||
|
uri: String,
|
||||||
|
privateBrowsing: Boolean,
|
||||||
|
): CookieBannerHandlingMode? {
|
||||||
|
return try {
|
||||||
|
withContext(mainScope.coroutineContext) {
|
||||||
|
geckoStorage.getCookieBannerModeForDomain(uri, privateBrowsing).await()
|
||||||
|
?.toCookieBannerHandlingMode() ?: throw IllegalArgumentException(
|
||||||
|
"An error happened trying to find cookie banners mode for the " +
|
||||||
|
"uri $uri and private browsing mode $privateBrowsing",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// This normally happen on internal sites like about:config or ip sites.
|
||||||
|
val disabledErrors = listOf("NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS", "NS_ERROR_HOST_IS_IP_ADDRESS")
|
||||||
|
if (disabledErrors.any { (e.message ?: "").contains(it) }) {
|
||||||
|
Logger("GeckoCookieBannersStorage").error("Unable to query cookie banners exception", e)
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun Int.toCookieBannerHandlingMode(): CookieBannerHandlingMode {
|
||||||
|
return CookieBannerHandlingMode.entries.first { it.mode == this }
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.cookiebanners
|
||||||
|
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.core.emptyPreferences
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import mozilla.components.browser.engine.gecko.cookiebanners.ReportSiteDomainsRepository.PreferencesKeys.REPORT_SITE_DOMAINS
|
||||||
|
import mozilla.components.support.base.log.logger.Logger
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A repository to save reported site domains with the datastore API.
|
||||||
|
*/
|
||||||
|
class ReportSiteDomainsRepository(
|
||||||
|
private val dataStore: DataStore<Preferences>,
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val SEPARATOR = "@<;>@"
|
||||||
|
const val REPORT_SITE_DOMAINS_REPOSITORY_NAME = "report_site_domains_preferences"
|
||||||
|
const val PREFERENCE_KEY_NAME = "report_site_domains"
|
||||||
|
}
|
||||||
|
|
||||||
|
private object PreferencesKeys {
|
||||||
|
val REPORT_SITE_DOMAINS = stringPreferencesKey(PREFERENCE_KEY_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given site's domain url is saved locally.
|
||||||
|
* @param siteDomain the [siteDomain] that will be checked.
|
||||||
|
*/
|
||||||
|
suspend fun isSiteDomainReported(siteDomain: String): Boolean {
|
||||||
|
return dataStore.data
|
||||||
|
.catch { exception ->
|
||||||
|
if (exception is IOException) {
|
||||||
|
Logger.error("Error reading preferences.", exception)
|
||||||
|
emit(emptyPreferences())
|
||||||
|
} else {
|
||||||
|
throw exception
|
||||||
|
}
|
||||||
|
}.map { preferences ->
|
||||||
|
val reportSiteDomainsString = preferences[REPORT_SITE_DOMAINS] ?: ""
|
||||||
|
val reportSiteDomainsList =
|
||||||
|
reportSiteDomainsString.split(SEPARATOR).filter { it.isNotEmpty() }
|
||||||
|
reportSiteDomainsList.contains(siteDomain)
|
||||||
|
}.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the given site's domain url in datastore to keep it persistent locally.
|
||||||
|
* This method gets called after the site domain was reported with Nimbus.
|
||||||
|
* @param siteDomain the [siteDomain] that will be saved.
|
||||||
|
*/
|
||||||
|
suspend fun saveSiteDomain(siteDomain: String) {
|
||||||
|
dataStore.edit { preferences ->
|
||||||
|
val siteDomainsPreferences = preferences[REPORT_SITE_DOMAINS] ?: ""
|
||||||
|
val siteDomainsList = siteDomainsPreferences.split(SEPARATOR).filter { it.isNotEmpty() }
|
||||||
|
if (siteDomainsList.contains(siteDomain)) {
|
||||||
|
return@edit
|
||||||
|
}
|
||||||
|
val domains = mutableListOf<String>()
|
||||||
|
domains.addAll(siteDomainsList)
|
||||||
|
domains.add(siteDomain)
|
||||||
|
preferences[REPORT_SITE_DOMAINS] = domains.joinToString(SEPARATOR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.ext
|
||||||
|
|
||||||
|
import mozilla.components.concept.storage.Address
|
||||||
|
import org.mozilla.geckoview.Autocomplete
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a GeckoView [Autocomplete.Address] to an Android Components [Address].
|
||||||
|
*/
|
||||||
|
fun Autocomplete.Address.toAddress() = Address(
|
||||||
|
guid = guid ?: "",
|
||||||
|
name = name,
|
||||||
|
organization = organization,
|
||||||
|
streetAddress = streetAddress,
|
||||||
|
addressLevel3 = addressLevel3,
|
||||||
|
addressLevel2 = addressLevel2,
|
||||||
|
addressLevel1 = addressLevel1,
|
||||||
|
postalCode = postalCode,
|
||||||
|
country = country,
|
||||||
|
tel = tel,
|
||||||
|
email = email,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an Android Components [Address] to a GeckoView [Autocomplete.Address].
|
||||||
|
*/
|
||||||
|
fun Address.toAutocompleteAddress() = Autocomplete.Address.Builder()
|
||||||
|
.guid(guid)
|
||||||
|
.name(name)
|
||||||
|
.organization(organization)
|
||||||
|
.streetAddress(streetAddress)
|
||||||
|
.addressLevel3(addressLevel3)
|
||||||
|
.addressLevel2(addressLevel2)
|
||||||
|
.addressLevel1(addressLevel1)
|
||||||
|
.postalCode(postalCode)
|
||||||
|
.country(country)
|
||||||
|
.tel(tel)
|
||||||
|
.email(email)
|
||||||
|
.build()
|
|
@ -0,0 +1,32 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.ext
|
||||||
|
|
||||||
|
import mozilla.components.concept.storage.CreditCardEntry
|
||||||
|
import mozilla.components.support.utils.creditCardIIN
|
||||||
|
import org.mozilla.geckoview.Autocomplete
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a GeckoView [Autocomplete.CreditCard] to an Android Components [CreditCardEntry].
|
||||||
|
*/
|
||||||
|
fun Autocomplete.CreditCard.toCreditCardEntry() = CreditCardEntry(
|
||||||
|
guid = guid,
|
||||||
|
name = name,
|
||||||
|
number = number,
|
||||||
|
expiryMonth = expirationMonth,
|
||||||
|
expiryYear = expirationYear,
|
||||||
|
cardType = number.creditCardIIN()?.creditCardIssuerNetwork?.name ?: "",
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an Android Components [CreditCardEntry] to a GeckoView [Autocomplete.CreditCard].
|
||||||
|
*/
|
||||||
|
fun CreditCardEntry.toAutocompleteCreditCard() = Autocomplete.CreditCard.Builder()
|
||||||
|
.guid(guid)
|
||||||
|
.name(name)
|
||||||
|
.number(number)
|
||||||
|
.expirationMonth(expiryMonth)
|
||||||
|
.expirationYear(expiryYear)
|
||||||
|
.build()
|
|
@ -0,0 +1,31 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.ext
|
||||||
|
|
||||||
|
import mozilla.components.browser.engine.gecko.prompt.GeckoChoice
|
||||||
|
import mozilla.components.concept.engine.prompt.Choice
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a GeckoView [GeckoChoice] to an Android Components [Choice].
|
||||||
|
*/
|
||||||
|
private fun GeckoChoice.toChoice(): Choice {
|
||||||
|
val choiceChildren = items?.map { it.toChoice() }?.toTypedArray()
|
||||||
|
// On the GeckoView docs states that label is a @NonNull, but on run-time
|
||||||
|
// we are getting null values
|
||||||
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1771149
|
||||||
|
@Suppress("USELESS_ELVIS")
|
||||||
|
return Choice(id, !disabled, label ?: "", selected, separator, choiceChildren)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an array of [GeckoChoice] to Choice array.
|
||||||
|
* @return array of Choice
|
||||||
|
*/
|
||||||
|
fun convertToChoices(
|
||||||
|
geckoChoices: Array<out GeckoChoice>,
|
||||||
|
): Array<Choice> = geckoChoices.map { geckoChoice ->
|
||||||
|
val choice = geckoChoice.toChoice()
|
||||||
|
choice
|
||||||
|
}.toTypedArray()
|
|
@ -0,0 +1,25 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.ext
|
||||||
|
|
||||||
|
import mozilla.components.browser.engine.gecko.GeckoEngineSession
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_ALLOW
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_TRACKING
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this Gecko permission is a tracking protection permission and it is excluded
|
||||||
|
* from the tracking protection policies.
|
||||||
|
*/
|
||||||
|
val ContentPermission.isExcludedForTrackingProtection: Boolean
|
||||||
|
get() = this.permission == PERMISSION_TRACKING &&
|
||||||
|
value == VALUE_ALLOW
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the tracking protection permission for the given [GeckoEngineSession].
|
||||||
|
* This is available after every onLocationChange call.
|
||||||
|
*/
|
||||||
|
val GeckoEngineSession.geckoTrackingProtectionPermission: ContentPermission?
|
||||||
|
get() = this.geckoPermissions.find { it.permission == PERMISSION_TRACKING }
|
|
@ -0,0 +1,43 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.ext
|
||||||
|
|
||||||
|
import mozilla.components.concept.storage.Login
|
||||||
|
import mozilla.components.concept.storage.LoginEntry
|
||||||
|
import org.mozilla.geckoview.Autocomplete
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a GeckoView [Autocomplete.LoginEntry] to an Android Components [LoginEntry].
|
||||||
|
*/
|
||||||
|
fun Autocomplete.LoginEntry.toLoginEntry() = LoginEntry(
|
||||||
|
origin = origin,
|
||||||
|
formActionOrigin = formActionOrigin,
|
||||||
|
httpRealm = httpRealm,
|
||||||
|
username = username,
|
||||||
|
password = password,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an Android Components [Login] to a GeckoView [Autocomplete.LoginEntry].
|
||||||
|
*/
|
||||||
|
fun Login.toLoginEntry() = Autocomplete.LoginEntry.Builder()
|
||||||
|
.guid(guid)
|
||||||
|
.origin(origin)
|
||||||
|
.formActionOrigin(formActionOrigin)
|
||||||
|
.httpRealm(httpRealm)
|
||||||
|
.username(username)
|
||||||
|
.password(password)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an Android Components [LoginEntry] to a GeckoView [Autocomplete.LoginEntry].
|
||||||
|
*/
|
||||||
|
fun LoginEntry.toLoginEntry() = Autocomplete.LoginEntry.Builder()
|
||||||
|
.origin(origin)
|
||||||
|
.formActionOrigin(formActionOrigin)
|
||||||
|
.httpRealm(httpRealm)
|
||||||
|
.username(username)
|
||||||
|
.password(password)
|
||||||
|
.build()
|
|
@ -0,0 +1,82 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.ext
|
||||||
|
|
||||||
|
import mozilla.components.concept.engine.EngineSession
|
||||||
|
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
|
||||||
|
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory
|
||||||
|
import org.mozilla.geckoview.ContentBlocking
|
||||||
|
import org.mozilla.geckoview.GeckoRuntimeSettings
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a [TrackingProtectionPolicy] into a GeckoView setting that can be used with [GeckoRuntimeSettings.Builder].
|
||||||
|
* Also contains the cookie banner handling settings for regular and private browsing.
|
||||||
|
*/
|
||||||
|
@Suppress("SpreadOperator")
|
||||||
|
fun TrackingProtectionPolicy.toContentBlockingSetting(
|
||||||
|
safeBrowsingPolicy: Array<EngineSession.SafeBrowsingPolicy> = arrayOf(EngineSession.SafeBrowsingPolicy.RECOMMENDED),
|
||||||
|
cookieBannerHandlingMode: EngineSession.CookieBannerHandlingMode = EngineSession.CookieBannerHandlingMode.DISABLED,
|
||||||
|
cookieBannerHandlingModePrivateBrowsing: EngineSession.CookieBannerHandlingMode =
|
||||||
|
EngineSession.CookieBannerHandlingMode.REJECT_ALL,
|
||||||
|
cookieBannerHandlingDetectOnlyMode: Boolean = false,
|
||||||
|
cookieBannerGlobalRulesEnabled: Boolean = false,
|
||||||
|
cookieBannerGlobalRulesSubFramesEnabled: Boolean = false,
|
||||||
|
queryParameterStripping: Boolean = false,
|
||||||
|
queryParameterStrippingPrivateBrowsing: Boolean = false,
|
||||||
|
queryParameterStrippingAllowList: String = "",
|
||||||
|
queryParameterStrippingStripList: String = "",
|
||||||
|
) = ContentBlocking.Settings.Builder().apply {
|
||||||
|
enhancedTrackingProtectionLevel(getEtpLevel())
|
||||||
|
antiTracking(getAntiTrackingPolicy())
|
||||||
|
cookieBehavior(cookiePolicy.id)
|
||||||
|
cookieBehaviorPrivateMode(cookiePolicyPrivateMode.id)
|
||||||
|
cookiePurging(cookiePurging)
|
||||||
|
safeBrowsing(safeBrowsingPolicy.sumOf { it.id })
|
||||||
|
strictSocialTrackingProtection(getStrictSocialTrackingProtection())
|
||||||
|
cookieBannerHandlingMode(cookieBannerHandlingMode.mode)
|
||||||
|
cookieBannerHandlingModePrivateBrowsing(cookieBannerHandlingModePrivateBrowsing.mode)
|
||||||
|
cookieBannerHandlingDetectOnlyMode(cookieBannerHandlingDetectOnlyMode)
|
||||||
|
cookieBannerGlobalRulesEnabled(cookieBannerGlobalRulesEnabled)
|
||||||
|
cookieBannerGlobalRulesSubFramesEnabled(cookieBannerGlobalRulesSubFramesEnabled)
|
||||||
|
queryParameterStrippingEnabled(queryParameterStripping)
|
||||||
|
queryParameterStrippingPrivateBrowsingEnabled(queryParameterStrippingPrivateBrowsing)
|
||||||
|
queryParameterStrippingAllowList(*queryParameterStrippingAllowList.split(",").toTypedArray())
|
||||||
|
queryParameterStrippingStripList(*queryParameterStrippingStripList.split(",").toTypedArray())
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether [TrackingCategory.STRICT] is enabled in the [TrackingProtectionPolicy].
|
||||||
|
*/
|
||||||
|
internal fun TrackingProtectionPolicy.getStrictSocialTrackingProtection(): Boolean {
|
||||||
|
return strictSocialTrackingProtection ?: trackingCategories.contains(TrackingCategory.STRICT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the [TrackingProtectionPolicy] categories as an Enhanced Tracking Protection level for GeckoView.
|
||||||
|
*/
|
||||||
|
internal fun TrackingProtectionPolicy.getEtpLevel(): Int {
|
||||||
|
return when {
|
||||||
|
trackingCategories.contains(TrackingCategory.NONE) -> ContentBlocking.EtpLevel.NONE
|
||||||
|
else -> ContentBlocking.EtpLevel.STRICT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the [TrackingProtectionPolicy] as a tracking policy for GeckoView.
|
||||||
|
*/
|
||||||
|
internal fun TrackingProtectionPolicy.getAntiTrackingPolicy(): Int {
|
||||||
|
/**
|
||||||
|
* The [TrackingProtectionPolicy.TrackingCategory.SCRIPTS_AND_SUB_RESOURCES] is an
|
||||||
|
* artificial category, created with the sole purpose of going around this bug
|
||||||
|
* https://bugzilla.mozilla.org/show_bug.cgi?id=1579264, for this reason we have to
|
||||||
|
* remove its value from the valid anti tracking categories, when is present.
|
||||||
|
*/
|
||||||
|
val total = trackingCategories.sumOf { it.id }
|
||||||
|
return if (contains(TrackingCategory.SCRIPTS_AND_SUB_RESOURCES)) {
|
||||||
|
total - TrackingCategory.SCRIPTS_AND_SUB_RESOURCES.id
|
||||||
|
} else {
|
||||||
|
total
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,139 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.fetch
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import mozilla.components.concept.fetch.Client
|
||||||
|
import mozilla.components.concept.fetch.Headers
|
||||||
|
import mozilla.components.concept.fetch.MutableHeaders
|
||||||
|
import mozilla.components.concept.fetch.Request
|
||||||
|
import mozilla.components.concept.fetch.Response
|
||||||
|
import mozilla.components.concept.fetch.Response.Companion.SUCCESS
|
||||||
|
import mozilla.components.concept.fetch.isBlobUri
|
||||||
|
import mozilla.components.concept.fetch.isDataUri
|
||||||
|
import org.mozilla.geckoview.GeckoRuntime
|
||||||
|
import org.mozilla.geckoview.GeckoWebExecutor
|
||||||
|
import org.mozilla.geckoview.WebRequest
|
||||||
|
import org.mozilla.geckoview.WebRequest.CACHE_MODE_DEFAULT
|
||||||
|
import org.mozilla.geckoview.WebRequest.CACHE_MODE_RELOAD
|
||||||
|
import org.mozilla.geckoview.WebRequestError
|
||||||
|
import org.mozilla.geckoview.WebResponse
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.SocketTimeoutException
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.TimeoutException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GeckoView ([GeckoWebExecutor]) based implementation of [Client].
|
||||||
|
*/
|
||||||
|
class GeckoViewFetchClient(
|
||||||
|
context: Context,
|
||||||
|
runtime: GeckoRuntime = GeckoRuntime.getDefault(context),
|
||||||
|
private val maxReadTimeOut: Pair<Long, TimeUnit> = Pair(MAX_READ_TIMEOUT_MINUTES, TimeUnit.MINUTES),
|
||||||
|
) : Client() {
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal var executor: GeckoWebExecutor = GeckoWebExecutor(runtime)
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun fetch(request: Request): Response {
|
||||||
|
if (request.isDataUri()) {
|
||||||
|
return fetchDataUri(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
val webRequest = request.toWebRequest()
|
||||||
|
|
||||||
|
val readTimeOut = request.readTimeout ?: maxReadTimeOut
|
||||||
|
val readTimeOutMillis = readTimeOut.let { (timeout, unit) ->
|
||||||
|
unit.toMillis(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val webResponse = executor.fetch(webRequest, request.fetchFlags).poll(readTimeOutMillis)
|
||||||
|
webResponse?.toResponse() ?: throw IOException("Fetch failed with null response")
|
||||||
|
} catch (e: TimeoutException) {
|
||||||
|
throw SocketTimeoutException()
|
||||||
|
} catch (e: WebRequestError) {
|
||||||
|
throw IOException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val Request.fetchFlags: Int
|
||||||
|
get() {
|
||||||
|
var fetchFlags = 0
|
||||||
|
if (cookiePolicy == Request.CookiePolicy.OMIT) {
|
||||||
|
fetchFlags += GeckoWebExecutor.FETCH_FLAGS_ANONYMOUS
|
||||||
|
}
|
||||||
|
if (private) {
|
||||||
|
fetchFlags += GeckoWebExecutor.FETCH_FLAGS_PRIVATE
|
||||||
|
}
|
||||||
|
if (redirect == Request.Redirect.MANUAL) {
|
||||||
|
fetchFlags += GeckoWebExecutor.FETCH_FLAGS_NO_REDIRECTS
|
||||||
|
}
|
||||||
|
return fetchFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val MAX_READ_TIMEOUT_MINUTES = 5L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Request.toWebRequest(): WebRequest = WebRequest.Builder(url)
|
||||||
|
.method(method.name)
|
||||||
|
.addHeadersFrom(this)
|
||||||
|
.addBodyFrom(this)
|
||||||
|
.referrer(referrerUrl)
|
||||||
|
.cacheMode(if (useCaches) CACHE_MODE_DEFAULT else CACHE_MODE_RELOAD)
|
||||||
|
.beConservative(conservative)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private fun WebRequest.Builder.addHeadersFrom(request: Request): WebRequest.Builder {
|
||||||
|
request.headers?.forEach { header ->
|
||||||
|
addHeader(header.name, header.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun WebRequest.Builder.addBodyFrom(request: Request): WebRequest.Builder {
|
||||||
|
request.body?.let { body ->
|
||||||
|
body.useStream { inStream ->
|
||||||
|
val bytes = inStream.readBytes()
|
||||||
|
val buffer = ByteBuffer.allocateDirect(bytes.size)
|
||||||
|
buffer.put(bytes)
|
||||||
|
this.body(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun WebResponse.toResponse(): Response {
|
||||||
|
val isDataUri = uri.startsWith("data:")
|
||||||
|
val isBlobUri = uri.startsWith("blob:")
|
||||||
|
val headers = translateHeaders(this)
|
||||||
|
// We use the same API for blobs, data URLs and HTTP requests, but blobs won't receive a status code.
|
||||||
|
// If no exception is thrown we assume success.
|
||||||
|
val status = if (isBlobUri || isDataUri) SUCCESS else statusCode
|
||||||
|
return Response(
|
||||||
|
uri,
|
||||||
|
status,
|
||||||
|
headers,
|
||||||
|
body?.let {
|
||||||
|
Response.Body(it, headers["Content-Type"])
|
||||||
|
} ?: Response.Body.empty(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun translateHeaders(webResponse: WebResponse): Headers {
|
||||||
|
val headers = MutableHeaders()
|
||||||
|
webResponse.headers.forEach { (k, v) ->
|
||||||
|
v.split(",").forEach { headers.append(k, it.trim()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.integration
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import mozilla.components.support.utils.ext.registerReceiverCompat
|
||||||
|
import org.mozilla.geckoview.GeckoRuntime
|
||||||
|
import androidx.core.os.LocaleListCompat as LocaleList
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to set the locales setting for geckoview, updating from the locale of the device.
|
||||||
|
*/
|
||||||
|
class LocaleSettingUpdater(
|
||||||
|
private val context: Context,
|
||||||
|
private val runtime: GeckoRuntime,
|
||||||
|
) : SettingUpdater<Array<String>>() {
|
||||||
|
|
||||||
|
override var value: Array<String> = findValue()
|
||||||
|
set(value) {
|
||||||
|
runtime.settings.locales = value
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
private val localeChangedReceiver by lazy {
|
||||||
|
object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
|
updateValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun registerForUpdates() {
|
||||||
|
context.registerReceiverCompat(
|
||||||
|
localeChangedReceiver,
|
||||||
|
IntentFilter(Intent.ACTION_LOCALE_CHANGED),
|
||||||
|
ContextCompat.RECEIVER_NOT_EXPORTED,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unregisterForUpdates() {
|
||||||
|
context.unregisterReceiver(localeChangedReceiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findValue(): Array<String> {
|
||||||
|
val localeList = LocaleList.getAdjustedDefault()
|
||||||
|
return arrayOfNulls<Unit>(localeList.size())
|
||||||
|
.mapIndexedNotNull { i, _ -> localeList.get(i)?.toLanguageTag() }
|
||||||
|
.toTypedArray()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.integration
|
||||||
|
|
||||||
|
abstract class SettingUpdater<T> {
|
||||||
|
/**
|
||||||
|
* Toggle the automatic tracking of a setting derived from the device state.
|
||||||
|
*/
|
||||||
|
var enabled: Boolean = false
|
||||||
|
set(value) {
|
||||||
|
if (value) {
|
||||||
|
updateValue()
|
||||||
|
registerForUpdates()
|
||||||
|
} else {
|
||||||
|
unregisterForUpdates()
|
||||||
|
}
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The setter for this property should change the GeckoView setting.
|
||||||
|
*/
|
||||||
|
abstract var value: T
|
||||||
|
|
||||||
|
internal fun updateValue() {
|
||||||
|
value = findValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register for updates from the device state. This is setting specific.
|
||||||
|
*/
|
||||||
|
abstract fun registerForUpdates()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister for updates from the device state.
|
||||||
|
*/
|
||||||
|
abstract fun unregisterForUpdates()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the value of the setting from the device state. This is setting specific.
|
||||||
|
*/
|
||||||
|
abstract fun findValue(): T
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.media
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import mozilla.components.browser.engine.gecko.GeckoEngineSession
|
||||||
|
import mozilla.components.concept.engine.media.RecordingDevice
|
||||||
|
import org.mozilla.geckoview.GeckoSession
|
||||||
|
import java.security.InvalidParameterException
|
||||||
|
import org.mozilla.geckoview.GeckoSession.MediaDelegate.RecordingDevice as GeckoRecordingDevice
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gecko-based GeckoMediaDelegate implementation.
|
||||||
|
*/
|
||||||
|
internal class GeckoMediaDelegate(private val geckoEngineSession: GeckoEngineSession) :
|
||||||
|
GeckoSession.MediaDelegate {
|
||||||
|
|
||||||
|
override fun onRecordingStatusChanged(
|
||||||
|
session: GeckoSession,
|
||||||
|
geckoDevices: Array<out GeckoRecordingDevice>,
|
||||||
|
) {
|
||||||
|
val devices = geckoDevices.map { geckoRecording ->
|
||||||
|
val type = geckoRecording.toType()
|
||||||
|
val status = geckoRecording.toStatus()
|
||||||
|
RecordingDevice(type, status)
|
||||||
|
}
|
||||||
|
geckoEngineSession.notifyObservers { onRecordingStateChanged(devices) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal fun GeckoRecordingDevice.toType(): RecordingDevice.Type {
|
||||||
|
return when (type) {
|
||||||
|
GeckoRecordingDevice.Type.CAMERA -> RecordingDevice.Type.CAMERA
|
||||||
|
GeckoRecordingDevice.Type.MICROPHONE -> RecordingDevice.Type.MICROPHONE
|
||||||
|
else -> {
|
||||||
|
throw InvalidParameterException("Unexpected Gecko Media type $type status $status")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal fun GeckoRecordingDevice.toStatus(): RecordingDevice.Status {
|
||||||
|
return when (status) {
|
||||||
|
GeckoRecordingDevice.Status.RECORDING -> RecordingDevice.Status.RECORDING
|
||||||
|
GeckoRecordingDevice.Status.INACTIVE -> RecordingDevice.Status.INACTIVE
|
||||||
|
else -> {
|
||||||
|
throw InvalidParameterException("Unexpected Gecko Media type $type status $status")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
package mozilla.components.browser.engine.gecko.mediaquery
|
||||||
|
|
||||||
|
import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
|
||||||
|
import org.mozilla.geckoview.GeckoRuntimeSettings
|
||||||
|
|
||||||
|
internal fun PreferredColorScheme.Companion.from(geckoValue: Int) =
|
||||||
|
when (geckoValue) {
|
||||||
|
GeckoRuntimeSettings.COLOR_SCHEME_DARK -> PreferredColorScheme.Dark
|
||||||
|
GeckoRuntimeSettings.COLOR_SCHEME_LIGHT -> PreferredColorScheme.Light
|
||||||
|
GeckoRuntimeSettings.COLOR_SCHEME_SYSTEM -> PreferredColorScheme.System
|
||||||
|
else -> PreferredColorScheme.System
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun PreferredColorScheme.toGeckoValue() =
|
||||||
|
when (this) {
|
||||||
|
is PreferredColorScheme.Dark -> GeckoRuntimeSettings.COLOR_SCHEME_DARK
|
||||||
|
is PreferredColorScheme.Light -> GeckoRuntimeSettings.COLOR_SCHEME_LIGHT
|
||||||
|
is PreferredColorScheme.System -> GeckoRuntimeSettings.COLOR_SCHEME_SYSTEM
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.mediasession
|
||||||
|
|
||||||
|
import mozilla.components.concept.engine.mediasession.MediaSession
|
||||||
|
import org.mozilla.geckoview.MediaSession as GeckoViewMediaSession
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [MediaSession.Controller] (`concept-engine`) implementation for GeckoView.
|
||||||
|
*/
|
||||||
|
internal class GeckoMediaSessionController(
|
||||||
|
private val mediaSession: GeckoViewMediaSession,
|
||||||
|
) : MediaSession.Controller {
|
||||||
|
|
||||||
|
override fun pause() {
|
||||||
|
mediaSession.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
mediaSession.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun play() {
|
||||||
|
mediaSession.play()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun seekTo(time: Double, fast: Boolean) {
|
||||||
|
mediaSession.seekTo(time, fast)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun seekForward() {
|
||||||
|
mediaSession.seekForward()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun seekBackward() {
|
||||||
|
mediaSession.seekBackward()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun nextTrack() {
|
||||||
|
mediaSession.nextTrack()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun previousTrack() {
|
||||||
|
mediaSession.previousTrack()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun skipAd() {
|
||||||
|
mediaSession.skipAd()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun muteAudio(mute: Boolean) {
|
||||||
|
mediaSession.muteAudio(mute)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,125 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.mediasession
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import mozilla.components.browser.engine.gecko.GeckoEngineSession
|
||||||
|
import mozilla.components.browser.engine.gecko.await
|
||||||
|
import mozilla.components.concept.engine.mediasession.MediaSession
|
||||||
|
import org.mozilla.geckoview.GeckoSession
|
||||||
|
import org.mozilla.geckoview.Image.ImageProcessingException
|
||||||
|
import org.mozilla.geckoview.MediaSession as GeckoViewMediaSession
|
||||||
|
|
||||||
|
private const val ARTWORK_RETRIEVE_TIMEOUT = 1000L
|
||||||
|
private const val ARTWORK_IMAGE_SIZE = 48
|
||||||
|
|
||||||
|
internal class GeckoMediaSessionDelegate(
|
||||||
|
private val engineSession: GeckoEngineSession,
|
||||||
|
) : GeckoViewMediaSession.Delegate {
|
||||||
|
|
||||||
|
override fun onActivated(geckoSession: GeckoSession, mediaSession: GeckoViewMediaSession) {
|
||||||
|
engineSession.notifyObservers {
|
||||||
|
onMediaActivated(GeckoMediaSessionController(mediaSession))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDeactivated(session: GeckoSession, mediaSession: GeckoViewMediaSession) {
|
||||||
|
engineSession.notifyObservers {
|
||||||
|
onMediaDeactivated()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMetadata(
|
||||||
|
session: GeckoSession,
|
||||||
|
mediaSession: GeckoViewMediaSession,
|
||||||
|
metaData: GeckoViewMediaSession.Metadata,
|
||||||
|
) {
|
||||||
|
val getArtwork: (suspend () -> Bitmap?)? = metaData.artwork?.let {
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
withTimeoutOrNull(ARTWORK_RETRIEVE_TIMEOUT) {
|
||||||
|
it.getBitmap(ARTWORK_IMAGE_SIZE).await()
|
||||||
|
}
|
||||||
|
} catch (e: ImageProcessingException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
engineSession.notifyObservers {
|
||||||
|
onMediaMetadataChanged(
|
||||||
|
MediaSession.Metadata(metaData.title, metaData.artist, metaData.album, getArtwork),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFeatures(
|
||||||
|
session: GeckoSession,
|
||||||
|
mediaSession: GeckoViewMediaSession,
|
||||||
|
features: Long,
|
||||||
|
) {
|
||||||
|
engineSession.notifyObservers {
|
||||||
|
onMediaFeatureChanged(MediaSession.Feature(features))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlay(session: GeckoSession, mediaSession: GeckoViewMediaSession) {
|
||||||
|
engineSession.notifyObservers {
|
||||||
|
onMediaPlaybackStateChanged(MediaSession.PlaybackState.PLAYING)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause(session: GeckoSession, mediaSession: GeckoViewMediaSession) {
|
||||||
|
engineSession.notifyObservers {
|
||||||
|
onMediaPlaybackStateChanged(MediaSession.PlaybackState.PAUSED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop(session: GeckoSession, mediaSession: GeckoViewMediaSession) {
|
||||||
|
engineSession.notifyObservers {
|
||||||
|
onMediaPlaybackStateChanged(MediaSession.PlaybackState.STOPPED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPositionState(
|
||||||
|
session: GeckoSession,
|
||||||
|
mediaSession: GeckoViewMediaSession,
|
||||||
|
positionState: GeckoViewMediaSession.PositionState,
|
||||||
|
) {
|
||||||
|
engineSession.notifyObservers {
|
||||||
|
onMediaPositionStateChanged(
|
||||||
|
MediaSession.PositionState(
|
||||||
|
positionState.duration,
|
||||||
|
positionState.position,
|
||||||
|
positionState.playbackRate,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFullscreen(
|
||||||
|
session: GeckoSession,
|
||||||
|
mediaSession: GeckoViewMediaSession,
|
||||||
|
enabled: Boolean,
|
||||||
|
elementMetaData: GeckoViewMediaSession.ElementMetadata?,
|
||||||
|
) {
|
||||||
|
val sessionElementMetaData =
|
||||||
|
elementMetaData?.let {
|
||||||
|
MediaSession.ElementMetadata(
|
||||||
|
elementMetaData.source,
|
||||||
|
elementMetaData.duration,
|
||||||
|
elementMetaData.width,
|
||||||
|
elementMetaData.height,
|
||||||
|
elementMetaData.audioTrackCount,
|
||||||
|
elementMetaData.videoTrackCount,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
engineSession.notifyObservers {
|
||||||
|
onMediaFullscreenChanged(enabled, sessionElementMetaData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,185 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.permission
|
||||||
|
|
||||||
|
import android.Manifest.permission.ACCESS_COARSE_LOCATION
|
||||||
|
import android.Manifest.permission.ACCESS_FINE_LOCATION
|
||||||
|
import android.Manifest.permission.CAMERA
|
||||||
|
import android.Manifest.permission.RECORD_AUDIO
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import mozilla.components.concept.engine.permission.Permission
|
||||||
|
import mozilla.components.concept.engine.permission.PermissionRequest
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_ALLOW
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_DENY
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource.SOURCE_AUDIOCAPTURE
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource.SOURCE_CAMERA
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource.SOURCE_MICROPHONE
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource.SOURCE_OTHER
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource.SOURCE_SCREEN
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_AUTOPLAY_AUDIBLE
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_AUTOPLAY_INAUDIBLE
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_DESKTOP_NOTIFICATION
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_GEOLOCATION
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_MEDIA_KEY_SYSTEM_ACCESS
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_PERSISTENT_STORAGE
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_STORAGE_ACCESS
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gecko-based implementation of [PermissionRequest].
|
||||||
|
*
|
||||||
|
* @property permissions the list of requested permissions.
|
||||||
|
* @property callback the callback to grant/reject the requested permissions.
|
||||||
|
* @property id a unique identifier for the request.
|
||||||
|
*/
|
||||||
|
sealed class GeckoPermissionRequest constructor(
|
||||||
|
override val permissions: List<Permission>,
|
||||||
|
private val callback: PermissionDelegate.Callback? = null,
|
||||||
|
override val id: String = UUID.randomUUID().toString(),
|
||||||
|
) : PermissionRequest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a gecko-based content permission request.
|
||||||
|
*
|
||||||
|
* @property uri the URI of the content requesting the permissions.
|
||||||
|
* @property type the type of the requested content permission (will be
|
||||||
|
* mapped to corresponding [Permission]).
|
||||||
|
* @property geckoPermission Indicates which gecko permissions is requested.
|
||||||
|
* @property geckoResult the gecko result that serves as a callback to grant/reject the requested permissions.
|
||||||
|
*/
|
||||||
|
data class Content(
|
||||||
|
override val uri: String,
|
||||||
|
private val type: Int,
|
||||||
|
internal val geckoPermission: PermissionDelegate.ContentPermission,
|
||||||
|
internal val geckoResult: GeckoResult<Int>,
|
||||||
|
) : GeckoPermissionRequest(
|
||||||
|
listOf(permissionsMap.getOrElse(type) { Permission.Generic("$type", "Gecko permission type = $type") }),
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
val permissionsMap = mapOf(
|
||||||
|
PERMISSION_DESKTOP_NOTIFICATION to Permission.ContentNotification(),
|
||||||
|
PERMISSION_GEOLOCATION to Permission.ContentGeoLocation(),
|
||||||
|
PERMISSION_AUTOPLAY_AUDIBLE to Permission.ContentAutoPlayAudible(),
|
||||||
|
PERMISSION_AUTOPLAY_INAUDIBLE to Permission.ContentAutoPlayInaudible(),
|
||||||
|
PERMISSION_PERSISTENT_STORAGE to Permission.ContentPersistentStorage(),
|
||||||
|
PERMISSION_MEDIA_KEY_SYSTEM_ACCESS to Permission.ContentMediaKeySystemAccess(),
|
||||||
|
PERMISSION_STORAGE_ACCESS to Permission.ContentCrossOriginStorageAccess(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal var isCompleted = false
|
||||||
|
|
||||||
|
override fun grant(permissions: List<Permission>) {
|
||||||
|
if (!isCompleted) {
|
||||||
|
geckoResult.complete(VALUE_ALLOW)
|
||||||
|
}
|
||||||
|
isCompleted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reject() {
|
||||||
|
if (!isCompleted) {
|
||||||
|
geckoResult.complete(VALUE_DENY)
|
||||||
|
}
|
||||||
|
isCompleted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a gecko-based application permission request.
|
||||||
|
*
|
||||||
|
* @property uri the URI of the content requesting the permissions.
|
||||||
|
* @property nativePermissions the list of requested app permissions (will be
|
||||||
|
* mapped to corresponding [Permission]s).
|
||||||
|
* @property callback the callback to grant/reject the requested permissions.
|
||||||
|
*/
|
||||||
|
data class App(
|
||||||
|
private val nativePermissions: List<String>,
|
||||||
|
private val callback: PermissionDelegate.Callback,
|
||||||
|
) : GeckoPermissionRequest(
|
||||||
|
nativePermissions.map { permissionsMap.getOrElse(it) { Permission.Generic(it) } },
|
||||||
|
callback,
|
||||||
|
) {
|
||||||
|
override val uri: String? = null
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val permissionsMap = mapOf(
|
||||||
|
ACCESS_COARSE_LOCATION to Permission.AppLocationCoarse(ACCESS_COARSE_LOCATION),
|
||||||
|
ACCESS_FINE_LOCATION to Permission.AppLocationFine(ACCESS_FINE_LOCATION),
|
||||||
|
CAMERA to Permission.AppCamera(CAMERA),
|
||||||
|
RECORD_AUDIO to Permission.AppAudio(RECORD_AUDIO),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a gecko-based media permission request.
|
||||||
|
*
|
||||||
|
* @property uri the URI of the content requesting the permissions.
|
||||||
|
* @property videoSources the list of requested video sources (will be
|
||||||
|
* mapped to the corresponding [Permission]).
|
||||||
|
* @property audioSources the list of requested audio sources (will be
|
||||||
|
* mapped to corresponding [Permission]).
|
||||||
|
* @property callback the callback to grant/reject the requested permissions.
|
||||||
|
*/
|
||||||
|
data class Media(
|
||||||
|
override val uri: String,
|
||||||
|
private val videoSources: List<MediaSource>,
|
||||||
|
private val audioSources: List<MediaSource>,
|
||||||
|
private val callback: PermissionDelegate.MediaCallback,
|
||||||
|
) : GeckoPermissionRequest(
|
||||||
|
videoSources.map { mapPermission(it) } + audioSources.map { mapPermission(it) },
|
||||||
|
) {
|
||||||
|
override fun grant(permissions: List<Permission>) {
|
||||||
|
val videos = permissions.mapNotNull { permission -> videoSources.find { it.id == permission.id } }
|
||||||
|
val audios = permissions.mapNotNull { permission -> audioSources.find { it.id == permission.id } }
|
||||||
|
callback.grant(videos.firstOrNull(), audios.firstOrNull())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun containsVideoAndAudioSources(): Boolean {
|
||||||
|
return videoSources.isNotEmpty() && audioSources.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reject() {
|
||||||
|
callback.reject()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun mapPermission(mediaSource: MediaSource): Permission =
|
||||||
|
if (mediaSource.type == MediaSource.TYPE_AUDIO) {
|
||||||
|
mapAudioPermission(mediaSource)
|
||||||
|
} else {
|
||||||
|
mapVideoPermission(mediaSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("SwitchIntDef")
|
||||||
|
private fun mapAudioPermission(mediaSource: MediaSource) = when (mediaSource.source) {
|
||||||
|
SOURCE_AUDIOCAPTURE -> Permission.ContentAudioCapture(mediaSource.id, mediaSource.name)
|
||||||
|
SOURCE_MICROPHONE -> Permission.ContentAudioMicrophone(mediaSource.id, mediaSource.name)
|
||||||
|
SOURCE_OTHER -> Permission.ContentAudioOther(mediaSource.id, mediaSource.name)
|
||||||
|
else -> Permission.Generic(mediaSource.id, mediaSource.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("ComplexMethod", "SwitchIntDef")
|
||||||
|
private fun mapVideoPermission(mediaSource: MediaSource) = when (mediaSource.source) {
|
||||||
|
SOURCE_CAMERA -> Permission.ContentVideoCamera(mediaSource.id, mediaSource.name)
|
||||||
|
SOURCE_SCREEN -> Permission.ContentVideoScreen(mediaSource.id, mediaSource.name)
|
||||||
|
SOURCE_OTHER -> Permission.ContentVideoOther(mediaSource.id, mediaSource.name)
|
||||||
|
else -> Permission.Generic(mediaSource.id, mediaSource.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun grant(permissions: List<Permission>) {
|
||||||
|
callback?.grant()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reject() {
|
||||||
|
callback?.reject()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,461 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.permission
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import androidx.paging.DataSource
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import mozilla.components.browser.engine.gecko.await
|
||||||
|
import mozilla.components.concept.engine.permission.PermissionRequest
|
||||||
|
import mozilla.components.concept.engine.permission.SitePermissions
|
||||||
|
import mozilla.components.concept.engine.permission.SitePermissions.AutoplayStatus
|
||||||
|
import mozilla.components.concept.engine.permission.SitePermissions.Status
|
||||||
|
import mozilla.components.concept.engine.permission.SitePermissions.Status.ALLOWED
|
||||||
|
import mozilla.components.concept.engine.permission.SitePermissions.Status.BLOCKED
|
||||||
|
import mozilla.components.concept.engine.permission.SitePermissions.Status.NO_DECISION
|
||||||
|
import mozilla.components.concept.engine.permission.SitePermissionsStorage
|
||||||
|
import mozilla.components.support.ktx.kotlin.stripDefaultPort
|
||||||
|
import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
|
||||||
|
import org.mozilla.geckoview.GeckoRuntime
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_ALLOW
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_DENY
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_PROMPT
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_AUTOPLAY_AUDIBLE
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_AUTOPLAY_INAUDIBLE
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_DESKTOP_NOTIFICATION
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_GEOLOCATION
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_MEDIA_KEY_SYSTEM_ACCESS
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_PERSISTENT_STORAGE
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_STORAGE_ACCESS
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_TRACKING
|
||||||
|
import org.mozilla.geckoview.StorageController
|
||||||
|
import org.mozilla.geckoview.StorageController.ClearFlags
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A storage to save [SitePermissions] using GeckoView APIs.
|
||||||
|
*/
|
||||||
|
@Suppress("LargeClass")
|
||||||
|
class GeckoSitePermissionsStorage(
|
||||||
|
runtime: GeckoRuntime,
|
||||||
|
private val onDiskStorage: SitePermissionsStorage,
|
||||||
|
) : SitePermissionsStorage {
|
||||||
|
|
||||||
|
private val geckoStorage: StorageController = runtime.storageController
|
||||||
|
private val mainScope = CoroutineScope(Dispatchers.Main)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Temporary permissions are created when users doesn't
|
||||||
|
* check the 'Remember my decision checkbox'. At the moment,
|
||||||
|
* gecko view doesn't handle temporary permission,
|
||||||
|
* we have to store them in memory, and clear them manually,
|
||||||
|
* until we have an API for it see:
|
||||||
|
* https://bugzilla.mozilla.org/show_bug.cgi?id=1710447
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
internal val geckoTemporaryPermissions = mutableListOf<ContentPermission>()
|
||||||
|
|
||||||
|
override suspend fun save(sitePermissions: SitePermissions, request: PermissionRequest?, private: Boolean) {
|
||||||
|
val geckoSavedPermissions = updateGeckoPermissionIfNeeded(sitePermissions, request, private)
|
||||||
|
onDiskStorage.save(geckoSavedPermissions, request, private)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveTemporary(request: PermissionRequest?) {
|
||||||
|
if (request is GeckoPermissionRequest.Content) {
|
||||||
|
geckoTemporaryPermissions.add(request.geckoPermission)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearTemporaryPermissions() {
|
||||||
|
geckoTemporaryPermissions.forEach {
|
||||||
|
geckoStorage.setPermission(it, VALUE_PROMPT)
|
||||||
|
}
|
||||||
|
geckoTemporaryPermissions.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun update(sitePermissions: SitePermissions, private: Boolean) {
|
||||||
|
val updatedPermission = updateGeckoPermissionIfNeeded(sitePermissions, private = private)
|
||||||
|
onDiskStorage.update(updatedPermission, private)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findSitePermissionsBy(
|
||||||
|
origin: String,
|
||||||
|
includeTemporary: Boolean,
|
||||||
|
private: Boolean,
|
||||||
|
): SitePermissions? {
|
||||||
|
/**
|
||||||
|
* GeckoView ony persists [GeckoPermissionRequest.Content] other permissions like
|
||||||
|
* [GeckoPermissionRequest.Media], we have to store them ourselves.
|
||||||
|
* For this reason, we query both storage ([geckoStorage] and [onDiskStorage]) and
|
||||||
|
* merge both results into one [SitePermissions] object.
|
||||||
|
*/
|
||||||
|
val onDiskPermission: SitePermissions? =
|
||||||
|
onDiskStorage.findSitePermissionsBy(origin, private = private)
|
||||||
|
val geckoPermissions = findGeckoContentPermissionBy(origin, includeTemporary, private).groupByType()
|
||||||
|
|
||||||
|
return mergePermissions(onDiskPermission, geckoPermissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getSitePermissionsPaged(): DataSource.Factory<Int, SitePermissions> {
|
||||||
|
val geckoPermissionsByHost = findAllGeckoContentPermissions().groupByDomain()
|
||||||
|
|
||||||
|
return onDiskStorage.getSitePermissionsPaged().map { onDiskPermission ->
|
||||||
|
val geckoPermissions = geckoPermissionsByHost[onDiskPermission.origin].groupByType()
|
||||||
|
mergePermissions(onDiskPermission, geckoPermissions) ?: onDiskPermission
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun remove(sitePermissions: SitePermissions, private: Boolean) {
|
||||||
|
onDiskStorage.remove(sitePermissions, private)
|
||||||
|
removeGeckoContentPermissionBy(sitePermissions.origin, private)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun removeAll() {
|
||||||
|
onDiskStorage.removeAll()
|
||||||
|
removeGeckoAllContentPermissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun all(): List<SitePermissions> {
|
||||||
|
val onDiskPermissions: List<SitePermissions> = onDiskStorage.all()
|
||||||
|
val geckoPermissionsByHost = findAllGeckoContentPermissions().groupByDomain()
|
||||||
|
|
||||||
|
return onDiskPermissions.mapNotNull { onDiskPermission ->
|
||||||
|
val map = geckoPermissionsByHost[onDiskPermission.origin].groupByType()
|
||||||
|
mergePermissions(onDiskPermission, map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal suspend fun findAllGeckoContentPermissions(): List<ContentPermission>? {
|
||||||
|
return withContext(mainScope.coroutineContext) {
|
||||||
|
geckoStorage.allPermissions.await()
|
||||||
|
.filterNotTemporaryPermissions(geckoTemporaryPermissions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the [geckoStorage] if the provided [userSitePermissions]
|
||||||
|
* exists on the [geckoStorage] or it's provided as a part of the [permissionRequest]
|
||||||
|
* otherwise nothing is updated.
|
||||||
|
* @param userSitePermissions the values provided by the user to be updated.
|
||||||
|
* @param permissionRequest the [PermissionRequest] from the web content.
|
||||||
|
* @return An updated [SitePermissions] with default values, if they were updated
|
||||||
|
* on the [geckoStorage] otherwise the same [SitePermissions].
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
@Suppress("LongMethod")
|
||||||
|
internal suspend fun updateGeckoPermissionIfNeeded(
|
||||||
|
userSitePermissions: SitePermissions,
|
||||||
|
permissionRequest: PermissionRequest? = null,
|
||||||
|
private: Boolean,
|
||||||
|
): SitePermissions {
|
||||||
|
var updatedPermission = userSitePermissions
|
||||||
|
val geckoPermissionsByType =
|
||||||
|
permissionRequest.extractGeckoPermissionsOrQueryTheStore(userSitePermissions.origin, private)
|
||||||
|
|
||||||
|
if (geckoPermissionsByType.isNotEmpty()) {
|
||||||
|
val geckoNotification = geckoPermissionsByType[PERMISSION_DESKTOP_NOTIFICATION]?.firstOrNull()
|
||||||
|
val geckoLocation = geckoPermissionsByType[PERMISSION_GEOLOCATION]?.firstOrNull()
|
||||||
|
val geckoMedia = geckoPermissionsByType[PERMISSION_MEDIA_KEY_SYSTEM_ACCESS]?.firstOrNull()
|
||||||
|
val geckoLocalStorage = geckoPermissionsByType[PERMISSION_PERSISTENT_STORAGE]?.firstOrNull()
|
||||||
|
val geckoCrossOriginStorageAccess = geckoPermissionsByType[PERMISSION_STORAGE_ACCESS]?.firstOrNull()
|
||||||
|
val geckoAudible = geckoPermissionsByType[PERMISSION_AUTOPLAY_AUDIBLE]?.firstOrNull()
|
||||||
|
val geckoInAudible = geckoPermissionsByType[PERMISSION_AUTOPLAY_INAUDIBLE]?.firstOrNull()
|
||||||
|
|
||||||
|
/*
|
||||||
|
* To avoid GeckoView caching previous request, we need to clear, previous data
|
||||||
|
* before updating. See: https://github.com/mozilla-mobile/android-components/issues/6322
|
||||||
|
*/
|
||||||
|
clearGeckoCacheFor(updatedPermission.origin)
|
||||||
|
|
||||||
|
if (geckoNotification != null) {
|
||||||
|
removeTemporaryPermissionIfAny(geckoNotification)
|
||||||
|
geckoStorage.setPermission(
|
||||||
|
geckoNotification,
|
||||||
|
userSitePermissions.notification.toGeckoStatus(),
|
||||||
|
)
|
||||||
|
updatedPermission = updatedPermission.copy(notification = NO_DECISION)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geckoLocation != null) {
|
||||||
|
removeTemporaryPermissionIfAny(geckoLocation)
|
||||||
|
geckoStorage.setPermission(
|
||||||
|
geckoLocation,
|
||||||
|
userSitePermissions.location.toGeckoStatus(),
|
||||||
|
)
|
||||||
|
updatedPermission = updatedPermission.copy(location = NO_DECISION)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geckoMedia != null) {
|
||||||
|
removeTemporaryPermissionIfAny(geckoMedia)
|
||||||
|
geckoStorage.setPermission(
|
||||||
|
geckoMedia,
|
||||||
|
userSitePermissions.mediaKeySystemAccess.toGeckoStatus(),
|
||||||
|
)
|
||||||
|
updatedPermission = updatedPermission.copy(mediaKeySystemAccess = NO_DECISION)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geckoLocalStorage != null) {
|
||||||
|
removeTemporaryPermissionIfAny(geckoLocalStorage)
|
||||||
|
geckoStorage.setPermission(
|
||||||
|
geckoLocalStorage,
|
||||||
|
userSitePermissions.localStorage.toGeckoStatus(),
|
||||||
|
)
|
||||||
|
updatedPermission = updatedPermission.copy(localStorage = NO_DECISION)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geckoCrossOriginStorageAccess != null) {
|
||||||
|
removeTemporaryPermissionIfAny(geckoCrossOriginStorageAccess)
|
||||||
|
geckoStorage.setPermission(
|
||||||
|
geckoCrossOriginStorageAccess,
|
||||||
|
userSitePermissions.crossOriginStorageAccess.toGeckoStatus(),
|
||||||
|
)
|
||||||
|
updatedPermission = updatedPermission.copy(crossOriginStorageAccess = NO_DECISION)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geckoAudible != null) {
|
||||||
|
removeTemporaryPermissionIfAny(geckoAudible)
|
||||||
|
geckoStorage.setPermission(
|
||||||
|
geckoAudible,
|
||||||
|
userSitePermissions.autoplayAudible.toGeckoStatus(),
|
||||||
|
)
|
||||||
|
updatedPermission = updatedPermission.copy(autoplayAudible = AutoplayStatus.BLOCKED)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geckoInAudible != null) {
|
||||||
|
removeTemporaryPermissionIfAny(geckoInAudible)
|
||||||
|
geckoStorage.setPermission(
|
||||||
|
geckoInAudible,
|
||||||
|
userSitePermissions.autoplayInaudible.toGeckoStatus(),
|
||||||
|
)
|
||||||
|
updatedPermission =
|
||||||
|
updatedPermission.copy(autoplayInaudible = AutoplayStatus.BLOCKED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updatedPermission
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines a permission that comes from our on disk storage with the gecko permissions,
|
||||||
|
* and combined both into a single a [SitePermissions].
|
||||||
|
* @param onDiskPermissions a permission from the on disk storage.
|
||||||
|
* @param geckoPermissionByType a list of all the gecko permissions mapped by permission type.
|
||||||
|
* @return a [SitePermissions] containing the values from the on disk and gecko permission.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
@Suppress("ComplexMethod")
|
||||||
|
internal fun mergePermissions(
|
||||||
|
onDiskPermissions: SitePermissions?,
|
||||||
|
geckoPermissionByType: Map<Int, List<ContentPermission>>,
|
||||||
|
): SitePermissions? {
|
||||||
|
var combinedPermissions = onDiskPermissions
|
||||||
|
|
||||||
|
if (geckoPermissionByType.isNotEmpty() && onDiskPermissions != null) {
|
||||||
|
val geckoNotification = geckoPermissionByType[PERMISSION_DESKTOP_NOTIFICATION]?.firstOrNull()
|
||||||
|
val geckoLocation = geckoPermissionByType[PERMISSION_GEOLOCATION]?.firstOrNull()
|
||||||
|
val geckoMedia = geckoPermissionByType[PERMISSION_MEDIA_KEY_SYSTEM_ACCESS]?.firstOrNull()
|
||||||
|
val geckoStorage = geckoPermissionByType[PERMISSION_PERSISTENT_STORAGE]?.firstOrNull()
|
||||||
|
// Currently we'll receive the "storage_access" permission for all iframes of the same parent
|
||||||
|
// so we need to ensure we are reporting the permission for the current iframe request.
|
||||||
|
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1746436 for more details.
|
||||||
|
val geckoCrossOriginStorageAccess = geckoPermissionByType[PERMISSION_STORAGE_ACCESS]?.firstOrNull {
|
||||||
|
it.thirdPartyOrigin == onDiskPermissions.origin.stripDefaultPort()
|
||||||
|
}
|
||||||
|
val geckoAudible = geckoPermissionByType[PERMISSION_AUTOPLAY_AUDIBLE]?.firstOrNull()
|
||||||
|
val geckoInAudible = geckoPermissionByType[PERMISSION_AUTOPLAY_INAUDIBLE]?.firstOrNull()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We only consider permissions from geckoView, when the values default value
|
||||||
|
* has been changed otherwise we favor the values [onDiskPermissions].
|
||||||
|
*/
|
||||||
|
if (geckoNotification != null && geckoNotification.value != VALUE_PROMPT) {
|
||||||
|
combinedPermissions = combinedPermissions?.copy(
|
||||||
|
notification = geckoNotification.value.toStatus(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geckoLocation != null && geckoLocation.value != VALUE_PROMPT) {
|
||||||
|
combinedPermissions = combinedPermissions?.copy(
|
||||||
|
location = geckoLocation.value.toStatus(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geckoMedia != null && geckoMedia.value != VALUE_PROMPT) {
|
||||||
|
combinedPermissions = combinedPermissions?.copy(
|
||||||
|
mediaKeySystemAccess = geckoMedia.value.toStatus(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geckoStorage != null && geckoStorage.value != VALUE_PROMPT) {
|
||||||
|
combinedPermissions = combinedPermissions?.copy(
|
||||||
|
localStorage = geckoStorage.value.toStatus(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geckoCrossOriginStorageAccess != null && geckoCrossOriginStorageAccess.value != VALUE_PROMPT) {
|
||||||
|
combinedPermissions = combinedPermissions?.copy(
|
||||||
|
crossOriginStorageAccess = geckoCrossOriginStorageAccess.value.toStatus(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Autoplay permissions don't have initial values, so when the value is changed on
|
||||||
|
* the gecko storage we trust it.
|
||||||
|
*/
|
||||||
|
if (geckoAudible != null && geckoAudible.value != VALUE_PROMPT) {
|
||||||
|
combinedPermissions = combinedPermissions?.copy(
|
||||||
|
autoplayAudible = geckoAudible.value.toAutoPlayStatus(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geckoInAudible != null && geckoInAudible.value != VALUE_PROMPT) {
|
||||||
|
combinedPermissions = combinedPermissions?.copy(
|
||||||
|
autoplayInaudible = geckoInAudible.value.toAutoPlayStatus(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return combinedPermissions
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal suspend fun findGeckoContentPermissionBy(
|
||||||
|
origin: String,
|
||||||
|
includeTemporary: Boolean = false,
|
||||||
|
private: Boolean,
|
||||||
|
): List<ContentPermission>? {
|
||||||
|
return withContext(mainScope.coroutineContext) {
|
||||||
|
val geckoPermissions = geckoStorage.getPermissions(origin, private).await()
|
||||||
|
if (includeTemporary) {
|
||||||
|
geckoPermissions
|
||||||
|
} else {
|
||||||
|
geckoPermissions.filterNotTemporaryPermissions(geckoTemporaryPermissions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal suspend fun clearGeckoCacheFor(origin: String) {
|
||||||
|
withContext(mainScope.coroutineContext) {
|
||||||
|
geckoStorage.clearDataFromHost(origin, ClearFlags.PERMISSIONS).await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun clearAllPermissionsGeckoCache() {
|
||||||
|
geckoStorage.clearData(ClearFlags.PERMISSIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun removeTemporaryPermissionIfAny(permission: ContentPermission) {
|
||||||
|
if (geckoTemporaryPermissions.any { permission.areSame(it) }) {
|
||||||
|
geckoTemporaryPermissions.removeAll { permission.areSame(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal suspend fun removeGeckoContentPermissionBy(origin: String, private: Boolean) {
|
||||||
|
findGeckoContentPermissionBy(
|
||||||
|
origin = origin,
|
||||||
|
private = private,
|
||||||
|
)?.forEach { geckoPermissions ->
|
||||||
|
removeGeckoContentPermission(geckoPermissions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun removeGeckoContentPermission(geckoPermissions: ContentPermission) {
|
||||||
|
val value = if (geckoPermissions.permission != PERMISSION_TRACKING) {
|
||||||
|
VALUE_PROMPT
|
||||||
|
} else {
|
||||||
|
VALUE_DENY
|
||||||
|
}
|
||||||
|
geckoStorage.setPermission(geckoPermissions, value)
|
||||||
|
removeTemporaryPermissionIfAny(geckoPermissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal suspend fun removeGeckoAllContentPermissions() {
|
||||||
|
findAllGeckoContentPermissions()?.forEach { geckoPermissions ->
|
||||||
|
removeGeckoContentPermission(geckoPermissions)
|
||||||
|
}
|
||||||
|
clearAllPermissionsGeckoCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun PermissionRequest?.extractGeckoPermissionsOrQueryTheStore(
|
||||||
|
origin: String,
|
||||||
|
private: Boolean,
|
||||||
|
): Map<Int, List<ContentPermission>> {
|
||||||
|
return if (this is GeckoPermissionRequest.Content) {
|
||||||
|
mapOf(geckoPermission.permission to listOf(geckoPermission))
|
||||||
|
} else {
|
||||||
|
findGeckoContentPermissionBy(origin, includeTemporary = true, private).groupByType()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun List<ContentPermission>?.groupByDomain(): Map<String, List<ContentPermission>> {
|
||||||
|
return this?.groupBy {
|
||||||
|
it.uri.tryGetHostFromUrl()
|
||||||
|
}.orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun List<ContentPermission>?.groupByType(): Map<Int, List<ContentPermission>> {
|
||||||
|
return this?.groupBy { it.permission }.orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun List<ContentPermission>?.filterNotTemporaryPermissions(
|
||||||
|
temporaryPermissions: List<ContentPermission>,
|
||||||
|
): List<ContentPermission>? {
|
||||||
|
return this?.filterNot { geckoPermission ->
|
||||||
|
temporaryPermissions.any { geckoPermission.areSame(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun ContentPermission.areSame(other: ContentPermission) =
|
||||||
|
other.uri.tryGetHostFromUrl() == this.uri.tryGetHostFromUrl() &&
|
||||||
|
other.permission == this.permission && other.privateMode == this.privateMode
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun Int.toStatus(): Status {
|
||||||
|
return when (this) {
|
||||||
|
VALUE_PROMPT -> NO_DECISION
|
||||||
|
VALUE_DENY -> BLOCKED
|
||||||
|
VALUE_ALLOW -> ALLOWED
|
||||||
|
else -> BLOCKED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun Int.toAutoPlayStatus(): AutoplayStatus {
|
||||||
|
return when (this) {
|
||||||
|
VALUE_PROMPT, VALUE_DENY -> AutoplayStatus.BLOCKED
|
||||||
|
VALUE_ALLOW -> AutoplayStatus.ALLOWED
|
||||||
|
else -> AutoplayStatus.BLOCKED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun Status.toGeckoStatus(): Int {
|
||||||
|
return when (this) {
|
||||||
|
NO_DECISION -> VALUE_PROMPT
|
||||||
|
BLOCKED -> VALUE_DENY
|
||||||
|
ALLOWED -> VALUE_ALLOW
|
||||||
|
else -> VALUE_ALLOW
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun AutoplayStatus.toGeckoStatus(): Int {
|
||||||
|
return when (this) {
|
||||||
|
AutoplayStatus.BLOCKED -> VALUE_DENY
|
||||||
|
AutoplayStatus.ALLOWED -> VALUE_ALLOW
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.profiler
|
||||||
|
|
||||||
|
import mozilla.components.concept.base.profiler.Profiler
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
import org.mozilla.geckoview.GeckoRuntime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gecko-based implementation of [Profiler], wrapping the
|
||||||
|
* ProfilerController object provided by GeckoView.
|
||||||
|
*/
|
||||||
|
class Profiler(
|
||||||
|
private val runtime: GeckoRuntime,
|
||||||
|
) : Profiler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [Profiler.isProfilerActive].
|
||||||
|
*/
|
||||||
|
override fun isProfilerActive(): Boolean {
|
||||||
|
return runtime.profilerController.isProfilerActive
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [Profiler.getProfilerTime].
|
||||||
|
*/
|
||||||
|
override fun getProfilerTime(): Double? {
|
||||||
|
return runtime.profilerController.profilerTime
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [Profiler.addMarker].
|
||||||
|
*/
|
||||||
|
override fun addMarker(markerName: String, startTime: Double?, endTime: Double?, text: String?) {
|
||||||
|
runtime.profilerController.addMarker(markerName, startTime, endTime, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [Profiler.addMarker].
|
||||||
|
*/
|
||||||
|
override fun addMarker(markerName: String, startTime: Double?, text: String?) {
|
||||||
|
runtime.profilerController.addMarker(markerName, startTime, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [Profiler.addMarker].
|
||||||
|
*/
|
||||||
|
override fun addMarker(markerName: String, startTime: Double?) {
|
||||||
|
runtime.profilerController.addMarker(markerName, startTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [Profiler.addMarker].
|
||||||
|
*/
|
||||||
|
override fun addMarker(markerName: String, text: String?) {
|
||||||
|
runtime.profilerController.addMarker(markerName, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [Profiler.addMarker].
|
||||||
|
*/
|
||||||
|
override fun addMarker(markerName: String) {
|
||||||
|
runtime.profilerController.addMarker(markerName)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startProfiler(filters: Array<String>, features: Array<String>) {
|
||||||
|
runtime.profilerController.startProfiler(filters, features)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stopProfiler(onSuccess: (ByteArray?) -> Unit, onError: (Throwable) -> Unit) {
|
||||||
|
runtime.profilerController.stopProfiler().then(
|
||||||
|
{ profileResult ->
|
||||||
|
onSuccess(profileResult)
|
||||||
|
GeckoResult<Void>()
|
||||||
|
},
|
||||||
|
{ throwable ->
|
||||||
|
onError(throwable)
|
||||||
|
GeckoResult<Void>()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.prompt
|
||||||
|
|
||||||
|
import mozilla.components.browser.engine.gecko.GeckoEngineSession
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.convertToChoices
|
||||||
|
import mozilla.components.concept.engine.prompt.PromptRequest
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.BasePrompt
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.ChoicePrompt
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.PromptInstanceDelegate
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of [PromptInstanceDelegate] used to update a
|
||||||
|
* prompt request when onPromptUpdate is invoked.
|
||||||
|
*
|
||||||
|
* @param geckoSession [GeckoEngineSession] used to notify the engine observer
|
||||||
|
* with the onPromptUpdate callback.
|
||||||
|
* @param previousPrompt [PromptRequest] to be updated.
|
||||||
|
*/
|
||||||
|
internal class ChoicePromptDelegate(
|
||||||
|
private val geckoSession: GeckoEngineSession,
|
||||||
|
private var previousPrompt: PromptRequest,
|
||||||
|
) : PromptInstanceDelegate {
|
||||||
|
|
||||||
|
override fun onPromptDismiss(prompt: BasePrompt) {
|
||||||
|
geckoSession.notifyObservers {
|
||||||
|
onPromptDismissed(previousPrompt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPromptUpdate(prompt: BasePrompt) {
|
||||||
|
if (prompt is ChoicePrompt) {
|
||||||
|
val promptRequest = updatePromptChoices(prompt)
|
||||||
|
if (promptRequest != null) {
|
||||||
|
geckoSession.notifyObservers {
|
||||||
|
this.onPromptUpdate(previousPrompt.uid, promptRequest)
|
||||||
|
}
|
||||||
|
previousPrompt = promptRequest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the received prompt to create the updated [PromptRequest]
|
||||||
|
* @param updatedPrompt The [ChoicePrompt] with the updated choices.
|
||||||
|
*/
|
||||||
|
private fun updatePromptChoices(updatedPrompt: ChoicePrompt): PromptRequest? {
|
||||||
|
return when (previousPrompt) {
|
||||||
|
is PromptRequest.MenuChoice -> {
|
||||||
|
(previousPrompt as PromptRequest.MenuChoice)
|
||||||
|
.copy(choices = convertToChoices(updatedPrompt.choices))
|
||||||
|
}
|
||||||
|
is PromptRequest.SingleChoice -> {
|
||||||
|
(previousPrompt as PromptRequest.SingleChoice)
|
||||||
|
.copy(choices = convertToChoices(updatedPrompt.choices))
|
||||||
|
}
|
||||||
|
is PromptRequest.MultipleChoice -> {
|
||||||
|
(previousPrompt as PromptRequest.MultipleChoice)
|
||||||
|
.copy(choices = convertToChoices(updatedPrompt.choices))
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,914 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.prompt
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import mozilla.components.browser.engine.gecko.GeckoEngineSession
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.convertToChoices
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.toAddress
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.toAutocompleteAddress
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.toAutocompleteCreditCard
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.toCreditCardEntry
|
||||||
|
import mozilla.components.browser.engine.gecko.ext.toLoginEntry
|
||||||
|
import mozilla.components.concept.engine.prompt.Choice
|
||||||
|
import mozilla.components.concept.engine.prompt.PromptRequest
|
||||||
|
import mozilla.components.concept.engine.prompt.PromptRequest.File.Companion.DEFAULT_UPLOADS_DIR_NAME
|
||||||
|
import mozilla.components.concept.engine.prompt.PromptRequest.MenuChoice
|
||||||
|
import mozilla.components.concept.engine.prompt.PromptRequest.MultipleChoice
|
||||||
|
import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice
|
||||||
|
import mozilla.components.concept.engine.prompt.ShareData
|
||||||
|
import mozilla.components.concept.identitycredential.Account
|
||||||
|
import mozilla.components.concept.identitycredential.Provider
|
||||||
|
import mozilla.components.concept.storage.Address
|
||||||
|
import mozilla.components.concept.storage.CreditCardEntry
|
||||||
|
import mozilla.components.concept.storage.Login
|
||||||
|
import mozilla.components.concept.storage.LoginEntry
|
||||||
|
import mozilla.components.support.ktx.android.net.toFileUri
|
||||||
|
import mozilla.components.support.ktx.kotlin.toDate
|
||||||
|
import mozilla.components.support.utils.TimePicker.shouldShowMillisecondsPicker
|
||||||
|
import mozilla.components.support.utils.TimePicker.shouldShowSecondsPicker
|
||||||
|
import org.mozilla.geckoview.AllowOrDeny
|
||||||
|
import org.mozilla.geckoview.Autocomplete
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
import org.mozilla.geckoview.GeckoSession
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.AutocompleteRequest
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.BeforeUnloadPrompt
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.DATE
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.DATETIME_LOCAL
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.MONTH
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.TIME
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.WEEK
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.IdentityCredential.AccountSelectorPrompt
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.IdentityCredential.PrivacyPolicyPrompt
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.IdentityCredential.ProviderSelectorPrompt
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.PromptResponse
|
||||||
|
import java.security.InvalidParameterException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
typealias GeckoAuthOptions = PromptDelegate.AuthPrompt.AuthOptions
|
||||||
|
typealias GeckoChoice = PromptDelegate.ChoicePrompt.Choice
|
||||||
|
typealias GECKO_AUTH_FLAGS = PromptDelegate.AuthPrompt.AuthOptions.Flags
|
||||||
|
typealias GECKO_AUTH_LEVEL = PromptDelegate.AuthPrompt.AuthOptions.Level
|
||||||
|
typealias GECKO_PROMPT_FILE_TYPE = PromptDelegate.FilePrompt.Type
|
||||||
|
typealias GECKO_PROMPT_PROVIDER_SELECTOR = ProviderSelectorPrompt.Provider
|
||||||
|
typealias GECKO_PROMPT_ACCOUNT_SELECTOR = AccountSelectorPrompt.Account
|
||||||
|
typealias GECKO_PROMPT_ACCOUNT_SELECTOR_PROVIDER = AccountSelectorPrompt.Provider
|
||||||
|
typealias GECKO_PROMPT_CHOICE_TYPE = PromptDelegate.ChoicePrompt.Type
|
||||||
|
typealias GECKO_PROMPT_FILE_CAPTURE = PromptDelegate.FilePrompt.Capture
|
||||||
|
typealias GECKO_PROMPT_SHARE_RESULT = PromptDelegate.SharePrompt.Result
|
||||||
|
typealias AC_AUTH_LEVEL = PromptRequest.Authentication.Level
|
||||||
|
typealias AC_AUTH_METHOD = PromptRequest.Authentication.Method
|
||||||
|
typealias AC_FILE_FACING_MODE = PromptRequest.File.FacingMode
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gecko-based PromptDelegate implementation.
|
||||||
|
*/
|
||||||
|
@Suppress("LargeClass")
|
||||||
|
internal class GeckoPromptDelegate(private val geckoEngineSession: GeckoEngineSession) :
|
||||||
|
PromptDelegate {
|
||||||
|
override fun onSelectIdentityCredentialProvider(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: ProviderSelectorPrompt,
|
||||||
|
): GeckoResult<PromptResponse> {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
|
||||||
|
val onConfirm: (Provider) -> Unit = { provider ->
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(
|
||||||
|
prompt.confirm(
|
||||||
|
provider.id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onDismiss: () -> Unit = {
|
||||||
|
prompt.dismissSafely(geckoResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.IdentityCredential.SelectProvider(
|
||||||
|
providers = prompt.providers.map { it.toProvider() },
|
||||||
|
onConfirm = onConfirm,
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSelectIdentityCredentialAccount(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: AccountSelectorPrompt,
|
||||||
|
): GeckoResult<PromptResponse> {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
|
||||||
|
val onConfirm: (Account) -> Unit = { account ->
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(
|
||||||
|
prompt.confirm(
|
||||||
|
account.id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onDismiss: () -> Unit = {
|
||||||
|
prompt.dismissSafely(geckoResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.IdentityCredential.SelectAccount(
|
||||||
|
accounts = prompt.accounts.map { it.toAccount() },
|
||||||
|
provider = prompt.provider.let { it.toProvider() },
|
||||||
|
onConfirm = onConfirm,
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShowPrivacyPolicyIdentityCredential(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: PrivacyPolicyPrompt,
|
||||||
|
): GeckoResult<PromptResponse> {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
|
||||||
|
val onConfirm: (Boolean) -> Unit = { confirmed ->
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(
|
||||||
|
prompt.confirm(confirmed),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onDismiss: () -> Unit = {
|
||||||
|
prompt.dismissSafely(geckoResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.IdentityCredential.PrivacyPolicy(
|
||||||
|
privacyPolicyUrl = prompt.privacyPolicyUrl,
|
||||||
|
termsOfServiceUrl = prompt.termsOfServiceUrl,
|
||||||
|
providerDomain = prompt.providerDomain,
|
||||||
|
host = prompt.host,
|
||||||
|
icon = prompt.icon,
|
||||||
|
onConfirm = onConfirm,
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreditCardSave(
|
||||||
|
session: GeckoSession,
|
||||||
|
request: AutocompleteRequest<Autocomplete.CreditCardSaveOption>,
|
||||||
|
): GeckoResult<PromptResponse> {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
|
||||||
|
val onConfirm: (CreditCardEntry) -> Unit = { creditCard ->
|
||||||
|
if (!request.isComplete) {
|
||||||
|
geckoResult.complete(
|
||||||
|
request.confirm(
|
||||||
|
Autocomplete.CreditCardSelectOption(creditCard.toAutocompleteCreditCard()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onDismiss: () -> Unit = {
|
||||||
|
request.dismissSafely(geckoResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.SaveCreditCard(
|
||||||
|
creditCard = request.options[0].value.toCreditCardEntry(),
|
||||||
|
onConfirm = onConfirm,
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
).also {
|
||||||
|
request.delegate = PromptInstanceDismissDelegate(
|
||||||
|
geckoEngineSession,
|
||||||
|
it,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a credit card selection prompt request. This is triggered by the user
|
||||||
|
* focusing on a credit card input field.
|
||||||
|
*
|
||||||
|
* @param session The [GeckoSession] that triggered the request.
|
||||||
|
* @param request The [AutocompleteRequest] containing the credit card selection request.
|
||||||
|
*/
|
||||||
|
override fun onCreditCardSelect(
|
||||||
|
session: GeckoSession,
|
||||||
|
request: AutocompleteRequest<Autocomplete.CreditCardSelectOption>,
|
||||||
|
): GeckoResult<PromptResponse>? {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
|
||||||
|
val onConfirm: (CreditCardEntry) -> Unit = { creditCard ->
|
||||||
|
if (!request.isComplete) {
|
||||||
|
geckoResult.complete(
|
||||||
|
request.confirm(
|
||||||
|
Autocomplete.CreditCardSelectOption(creditCard.toAutocompleteCreditCard()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onDismiss: () -> Unit = {
|
||||||
|
request.dismissSafely(geckoResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.SelectCreditCard(
|
||||||
|
creditCards = request.options.map { it.value.toCreditCardEntry() },
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
onConfirm = onConfirm,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoginSave(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: AutocompleteRequest<Autocomplete.LoginSaveOption>,
|
||||||
|
): GeckoResult<PromptResponse>? {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
val onConfirmSave: (LoginEntry) -> Unit = { entry ->
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(Autocomplete.LoginSelectOption(entry.toLoginEntry())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val onDismiss: () -> Unit = {
|
||||||
|
prompt.dismissSafely(geckoResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.SaveLoginPrompt(
|
||||||
|
hint = prompt.options[0].hint,
|
||||||
|
logins = prompt.options.map { it.value.toLoginEntry() },
|
||||||
|
onConfirm = onConfirmSave,
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
).also {
|
||||||
|
prompt.delegate = PromptInstanceDismissDelegate(
|
||||||
|
geckoEngineSession,
|
||||||
|
it,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
override fun onLoginSelect(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: AutocompleteRequest<Autocomplete.LoginSelectOption>,
|
||||||
|
): GeckoResult<PromptResponse>? {
|
||||||
|
val promptOptions = prompt.options
|
||||||
|
|
||||||
|
val generatedPassword =
|
||||||
|
promptOptions.firstOrNull { option -> option.hint == Autocomplete.SelectOption.Hint.GENERATED }?.value?.password
|
||||||
|
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
val onConfirmSelect: (Login) -> Unit = { login ->
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
var hint = Autocomplete.SelectOption.Hint.NONE
|
||||||
|
if (generatedPassword != null && login.password == generatedPassword) {
|
||||||
|
hint = Autocomplete.SelectOption.Hint.GENERATED
|
||||||
|
}
|
||||||
|
geckoResult.complete(prompt.confirm(Autocomplete.LoginSelectOption(login.toLoginEntry(), hint)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val onDismiss: () -> Unit = {
|
||||||
|
prompt.dismissSafely(geckoResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
// `guid` plus exactly one of `httpRealm` and `formSubmitURL` must be present to be a valid login entry.
|
||||||
|
val loginList = promptOptions.filter { option ->
|
||||||
|
option.value.guid != null && (option.value.formActionOrigin != null || option.value.httpRealm != null)
|
||||||
|
}.map { option ->
|
||||||
|
Login(
|
||||||
|
guid = option.value.guid!!,
|
||||||
|
origin = option.value.origin,
|
||||||
|
formActionOrigin = option.value.formActionOrigin,
|
||||||
|
httpRealm = option.value.httpRealm,
|
||||||
|
username = option.value.username,
|
||||||
|
password = option.value.password,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.SelectLoginPrompt(
|
||||||
|
logins = loginList,
|
||||||
|
generatedPassword = generatedPassword,
|
||||||
|
onConfirm = onConfirmSelect,
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onChoicePrompt(
|
||||||
|
session: GeckoSession,
|
||||||
|
geckoPrompt: PromptDelegate.ChoicePrompt,
|
||||||
|
): GeckoResult<PromptResponse>? {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
val choices = convertToChoices(geckoPrompt.choices)
|
||||||
|
|
||||||
|
val onDismiss: () -> Unit = {
|
||||||
|
geckoPrompt.dismissSafely(geckoResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
val onConfirmSingleChoice: (Choice) -> Unit = { selectedChoice ->
|
||||||
|
if (!geckoPrompt.isComplete) {
|
||||||
|
geckoResult.complete(geckoPrompt.confirm(selectedChoice.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val onConfirmMultipleSelection: (Array<Choice>) -> Unit = { selectedChoices ->
|
||||||
|
if (!geckoPrompt.isComplete) {
|
||||||
|
val ids = selectedChoices.toIdsArray()
|
||||||
|
geckoResult.complete(geckoPrompt.confirm(ids))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val promptRequest = when (geckoPrompt.type) {
|
||||||
|
GECKO_PROMPT_CHOICE_TYPE.SINGLE -> SingleChoice(
|
||||||
|
choices,
|
||||||
|
onConfirmSingleChoice,
|
||||||
|
onDismiss,
|
||||||
|
)
|
||||||
|
GECKO_PROMPT_CHOICE_TYPE.MENU -> MenuChoice(
|
||||||
|
choices,
|
||||||
|
onConfirmSingleChoice,
|
||||||
|
onDismiss,
|
||||||
|
)
|
||||||
|
GECKO_PROMPT_CHOICE_TYPE.MULTIPLE -> MultipleChoice(
|
||||||
|
choices,
|
||||||
|
onConfirmMultipleSelection,
|
||||||
|
onDismiss,
|
||||||
|
)
|
||||||
|
else -> throw InvalidParameterException("${geckoPrompt.type} is not a valid Gecko @Choice.ChoiceType")
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoPrompt.delegate = ChoicePromptDelegate(
|
||||||
|
geckoEngineSession,
|
||||||
|
promptRequest,
|
||||||
|
)
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(promptRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAddressSelect(
|
||||||
|
session: GeckoSession,
|
||||||
|
request: AutocompleteRequest<Autocomplete.AddressSelectOption>,
|
||||||
|
): GeckoResult<PromptResponse> {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
|
||||||
|
val onConfirm: (Address) -> Unit = { address ->
|
||||||
|
if (!request.isComplete) {
|
||||||
|
geckoResult.complete(
|
||||||
|
request.confirm(
|
||||||
|
Autocomplete.AddressSelectOption(address.toAutocompleteAddress()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onDismiss: () -> Unit = {
|
||||||
|
request.dismissSafely(geckoResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.SelectAddress(
|
||||||
|
addresses = request.options.map { it.value.toAddress() },
|
||||||
|
onConfirm = onConfirm,
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAlertPrompt(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: PromptDelegate.AlertPrompt,
|
||||||
|
): GeckoResult<PromptResponse> {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
val onDismiss: () -> Unit = { prompt.dismissSafely(geckoResult) }
|
||||||
|
val onConfirm: (Boolean) -> Unit = { _ -> onDismiss() }
|
||||||
|
val title = prompt.title ?: ""
|
||||||
|
val message = prompt.message ?: ""
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.Alert(
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
false,
|
||||||
|
onConfirm,
|
||||||
|
onDismiss,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFilePrompt(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: PromptDelegate.FilePrompt,
|
||||||
|
): GeckoResult<PromptResponse>? {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
val isMultipleFilesSelection = prompt.type == GECKO_PROMPT_FILE_TYPE.MULTIPLE
|
||||||
|
|
||||||
|
val captureMode = when (prompt.capture) {
|
||||||
|
GECKO_PROMPT_FILE_CAPTURE.ANY -> AC_FILE_FACING_MODE.ANY
|
||||||
|
GECKO_PROMPT_FILE_CAPTURE.USER -> AC_FILE_FACING_MODE.FRONT_CAMERA
|
||||||
|
GECKO_PROMPT_FILE_CAPTURE.ENVIRONMENT -> AC_FILE_FACING_MODE.BACK_CAMERA
|
||||||
|
else -> AC_FILE_FACING_MODE.NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
val onSelectMultiple: (Context, Array<Uri>) -> Unit = { context, uris ->
|
||||||
|
val filesUris = uris.map {
|
||||||
|
toFileUri(uri = it, context)
|
||||||
|
}.toTypedArray()
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(context, filesUris))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onSelectSingle: (Context, Uri) -> Unit = { context, uri ->
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(context, toFileUri(uri, context)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onDismiss: () -> Unit = {
|
||||||
|
prompt.dismissSafely(geckoResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.File(
|
||||||
|
prompt.mimeTypes ?: emptyArray(),
|
||||||
|
isMultipleFilesSelection,
|
||||||
|
captureMode,
|
||||||
|
onSelectSingle,
|
||||||
|
onSelectMultiple,
|
||||||
|
onDismiss,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("ComplexMethod")
|
||||||
|
override fun onDateTimePrompt(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: PromptDelegate.DateTimePrompt,
|
||||||
|
): GeckoResult<PromptResponse>? {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
val onConfirm: (String) -> Unit = {
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onDismiss: () -> Unit = {
|
||||||
|
prompt.dismissSafely(geckoResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
val onClear: () -> Unit = {
|
||||||
|
onConfirm("")
|
||||||
|
}
|
||||||
|
val initialDateString = prompt.defaultValue ?: ""
|
||||||
|
val stepValue = with(prompt.stepValue) {
|
||||||
|
if (this?.toDoubleOrNull() == null) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val format = when (prompt.type) {
|
||||||
|
DATE -> "yyyy-MM-dd"
|
||||||
|
MONTH -> "yyyy-MM"
|
||||||
|
WEEK -> "yyyy-'W'ww"
|
||||||
|
TIME -> {
|
||||||
|
if (shouldShowMillisecondsPicker(stepValue?.toFloat())) {
|
||||||
|
"HH:mm:ss.SSS"
|
||||||
|
} else if (shouldShowSecondsPicker(stepValue?.toFloat())) {
|
||||||
|
"HH:mm:ss"
|
||||||
|
} else {
|
||||||
|
"HH:mm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DATETIME_LOCAL -> "yyyy-MM-dd'T'HH:mm"
|
||||||
|
else -> {
|
||||||
|
throw InvalidParameterException("${prompt.type} is not a valid DatetimeType")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyDatePromptRequest(
|
||||||
|
prompt.title ?: "",
|
||||||
|
initialDateString,
|
||||||
|
prompt.minValue,
|
||||||
|
prompt.maxValue,
|
||||||
|
stepValue,
|
||||||
|
onClear,
|
||||||
|
format,
|
||||||
|
onConfirm,
|
||||||
|
onDismiss,
|
||||||
|
)
|
||||||
|
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthPrompt(
|
||||||
|
session: GeckoSession,
|
||||||
|
geckoPrompt: PromptDelegate.AuthPrompt,
|
||||||
|
): GeckoResult<PromptResponse>? {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
val title = geckoPrompt.title ?: ""
|
||||||
|
val message = geckoPrompt.message ?: ""
|
||||||
|
val uri = geckoPrompt.authOptions.uri
|
||||||
|
val flags = geckoPrompt.authOptions.flags
|
||||||
|
val userName = geckoPrompt.authOptions.username ?: ""
|
||||||
|
val password = geckoPrompt.authOptions.password ?: ""
|
||||||
|
val method =
|
||||||
|
if (flags in GECKO_AUTH_FLAGS.HOST) AC_AUTH_METHOD.HOST else AC_AUTH_METHOD.PROXY
|
||||||
|
val level = geckoPrompt.authOptions.toACLevel()
|
||||||
|
val onlyShowPassword = flags in GECKO_AUTH_FLAGS.ONLY_PASSWORD
|
||||||
|
val previousFailed = flags in GECKO_AUTH_FLAGS.PREVIOUS_FAILED
|
||||||
|
val isCrossOrigin = flags in GECKO_AUTH_FLAGS.CROSS_ORIGIN_SUB_RESOURCE
|
||||||
|
|
||||||
|
val onConfirm: (String, String) -> Unit =
|
||||||
|
{ user, pass ->
|
||||||
|
if (!geckoPrompt.isComplete) {
|
||||||
|
if (onlyShowPassword) {
|
||||||
|
geckoResult.complete(geckoPrompt.confirm(pass))
|
||||||
|
} else {
|
||||||
|
geckoResult.complete(geckoPrompt.confirm(user, pass))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onDismiss: () -> Unit = { geckoPrompt.dismissSafely(geckoResult) }
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.Authentication(
|
||||||
|
uri,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
userName,
|
||||||
|
password,
|
||||||
|
method,
|
||||||
|
level,
|
||||||
|
onlyShowPassword,
|
||||||
|
previousFailed,
|
||||||
|
isCrossOrigin,
|
||||||
|
onConfirm,
|
||||||
|
onDismiss,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTextPrompt(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: PromptDelegate.TextPrompt,
|
||||||
|
): GeckoResult<PromptResponse>? {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
val title = prompt.title ?: ""
|
||||||
|
val inputLabel = prompt.message ?: ""
|
||||||
|
val inputValue = prompt.defaultValue ?: ""
|
||||||
|
val onDismiss: () -> Unit = { prompt.dismissSafely(geckoResult) }
|
||||||
|
val onConfirm: (Boolean, String) -> Unit = { _, valueInput ->
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(valueInput))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.TextPrompt(
|
||||||
|
title,
|
||||||
|
inputLabel,
|
||||||
|
inputValue,
|
||||||
|
false,
|
||||||
|
onConfirm,
|
||||||
|
onDismiss,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onColorPrompt(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: PromptDelegate.ColorPrompt,
|
||||||
|
): GeckoResult<PromptResponse>? {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
val onConfirm: (String) -> Unit = {
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val onDismiss: () -> Unit = { prompt.dismissSafely(geckoResult) }
|
||||||
|
|
||||||
|
val defaultColor = prompt.defaultValue ?: ""
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.Color(defaultColor, onConfirm, onDismiss),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPopupPrompt(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: PromptDelegate.PopupPrompt,
|
||||||
|
): GeckoResult<PromptResponse> {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
val onAllow: () -> Unit = {
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(AllowOrDeny.ALLOW))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val onDeny: () -> Unit = {
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(AllowOrDeny.DENY))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.Popup(prompt.targetUri ?: "", onAllow, onDeny),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBeforeUnloadPrompt(
|
||||||
|
session: GeckoSession,
|
||||||
|
geckoPrompt: BeforeUnloadPrompt,
|
||||||
|
): GeckoResult<PromptResponse>? {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
val title = geckoPrompt.title ?: ""
|
||||||
|
val onAllow: () -> Unit = {
|
||||||
|
if (!geckoPrompt.isComplete) {
|
||||||
|
geckoResult.complete(geckoPrompt.confirm(AllowOrDeny.ALLOW))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val onDeny: () -> Unit = {
|
||||||
|
if (!geckoPrompt.isComplete) {
|
||||||
|
geckoResult.complete(geckoPrompt.confirm(AllowOrDeny.DENY))
|
||||||
|
geckoEngineSession.notifyObservers { onBeforeUnloadPromptDenied() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(PromptRequest.BeforeUnload(title, onAllow, onDeny))
|
||||||
|
}
|
||||||
|
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSharePrompt(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: PromptDelegate.SharePrompt,
|
||||||
|
): GeckoResult<PromptResponse> {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
val onSuccess = {
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(GECKO_PROMPT_SHARE_RESULT.SUCCESS))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val onFailure = {
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(GECKO_PROMPT_SHARE_RESULT.FAILURE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val onDismiss = { prompt.dismissSafely(geckoResult) }
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.Share(
|
||||||
|
ShareData(
|
||||||
|
title = prompt.title,
|
||||||
|
text = prompt.text,
|
||||||
|
url = prompt.uri,
|
||||||
|
),
|
||||||
|
onSuccess,
|
||||||
|
onFailure,
|
||||||
|
onDismiss,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onButtonPrompt(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: PromptDelegate.ButtonPrompt,
|
||||||
|
): GeckoResult<PromptResponse>? {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
val title = prompt.title ?: ""
|
||||||
|
val message = prompt.message ?: ""
|
||||||
|
|
||||||
|
val onConfirmPositiveButton: (Boolean) -> Unit = {
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(PromptDelegate.ButtonPrompt.Type.POSITIVE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val onConfirmNegativeButton: (Boolean) -> Unit = {
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(PromptDelegate.ButtonPrompt.Type.NEGATIVE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onDismiss: (Boolean) -> Unit = { prompt.dismissSafely(geckoResult) }
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.Confirm(
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
onConfirmPositiveButton,
|
||||||
|
onConfirmNegativeButton,
|
||||||
|
onDismiss,
|
||||||
|
) {
|
||||||
|
onDismiss(false)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRepostConfirmPrompt(
|
||||||
|
session: GeckoSession,
|
||||||
|
prompt: PromptDelegate.RepostConfirmPrompt,
|
||||||
|
): GeckoResult<PromptResponse>? {
|
||||||
|
val geckoResult = GeckoResult<PromptResponse>()
|
||||||
|
|
||||||
|
val onConfirm: () -> Unit = {
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(AllowOrDeny.ALLOW))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val onCancel: () -> Unit = {
|
||||||
|
if (!prompt.isComplete) {
|
||||||
|
geckoResult.complete(prompt.confirm(AllowOrDeny.DENY))
|
||||||
|
geckoEngineSession.notifyObservers { onRepostPromptCancelled() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.Repost(
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return geckoResult
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
private fun notifyDatePromptRequest(
|
||||||
|
title: String,
|
||||||
|
initialDateString: String,
|
||||||
|
minDateString: String?,
|
||||||
|
maxDateString: String?,
|
||||||
|
stepValue: String?,
|
||||||
|
onClear: () -> Unit,
|
||||||
|
format: String,
|
||||||
|
onConfirm: (String) -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
val initialDate = initialDateString.toDate(format)
|
||||||
|
val minDate = if (minDateString.isNullOrEmpty()) null else minDateString.toDate()
|
||||||
|
val maxDate = if (maxDateString.isNullOrEmpty()) null else maxDateString.toDate()
|
||||||
|
val onSelect: (Date) -> Unit = {
|
||||||
|
val stringDate = it.toString(format)
|
||||||
|
onConfirm(stringDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
val selectionType = when (format) {
|
||||||
|
"HH:mm", "HH:mm:ss", "HH:mm:ss.SSS" -> PromptRequest.TimeSelection.Type.TIME
|
||||||
|
"yyyy-MM" -> PromptRequest.TimeSelection.Type.MONTH
|
||||||
|
"yyyy-MM-dd'T'HH:mm" -> PromptRequest.TimeSelection.Type.DATE_AND_TIME
|
||||||
|
else -> PromptRequest.TimeSelection.Type.DATE
|
||||||
|
}
|
||||||
|
|
||||||
|
geckoEngineSession.notifyObservers {
|
||||||
|
onPromptRequest(
|
||||||
|
PromptRequest.TimeSelection(
|
||||||
|
title,
|
||||||
|
initialDate,
|
||||||
|
minDate,
|
||||||
|
maxDate,
|
||||||
|
stepValue,
|
||||||
|
selectionType,
|
||||||
|
onSelect,
|
||||||
|
onClear,
|
||||||
|
onDismiss,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun GeckoAuthOptions.toACLevel(): AC_AUTH_LEVEL {
|
||||||
|
return when (level) {
|
||||||
|
GECKO_AUTH_LEVEL.NONE -> AC_AUTH_LEVEL.NONE
|
||||||
|
GECKO_AUTH_LEVEL.PW_ENCRYPTED -> AC_AUTH_LEVEL.PASSWORD_ENCRYPTED
|
||||||
|
GECKO_AUTH_LEVEL.SECURE -> AC_AUTH_LEVEL.SECURED
|
||||||
|
else -> {
|
||||||
|
AC_AUTH_LEVEL.NONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private operator fun Int.contains(mask: Int): Boolean {
|
||||||
|
return (this and mask) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun toFileUri(uri: Uri, context: Context): Uri {
|
||||||
|
return uri.toFileUri(context, dirToCopy = DEFAULT_UPLOADS_DIR_NAME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal fun Array<Choice>.toIdsArray(): Array<String> {
|
||||||
|
return this.map { it.id }.toTypedArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal fun Date.toString(format: String): String {
|
||||||
|
val formatter = SimpleDateFormat(format, Locale.ROOT)
|
||||||
|
return formatter.format(this) ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only dismiss if the prompt is not already dismissed.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal fun PromptDelegate.BasePrompt.dismissSafely(geckoResult: GeckoResult<PromptResponse>) {
|
||||||
|
if (!this.isComplete) {
|
||||||
|
geckoResult.complete(dismiss())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal fun GECKO_PROMPT_PROVIDER_SELECTOR.toProvider(): Provider {
|
||||||
|
return Provider(id, icon, name, domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal fun GECKO_PROMPT_ACCOUNT_SELECTOR.toAccount(): Account {
|
||||||
|
return Account(id, email, name, icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal fun GECKO_PROMPT_ACCOUNT_SELECTOR_PROVIDER.toProvider(): Provider {
|
||||||
|
return Provider(0, icon, name, domain)
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.prompt
|
||||||
|
|
||||||
|
import mozilla.components.browser.engine.gecko.GeckoEngineSession
|
||||||
|
import mozilla.components.concept.engine.prompt.PromptRequest
|
||||||
|
import org.mozilla.geckoview.GeckoSession
|
||||||
|
|
||||||
|
internal class PromptInstanceDismissDelegate(
|
||||||
|
private val geckoSession: GeckoEngineSession,
|
||||||
|
private val promptRequest: PromptRequest,
|
||||||
|
) : GeckoSession.PromptDelegate.PromptInstanceDelegate {
|
||||||
|
|
||||||
|
override fun onPromptDismiss(prompt: GeckoSession.PromptDelegate.BasePrompt) {
|
||||||
|
geckoSession.notifyObservers {
|
||||||
|
onPromptDismissed(promptRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.selection
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.MenuItem
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import mozilla.components.concept.engine.selection.SelectionActionDelegate
|
||||||
|
import org.mozilla.geckoview.BasicSelectionActionDelegate
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An adapter between the GV [BasicSelectionActionDelegate] and a generic [SelectionActionDelegate].
|
||||||
|
*
|
||||||
|
* @param customDelegate handles as much of this logic as possible.
|
||||||
|
*/
|
||||||
|
open class GeckoSelectionActionDelegate(
|
||||||
|
activity: Activity,
|
||||||
|
@get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal val customDelegate: SelectionActionDelegate,
|
||||||
|
) : BasicSelectionActionDelegate(activity) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* @returns a [GeckoSelectionActionDelegate] if [customDelegate] is non-null and [context]
|
||||||
|
* is an instance of [Activity]. Otherwise, returns null.
|
||||||
|
*/
|
||||||
|
fun maybeCreate(context: Context, customDelegate: SelectionActionDelegate?): GeckoSelectionActionDelegate? {
|
||||||
|
return if (context is Activity && customDelegate != null) {
|
||||||
|
GeckoSelectionActionDelegate(context, customDelegate)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAllActions(): Array<String> {
|
||||||
|
return customDelegate.sortedActions(super.getAllActions() + customDelegate.getAllActions())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isActionAvailable(id: String): Boolean {
|
||||||
|
val selectedText = mSelection?.text
|
||||||
|
|
||||||
|
val customActionIsAvailable = !selectedText.isNullOrEmpty() &&
|
||||||
|
customDelegate.isActionAvailable(id, selectedText)
|
||||||
|
|
||||||
|
return customActionIsAvailable ||
|
||||||
|
super.isActionAvailable(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun prepareAction(id: String, item: MenuItem) {
|
||||||
|
val title = customDelegate.getActionTitle(id)
|
||||||
|
?: return super.prepareAction(id, item)
|
||||||
|
|
||||||
|
item.title = title
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun performAction(id: String, item: MenuItem): Boolean {
|
||||||
|
/* Temporary, removed once https://bugzilla.mozilla.org/show_bug.cgi?id=1694983 is fixed */
|
||||||
|
try {
|
||||||
|
val selectedText = mSelection?.text ?: return super.performAction(id, item)
|
||||||
|
|
||||||
|
return customDelegate.performAction(id, selectedText) || super.performAction(id, item)
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.serviceworker
|
||||||
|
|
||||||
|
import mozilla.components.browser.engine.gecko.GeckoEngineSession
|
||||||
|
import mozilla.components.concept.engine.EngineSession
|
||||||
|
import mozilla.components.concept.engine.Settings
|
||||||
|
import mozilla.components.concept.engine.serviceworker.ServiceWorkerDelegate
|
||||||
|
import org.mozilla.geckoview.GeckoResult
|
||||||
|
import org.mozilla.geckoview.GeckoRuntime
|
||||||
|
import org.mozilla.geckoview.GeckoSession
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation for supporting Gecko service workers.
|
||||||
|
*
|
||||||
|
* @param delegate [ServiceWorkerDelegate] handling service workers requests.
|
||||||
|
* @param runtime [GeckoRuntime] current engine's runtime.
|
||||||
|
* @param engineSettings [Settings] default settings used when new [EngineSession]s are to be created.
|
||||||
|
*/
|
||||||
|
class GeckoServiceWorkerDelegate(
|
||||||
|
internal val delegate: ServiceWorkerDelegate,
|
||||||
|
internal val runtime: GeckoRuntime,
|
||||||
|
internal val engineSettings: Settings?,
|
||||||
|
) : GeckoRuntime.ServiceWorkerDelegate {
|
||||||
|
override fun onOpenWindow(url: String): GeckoResult<GeckoSession> {
|
||||||
|
val newEngineSession = GeckoEngineSession(runtime, false, engineSettings, openGeckoSession = false)
|
||||||
|
|
||||||
|
return when (delegate.addNewTab(newEngineSession)) {
|
||||||
|
true -> GeckoResult.fromValue(newEngineSession.geckoSession)
|
||||||
|
false -> GeckoResult.fromValue(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
package mozilla.components.browser.engine.gecko.translate
|
||||||
|
|
||||||
|
import mozilla.components.browser.engine.gecko.GeckoEngineSession
|
||||||
|
import mozilla.components.concept.engine.translate.DetectedLanguages
|
||||||
|
import mozilla.components.concept.engine.translate.TranslationEngineState
|
||||||
|
import mozilla.components.concept.engine.translate.TranslationPair
|
||||||
|
import org.mozilla.geckoview.GeckoSession
|
||||||
|
import org.mozilla.geckoview.TranslationsController
|
||||||
|
|
||||||
|
internal class GeckoTranslateSessionDelegate(
|
||||||
|
private val engineSession: GeckoEngineSession,
|
||||||
|
) : TranslationsController.SessionTranslation.Delegate {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This delegate function is triggered when requesting a translation on the page is likely.
|
||||||
|
*
|
||||||
|
* The criteria is that the page is in a different language than the user's known languages and
|
||||||
|
* that the page is translatable (a model is available).
|
||||||
|
*
|
||||||
|
* @param session The session that this delegate event corresponds to.
|
||||||
|
*/
|
||||||
|
override fun onExpectedTranslate(session: GeckoSession) {
|
||||||
|
engineSession.notifyObservers {
|
||||||
|
onTranslateExpected()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This delegate function is triggered when the app should offer a translation.
|
||||||
|
*
|
||||||
|
* The criteria is that the translation is likely and it is the user's first visit to the host site.
|
||||||
|
*
|
||||||
|
* @param session The session that this delegate event corresponds to.
|
||||||
|
*/
|
||||||
|
override fun onOfferTranslate(session: GeckoSession) {
|
||||||
|
engineSession.notifyObservers {
|
||||||
|
onTranslateOffer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This delegate function is triggered when the state of the translation or translation options
|
||||||
|
* for the page has changed. State changes usually occur on navigation or if a translation
|
||||||
|
* action was requested, such as translating or restoring to the original page.
|
||||||
|
*
|
||||||
|
* This provides the translations engine state and information for the page.
|
||||||
|
*
|
||||||
|
* @param session The session that this delegate event corresponds to.
|
||||||
|
* @param state The reported translations state. Not to be confused
|
||||||
|
* with the browser translation state.
|
||||||
|
*/
|
||||||
|
override fun onTranslationStateChange(
|
||||||
|
session: GeckoSession,
|
||||||
|
state: TranslationsController.SessionTranslation.TranslationState?,
|
||||||
|
) {
|
||||||
|
val detectedLanguages = DetectedLanguages(
|
||||||
|
state?.detectedLanguages?.docLangTag,
|
||||||
|
state?.detectedLanguages?.isDocLangTagSupported,
|
||||||
|
state?.detectedLanguages?.userLangTag,
|
||||||
|
)
|
||||||
|
val pair = TranslationPair(
|
||||||
|
state?.requestedTranslationPair?.fromLanguage,
|
||||||
|
state?.requestedTranslationPair?.toLanguage,
|
||||||
|
)
|
||||||
|
val translationsState = TranslationEngineState(
|
||||||
|
detectedLanguages = detectedLanguages,
|
||||||
|
error = state?.error,
|
||||||
|
isEngineReady = state?.isEngineReady,
|
||||||
|
hasVisibleChange = state?.hasVisibleChange,
|
||||||
|
requestedTranslationPair = pair,
|
||||||
|
)
|
||||||
|
|
||||||
|
engineSession.notifyObservers {
|
||||||
|
onTranslateStateChange(translationsState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
package mozilla.components.browser.engine.gecko.translate
|
||||||
|
|
||||||
|
import mozilla.components.concept.engine.translate.TranslationError
|
||||||
|
import org.mozilla.geckoview.TranslationsController.TranslationsException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility file for translations functions related to the Gecko implementation.
|
||||||
|
*/
|
||||||
|
object GeckoTranslationUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method for mapping a [TranslationsException] to the Android Components defined
|
||||||
|
* error type of [TranslationError].
|
||||||
|
*
|
||||||
|
* Throwable is the engine throwable that occurred during translating. Ordinarily should be
|
||||||
|
* a [TranslationsException].
|
||||||
|
*/
|
||||||
|
fun Throwable.intoTranslationError(): TranslationError {
|
||||||
|
return if (this is TranslationsException) {
|
||||||
|
when ((this).code) {
|
||||||
|
TranslationsException.ERROR_UNKNOWN ->
|
||||||
|
TranslationError.UnknownError(this)
|
||||||
|
|
||||||
|
TranslationsException.ERROR_ENGINE_NOT_SUPPORTED ->
|
||||||
|
TranslationError.EngineNotSupportedError(this)
|
||||||
|
|
||||||
|
TranslationsException.ERROR_COULD_NOT_TRANSLATE ->
|
||||||
|
TranslationError.CouldNotTranslateError(this)
|
||||||
|
|
||||||
|
TranslationsException.ERROR_COULD_NOT_RESTORE ->
|
||||||
|
TranslationError.CouldNotRestoreError(this)
|
||||||
|
|
||||||
|
TranslationsException.ERROR_COULD_NOT_LOAD_LANGUAGES ->
|
||||||
|
TranslationError.CouldNotLoadLanguagesError(this)
|
||||||
|
|
||||||
|
TranslationsException.ERROR_LANGUAGE_NOT_SUPPORTED ->
|
||||||
|
TranslationError.LanguageNotSupportedError(this)
|
||||||
|
|
||||||
|
TranslationsException.ERROR_MODEL_COULD_NOT_RETRIEVE ->
|
||||||
|
TranslationError.ModelCouldNotRetrieveError(this)
|
||||||
|
|
||||||
|
TranslationsException.ERROR_MODEL_COULD_NOT_DELETE ->
|
||||||
|
TranslationError.ModelCouldNotDeleteError(this)
|
||||||
|
|
||||||
|
TranslationsException.ERROR_MODEL_COULD_NOT_DOWNLOAD ->
|
||||||
|
TranslationError.ModelCouldNotDownloadError(this)
|
||||||
|
|
||||||
|
TranslationsException.ERROR_MODEL_LANGUAGE_REQUIRED ->
|
||||||
|
TranslationError.ModelLanguageRequiredError(this)
|
||||||
|
|
||||||
|
TranslationsException.ERROR_MODEL_DOWNLOAD_REQUIRED ->
|
||||||
|
TranslationError.ModelDownloadRequiredError(this)
|
||||||
|
|
||||||
|
else -> TranslationError.UnknownError(this)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TranslationError.UnknownError(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,154 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.browser.engine.gecko.util
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import mozilla.components.browser.engine.gecko.GeckoEngineSession
|
||||||
|
import mozilla.components.concept.engine.EngineSession
|
||||||
|
import mozilla.components.concept.engine.Settings
|
||||||
|
import org.mozilla.geckoview.GeckoRuntime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper factory for creating and maintaining a speculative [EngineSession].
|
||||||
|
*/
|
||||||
|
internal class SpeculativeSessionFactory {
|
||||||
|
@VisibleForTesting
|
||||||
|
internal var speculativeEngineSession: SpeculativeEngineSession? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a speculative [EngineSession] using the provided [contextId] and [defaultSettings].
|
||||||
|
* Creates a private session if [private] is set to true.
|
||||||
|
*
|
||||||
|
* The speculative [EngineSession] is kept internally until explicitly needed and access via [get].
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun create(
|
||||||
|
runtime: GeckoRuntime,
|
||||||
|
private: Boolean,
|
||||||
|
contextId: String?,
|
||||||
|
defaultSettings: Settings?,
|
||||||
|
) {
|
||||||
|
if (speculativeEngineSession?.matches(private, contextId) == true) {
|
||||||
|
// We already have a speculative engine session for this configuration. Nothing to do here.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any potentially non-matching engine session
|
||||||
|
clear()
|
||||||
|
|
||||||
|
speculativeEngineSession = SpeculativeEngineSession.create(
|
||||||
|
this,
|
||||||
|
runtime,
|
||||||
|
private,
|
||||||
|
contextId,
|
||||||
|
defaultSettings,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the internal speculative [EngineSession].
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun clear() {
|
||||||
|
speculativeEngineSession?.cleanUp()
|
||||||
|
speculativeEngineSession = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns and consumes a previously created [private] speculative [EngineSession] if it uses
|
||||||
|
* the same [contextId]. Returns `null` if no speculative [EngineSession] for that
|
||||||
|
* configuration is available.
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun get(
|
||||||
|
private: Boolean,
|
||||||
|
contextId: String?,
|
||||||
|
): GeckoEngineSession? {
|
||||||
|
val speculativeEngineSession = speculativeEngineSession ?: return null
|
||||||
|
|
||||||
|
return if (speculativeEngineSession.matches(private, contextId)) {
|
||||||
|
this.speculativeEngineSession = null
|
||||||
|
speculativeEngineSession.unwrap()
|
||||||
|
} else {
|
||||||
|
clear()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun hasSpeculativeSession(): Boolean {
|
||||||
|
return speculativeEngineSession != null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal wrapper for [GeckoEngineSession] that takes care of registering and unregistering an
|
||||||
|
* observer for handling content process crashes/kills.
|
||||||
|
*/
|
||||||
|
internal class SpeculativeEngineSession constructor(
|
||||||
|
@get:VisibleForTesting internal val engineSession: GeckoEngineSession,
|
||||||
|
@get:VisibleForTesting internal val observer: SpeculativeSessionObserver,
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Checks whether the [SpeculativeEngineSession] matches the given configuration.
|
||||||
|
*/
|
||||||
|
fun matches(private: Boolean, contextId: String?): Boolean {
|
||||||
|
return engineSession.geckoSession.settings.usePrivateMode == private &&
|
||||||
|
engineSession.geckoSession.settings.contextId == contextId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unwraps the internal [GeckoEngineSession].
|
||||||
|
*
|
||||||
|
* After calling [unwrap] the wrapper will no longer observe the [GeckoEngineSession] and further
|
||||||
|
* crash handling is left to the application.
|
||||||
|
*/
|
||||||
|
fun unwrap(): GeckoEngineSession {
|
||||||
|
engineSession.unregister(observer)
|
||||||
|
return engineSession
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up the internal state of this [SpeculativeEngineSession]. After calling this method
|
||||||
|
* his [SpeculativeEngineSession] cannot be used anymore.
|
||||||
|
*/
|
||||||
|
fun cleanUp() {
|
||||||
|
engineSession.unregister(observer)
|
||||||
|
engineSession.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun create(
|
||||||
|
factory: SpeculativeSessionFactory,
|
||||||
|
runtime: GeckoRuntime,
|
||||||
|
private: Boolean,
|
||||||
|
contextId: String?,
|
||||||
|
defaultSettings: Settings?,
|
||||||
|
): SpeculativeEngineSession {
|
||||||
|
val engineSession = GeckoEngineSession(runtime, private, defaultSettings, contextId)
|
||||||
|
val observer = SpeculativeSessionObserver(factory)
|
||||||
|
engineSession.register(observer)
|
||||||
|
|
||||||
|
return SpeculativeEngineSession(engineSession, observer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [EngineSession.Observer] implementation that will notify the [SpeculativeSessionFactory] if an
|
||||||
|
* [GeckoEngineSession] can no longer be used after a crash.
|
||||||
|
*/
|
||||||
|
internal class SpeculativeSessionObserver(
|
||||||
|
private val factory: SpeculativeSessionFactory,
|
||||||
|
|
||||||
|
) : EngineSession.Observer {
|
||||||
|
override fun onCrash() {
|
||||||
|
factory.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onProcessKilled() {
|
||||||
|
factory.clear()
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue