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-rustlog
|
||||
- support-test
|
||||
- support-test-fakes
|
||||
- support-test-libstate
|
||||
- support-utils
|
||||
- support-webextensions
|
||||
- tooling-lint
|
||||
- ui-autocomplete
|
||||
- ui-colors
|
||||
- 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
|
||||
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
|
||||
uses: abhijitvalluri/sign-apks@v0.8
|
||||
with:
|
||||
|
@ -62,6 +68,8 @@ jobs:
|
|||
alias: ${{ secrets.DEBUG_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.DEBUG_KEY_STORE_PASSWORD }}
|
||||
keyPassword: ${{ secrets.DEBUG_KEY_PASSWORD }}
|
||||
env:
|
||||
BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
|
||||
|
||||
- name: Upload arm64 apk
|
||||
uses: actions/upload-artifact@v3
|
||||
|
|
|
@ -52,6 +52,12 @@ jobs:
|
|||
gradle-executable: /usr/bin/time
|
||||
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
|
||||
if: "contains(toJSON(github.event.ref_type), 'tag') && contains(toJSON(github.event.ref), 'iceraven')"
|
||||
uses: abhijitvalluri/sign-apks@v0.8
|
||||
|
@ -61,6 +67,8 @@ jobs:
|
|||
alias: ${{ secrets.DEBUG_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.DEBUG_KEY_STORE_PASSWORD }}
|
||||
keyPassword: ${{ secrets.DEBUG_KEY_PASSWORD }}
|
||||
env:
|
||||
BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
|
||||
|
||||
- name: Create changelog
|
||||
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!
|
||||
|
||||
|
||||
Iceraven Browser is a web browser for Android, based on [Mozilla's Fenix version of Firefox](https://github.com/mozilla-mobile/fenix/), [GeckoView](https://mozilla.github.io/geckoview/) and [Mozilla Android Components](https://mozac.org/).
|
||||
|
||||
Our goal is to be a close fork of the new Firefox for Android that seeks to provide users with more options, more opportunities to customize (including a broad extension library), and more information about the pages they visit and how their browsers are interacting with those pages.
|
||||
|
|
|
@ -1,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