Merge branch 'fork' into mozilla_main

pull/149/head
Abhijit Valluri 4 years ago
commit ea7c8255ae

@ -0,0 +1,99 @@
name: Android build
on:
push:
branches:
- fork
pull_request:
branches:
- fork
jobs:
run-build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 11
- name: Build forkRelease variant of app
uses: eskatos/gradle-command-action@v1
with:
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
arguments: assembleForkRelease -PversionName="$(git describe --tags HEAD)"
run-testDebug:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 11
- name: Run tests
uses: eskatos/gradle-command-action@v1
with:
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
arguments: testDebug
run-detekt:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 11
- name: Run detekt
uses: eskatos/gradle-command-action@v1
with:
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
arguments: detekt
- name: Archive detekt results
uses: actions/upload-artifact@v2
with:
name: detekt report
path: build/reports/detekt.html
run-ktlint:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 11
- name: Run ktlint
uses: eskatos/gradle-command-action@v1
with:
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
arguments: ktlint
run-lintDebug:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 11
- name: Run lintDebug
uses: eskatos/gradle-command-action@v1
with:
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
arguments: lintDebug
- name: Archive lint results
uses: actions/upload-artifact@v2
with:
name: lintDebug report
path: app/build/reports/lint-results-debug.html

@ -0,0 +1,13 @@
language: android
dist: trusty
script:
- sudo mkdir -p /usr/local/android-sdk/licenses/
- sudo touch /usr/local/android-sdk/licenses/android-sdk-license
- echo "8933bad161af4178b1185d1a37fbf41ea5269c55" | sudo tee -a /usr/local/android-sdk/licenses/android-sdk-license
- echo "d56f5187479451eabf01fb78af6dfcb131a6481e" | sudo tee -a /usr/local/android-sdk/licenses/android-sdk-license
- echo "24333f8a63b6825ea9c5514f83c2829b004d1fee" | sudo tee -a /usr/local/android-sdk/licenses/android-sdk-license
# Run tests
- ./gradlew -q testDebug 2>&1
# Make sure a release build builds
- ./gradlew assembleForkRelease -PversionName="$(git describe --tags HEAD)"

@ -1,223 +1,106 @@
# Firefox for Android
# Iceraven Browser! [![Build Status](https://travis-ci.org/fork-maintainers/iceraven-browser.svg?branch=fork)](https://travis-ci.org/fork-maintainers/iceraven-browser) ![Android build](https://github.com/fork-maintainers/iceraven-browser/workflows/Android%20build/badge.svg)
[![Task Status](https://github.taskcluster.net/v1/repository/mozilla-mobile/fenix/master/badge.svg)](https://github.taskcluster.net/v1/repository/mozilla-mobile/fenix/master/latest)
[![codecov](https://codecov.io/gh/mozilla-mobile/fenix/branch/master/graph/badge.svg)](https://codecov.io/gh/mozilla-mobile/fenix)
Definitely not brought to you by Mozilla!
Fenix (internal codename) is the all-new Firefox for Android browser, based on [GeckoView](https://mozilla.github.io/geckoview/) and [Mozilla Android Components](https://mozac.org/).
Iceraven Browser is a web browser for Android, based on [Mozilla's Fenix version of Firefox](https://github.com/mozilla-mobile/fenix/), [GeckoView](https://mozilla.github.io/geckoview/) and [Mozilla Android Components](https://mozac.org/).
<a href="https://play.google.com/store/apps/details?id=org.mozilla.firefox" target="_blank"><img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" alt="Get it on Google Play" height="90"/></a>
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.
** Note: The team is currently experiencing heavy triage and review load, so when triaging issues, we will mainly be looking to identify [S1 (high severity)](https://github.com/mozilla-mobile/fenix/labels/S1) issues. See our triage process [here](https://github.com/mozilla-mobile/fenix/wiki/Triage-Process). Please be patient if you don't hear back from us immediately on your issue! **
Notable features include:
* `about:config` support
* The ability to *attempt* to install a much longer list of add-ons than Mozilla's Fenix version of Firefox accepts. Currently the browser queries [this AMO collection](https://addons.mozilla.org/en-US/firefox/collections/16201230/What-I-want-on-Fenix/) **Most of them will not work**, because they depend on code that Mozilla is still working on writing in `android-components`, but you may attempt to install them. If you don't see an add-on you want, you can [request it](https://github.com/fork-maintainers/iceraven-browser/issues/new).
* **No warranties or guarantee of security or updates**. Binaries are currently are manually built and are not meaningfully signed. Why should you trust random people on the Internet to provide your web browser, one of the most important pieces of software you use? Iceraven Browser could not exist without the hardworking folks at the Mozilla Corporation who work on the Mozilla Android Components and Firefox projects, but it is not a Mozilla product, and is not provided, endorsed, vetted, approved, or secured by Mozilla.
## Getting Involved
Please read the [Community Participation Guidelines](https://www.mozilla.org/en-US/about/governance/policies/participation/) and the [Bugzilla Etiquette guidelines](https://bugzilla.mozilla.org/page.cgi?id=etiquette.html) before filing an issue. This is our professional working environment as much as it is our bug tracker, and we want to keep our workspace clean and healthy.
* [Guide to Contributing](https://github.com/mozilla-mobile/shared-docs/blob/master/android/CONTRIBUTING.md) (**New contributors start here!**)
* Browse our [current Issues](https://github.com/mozilla-mobile/fenix/issues), or [file a security issue][sec issue].
* Matrix: [#fenix:mozilla.org channel](https://chat.mozilla.org/#/room/#fenix:mozilla.org) (**We're available Monday-Friday, GMT and PST working hours**). Related channels:
* [#mobile-test-eng:mozilla.org channel](https://chat.mozilla.org/#/room/#mobile-test-eng:mozilla.org): for UI test automation
* [#perf-android-frontend:mozilla.org channel](https://chat.mozilla.org/#/room/#perf-android-frontend:mozilla.org): for front-end (JVM) performance of Android apps
* [#android-tips:mozilla.org channel](https://chat.mozilla.org/#/room/#android-tips:mozilla.org): for tips on Android development
* Check out the [project wiki](https://github.com/mozilla-mobile/fenix/wiki) for more information.
* 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.
**Beginners!** - Watch out for [Issues with the "Good First Issue" label](https://github.com/mozilla-mobile/fenix/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). These are easy bugs that have been left for first timers to have a go, get involved and make a positive contribution to the project!
## I want to open a Pull Request!
We encourage you to participate in this open source project. We love Pull Requests, Bug Reports, ideas, (security) code reviews or any other kind of positive contribution.
Since we are a small team, however, **we do not have the bandwidth to review unsolicited PRs**. Please follow our [Pull Request guidelines](https://github.com/mozilla-mobile/shared-docs/blob/master/android/CONTRIBUTING_code.md#creating-a-pull-request), or **we may close the PR**.
To make it easier to review, we have these PR requirements:
* Every PR must have **exactly** one issue associated with it.
* Write a clear explanation of what the code is doing when opening the pull request, and optionally add comments to the PR.
* Make sure there are tests - or ask for help on how the code should be tested in the Issue!
* Keep PRs small and to the point. For extra code-health changes, either file a separate issue, or make it a separate PR that can be easily reviewed.
* Use micro-commits. This makes it easier and faster to review.
* Add a screenshot for UX changes (this is part of the PR checklist)
As a small team, we have to prioritize our work, and reviewing PRs takes time. We receive lots of PRs every day, so if you can keep your PRs small, it helps our small team review and merge code faster, minimizing stale code.
Keep in mind that the team is very overloaded, so PRs sometimes wait
for a *very* long time. However this is not for lack of interest, but
because we find ourselves in a constant need to prioritize
certain issues/PRs over others. If you think your issue/PR is very important,
try to popularize it by getting other users to comment and share their point of view.
## I want to file an issue!
Great! We encourage you to participate in this open source project. We love Pull Requests, Bug Reports, ideas, (security) code reviews or any other kind of positive contribution.
To make it easier to triage, we have these issue requirements:
* 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 bug/feature request described in it. Please do not file meta feedback list tickets as it is difficult to parse them and address their individual points.
* Feature Requests are better when theyre open-ended instead of demanding a specific solution -ie “I want an easier way to do X” instead of “add Y”
* Issues are not the place to go off topic or debate. If you have questions, please join the [#fenix:mozilla.org channel](https://chat.mozilla.org/#/room/#fenix:mozilla.org).
* Please always remember our [Community Participation Guidelines](https://www.mozilla.org/en-US/about/governance/policies/participation/)
* Please do not tag specific team members to try to get your issue looked at faster. We have a triage process that will tag and label issues correctly in due time. If you think an issue is very severe, you can ask about it in Matrix.
Please keep in mind that even though a feature you have in mind may seem like a small ask, as a small team, we have to prioritize our planned work and every new feature adds complexity and maintenance and may take up design, research, marketing, product, and engineering time. We appreciate everyones passion but we will not be able to incorporate every feature request or even fix every bug. That being said, just because we haven't replied, doesn't mean we don't care about the issue, please be patient with our response times as we're very busy.
## Build Instructions
Pre-requisites:
* Android SDK
* To run command line tools, you'll need to configure Java: see [our how-to guide](https://github.com/mozilla-mobile/shared-docs/blob/master/android/configure_java.md).
1. Clone or Download the repository:
```shell
git clone https://github.com/mozilla-mobile/fenix
```
In addition, we intend to try to cut down on telemetry and proprietary code to as great of an extent as possible as long as doing so does not compromise the user experience or make the fork too hard to maintain. Right now, webelieve that no telemetry should be being sent to Mozilla anymore, but we cannot guarantee this; data may still be sent. **If you catch the app sending data to Mozilla, Adjust, Leanplum, Firebase, or any other such service, please open an issue!** Presumably data that reaches Mozilla is governed by Mozilla's privacy policy, but as Iceraven Browser is, again **not a Mozilla product**, we can make no promises.
2. **Import** the project into Android Studio **or** build on the command line:
Iceraven Browser combines the power of Fenix (of which we are a fork) and the spirit of Fennec, with a respectful nod toward the grand tradition of Netscape Navigator, from which all Gecko-based projects came, including the earliest of our predecessors, the old Mozilla Phoenix and Mozilla Firefox desktop browsers.
```shell
./gradlew clean app:assembleDebug
```
That said, Iceraven Browser is an independent all-volunteer project, and has no affiliation with Netscape, Netscape Navigator, Mozilla, Mozilla Firefox, Mozila Phoenix, Debian, Debian Iceweasel, Parabola GNU/Linux-libre Iceweasel, America Online, or Verizon, among others. :) Basically, if you don't like the browser, it's not their fault. :)
If this errors out, make sure that you have an `ANDROID_SDK_ROOT` environment
variable pointing to the right path.
## Installation
3. Make sure to select the correct build variant in Android Studio. See the next section.
[**Download APKs from the Releases Page**](https://github.com/fork-maintainers/iceraven-browser/releases)
### Build Variants
For general development, we recommend the **debug** build variant. Here's an explanation of each variant:
## Building
- **debug**: the default for developers, similar to most other Android apps. It is debuggable, uses a Nightly GeckoView with debug symbols, adds tools like LeakCanary for troublingshooting, and does not strip unused code.
- **nightly**: what we ship to the Firefox Nightly channel, using GeckoView Nightly.
- **beta**: what we ship to the Firefox Beta channel, using GeckoView Beta. It is more stable than nightly.
- **release**: what we ship as Firefox for Android, using GeckoView Release. It is the most stable.
1. Set up the environment. We need the Android SDK at `$ANDROID_SDK_ROOT` and a Java JDK at `$JAVA_HOME` that isn't the Ubuntu Java 8 one. We want environment variables that look something like:
nightly, beta, and release are unsigned and `debuggable=false` by default. If
you want these variants to be:
- automatically signed, see [Automatically signing release builds](#automatically-sign-release-builds)
- `debuggable=true`, see [Building debuggable release variants](#building-debuggable-release-variants)
#### Performance Build Variants
For accurate performance measurements, read this section!
To analyze performance during **local development** build a production variant locally (this could either be the Nightly, beta or release). Otherwise, you could also grab a pre-existing APK if you don't need to test some local changes. Then, use the Firefox profiler to profile what you need!
For more information on how to use the profiler or how to use the build, refer to this [how to measure performance with the build](https://wiki.mozilla.org/Performance/How_to_get_started_on_Fenix)
If you want to run **performance tests/benchmarks** in automation or locally use a production build since it is much closer in behavior compared to what users see in the wild.
Before you can install any release builds, **You will need to sign production build variants:** see [Automatically signing release builds](#automatically-sign-release-builds) for details.
##### Known disabled-by-default features
Some features are disabled by default when Fenix is built locally. This can be problematic at times for checking performance since you might want to know how your code behaves with those features.
The known features that are disabled by default are:
- Sentry
- Leanplum
- Adjust
- Mozilla Location Services (also known as MLS)
- Firebase Push Services
- Telemetry (only disabled by default in debug builds)
## Pre-push hooks
To reduce review turn-around time, we'd like all pushes to run tests locally. We'd
recommend you use our provided pre-push hook in `config/pre-push-recommended.sh`.
Using this hook will guarantee your hook gets updated as the repository changes.
This hook tries to run as much as possible without taking too much time.
Before you can run the hook, you'll need to configure Java properly because it relies on command line tools: see
[our how-to guide](https://github.com/mozilla-mobile/shared-docs/blob/master/android/configure_java.md).
To add it on Mac/Linux, run this command from the project root:
```sh
ln -s ../../config/pre-push-recommended.sh .git/hooks/pre-push
```
or for Windows run this command using the Command Prompt with administrative privileges:
```sh
mklink .git\hooks\pre-push ..\..\config\pre-push-recommended.sh
```
or using PowerShell:
```sh
New-Item -ItemType SymbolicLink -Path .git\hooks\pre-push -Value (Resolve-Path config\pre-push-recommended.sh)
# Where does our system install the JDK? This is the right path for the Ubuntu Java 11 JDK, if it is installed.
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
# Where did we install the Android SDK?
export ANDROID_SDK_ROOT=$HOME/android-sdk/android-sdk-linux/
```
To push without running the pre-push hook (e.g. doc updates):
If we don't have the Android SDK, we can install it thusly on Linux:
```sh
git push <remote> --no-verify
mkdir -p $HOME/android-sdk/android-sdk-linux
cd $HOME/android-sdk/android-sdk-linux
mkdir -p licenses
echo "8933bad161af4178b1185d1a37fbf41ea5269c55" >> licenses/android-sdk-license
echo "d56f5187479451eabf01fb78af6dfcb131a6481e" >> licenses/android-sdk-license
echo "24333f8a63b6825ea9c5514f83c2829b004d1fee" >> licenses/android-sdk-license
mkdir cmdline-tools
cd cmdline-tools
wget "$(curl -s https://developer.android.com/studio | grep -oP "https://dl.google.com/android/repository/commandlinetools-linux-[0-9]+_latest.zip")"
unzip commandlinetools-linux-*_latest.zip
cd ..
```
Note: If while pushing you encounter this error "Could not initialize class org.codehaus.groovy.runtime.InvokerHelper" and are currently on Java14 then downgrading your Java version to Java13 or lower can resolve the issue
Steps to downgrade Java Version on Mac with Brew:
1. Install Homebrew (https://brew.sh/)
2. run ```brew update```
3. To uninstall your current java version, run ```sudo rm -fr /Library/Java/JavaVirtualMachines/<jdk-version>```
4. run ```brew tap homebrew/cask-versions```
5. run ```brew search java```
6. If you see java11, then run ```brew install java11```
7. Verify java-version by running ```java -version```
2. Clone the project.
## local.properties helpers
You can speed up local development by setting a few helper flags available in `local.properties`. Some flags will make it easy to
work across multiple layers of the dependency stack - specifically, with android-components, geckoview or application-services.
```sh
git clone https://github.com/fork-maintainers/iceraven-browser
```
### Automatically sign release builds
To sign your release builds with your debug key automatically, add the following to `<proj-root>/local.properties`:
4. Go inside `iceraven-browser`. That's where the build is coordinated from.
```sh
autosignReleaseWithDebugKey
cd iceraven-browser
```
With this line, release build variants will automatically be signed with your debug key (like debug builds), allowing them to be built and installed directly through Android Studio or the command line.
5. Configure the project. We need to set the release builds to be signed with the debug key, because proper code signing isn't set up yet and the completely unsigned APKs that are produced by default cannot be installed.
This is helpful when you're building release variants frequently, for example to test feature flags and or do performance analyses.
### Building debuggable release variants
```sh
echo "autosignReleaseWithDebugKey=" >>local.properties
```
Nightly, Beta and Release variants are getting published to Google Play and therefore are not debuggable. To locally create debuggable builds of those variants, add the following to `<proj-root>/local.properties`:
6. Build the project. To build the Iceraven-branded release APKs, you can do:
```sh
debuggable
./gradlew assembleForkRelease -PversionName="$(git describe --tags HEAD)"
```
### Setting raptor manifest flag
The APKs will show up in `app/build/outputs/apk/forkRelease/`.
To set the raptor manifest flag in Nightly, Beta and Release variants, add the following to `<proj-root>/local.properties`:
## Getting Involved
```sh
raptorEnabled
```
This is an all-volunteer project. No one is getting paid (at least not by the project itself.).
### Auto-publication workflow for android-components and application-services
If you're making changes to these projects and want to test them in Fenix, auto-publication workflow is the fastest, most reliable
way to do that.
Therefore, everyone should feel free to open issues and pull requests. Join the club!
In `local.properties`, specify a relative path to your local `android-components` and/or `application-services` checkouts. E.g.:
- `autoPublish.android-components.dir=../android-components`
- `autoPublish.application-services.dir=../application-services`
Developers are especially welcome, wanted, and needed.
Once these flags are set, your Fenix builds will include any local modifications present in these projects.
## I want to open a Pull Request!
See a [demo of auto-publication workflow in action](https://www.youtube.com/watch?v=qZKlBzVvQGc).
We encourage you to participate in this open source project. We love Pull Requests, Bug Reports, ideas, (security) code reviews or any other kind of positive contribution.
### GeckoView
Specify a relative path to your local `mozilla-central` checkout via `dependencySubstitutions.geckoviewTopsrcdir`,
and optional a path to m-c object directory via `dependencySubstitutions.geckoviewTopobjdir`.
## I want to file an issue!
If these are configured, local builds of GeckoView will be used instead of what's configured in Dependencies.kt.
For more details, see https://firefox-source-docs.mozilla.org/mobile/android/geckoview/contributor/geckoview-quick-start.html#include-geckoview-as-a-dependency
Great! We encourage you to participate in this open source project. We love Pull Requests, Bug Reports, ideas, (security) code reviews or any other kind of positive contribution.
To make it easier to triage, we have these issue requirements:
* 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 bug/feature request described in it. Please do not file meta feedback list tickets as it is difficult to parse them and address their individual points.
* Feature Requests are better when theyre open-ended instead of demanding a specific solution -ie “I want an easier way to do X” instead of “add Y”
* Issues are not the place to go off topic or debate.
* While we do not yet have Community Participation Guidelines of our own, we ask that you show respect to everyone and treat others as you would like to be treated. Behavior that would violate [Mozilla's Community Participation Guidelines](https://www.mozilla.org/en-US/about/governance/policies/participation/) is almost certainly unwelcome. However, as a small project without community managers, we cannot promise prompt and consistent enforcement.
## License
Please keep in mind that even though a feature you have in mind may seem like a small ask, as a small team, we have to prioritize our planned work and every new feature adds complexity and maintenance and may take up design, research, product, and engineering time. We appreciate everyones passion but we will not be able to incorporate every feature request or even fix every bug. That being said, just because we haven't replied, doesn't mean we don't care about the issue, please be patient with our response times as we're very busy.
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/
[sec issue]: https://bugzilla.mozilla.org/enter_bug.cgi?assigned_to=nobody%40mozilla.org&bug_ignored=0&bug_severity=normal&bug_status=NEW&cf_fx_iteration=---&cf_fx_points=---&component=Security%3A%20Android&contenttypemethod=list&contenttypeselection=text%2Fplain&defined_groups=1&flag_type-4=X&flag_type-607=X&flag_type-791=X&flag_type-800=X&flag_type-803=X&flag_type-936=X&flag_type-937=X&form_name=enter_bug&groups=mobile-core-security&maketemplate=Remember%20values%20as%20bookmarkable%20template&op_sys=Unspecified&priority=--&product=Fenix&rep_platform=Unspecified&target_milestone=---&version=unspecified

@ -21,7 +21,7 @@ import static org.gradle.api.tasks.testing.TestResult.ResultType
android {
compileSdkVersion Config.compileSdkVersion
defaultConfig {
applicationId "org.mozilla"
applicationId "io.github.forkmaintainers"
minSdkVersion Config.minSdkVersion
targetSdkVersion Config.targetSdkVersion
versionCode 1
@ -31,6 +31,7 @@ android {
testInstrumentationRunnerArguments clearPackageData: 'true'
resValue "bool", "IS_DEBUG", "false"
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "false"
buildConfigField "String", "AMO_ACCOUNT", "\"mozilla\""
buildConfigField "String", "AMO_COLLECTION", "\"3204bb44a6ef44d39ee34917f28055\""
def deepLinkSchemeValue = "fenix-dev"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
@ -105,6 +106,34 @@ android {
"deepLinkScheme": deepLinkSchemeValue
]
}
forkDebug {
shrinkResources false
minifyEnabled false
applicationIdSuffix ".iceraven.debug"
resValue "bool", "IS_DEBUG", "true"
pseudoLocalesEnabled true
// Need to replicate default debug config features
signingConfig signingConfigs.debug
debuggable true
def deepLinkSchemeValue = "iceraven-debug"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
// Use custom default allowed addon list
buildConfigField "String", "AMO_ACCOUNT", "\"16201230\""
buildConfigField "String", "AMO_COLLECTION", "\"What-I-want-on-Fenix\""
}
forkRelease releaseTemplate >> {
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
applicationIdSuffix ".iceraven"
def deepLinkSchemeValue = "iceraven"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = [
"deepLinkScheme": deepLinkSchemeValue
]
// Use custom default allowed addon list
buildConfigField "String", "AMO_ACCOUNT", "\"16201230\""
buildConfigField "String", "AMO_COLLECTION", "\"What-I-want-on-Fenix\""
}
}
aaptOptions {
@ -145,6 +174,13 @@ android {
java.srcDirs = ['src/migration/java', 'src/geckoRelease/java']
manifest.srcFile "src/migration/AndroidManifest.xml"
}
forkDebug {
java.srcDirs = ['src/geckoNightly/java']
}
forkRelease {
// No migration needed for fork
java.srcDirs = ['src/geckoRelease/java']
}
}
splits {
@ -195,7 +231,7 @@ android.applicationVariants.all { variant ->
def isDebug = variant.buildType.resValues['IS_DEBUG']?.value ?: false
def useReleaseVersioning = variant.buildType.buildConfigFields['USE_RELEASE_VERSIONING']?.value ?: false
def versionName = Config.releaseVersionName(project)
def versionName = Config.releaseVersionName(project) == "" ? Config.generateDebugVersionName() : Config.releaseVersionName(project)
println("----------------------------------------------")
println("Variant name: " + variant.name)
@ -204,11 +240,14 @@ android.applicationVariants.all { variant ->
println("Flavor: " + variant.flavorName)
println("Telemetry enabled: " + !isDebug)
if (useReleaseVersioning) {
// The Google Play Store does not allow multiple APKs for the same app that all have the
// same version code. Therefore we need to have different version codes for our ARM and x86
// builds.
println("Version Name: " + versionName)
variant.outputs.each { output ->
def abi = output.getFilter(OutputFile.ABI)
// We use the same version code generator, that we inherited from Fennec, across all channels - even on
@ -319,10 +358,12 @@ apply plugin: "org.mozilla.telemetry.glean-gradle-plugin"
dependencies {
debugImplementation Deps.mozilla_browser_engine_gecko_nightly
forkDebugImplementation Deps.mozilla_browser_engine_gecko_nightly
nightlyImplementation Deps.mozilla_browser_engine_gecko_nightly
betaImplementation Deps.mozilla_browser_engine_gecko_beta
releaseImplementation Deps.mozilla_browser_engine_gecko_release
forkReleaseImplementation Deps.mozilla_browser_engine_gecko_release
implementation Deps.kotlin_stdlib
implementation Deps.kotlin_coroutines
@ -334,8 +375,6 @@ dependencies {
implementation Deps.sentry
implementation Deps.leanplum_core
implementation Deps.leanplum_fcm
implementation Deps.mozilla_concept_engine
implementation Deps.mozilla_concept_menu
@ -415,9 +454,9 @@ dependencies {
implementation Deps.mozilla_ui_widgets
implementation Deps.mozilla_lib_crash
implementation Deps.mozilla_lib_push_firebase
implementation Deps.mozilla_lib_dataprotect
debugImplementation Deps.leakcanary
forkDebugImplementation Deps.leakcanary
implementation Deps.androidx_legacy
implementation Deps.androidx_biometric
@ -438,12 +477,6 @@ dependencies {
implementation Deps.lottie
implementation Deps.adjust
implementation Deps.installreferrer // Required by Adjust
implementation Deps.google_ads_id // Required for the Google Advertising ID
implementation Deps.google_play_store // Required for in-app reviews
androidTestImplementation Deps.uiautomator
// Removed pending AndroidX fixes
@ -531,6 +564,9 @@ if (project.hasProperty("coverage")) {
debug {
testCoverageEnabled true
}
forkDebug {
testCoverageEnabled true
}
}
}
}

@ -0,0 +1,23 @@
<manifest
package="org.mozilla.fenix"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- Allows unlocking your device and activating its screen so UI tests can succeed -->
<uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<!-- Allows for storing and retrieving screenshots -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- Allows changing locales -->
<uses-permission android:name="android.permission.CHANGE_CONFIGURATION"
tools:ignore="ProtectedPermissions" />
<application
tools:replace="android:name"
android:name="org.mozilla.fenix.DebugFenixApplication" />
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

@ -0,0 +1,28 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.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 org.mozilla.fenix
import android.os.StrictMode
import androidx.preference.PreferenceManager
import leakcanary.AppWatcher
import leakcanary.LeakCanary
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.resetPoliciesAfter
class DebugFenixApplication : FenixApplication() {
override fun setupLeakCanary() {
val isEnabled = StrictMode.allowThreadDiskReads().resetPoliciesAfter {
PreferenceManager.getDefaultSharedPreferences(this)
.getBoolean(getPreferenceKey(R.string.pref_key_leakcanary), true)
}
updateLeakCanaryState(isEnabled)
}
override fun updateLeakCanaryState(isEnabled: Boolean) {
AppWatcher.config = AppWatcher.config.copy(enabled = isEnabled)
LeakCanary.config = LeakCanary.config.copy(dumpHeap = isEnabled)
}
}

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="208"
android:viewportHeight="208">
<path android:pathData="M163.2,113.7c6.4,-1.2 3.9,-14.4 6.6,-5.2"
android:strokeWidth="1.1"
android:fillColor="#000"
android:strokeColor="#000"/>
<path android:pathData="M171.8,59c2.1,-5.6 5.7,-6.7 8.6,-8.3 -4.7,7.3 -1.4,10.2 -2.5,13 -2.3,5.5 -7.9,0 -6,-4.8z"
android:strokeWidth="1.1"
android:fillColor="#fe9d2a"
android:strokeColor="#000"/>
<path android:pathData="M44.3,75.2c1.8,-4.2 4.8,-5 7.3,-6.1 -4,5.4 -1.2,7.6 -2.2,9.7 -1.9,4.1 -6.6,0 -5,-3.6zM27.3,51.9c4,5.4 3,9.6 4,11.7 1.8,4.1 5.9,0.5 4.3,-3.1a19,19 0,0 0,-8.4 -8.6z"
android:strokeWidth="1.1"
android:fillColor="#f3611e"
android:strokeColor="#000"/>
<path android:pathData="M89.3,91.9l2.8,12.1L23,104l2.5,-19.7a50.9,50.9 0,0 1,18.3 -19c-6.4,7.6 -5.7,21.1 8.6,24.8 9.2,2.4 28.6,-9.8 36.8,1.8zM120.7,92.6c21.9,-9.5 26.4,-1.8 34.8,-4 17.2,-5.3 15.7,-13.6 9.7,-23.4A51,51 0,0 1,183.5 84l2.5,20h-66.6l1.3,-11.4zM89.3,91.9zM159.5,78c0.3,3 5.5,3.2 6.4,0 -0.3,-7 -5.5,-9.2 -10.7,-10.8 2.8,3.6 4.3,5.3 4.3,10.8zM34.1,64.4c-1.8,-4.1 -6.6,-5.2 -9.1,-6.3 4,5.4 3,7.8 4,9.9 1.8,4.1 6.6,0 5,-3.6z"
android:strokeWidth="1.1"
android:fillColor="#f00027"
android:strokeColor="#000"/>
<path android:pathData="M87.3,92.8c-24.6,-8.2 -41,7 -45.9,6.5 -19,-2.3 -19,-16.2 -13.8,-28.6 -4.3,6.5 -9.6,7 -12.3,22.7 -2.3,-2.3 -4.8,-3 -5.8,-11.6 -5.4,16.9 -2,20.8 0.5,26.2 -2.6,-1.6 -5,-3.5 -7.2,-5.5 0.7,7.9 3.5,11.6 5.8,16.3l-5.8,-3.5c9.5,25.9 23.3,35.6 39.6,36.1 16,-1.5 24.2,-4 33.7,-6.3l11.2,-52.3zM123.3,94.8c4,-2.3 10.3,-3.1 15.4,-3 8.7,-0.5 25.2,7.9 29,7.9 9.3,0.1 21.1,-9.6 14,-28.3 4.9,6.5 9.9,10.8 12,22 3.5,-2 4.6,-6.3 6,-10.4 4,9.1 3,17.4 -0.2,25.3 2,-1.1 4,-2.4 6.6,-5.6 0.3,7 -3.6,11 -6,16l5,-2.6c-6,17.8 -15.6,32.7 -36.1,35.6 -13,-2.9 -25.4,-6.9 -41,-6.4l-9.2,-24.4s6.1,-28.4 4.6,-26.1"
android:strokeWidth="1.1"
android:fillColor="#cc002b"
android:strokeColor="#000"/>
<path android:pathData="M163,113.5c2.8,-0.6 4.8,-3.2 5.9,-8.7l1,3.6"
android:strokeWidth="1.1"
android:fillColor="#0000"
android:strokeColor="#000"
android:strokeLineCap="square"/>
<path android:pathData="M177,110a19,19 0,0 0,5.4 -6.8l1,7M154,129.3c5.6,0.8 6.9,-2.7 9.4,-5l-0.5,3.7"
android:strokeLineJoin="bevel"
android:strokeWidth="1.1"
android:fillColor="#0000"
android:strokeColor="#000"/>
<path android:pathData="M44,112.7c-2.7,-0.6 -4.7,-3.2 -5.9,-8.7l-1,3.6"
android:strokeWidth="1.1"
android:fillColor="#0000"
android:strokeColor="#000"
android:strokeLineCap="square"/>
<path android:pathData="M30.1,109.2a19,19 0,0 1,-5.4 -6.8l-1,7M53,128.4c-5.5,0.7 -6.8,-2.7 -9.3,-5l0.5,3.7"
android:strokeLineJoin="bevel"
android:strokeWidth="1.1"
android:fillColor="#0000"
android:strokeColor="#000"/>
<path android:pathData="M24.3 105c0.3 7 1.1 13.8 6.2 18.6a24 24 0 0 1-6.8-2.7c2.2 4.4 4.5 8.8 9 12l-4.7-0.3c2.5 4.5 6.3 8.1 10 11.8-8.3-3.7-17.7-5.7-20.6-18.9 2.3 2.2 3 1.6 4.1 1.9-4-4-5.3-9.3-7-13.8 2 2 4.3 3.5 6.8 4.4-6-6.2-4.5-14.3-4.2-22.2 1.8 3.8 3.9 7.3 7.2 9.2zm10.2 18c3 1.6 4.2 4.3 9.7 4 1 5.4 5 8.4 9.4 11.4-6.1 0.7-14.1-0.1-19-15.4zm5.8-0.8C30.3 119 29 109.4 31 98.9c2 6.6 3.9 7.2 6 8.2-0.7 6-0.5 12 3.3 15z"
android:strokeWidth="1.1"
android:fillColor="#f00027"
android:strokeColor="#000"/>
<path android:pathData="M182.8 105.8c3.3-1.9 5.3-5.4 7.2-9.1 0.3 7.8 1.8 16-4.2 22.1a17 17 0 0 0 6.7-4.3c-1.6 4.4-2.8 9.6-6.9 13.7 1.2-0.3 1.8 0.3 4.2-1.9-3 13.2-12.4 15.3-20.7 19 3.7-3.8 7.5-7.4 10-12-1.8 0.3-3.4 0.4-4.7 0.4 4.5-3.2 6.8-7.6 9-12a24 24 0 0 1-6.8 2.7c5-4.8 6-11.6 6.2-18.6zm-20 22c5.6 0.3 6.8-2.4 9.8-4-5 15.3-13 16-19.1 15.4 4.3-3 8.4-6 9.4-11.3zm4-3.1c10-3.2 11.3-12.8 9.3-23.3-2 6.6-3.9 7.2-6 8.2 0.7 6 0.5 12-3.3 15z"
android:strokeWidth="1.1"
android:fillColor="#f00027"
android:strokeColor="#000"/>
<path android:pathData="M146.8,95.8c0,-3.6 -2.8,-6.5 -6.4,-6.5a6.5,6.5 0,0 0,-6.4 6.5v26.7c0,3.5 3,6.4 6.4,6.4 3.5,0 6.4,-3 6.4,-6.4L146.8,95.7zM72,95.8c0,-3.6 -2.8,-6.5 -6.4,-6.5a6.5,6.5 0,0 0,-6.4 6.5v26.7c0,3.5 3,6.4 6.4,6.4 3.5,0 6.4,-3 6.5,-6.4L72.1,95.7zM117,65.2l4.6,-8.2s0.4,-0.7 -0.4,-1.2c-0.7,-0.4 -1.2,0.4 -1.2,0.4l-4.6,8.2a30.4,30.4 0,0 0,-24.7 0L86,56.2s-0.5,-0.8 -1.2,-0.4c-0.8,0.4 -0.4,1.3 -0.4,1.3l4.5,8.1a26,26 0,0 0,-14.6 23h57.2a26,26 0,0 0,-14.5 -23zM74.4,90.5v41.4a7,7 0,0 0,7 6.9h4.5v14.1c0,3.6 3,6.4 6.4,6.4 3.6,0 6.4,-2.8 6.5,-6.4v-14.1h8.5v14.1c0,3.6 3,6.4 6.4,6.4 3.5,0 6.4,-2.8 6.4,-6.4v-14.1h4.6a7,7 0,0 0,7 -6.9L131.7,90.4L74.3,90.4z"
android:fillColor="#79c257"/>
<path android:pathData="M94.6,88.6c4.5,5 11.5,3.3 17.2,3 -6,-1 -12.6,-2.5 -17,-8.6"
android:strokeWidth="1"
android:fillColor="#f8db00"
android:strokeColor="#000"/>
<path android:pathData="M106.5,90.2c4,-2.8 8.9,-2.5 13.3,-1.4 -2.3,-1.8 -1.5,-3.4 -14.6,-2.6a17,17 0,0 1,-9.4 -2.4c1.8,3.2 6.1,6 10.7,6.4z"
android:strokeWidth="1"
android:fillColor="#d36224"
android:strokeColor="#000"/>
<path android:pathData="M122.2,91c2,-4 -5,-9.6 -6.8,-9.6 -5,-0.5 -14.7,-0.3 -14.4,-3 -3,-0.4 -5,1 -5.8,4 2.7,2.2 5.7,3.7 9.3,3.8 12.3,-0.9 14,0.8 15,2.6l2.7,2.2z"
android:strokeLineJoin="bevel"
android:strokeWidth="1"
android:fillColor="#ecd404"
android:strokeColor="#000"/>
<path android:pathData="M89.7,77.2L84,79.5l1,-2.3L89,74l0.8,1v2.2zM114.8,76.7l-5.7,1.9 1.2,-2.2 4,-3 0.7,1 -0.2,2.3z"
android:strokeWidth="1"
android:fillColor="#680000"
android:strokeColor="#000"/>
<path android:pathData="M84.3,78.6c1.8,-4.7 3.7,-6.3 7.3,-5.1 0.6,0.3 1.3,0.3 1.5,2.2 -0.1,1.4 -0.8,2 -2,2 -2.3,0 -2,-1.5 -2,-3 -2.4,0 -4.8,8.6 -7,3h0.7c0,1.1 1.3,1.4 1.5,0.9zM109.3,77.7c2.2,-4.5 4.2,-6 7.7,-4.6 0.6,0.4 1.3,0.4 1.3,2.4 -0.2,1.3 -0.9,1.9 -2,1.8 -2.3,0 -2,-1.7 -1.8,-3.2 -2.5,-0.1 -5.5,8.3 -7.2,2.6h0.6c0,1.1 1.2,1.5 1.4,1z"
android:strokeWidth="1"
android:fillColor="#000"
android:strokeColor="#000"/>
<path android:pathData="M92.3,75.7c0,0.6 -0.4,1 -1,1a1,1 0,0 1,-1 -1c0,-0.5 0.5,-1 1,-1 0.6,0 1,0.5 1,1zM117.6,75.4c0,0.6 -0.5,1 -1,1a1,1 0,0 1,-1 -1.1c0,-0.5 0.5,-1 1,-1a1,1 0,0 1,1 1.1z"
android:fillColor="#fff"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<resources>
<color name="ic_launcher_background">@color/debug_launcher_background</color>
</resources>

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:shortcutId="open_new_tab"
android:enabled="true"
android:icon="@drawable/ic_static_shortcut_tab"
android:shortcutShortLabel="@string/home_screen_shortcut_open_new_tab_2"
android:shortcutLongLabel="@string/home_screen_shortcut_open_new_tab_2">
<intent
android:action="org.mozilla.fenix.OPEN_TAB"
android:targetPackage="org.mozilla.fenix.debug"
android:targetClass="org.mozilla.fenix.IntentReceiverActivity" />
</shortcut>
<shortcut
android:shortcutId="open_new_private_tab"
android:enabled="true"
android:icon="@drawable/ic_static_shortcut_private_tab"
android:shortcutShortLabel="@string/home_screen_shortcut_open_new_private_tab_2"
android:shortcutLongLabel="@string/home_screen_shortcut_open_new_private_tab_2">
<intent
android:action="org.mozilla.fenix.OPEN_PRIVATE_TAB"
android:targetPackage="org.mozilla.fenix.debug"
android:targetClass="org.mozilla.fenix.IntentReceiverActivity" />
</shortcut>
</shortcuts>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group android:scaleX="1.0711575"
android:scaleY="1.0711575"
android:translateX="25.634476"
android:translateY="22.68">
<path
android:pathData="M28.3719,28.26m-19.5905,0a19.5905,19.5905 0,1 1,39.181 0a19.5905,19.5905 0,1 1,-39.181 0"
android:strokeWidth="0.52916664"
android:fillColor="#3fb6e4"
android:strokeColor="#00000000"/>
<path
android:pathData="m5.5977,16.5538c0.2014,-1.0367 1.2707,-6.9964 6.2074,-9.3502 0.3143,-0.1499 -2.4423,-0.4567 -2.4423,-0.4567 0,0 4.1451,-0.2164 4.5249,-0.3157 0.5581,-0.146 -1.2765,-0.7155 -1.2765,-0.7155 0,0 1.3305,0.1329 2.6028,0.2679 0.4649,0.0494 -0.8314,-0.8593 -0.8314,-0.8593 0,0 2.9055,1.0617 3.0391,1.1162 1.0769,0.4389 2.4513,1.0046 2.9726,2.6058 4.3403,0.0472 8.3695,0.6379 9.5297,3.9432h-9.5297c-4.506,1.7449 -4.6979,3.3341 -2.3175,4.9098 -3.5193,1.5494 -12.4791,-1.1454 -12.4791,-1.1454z"
android:strokeLineJoin="miter"
android:strokeWidth="0.26458332"
android:fillColor="#000080"
android:strokeColor="#000080"
android:strokeLineCap="butt"/>
<path
android:pathData="m5.5977,16.5538c-2.9204,2.3027 8.554,34.4432 16.3678,36.7738 4.1443,-4.8153 1.8719,-31.8116 0.1226,-34.0038 -1.7705,-2.2188 -4.0112,-1.6246 -4.0112,-1.6246z"
android:strokeLineJoin="miter"
android:strokeWidth="0.26458332"
android:fillColor="#000080"
android:strokeColor="#000080"
android:strokeLineCap="butt"/>
</group>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<resources>
<color name="ic_launcher_background">#ffffffff</color>
</resources>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<resources>
<!-- Name of the application -->
<string name="app_name" translatable="false">Iceraven</string>
</resources>

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:shortcutId="open_new_tab"
android:enabled="true"
android:icon="@drawable/ic_static_shortcut_tab"
android:shortcutShortLabel="@string/home_screen_shortcut_open_new_tab_2"
android:shortcutLongLabel="@string/home_screen_shortcut_open_new_tab_2">
<intent
android:action="org.mozilla.fenix.OPEN_TAB"
android:targetPackage="io.github.forkmaintainers.iceraven"
android:targetClass="org.mozilla.fenix.IntentReceiverActivity" />
</shortcut>
<shortcut
android:shortcutId="open_new_private_tab"
android:enabled="true"
android:icon="@drawable/ic_static_shortcut_private_tab"
android:shortcutShortLabel="@string/home_screen_shortcut_open_new_private_tab_2"
android:shortcutLongLabel="@string/home_screen_shortcut_open_new_private_tab_2">
<intent
android:action="org.mozilla.fenix.OPEN_PRIVATE_TAB"
android:targetPackage="io.github.forkmaintainers.iceraven"
android:targetClass="org.mozilla.fenix.IntentReceiverActivity" />
</shortcut>
</shortcuts>

@ -42,7 +42,7 @@ object GeckoProvider {
.crashHandler(CrashHandlerService::class.java)
.telemetryDelegate(GeckoAdapter())
.contentBlocking(policy.toContentBlockingSetting())
.aboutConfigEnabled(Config.channel.isBeta)
.aboutConfigEnabled(true)
.debugLogging(Config.channel.isDebug)
.build()

@ -52,7 +52,7 @@ object GeckoProvider {
.crashHandler(CrashHandlerService::class.java)
.telemetryDelegate(GeckoAdapter())
.contentBlocking(policy.toContentBlockingSetting())
.aboutConfigEnabled(Config.channel.isBeta)
.aboutConfigEnabled(true)
.debugLogging(Config.channel.isDebug)
.build()

@ -0,0 +1,40 @@
/*
* Copyright (c) 2012-2017 adjust GmbH,
* http://www.adjust.com
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.adjust.sdk;
public class Adjust {
public static void onCreate(AdjustConfig adjustConfig) {
}
public static void onResume() {
}
public static void onPause() {
}
public static void setEnabled(boolean enabled) {
}
}

@ -0,0 +1,49 @@
/*
* Copyright (c) 2012-2017 adjust GmbH,
* http://www.adjust.com
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.adjust.sdk;
import java.io.Serializable;
public class AdjustAttribution implements Serializable {
public String network;
public String campaign;
public String adgroup;
public String creative;
@Override
public boolean equals(Object other) {
return false;
}
@Override
public int hashCode() {
return 0;
}
@Override
public String toString() {
return "";
}
}

@ -0,0 +1,46 @@
/*
* Copyright (c) 2012-2017 adjust GmbH,
* http://www.adjust.com
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.adjust.sdk;
import android.content.Context;
import java.util.List;
public class AdjustConfig {
public static final String ENVIRONMENT_SANDBOX = "sandbox";
public static final String ENVIRONMENT_PRODUCTION = "production";
public AdjustConfig(Context context, String appToken, String environment) {
}
public AdjustConfig(Context context, String appToken, String environment, boolean allowSuppressLogLevel) {
}
public void setOnAttributionChangedListener(OnAttributionChangedListener onAttributionChangedListener) {
}
public void setLogLevel(LogLevel logLevel) {
}
}

@ -0,0 +1,43 @@
/*
* Copyright (c) 2012-2017 adjust GmbH,
* http://www.adjust.com
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.adjust.sdk;
import android.util.Log;
/**
* Created by pfms on 11/03/15.
*/
public enum LogLevel {
VERBOSE(Log.VERBOSE), DEBUG(Log.DEBUG), INFO(Log.INFO), WARN(Log.WARN), ERROR(Log.ERROR), ASSERT(Log.ASSERT), SUPRESS(8);
final int androidLogLevel;
LogLevel(final int androidLogLevel) {
this.androidLogLevel = androidLogLevel;
}
public int getAndroidLogLevel() {
return androidLogLevel;
}
}

@ -0,0 +1,29 @@
/*
* Copyright (c) 2012-2017 adjust GmbH,
* http://www.adjust.com
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.adjust.sdk;
public interface OnAttributionChangedListener {
void onAttributionChanged(AdjustAttribution attribution);
}

@ -0,0 +1,41 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.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 com.google.android.gms.ads.identifier;
import android.content.Context;
import com.google.android.gms.common.GooglePlayServicesNotAvailableException;
import com.google.android.gms.common.GooglePlayServicesRepairableException;
public class AdvertisingIdClient {
public static final class Info {
private String mId;
public Info() {
mId = "";
}
public Info(String id, Boolean ignored) {
// We need to preserve the passed ID to pass Mozilla's tests.
mId = id;
}
public String getId() {
return mId;
}
public String toString() {
return mId;
}
}
public static Info getAdvertisingIdInfo(Context context) throws GooglePlayServicesNotAvailableException, GooglePlayServicesRepairableException {
return new Info();
}
}

@ -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/. */
package com.google.android.gms.common;
public class GooglePlayServicesNotAvailableException extends Exception {
public GooglePlayServicesNotAvailableException(int arg1) {
}
}

@ -0,0 +1,10 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.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 com.google.android.gms.common;
public class GooglePlayServicesRepairableException extends Exception {
public GooglePlayServicesRepairableException(int arg1, String arg2, Object arg3) {
}
}

@ -0,0 +1,43 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.firebase.messaging;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
public class FirebaseMessagingService extends Service {
private final IBinder mBinder = new Binder();
public void onMessageReceived(RemoteMessage message) {
}
public void onMessageSent(String msgId) {
}
public void onNewToken(String token) {
}
public void onSendError(String msgId, Exception exception) {
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}

@ -0,0 +1,53 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.firebase.messaging;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.Map;
public class RemoteMessage implements Parcelable {
protected RemoteMessage(Parcel in)
{
}
public static final Creator<RemoteMessage> CREATOR = new Creator<RemoteMessage>()
{
@Override
public RemoteMessage createFromParcel(Parcel in)
{
return new RemoteMessage(in);
}
@Override
public RemoteMessage[] newArray(int size)
{
return new RemoteMessage[size];
}
};
public int describeContents() {
return 0;
}
public void writeToParcel(Parcel out, int flags) {
}
public Map<String, String> getData() {
return null;
}
}

@ -0,0 +1,91 @@
/*
* Copyright 2016, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum;
import android.content.Context;
import com.leanplum.callbacks.StartCallback;
import java.util.Map;
public class Leanplum {
public static void setAppIdForDevelopmentMode(String appId, String accessKey) {
}
public static void setAppIdForProductionMode(String appId, String accessKey) {
}
public static void setApplicationContext(Context context) {
}
public static void setDeviceId(String deviceId) {
}
public static void setIsTestModeEnabled(boolean isTestModeEnabled) {
}
public static void setUserAttributes(Map<String, ?> userAttributes) {
}
public static void start(Context context) {
}
public static void start(Context context, StartCallback callback) {
}
public static void start(Context context, Map<String, ?> userAttributes) {
}
public static void start(Context context, String userId) {
}
public static void start(Context context, String userId, StartCallback callback) {
}
public static void start(Context context, String userId, Map<String, ?> userAttributes) {
}
public static synchronized void start(final Context context, String userId, Map<String, ?> attributes, StartCallback response) {
}
static synchronized void start(final Context context, final String userId, final Map<String, ?> attributes, StartCallback response, final Boolean isBackground) {
}
public static void track(final String event, double value, String info, Map<String, ?> params) {
}
public static void track(String event) {
}
public static void track(String event, double value) {
}
public static void track(String event, String info) {
}
public static void track(String event, Map<String, ?> params) {
}
public static void track(String event, double value, Map<String, ?> params) {
}
public static void track(String event, double value, String info) {
}
}

@ -0,0 +1,29 @@
/*
* Copyright 2013, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum;
import android.app.Application;
public class LeanplumActivityHelper {
public static void enableLifecycleCallbacks(final Application app) {
}
}

@ -0,0 +1,44 @@
/*
* Copyright 2016, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum;
import android.annotation.SuppressLint;
import android.os.Build;
import android.os.Bundle;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
@SuppressLint("Registered")
public class LeanplumPushFirebaseMessagingService extends FirebaseMessagingService {
@Override
public void onCreate() {
}
@Override
public void onNewToken(String token) {
}
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
}
}

@ -0,0 +1,65 @@
/*
* Copyright 2015, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum;
import android.app.Notification;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
/**
* Implement LeanplumPushNotificationCustomizer to customize the appearance of notifications.
*/
public interface LeanplumPushNotificationCustomizer {
/**
* Implement this method to customize push notification. Please call {@link
* LeanplumPushService#setCustomizer(LeanplumPushNotificationCustomizer)} to activate this method.
* Leave this method empty if you want to support 2 lines of text
* in BigPicture style push notification and implement {@link
* LeanplumPushNotificationCustomizer#customize(Notification.Builder, Bundle, Notification.Style)}
*
* @param builder NotificationCompat.Builder for push notification.
* @param notificationPayload Bundle notification payload.
*/
void customize(NotificationCompat.Builder builder, Bundle notificationPayload);
/**
* Implement this method to support 2 lines of text in BigPicture style push notification,
* otherwise implement {@link
* LeanplumPushNotificationCustomizer#customize(NotificationCompat.Builder, Bundle)} and leave
* this method empty. Please call {@link
* LeanplumPushService#setCustomizer(LeanplumPushNotificationCustomizer, boolean)} with true
* value to activate this method.
*
* @param builder Notification.Builder for push notification.
* @param notificationPayload Bundle notification payload.
* @param notificationStyle - Notification.BigPictureStyle or null - BigPicture style for current
* push notification. Call ((Notification.BigPictureStyle) notificationStyle).bigLargeIcon(largeIcon)
* if you want to set large icon on expanded push notification. If notificationStyle wasn't null
* it will be set to push notification. Note: If you call notificationStyle = new
* Notification.BigPictureStyle() or other Notification.Style - there will be no support 2 lines
* of text on BigPicture push and you need to call builder.setStyle(notificationStyle) to set
* yours expanded layout for push notification.
*/
void customize(Notification.Builder builder, Bundle notificationPayload,
@Nullable Notification.Style notificationStyle);
}

@ -0,0 +1,31 @@
/*
* Copyright 2014, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum;
public class LeanplumPushService {
public static void setCustomizer(LeanplumPushNotificationCustomizer customizer) {
}
public static void setCustomizer(LeanplumPushNotificationCustomizer customizer,
boolean useNotificationBuilderCustomizer) {
}
}

@ -0,0 +1,27 @@
/*
* Copyright 2013, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum.annotations;
public class Parser {
public static void parseVariables(Object... instances) {
}
}

@ -0,0 +1,41 @@
/*
* Copyright 2013, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum.callbacks;
/**
* Callback that gets run when Leanplum is started.
*
* @author Andrew First
*/
public abstract class StartCallback implements Runnable {
private boolean success;
public void setSuccess(boolean success) {
this.success = success;
}
public void run() {
this.onResponse(success);
}
public abstract void onResponse(boolean success);
}

@ -0,0 +1,33 @@
/*
* Copyright 2016, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum.internal;
public class LeanplumInternal {
public static void setCalledStart(boolean calledStart) {
}
public static void setHasStarted(boolean hasStarted) {
}
public static void setStartedInBackground(boolean startedInBackground) {
}
}

@ -0,0 +1,360 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.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 io.github.forkmaintainers.iceraven.components
import android.content.Context
import android.util.AtomicFile
import androidx.annotation.VisibleForTesting
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import mozilla.components.concept.fetch.Client
import mozilla.components.concept.fetch.Request
import mozilla.components.concept.fetch.isSuccess
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.AddonsProvider
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.ktx.kotlin.sanitizeURL
import mozilla.components.support.ktx.util.readAndDeserialize
import mozilla.components.support.ktx.util.writeString
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import java.io.File
import java.io.IOException
import java.util.Date
import java.util.concurrent.TimeUnit
internal const val API_VERSION = "api/v4"
internal const val DEFAULT_SERVER_URL = "https://addons.mozilla.org"
internal const val DEFAULT_COLLECTION_ACCOUNT = "mozilla"
internal const val DEFAULT_COLLECTION_NAME = "7e8d6dc651b54ab385fb8791bf9dac"
internal const val COLLECTION_FILE_NAME = "%s_components_addon_collection_%s.json"
internal const val MINUTE_IN_MS = 60 * 1000
internal const val DEFAULT_READ_TIMEOUT_IN_SECONDS = 20L
/**
* Provide access to the collections AMO API.
* https://addons-server.readthedocs.io/en/latest/topics/api/collections.html
*
* Unlike the android-components version, supports multiple-page responses and
* custom collection accounts.
*
* Needs to extend AddonCollectionProvider because AddonsManagerAdapter won't
* take just any AddonsProvider.
*
* @property serverURL The url of the endpoint to interact with e.g production, staging
* or testing. Defaults to [DEFAULT_SERVER_URL].
* @property collectionAccount The account owning the collection to access, defaults
* to [DEFAULT_COLLECTION_ACCOUNT].
* @property collectionName The name of the collection to access, defaults
* to [DEFAULT_COLLECTION_NAME].
* @property maxCacheAgeInMinutes maximum time (in minutes) the collection cache
* should remain valid. Defaults to -1, meaning no cache is being used by default.
* @property client A reference of [Client] for interacting with the AMO HTTP api.
*/
@Suppress("LongParameterList")
class PagedAddonCollectionProvider(
private val context: Context,
private val client: Client,
private val serverURL: String = DEFAULT_SERVER_URL,
private var collectionAccount: String = DEFAULT_COLLECTION_ACCOUNT,
private var collectionName: String = DEFAULT_COLLECTION_NAME,
private val maxCacheAgeInMinutes: Long = -1
) : AddonsProvider {
private val logger = Logger("PagedAddonCollectionProvider")
private val diskCacheLock = Any()
fun setCollectionAccount(account: String) {
collectionAccount = account
}
fun setCollectionName(collection: String) {
collectionName = collection
}
/**
* Interacts with the collections endpoint to provide a list of available
* add-ons. May return a cached response, if available, not expired (see
* [maxCacheAgeInMinutes]) and allowed (see [allowCache]).
*
* @param allowCache whether or not the result may be provided
* from a previously cached response, defaults to true.
* @param readTimeoutInSeconds optional timeout in seconds to use when fetching
* available add-ons from a remote endpoint. If not specified [DEFAULT_READ_TIMEOUT_IN_SECONDS]
* will be used.
* @throws IOException if the request failed, or could not be executed due to cancellation,
* a connectivity problem or a timeout.
*/
@Throws(IOException::class)
override suspend fun getAvailableAddons(allowCache: Boolean, readTimeoutInSeconds: Long?): List<Addon> {
val cachedAddons = if (allowCache && !cacheExpired(context)) {
readFromDiskCache()
} else {
null
}
if (cachedAddons != null) {
return cachedAddons
} else {
return getAllPages(listOf(
serverURL,
API_VERSION,
"accounts/account",
collectionAccount,
"collections",
collectionName,
"addons"
).joinToString("/"), readTimeoutInSeconds ?: DEFAULT_READ_TIMEOUT_IN_SECONDS).also {
// Cache the JSON object before we parse out the addons
if (maxCacheAgeInMinutes > 0) {
writeToDiskCache(it.toString())
}
}.getAddons()
}
}
/**
* Fetches all pages of add-ons from the given URL (following the "next"
* field in the returned JSON) and combines the "results" arrays into that
* of the first page. Returns that coalesced object.
*
* @param url URL of the first page to fetch
* @param readTimeoutInSeconds timeout in seconds to use when fetching each page.
* @throws IOException if the request failed, or could not be executed due to cancellation,
* a connectivity problem or a timeout.
*/
@Throws(IOException::class)
suspend fun getAllPages(url: String, readTimeoutInSeconds: Long): JSONObject {
// Fetch and compile all the pages into one object we can return
var compiledResponse: JSONObject? = null
// Each page tells us where to get the next page, if there is one
var nextURL: String? = url
while (nextURL != null) {
client.fetch(
Request(
url = nextURL,
readTimeout = Pair(readTimeoutInSeconds, TimeUnit.SECONDS)
)
)
.use { response ->
if (!response.isSuccess) {
val errorMessage = "Failed to fetch addon collection. Status code: ${response.status}"
logger.error(errorMessage)
throw IOException(errorMessage)
}
val currentResponse = try {
JSONObject(response.body.string(Charsets.UTF_8))
} catch (e: JSONException) {
throw IOException(e)
}
if (compiledResponse == null) {
compiledResponse = currentResponse
} else {
// Write the addons into the first response
compiledResponse!!.getJSONArray("results").concat(currentResponse.getJSONArray("results"))
}
nextURL = if (currentResponse.isNull("next")) null else currentResponse.getString("next")
}
}
return compiledResponse!!
}
/**
* Fetches given Addon icon from the url and returns a decoded Bitmap
* @throws IOException if the request could not be executed due to cancellation,
* a connectivity problem or a timeout.
*/
@Throws(IOException::class)
suspend fun getAddonIconBitmap(addon: Addon): Bitmap? {
var bitmap: Bitmap? = null
if (addon.iconUrl != "") {
client.fetch(
Request(url = addon.iconUrl.sanitizeURL())
).use { response ->
if (response.isSuccess) {
response.body.useStream {
bitmap = BitmapFactory.decodeStream(it)
}
}
}
}
return bitmap
}
@VisibleForTesting
internal fun writeToDiskCache(collectionResponse: String) {
synchronized(diskCacheLock) {
getCacheFile(context).writeString { collectionResponse }
}
}
@VisibleForTesting
internal fun readFromDiskCache(): List<Addon>? {
synchronized(diskCacheLock) {
return getCacheFile(context).readAndDeserialize {
JSONObject(it).getAddons()
}
}
}
@VisibleForTesting
internal fun cacheExpired(context: Context): Boolean {
return getCacheLastUpdated(context) < Date().time - maxCacheAgeInMinutes * MINUTE_IN_MS
}
@VisibleForTesting
internal fun getCacheLastUpdated(context: Context): Long {
val file = getBaseCacheFile(context)
return if (file.exists()) file.lastModified() else -1
}
private fun getCacheFile(context: Context): AtomicFile {
return AtomicFile(getBaseCacheFile(context))
}
private fun getBaseCacheFile(context: Context): File {
return File(context.filesDir, COLLECTION_FILE_NAME.format(collectionAccount, collectionName))
}
fun deleteCacheFile(context: Context): Boolean {
val file = getBaseCacheFile(context)
return if (file.exists()) file.delete() else false
}
}
internal fun JSONObject.getAddons(): List<Addon> {
val addonsJson = getJSONArray("results")
return (0 until addonsJson.length()).map { index ->
addonsJson.getJSONObject(index).toAddons()
}
}
internal fun JSONObject.toAddons(): Addon {
return with(getJSONObject("addon")) {
Addon(
id = getSafeString("guid"),
authors = getAuthors(),
categories = getCategories(),
createdAt = getSafeString("created"),
updatedAt = getSafeString("last_updated"),
downloadUrl = getDownloadUrl(),
version = getCurrentVersion(),
permissions = getPermissions(),
translatableName = getSafeMap("name"),
translatableDescription = getSafeMap("description"),
translatableSummary = getSafeMap("summary"),
iconUrl = getSafeString("icon_url"),
siteUrl = getSafeString("url"),
rating = getRating(),
defaultLocale = getSafeString("default_locale").ifEmpty { Addon.DEFAULT_LOCALE }
)
}
}
internal fun JSONObject.getRating(): Addon.Rating? {
val jsonRating = optJSONObject("ratings")
return if (jsonRating != null) {
Addon.Rating(
reviews = jsonRating.optInt("count"),
average = jsonRating.optDouble("average").toFloat()
)
} else {
null
}
}
internal fun JSONObject.getCategories(): List<String> {
val jsonCategories = optJSONObject("categories")
return if (jsonCategories == null) {
emptyList()
} else {
val jsonAndroidCategories = jsonCategories.getSafeJSONArray("android")
(0 until jsonAndroidCategories.length()).map { index ->
jsonAndroidCategories.getString(index)
}
}
}
internal fun JSONObject.getPermissions(): List<String> {
val fileJson = getJSONObject("current_version")
.getSafeJSONArray("files")
.getJSONObject(0)
val permissionsJson = fileJson.getSafeJSONArray("permissions")
return (0 until permissionsJson.length()).map { index ->
permissionsJson.getString(index)
}
}
internal fun JSONObject.getCurrentVersion(): String {
return optJSONObject("current_version")?.getSafeString("version") ?: ""
}
internal fun JSONObject.getDownloadUrl(): String {
return (getJSONObject("current_version")
.optJSONArray("files")
?.getJSONObject(0))
?.getSafeString("url") ?: ""
}
internal fun JSONObject.getAuthors(): List<Addon.Author> {
val authorsJson = getSafeJSONArray("authors")
return (0 until authorsJson.length()).map { index ->
val authorJson = authorsJson.getJSONObject(index)
Addon.Author(
id = authorJson.getSafeString("id"),
name = authorJson.getSafeString("name"),
username = authorJson.getSafeString("username"),
url = authorJson.getSafeString("url")
)
}
}
internal fun JSONObject.getSafeString(key: String): String {
return if (isNull(key)) {
""
} else {
getString(key)
}
}
internal fun JSONObject.getSafeJSONArray(key: String): JSONArray {
return if (isNull(key)) {
JSONArray("[]")
} else {
getJSONArray(key)
}
}
internal fun JSONObject.getSafeMap(valueKey: String): Map<String, String> {
return if (isNull(valueKey)) {
emptyMap()
} else {
val map = mutableMapOf<String, String>()
val jsonObject = getJSONObject(valueKey)
jsonObject.keys()
.forEach { key ->
map[key] = jsonObject.getSafeString(key)
}
map
}
}
/**
* Concatenates the given JSONArray onto this one.
*/
internal fun JSONArray.concat(other: JSONArray) {
(0 until other.length()).map { index ->
put(length(), other.getJSONObject(index))
}
}

@ -0,0 +1,301 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.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 io.github.forkmaintainers.iceraven.components
import android.annotation.SuppressLint
import android.app.Dialog
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.Button
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.ColorRes
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.appcompat.widget.AppCompatCheckBox
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentManager
import kotlinx.android.synthetic.main.mozac_feature_addons_fragment_dialog_addon_installed.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.R
import mozilla.components.feature.addons.ui.translatedName
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.ktx.android.content.appName
import mozilla.components.support.ktx.android.content.res.resolveAttribute
import java.io.IOException
@VisibleForTesting internal const val KEY_INSTALLED_ADDON = "KEY_ADDON"
private const val KEY_DIALOG_GRAVITY = "KEY_DIALOG_GRAVITY"
private const val KEY_DIALOG_WIDTH_MATCH_PARENT = "KEY_DIALOG_WIDTH_MATCH_PARENT"
private const val KEY_CONFIRM_BUTTON_BACKGROUND_COLOR = "KEY_CONFIRM_BUTTON_BACKGROUND_COLOR"
private const val KEY_CONFIRM_BUTTON_TEXT_COLOR = "KEY_CONFIRM_BUTTON_TEXT_COLOR"
private const val KEY_CONFIRM_BUTTON_RADIUS = "KEY_CONFIRM_BUTTON_RADIUS"
@VisibleForTesting internal const val KEY_ICON = "KEY_ICON"
private const val DEFAULT_VALUE = Int.MAX_VALUE
/**
* A dialog that shows [Addon] installation confirmation.
*/
class PagedAddonInstallationDialogFragment : AppCompatDialogFragment() {
private val scope = CoroutineScope(Dispatchers.IO)
@VisibleForTesting internal var iconJob: Job? = null
private val logger = Logger("PagedAddonInstallationDialogFragment")
/**
* A lambda called when the confirm button is clicked.
*/
var onConfirmButtonClicked: ((Addon, Boolean) -> Unit)? = null
/**
* Reference to the application's [PagedAddonCollectionProvider] to fetch add-on icons.
*/
var addonCollectionProvider: PagedAddonCollectionProvider? = null
private val safeArguments get() = requireNotNull(arguments)
internal val addon get() = requireNotNull(safeArguments.getParcelable<Addon>(KEY_ADDON))
private var allowPrivateBrowsing: Boolean = false
internal val confirmButtonRadius
get() =
safeArguments.getFloat(KEY_CONFIRM_BUTTON_RADIUS, DEFAULT_VALUE.toFloat())
internal val dialogGravity: Int
get() =
safeArguments.getInt(
KEY_DIALOG_GRAVITY,
DEFAULT_VALUE
)
internal val dialogShouldWidthMatchParent: Boolean
get() =
safeArguments.getBoolean(KEY_DIALOG_WIDTH_MATCH_PARENT)
internal val confirmButtonBackgroundColor
get() =
safeArguments.getInt(
KEY_CONFIRM_BUTTON_BACKGROUND_COLOR,
DEFAULT_VALUE
)
internal val confirmButtonTextColor
get() =
safeArguments.getInt(
KEY_CONFIRM_BUTTON_TEXT_COLOR,
DEFAULT_VALUE
)
override fun onStop() {
super.onStop()
iconJob?.cancel()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val sheetDialog = Dialog(requireContext())
sheetDialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
sheetDialog.setCanceledOnTouchOutside(true)
val rootView = createContainer()
sheetDialog.setContainerView(rootView)
sheetDialog.window?.apply {
if (dialogGravity != DEFAULT_VALUE) {
setGravity(dialogGravity)
}
if (dialogShouldWidthMatchParent) {
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
// This must be called after addContentView, or it won't fully fill to the edge.
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
}
}
return sheetDialog
}
private fun Dialog.setContainerView(rootView: View) {
if (dialogShouldWidthMatchParent) {
setContentView(rootView)
} else {
addContentView(
rootView,
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
)
)
}
}
@SuppressLint("InflateParams")
private fun createContainer(): View {
val rootView = LayoutInflater.from(requireContext()).inflate(
R.layout.mozac_feature_addons_fragment_dialog_addon_installed,
null,
false
)
rootView.findViewById<TextView>(R.id.title).text =
requireContext().getString(
R.string.mozac_feature_addons_installed_dialog_title,
addon.translatedName,
requireContext().appName
)
val icon = safeArguments.getParcelable<Bitmap>(KEY_ICON)
if (icon != null) {
rootView.icon.setImageDrawable(BitmapDrawable(resources, icon))
} else {
iconJob = fetchIcon(addon, rootView.icon)
}
val allowedInPrivateBrowsing = rootView.findViewById<AppCompatCheckBox>(R.id.allow_in_private_browsing)
allowedInPrivateBrowsing.setOnCheckedChangeListener { _, isChecked ->
allowPrivateBrowsing = isChecked
}
val confirmButton = rootView.findViewById<Button>(R.id.confirm_button)
confirmButton.setOnClickListener {
onConfirmButtonClicked?.invoke(addon, allowPrivateBrowsing)
dismiss()
}
if (confirmButtonBackgroundColor != DEFAULT_VALUE) {
val backgroundTintList =
ContextCompat.getColorStateList(requireContext(), confirmButtonBackgroundColor)
confirmButton.backgroundTintList = backgroundTintList
}
if (confirmButtonTextColor != DEFAULT_VALUE) {
val color = ContextCompat.getColor(requireContext(), confirmButtonTextColor)
confirmButton.setTextColor(color)
}
if (confirmButtonRadius != DEFAULT_VALUE.toFloat()) {
val shape = GradientDrawable()
shape.shape = GradientDrawable.RECTANGLE
shape.setColor(
ContextCompat.getColor(
requireContext(),
confirmButtonBackgroundColor
)
)
shape.cornerRadius = confirmButtonRadius
confirmButton.background = shape
}
return rootView
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun fetchIcon(addon: Addon, iconView: ImageView, scope: CoroutineScope = this.scope): Job {
return scope.launch {
try {
val iconBitmap = addonCollectionProvider?.getAddonIconBitmap(addon)
iconBitmap?.let {
scope.launch(Dispatchers.Main) {
safeArguments.putParcelable(KEY_ICON, it)
iconView.setImageDrawable(BitmapDrawable(iconView.resources, it))
}
}
} catch (e: IOException) {
scope.launch(Dispatchers.Main) {
val context = iconView.context
val att = context.theme.resolveAttribute(android.R.attr.textColorPrimary)
iconView.setColorFilter(ContextCompat.getColor(context, att))
iconView.setImageDrawable(context.getDrawable(R.drawable.mozac_ic_extensions))
}
logger.error("Attempt to fetch the ${addon.id} icon failed", e)
}
}
}
override fun show(manager: FragmentManager, tag: String?) {
// This dialog is shown as a result of an async operation (installing
// an add-on). Once installation succeeds, the activity may already be
// in the process of being destroyed. Since the dialog doesn't have any
// state we need to keep, and since it's also fine to not display the
// dialog at all in case the user navigates away, we can simply use
// commitAllowingStateLoss here to prevent crashing on commit:
// https://github.com/mozilla-mobile/android-components/issues/7782
val ft = manager.beginTransaction()
ft.add(this, tag)
ft.commitAllowingStateLoss()
}
@Suppress("LongParameterList")
companion object {
/**
* Returns a new instance of [AddonInstallationDialogFragment].
* @param addon The addon to show in the dialog.
* @param promptsStyling Styling properties for the dialog.
* @param onConfirmButtonClicked A lambda called when the confirm button is clicked.
*/
fun newInstance(
addon: Addon,
addonCollectionProvider: PagedAddonCollectionProvider,
promptsStyling: PromptsStyling? = PromptsStyling(
gravity = Gravity.BOTTOM,
shouldWidthMatchParent = true
),
onConfirmButtonClicked: ((Addon, Boolean) -> Unit)? = null
): PagedAddonInstallationDialogFragment {
val fragment = PagedAddonInstallationDialogFragment()
val arguments = fragment.arguments ?: Bundle()
arguments.apply {
putParcelable(KEY_INSTALLED_ADDON, addon)
promptsStyling?.gravity?.apply {
putInt(KEY_DIALOG_GRAVITY, this)
}
promptsStyling?.shouldWidthMatchParent?.apply {
putBoolean(KEY_DIALOG_WIDTH_MATCH_PARENT, this)
}
promptsStyling?.confirmButtonBackgroundColor?.apply {
putInt(KEY_CONFIRM_BUTTON_BACKGROUND_COLOR, this)
}
promptsStyling?.confirmButtonTextColor?.apply {
putInt(KEY_CONFIRM_BUTTON_TEXT_COLOR, this)
}
}
fragment.onConfirmButtonClicked = onConfirmButtonClicked
fragment.arguments = arguments
fragment.addonCollectionProvider = addonCollectionProvider
return fragment
}
}
/**
* Styling for the addon installation dialog.
*/
data class PromptsStyling(
val gravity: Int,
val shouldWidthMatchParent: Boolean = false,
@ColorRes
val confirmButtonBackgroundColor: Int? = null,
@ColorRes
val confirmButtonTextColor: Int? = null,
val confirmButtonRadius: Float? = null
)
}
internal const val KEY_ADDON = "KEY_ADDON"

@ -0,0 +1,429 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.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 io.github.forkmaintainers.iceraven.components
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.graphics.Typeface
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.TransitionDrawable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.RatingBar
import android.widget.TextView
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.annotation.VisibleForTesting
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.R
import mozilla.components.feature.addons.ui.AddonsManagerAdapterDelegate
import mozilla.components.feature.addons.ui.CustomViewHolder
import mozilla.components.feature.addons.ui.CustomViewHolder.AddonViewHolder
import mozilla.components.feature.addons.ui.CustomViewHolder.SectionViewHolder
import mozilla.components.feature.addons.ui.CustomViewHolder.UnsupportedSectionViewHolder
import mozilla.components.feature.addons.ui.translatedName
import mozilla.components.feature.addons.ui.translatedSummary
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.ktx.android.content.res.resolveAttribute
import java.io.IOException
import java.text.NumberFormat
import java.util.Locale
private const val VIEW_HOLDER_TYPE_SECTION = 0
private const val VIEW_HOLDER_TYPE_NOT_YET_SUPPORTED_SECTION = 1
private const val VIEW_HOLDER_TYPE_ADDON = 2
/**
* An adapter for displaying add-on items. This will display information related to the state of
* an add-on such as recommended, unsupported or installed. In addition, it will perform actions
* such as installing an add-on.
*
* @property addonCollectionProvider Provider of AMO collection API.
* @property addonsManagerDelegate Delegate that will provides method for handling the add-on items.
* @param addons The list of add-on based on the AMO store.
* @property style Indicates how items should look like.
*/
@Suppress("TooManyFunctions", "LargeClass")
class PagedAddonsManagerAdapter(
private val addonCollectionProvider: PagedAddonCollectionProvider,
private val addonsManagerDelegate: AddonsManagerAdapterDelegate,
addons: List<Addon>,
private val style: Style? = null
) : ListAdapter<Any, CustomViewHolder>(DifferCallback) {
private val scope = CoroutineScope(Dispatchers.IO)
private val logger = Logger("PagedAddonsManagerAdapter")
/**
* Represents all the add-ons that will be distributed in multiple headers like
* enabled, recommended and unsupported, this help have the data source of the items,
* displayed in the UI.
*/
@VisibleForTesting
internal var addonsMap: MutableMap<String, Addon> = addons.associateBy({ it.id }, { it }).toMutableMap()
init {
submitList(createListWithSections(addons))
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder {
return when (viewType) {
VIEW_HOLDER_TYPE_ADDON -> createAddonViewHolder(parent)
VIEW_HOLDER_TYPE_SECTION -> createSectionViewHolder(parent)
VIEW_HOLDER_TYPE_NOT_YET_SUPPORTED_SECTION -> createUnsupportedSectionViewHolder(parent)
else -> throw IllegalArgumentException("Unrecognized viewType")
}
}
private fun createSectionViewHolder(parent: ViewGroup): CustomViewHolder {
val context = parent.context
val inflater = LayoutInflater.from(context)
val view = inflater.inflate(R.layout.mozac_feature_addons_section_item, parent, false)
val titleView = view.findViewById<TextView>(R.id.title)
return SectionViewHolder(view, titleView)
}
private fun createUnsupportedSectionViewHolder(parent: ViewGroup): CustomViewHolder {
val context = parent.context
val inflater = LayoutInflater.from(context)
val view = inflater.inflate(
R.layout.mozac_feature_addons_section_unsupported_section_item,
parent,
false
)
val titleView = view.findViewById<TextView>(R.id.title)
val descriptionView = view.findViewById<TextView>(R.id.description)
return UnsupportedSectionViewHolder(view, titleView, descriptionView)
}
private fun createAddonViewHolder(parent: ViewGroup): AddonViewHolder {
val context = parent.context
val inflater = LayoutInflater.from(context)
val view = inflater.inflate(R.layout.mozac_feature_addons_item, parent, false)
val iconView = view.findViewById<ImageView>(R.id.add_on_icon)
val titleView = view.findViewById<TextView>(R.id.add_on_name)
val summaryView = view.findViewById<TextView>(R.id.add_on_description)
val ratingView = view.findViewById<RatingBar>(R.id.rating)
val ratingAccessibleView = view.findViewById<TextView>(R.id.rating_accessibility)
val userCountView = view.findViewById<TextView>(R.id.users_count)
val addButton = view.findViewById<ImageView>(R.id.add_button)
val allowedInPrivateBrowsingLabel = view.findViewById<ImageView>(R.id.allowed_in_private_browsing_label)
return AddonViewHolder(
view,
iconView,
titleView,
summaryView,
ratingView,
ratingAccessibleView,
userCountView,
addButton,
allowedInPrivateBrowsingLabel
)
}
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is Addon -> VIEW_HOLDER_TYPE_ADDON
is Section -> VIEW_HOLDER_TYPE_SECTION
is NotYetSupportedSection -> VIEW_HOLDER_TYPE_NOT_YET_SUPPORTED_SECTION
else -> throw IllegalArgumentException("items[position] has unrecognized type")
}
}
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
val item = getItem(position)
when (holder) {
is SectionViewHolder -> bindSection(holder, item as Section)
is AddonViewHolder -> bindAddon(holder, item as Addon)
is UnsupportedSectionViewHolder -> bindNotYetSupportedSection(
holder,
item as NotYetSupportedSection
)
}
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun bindSection(holder: SectionViewHolder, section: Section) {
holder.titleView.setText(section.title)
style?.maybeSetSectionsTextColor(holder.titleView)
style?.maybeSetSectionsTypeFace(holder.titleView)
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun bindNotYetSupportedSection(
holder: UnsupportedSectionViewHolder,
section: NotYetSupportedSection
) {
val unsupportedAddons = addonsMap.values.filter { it.inUnsupportedSection() }
val context = holder.itemView.context
holder.titleView.setText(section.title)
holder.descriptionView.text =
if (unsupportedAddons.size == 1) {
context.getString(R.string.mozac_feature_addons_unsupported_caption)
} else {
context.getString(
R.string.mozac_feature_addons_unsupported_caption_plural,
unsupportedAddons.size.toString()
)
}
holder.itemView.setOnClickListener {
addonsManagerDelegate.onNotYetSupportedSectionClicked(unsupportedAddons)
}
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun bindAddon(holder: AddonViewHolder, addon: Addon) {
val context = holder.itemView.context
addon.rating?.let {
val userCount = context.getString(R.string.mozac_feature_addons_user_rating_count_2)
val ratingContentDescription =
String.format(
context.getString(R.string.mozac_feature_addons_rating_content_description),
it.average
)
holder.ratingView.contentDescription = ratingContentDescription
// Android RatingBar is not very accessibility-friendly, we will use non visible TextView
// for contentDescription for the TalkBack feature
holder.ratingAccessibleView.text = ratingContentDescription
holder.ratingView.rating = it.average
holder.userCountView.text = String.format(userCount, getFormattedAmount(it.reviews))
}
holder.titleView.text =
if (addon.translatableName.isNotEmpty()) {
addon.translatedName
} else {
addon.id
}
if (addon.translatableSummary.isNotEmpty()) {
holder.summaryView.text = addon.translatedSummary
} else {
holder.summaryView.visibility = View.GONE
}
holder.itemView.tag = addon
holder.itemView.setOnClickListener {
addonsManagerDelegate.onAddonItemClicked(addon)
}
holder.addButton.isVisible = !addon.isInstalled()
holder.addButton.setOnClickListener {
if (!addon.isInstalled()) {
addonsManagerDelegate.onInstallAddonButtonClicked(addon)
}
}
holder.allowedInPrivateBrowsingLabel.isVisible = addon.isAllowedInPrivateBrowsing()
style?.maybeSetPrivateBrowsingLabelDrawale(holder.allowedInPrivateBrowsingLabel)
fetchIcon(addon, holder.iconView)
style?.maybeSetAddonNameTextColor(holder.titleView)
style?.maybeSetAddonSummaryTextColor(holder.summaryView)
}
@Suppress("MagicNumber")
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun fetchIcon(addon: Addon, iconView: ImageView, scope: CoroutineScope = this.scope): Job {
return scope.launch {
try {
// We calculate how much time takes to fetch an icon,
// if takes less than a second, we assume it comes
// from a cache and we don't show any transition animation.
val startTime = System.currentTimeMillis()
val iconBitmap = addonCollectionProvider.getAddonIconBitmap(addon)
val timeToFetch: Double = (System.currentTimeMillis() - startTime) / 1000.0
val isFromCache = timeToFetch < 1
iconBitmap?.let {
scope.launch(Main) {
if (isFromCache) {
iconView.setImageDrawable(BitmapDrawable(iconView.resources, it))
} else {
setWithCrossFadeAnimation(iconView, it)
}
}
}
} catch (e: IOException) {
scope.launch(Main) {
val context = iconView.context
val att = context.theme.resolveAttribute(android.R.attr.textColorPrimary)
iconView.setColorFilter(ContextCompat.getColor(context, att))
iconView.setImageDrawable(context.getDrawable(R.drawable.mozac_ic_extensions))
}
logger.error("Attempt to fetch the ${addon.id} icon failed", e)
}
}
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
@Suppress("ComplexMethod")
internal fun createListWithSections(addons: List<Addon>): List<Any> {
val itemsWithSections = ArrayList<Any>()
val installedAddons = ArrayList<Addon>()
val recommendedAddons = ArrayList<Addon>()
val disabledAddons = ArrayList<Addon>()
val unsupportedAddons = ArrayList<Addon>()
addons.forEach { addon ->
when {
addon.inUnsupportedSection() -> unsupportedAddons.add(addon)
addon.inRecommendedSection() -> recommendedAddons.add(addon)
addon.inInstalledSection() -> installedAddons.add(addon)
addon.inDisabledSection() -> disabledAddons.add(addon)
}
}
// Add installed section and addons if available
if (installedAddons.isNotEmpty()) {
itemsWithSections.add(Section(R.string.mozac_feature_addons_enabled))
itemsWithSections.addAll(installedAddons)
}
// Add disabled section and addons if available
if (disabledAddons.isNotEmpty()) {
itemsWithSections.add(Section(R.string.mozac_feature_addons_disabled_section))
itemsWithSections.addAll(disabledAddons)
}
// Add recommended section and addons if available
if (recommendedAddons.isNotEmpty()) {
itemsWithSections.add(Section(R.string.mozac_feature_addons_recommended_section))
itemsWithSections.addAll(recommendedAddons)
}
// Add unsupported section
if (unsupportedAddons.isNotEmpty()) {
itemsWithSections.add(NotYetSupportedSection(R.string.mozac_feature_addons_unavailable_section))
}
return itemsWithSections
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal data class Section(@StringRes val title: Int)
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal data class NotYetSupportedSection(@StringRes val title: Int)
/**
* Allows to customize how items should look like.
*/
data class Style(
@ColorRes
val sectionsTextColor: Int? = null,
@ColorRes
val addonNameTextColor: Int? = null,
@ColorRes
val addonSummaryTextColor: Int? = null,
val sectionsTypeFace: Typeface? = null,
@DrawableRes
val addonAllowPrivateBrowsingLabelDrawableRes: Int? = null
) {
internal fun maybeSetSectionsTextColor(textView: TextView) {
sectionsTextColor?.let {
val color = ContextCompat.getColor(textView.context, it)
textView.setTextColor(color)
}
}
internal fun maybeSetSectionsTypeFace(textView: TextView) {
sectionsTypeFace?.let {
textView.typeface = it
}
}
internal fun maybeSetAddonNameTextColor(textView: TextView) {
addonNameTextColor?.let {
val color = ContextCompat.getColor(textView.context, it)
textView.setTextColor(color)
}
}
internal fun maybeSetAddonSummaryTextColor(textView: TextView) {
addonSummaryTextColor?.let {
val color = ContextCompat.getColor(textView.context, it)
textView.setTextColor(color)
}
}
internal fun maybeSetPrivateBrowsingLabelDrawale(imageView: ImageView) {
addonAllowPrivateBrowsingLabelDrawableRes?.let {
imageView.setImageDrawable(ContextCompat.getDrawable(imageView.context, it))
}
}
}
/**
* Update the portion of the list that contains the provided [addon].
* @property addon The add-on to be updated.
*/
fun updateAddon(addon: Addon) {
addonsMap[addon.id] = addon
submitList(createListWithSections(addonsMap.values.toList()))
}
/**
* Updates only the portion of the list that changes between the current list and the new provided [addons].
* Be aware that updating a subset of the visible list is not supported, [addons] will replace
* the current list, but only the add-ons that have been changed will be updated in the UI.
* If you provide a subset it will replace the current list.
* @property addons A list of add-on to replace the actual list.
*/
fun updateAddons(addons: List<Addon>) {
addonsMap = addons.associateBy({ it.id }, { it }).toMutableMap()
submitList(createListWithSections(addons))
}
internal object DifferCallback : DiffUtil.ItemCallback<Any>() {
override fun areItemsTheSame(oldItem: Any, newItem: Any): Boolean {
return when {
oldItem is Addon && newItem is Addon -> oldItem.id == newItem.id
oldItem is Section && newItem is Section -> oldItem.title == newItem.title
oldItem is NotYetSupportedSection && newItem is NotYetSupportedSection -> oldItem.title == newItem.title
else -> false
}
}
@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean {
return oldItem == newItem
}
}
internal fun setWithCrossFadeAnimation(image: ImageView, bitmap: Bitmap, durationMillis: Int = 1700) {
with(image) {
val bitmapDrawable = BitmapDrawable(context.resources, bitmap)
val animation = TransitionDrawable(arrayOf(drawable, bitmapDrawable))
animation.isCrossFadeEnabled = true
setImageDrawable(animation)
animation.startTransition(durationMillis)
}
}
}
private fun Addon.inUnsupportedSection() = isInstalled() && !isSupported()
private fun Addon.inRecommendedSection() = !isInstalled()
private fun Addon.inInstalledSection() = isInstalled() && isSupported() && isEnabled()
private fun Addon.inDisabledSection() = isInstalled() && isSupported() && !isEnabled()
/**
* Get the formatted number amount for the current default locale.
*/
internal fun getFormattedAmount(amount: Int): String {
return NumberFormat.getNumberInstance(Locale.getDefault()).format(amount)
}

@ -0,0 +1,37 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.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.lib.push.firebase
import android.content.Context
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import mozilla.components.concept.push.PushService
abstract class AbstractFirebasePushService : FirebaseMessagingService(), PushService {
override fun start(context: Context) {
// no-op
}
override fun onNewToken(newToken: String) {
// no-op
}
override fun onMessageReceived(remoteMessage: RemoteMessage?) {
// no-op
}
final override fun stop() {
// no-op
}
override fun deleteToken() {
// no-op
}
override fun isServiceAvailable(context: Context): Boolean {
return false
}
}

@ -6,13 +6,16 @@ package org.mozilla.fenix
enum class ReleaseChannel {
Debug,
ForkDebug,
Nightly,
Beta,
Release;
Release,
ForkRelease;
val isReleased: Boolean
get() = when (this) {
Debug -> false
ForkDebug -> false
else -> true
}
@ -27,11 +30,12 @@ enum class ReleaseChannel {
get() = !this.isReleased
val isReleaseOrBeta: Boolean
get() = this == Release || this == Beta
get() = this == Release || this == Beta || this == ForkRelease
val isRelease: Boolean
get() = when (this) {
Release -> true
ForkRelease -> true
else -> false
}
@ -39,7 +43,7 @@ enum class ReleaseChannel {
get() = this == Beta
val isNightlyOrDebug: Boolean
get() = this == Debug || this == Nightly
get() = this == Debug || this == Nightly || this == ForkDebug
/**
* Is this a build for a release channel that we used to ship Fennec on?
@ -52,6 +56,16 @@ enum class ReleaseChannel {
*/
val isFenix: Boolean
get() = !isFennec
/**
* Is this a rebranded fork?
*/
val isFork: Boolean
get() = when (this) {
ForkDebug -> true
ForkRelease -> true
else -> false
}
}
object Config {
@ -60,6 +74,8 @@ object Config {
"nightly" -> ReleaseChannel.Nightly
"beta" -> ReleaseChannel.Beta
"release" -> ReleaseChannel.Release
"forkDebug" -> ReleaseChannel.ForkDebug
"forkRelease" -> ReleaseChannel.ForkRelease
else -> {
throw IllegalStateException("Unknown build type: ${BuildConfig.BUILD_TYPE}")
}

@ -12,7 +12,7 @@ object FeatureFlags {
* Pull-to-refresh allows you to pull the web content down far enough to have the page to
* reload.
*/
const val pullToRefreshEnabled = false
const val pullToRefreshEnabled = true
/**
* Shows Synced Tabs in the tabs tray.
@ -34,7 +34,7 @@ object FeatureFlags {
/**
* Enables wait til first contentful paint
*/
val waitUntilPaintToDraw = Config.channel.isNightlyOrDebug
const val waitUntilPaintToDraw = true
/**
* Enables downloads with external download managers.

@ -7,26 +7,35 @@ package org.mozilla.fenix.addons
import android.content.Context
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.View
import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import android.view.inputmethod.EditorInfo
import androidx.appcompat.widget.SearchView
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.fragment_add_ons_management.*
import kotlinx.android.synthetic.main.fragment_add_ons_management.view.*
import kotlinx.android.synthetic.main.overlay_add_on_progress.view.*
import kotlinx.android.synthetic.main.fragment_add_ons_management.addonProgressOverlay
import kotlinx.android.synthetic.main.fragment_add_ons_management.view.add_ons_empty_message
import kotlinx.android.synthetic.main.fragment_add_ons_management.view.add_ons_list
import kotlinx.android.synthetic.main.fragment_add_ons_management.view.add_ons_progress_bar
import kotlinx.android.synthetic.main.overlay_add_on_progress.view.add_ons_overlay_text
import kotlinx.android.synthetic.main.overlay_add_on_progress.view.cancel_button
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.AddonManagerException
import mozilla.components.feature.addons.ui.AddonInstallationDialogFragment
import mozilla.components.feature.addons.ui.AddonsManagerAdapter
import mozilla.components.feature.addons.ui.PermissionsDialogFragment
import mozilla.components.feature.addons.ui.translatedName
import io.github.forkmaintainers.iceraven.components.PagedAddonInstallationDialogFragment
import io.github.forkmaintainers.iceraven.components.PagedAddonsManagerAdapter
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
@ -35,25 +44,90 @@ import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.theme.ThemeManager
import java.util.Locale
import java.util.concurrent.CancellationException
/**
* Fragment use for managing add-ons.
*/
@Suppress("TooManyFunctions")
@Suppress("LargeClass", "TooManyFunctions")
class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management) {
/**
* Whether or not an add-on installation is in progress.
*/
private var isInstallationInProgress = false
private var adapter: AddonsManagerAdapter? = null
private var adapter: PagedAddonsManagerAdapter? = null
private var addons: List<Addon>? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
setHasOptionsMenu(true)
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
bindRecyclerView(view)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.addons_menu, menu)
val searchItem = menu.findItem(R.id.search)
val searchView: SearchView = searchItem.actionView as SearchView
searchView.imeOptions = EditorInfo.IME_ACTION_DONE
searchView.queryHint = getString(R.string.addons_search_hint)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
return searchAddons(query.trim())
}
override fun onQueryTextChange(newText: String): Boolean {
return searchAddons(newText.trim())
}
})
}
private fun searchAddons(addonNameSubStr: String): Boolean {
if (adapter == null) {
return false
}
val searchedAddons = arrayListOf<Addon>()
addons?.forEach { addon ->
val names = addon.translatableName
names["en-US"]?.let { name ->
if (name.toLowerCase(Locale.ENGLISH).contains(addonNameSubStr.toLowerCase(Locale.ENGLISH))) {
searchedAddons.add(addon)
}
}
}
updateUI(searchedAddons)
return true
}
private fun updateUI(searchedAddons: List<Addon>) {
adapter?.updateAddons(searchedAddons)
if (searchedAddons.isEmpty()) {
view?.let { view ->
view.add_ons_empty_message.visibility = View.VISIBLE
view.add_ons_list.visibility = View.GONE
}
} else {
view?.let { view ->
view.add_ons_empty_message.visibility = View.GONE
view.add_ons_list.visibility = View.VISIBLE
}
}
}
override fun onResume() {
super.onResume()
showToolbar(getString(R.string.preferences_addons))
@ -83,14 +157,14 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
val shouldRefresh = adapter != null
lifecycleScope.launch(IO) {
try {
val addons = requireContext().components.addonManager.getAddons()
addons = requireContext().components.addonManager.getAddons()
lifecycleScope.launch(Dispatchers.Main) {
runIfFragmentIsAttached {
if (!shouldRefresh) {
adapter = AddonsManagerAdapter(
adapter = PagedAddonsManagerAdapter(
requireContext().components.addonCollectionProvider,
managementView,
addons,
addons!!,
style = createAddonStyle(requireContext())
)
}
@ -100,7 +174,7 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
recyclerView.adapter = adapter
if (shouldRefresh) {
adapter?.updateAddons(addons)
adapter?.updateAddons(addons!!)
}
}
}
@ -120,8 +194,8 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
}
}
private fun createAddonStyle(context: Context): AddonsManagerAdapter.Style {
return AddonsManagerAdapter.Style(
private fun createAddonStyle(context: Context): PagedAddonsManagerAdapter.Style {
return PagedAddonsManagerAdapter.Style(
sectionsTextColor = ThemeManager.resolveAttribute(R.attr.primaryText, context),
addonNameTextColor = ThemeManager.resolveAttribute(R.attr.primaryText, context),
addonSummaryTextColor = ThemeManager.resolveAttribute(R.attr.secondaryText, context),
@ -140,7 +214,7 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
private fun hasExistingAddonInstallationDialogFragment(): Boolean {
return parentFragmentManager.findFragmentByTag(INSTALLATION_DIALOG_FRAGMENT_TAG)
as? AddonInstallationDialogFragment != null
as? PagedAddonInstallationDialogFragment != null
}
private fun showPermissionDialog(addon: Addon) {
@ -171,10 +245,10 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
requireComponents.analytics.metrics.track(Event.AddonInstalled(addon.id))
val addonCollectionProvider = requireContext().components.addonCollectionProvider
val dialog = AddonInstallationDialogFragment.newInstance(
val dialog = PagedAddonInstallationDialogFragment.newInstance(
addon = addon,
addonCollectionProvider = addonCollectionProvider,
promptsStyling = AddonInstallationDialogFragment.PromptsStyling(
promptsStyling = PagedAddonInstallationDialogFragment.PromptsStyling(
gravity = Gravity.BOTTOM,
shouldWidthMatchParent = true,
confirmButtonBackgroundColor = ThemeManager.resolveAttribute(

@ -99,6 +99,7 @@ import org.mozilla.fenix.components.toolbar.ToolbarIntegration
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.downloads.DownloadService
import org.mozilla.fenix.downloads.DynamicDownloadDialog
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.accessibilityManager
import org.mozilla.fenix.ext.breadcrumb
import org.mozilla.fenix.ext.components

@ -9,16 +9,16 @@ import android.content.Context
import android.content.Intent
import androidx.core.net.toUri
import mozilla.components.feature.addons.AddonManager
import mozilla.components.feature.addons.amo.AddonCollectionProvider
import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker
import mozilla.components.feature.addons.migration.SupportedAddonsChecker
import mozilla.components.feature.addons.update.AddonUpdater
import mozilla.components.feature.addons.update.DefaultAddonUpdater
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.support.migration.state.MigrationStore
import org.mozilla.fenix.BuildConfig
import io.github.forkmaintainers.iceraven.components.PagedAddonCollectionProvider
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.components.metrics.AppStartupTelemetry
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.utils.ClipboardHandler
import org.mozilla.fenix.utils.Mockable
import org.mozilla.fenix.utils.Settings
@ -71,16 +71,15 @@ class Components(private val context: Context) {
}
val addonCollectionProvider by lazy {
if (!BuildConfig.AMO_COLLECTION.isNullOrEmpty()) {
AddonCollectionProvider(
context,
core.client,
collectionName = BuildConfig.AMO_COLLECTION,
maxCacheAgeInMinutes = DAY_IN_MINUTES
)
} else {
AddonCollectionProvider(context, core.client, maxCacheAgeInMinutes = DAY_IN_MINUTES)
}
val addonsAccount = context.settings().customAddonsAccount
val addonsCollection = context.settings().customAddonsCollection
PagedAddonCollectionProvider(
context,
core.client,
collectionAccount = addonsAccount,
collectionName = addonsCollection,
maxCacheAgeInMinutes = DAY_IN_MINUTES
)
}
val appStartupTelemetry by lazy { AppStartupTelemetry(analytics.metrics) }
@ -105,6 +104,15 @@ class Components(private val context: Context) {
AddonManager(core.store, core.engine, addonCollectionProvider, addonUpdater)
}
fun updateAddonManager() {
addonCollectionProvider.deleteCacheFile(context)
val addonsAccount = context.settings().customAddonsAccount
val addonsCollection = context.settings().customAddonsCollection
addonCollectionProvider.setCollectionAccount(addonsAccount)
addonCollectionProvider.setCollectionName(addonsCollection)
}
val analytics by lazy { Analytics(context) }
val publicSuffixList by lazy { PublicSuffixList(context) }
val clipboardHandler by lazy { ClipboardHandler(context) }

@ -7,9 +7,6 @@ package org.mozilla.fenix.components
import android.app.Activity
import android.content.Context
import androidx.annotation.VisibleForTesting
import com.google.android.play.core.review.ReviewManagerFactory
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.withContext
import org.mozilla.fenix.utils.Settings
/**
@ -44,17 +41,7 @@ class ReviewPromptController(
private val context: Context,
private val reviewSettings: ReviewSettings,
private val timeNowInMillis: () -> Long = { System.currentTimeMillis() },
private val tryPromptReview: suspend (Activity) -> Unit = { activity ->
val manager = ReviewManagerFactory.create(context)
val flow = manager.requestReviewFlow()
withContext(Main) {
flow.addOnCompleteListener {
if (it.isSuccessful) {
manager.launchReviewFlow(activity, it.result)
}
}
}
private val tryPromptReview: suspend (Activity) -> Unit = { _ ->
}
) {
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)

@ -139,7 +139,12 @@ class BrowserToolbarView(
ThemeManager.resolveAttribute(R.attr.toolbarDivider, container.context)
)
display.urlFormatter = { url -> URLStringUtils.toDisplayUrl(url) }
display.urlFormatter =
if (context.settings().shouldStripUrl) {
url -> URLStringUtils.toDisplayUrl(url)
} else {
url -> url
}
display.colors = display.colors.copy(
text = primaryTextColor,

@ -0,0 +1,827 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* Added the Mozilla Public License above to avoid failing detekt rule */
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mozilla.fenix.components.topsheet
import android.animation.ValueAnimator
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import android.util.AttributeSet
import android.util.TypedValue
import android.view.AbsSavedState
import android.view.MotionEvent
import android.view.VelocityTracker
import android.view.View
import android.view.ViewConfiguration
import android.view.ViewGroup
import androidx.annotation.IntDef
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.NestedScrollingChild
import androidx.core.view.ViewCompat
import androidx.customview.widget.ViewDragHelper
import com.google.android.material.R
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.shape.ShapeAppearanceModel
import java.lang.ref.WeakReference
import kotlin.math.abs
/**
* An interaction behavior plugin for a child view of [CoordinatorLayout] to make it work as
* a top sheet.
*/
@Suppress("TooManyFunctions", "LargeClass")
class TopSheetBehavior<V : View?>
/**
* Default constructor for inflating TopSheetBehaviors from layout.
*
* @param context The [Context].
* @param attrs The [AttributeSet].
*/(context: Context, attrs: AttributeSet?) : CoordinatorLayout.Behavior<V>(context, attrs) {
/**
* Callback for monitoring events about top sheets.
*/
interface TopSheetCallback {
/**
* Called when the top sheet changes its state.
*
* @param topSheet The top sheet view.
* @param newState The new state. This will be one of [.STATE_DRAGGING],
* [.STATE_SETTLING], [.STATE_EXPANDED],
* [.STATE_COLLAPSED], or [.STATE_HIDDEN].
*/
fun onStateChanged(topSheet: View, @State newState: Int)
/**
* Called when the top sheet is being dragged.
*
* @param topSheet The top sheet view.
* @param slideOffset The new offset of this top sheet within its range, from 0 to 1
* when it is moving upward, and from 0 to -1 when it moving downward.
* @param isOpening detect showing
*/
fun onSlide(topSheet: View, slideOffset: Float, isOpening: Boolean?)
}
/**
* @hide
*/
@IntDef(
STATE_EXPANDED,
STATE_COLLAPSED,
STATE_DRAGGING,
STATE_SETTLING,
STATE_HIDDEN
)
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
annotation class State
private var mMaximumVelocity = 0f
private var mPeekHeight = 0
private var mMinOffset = 0
private var mMaxOffset = 0
private var skipCollapsed = false
/**
* Gets/Sets the height of the top sheet when it is collapsed.
*
* @var peekHeight The height of the collapsed top sheet in pixels.
* @attr ref com.google.android.material.R.styleable#TopSheetBehavior_Params_behavior_peekHeight
*/
private var peekHeight: Int
get() = mPeekHeight
set(peekHeight) {
mPeekHeight = 0.coerceAtLeast(peekHeight)
if (mViewRef != null && mViewRef!!.get() != null) {
mMinOffset =
(-mViewRef!!.get()!!.height).coerceAtLeast(
-(mViewRef!!.get()!!.height - mPeekHeight))
}
}
@State
private var mState = STATE_COLLAPSED
private var mViewDragHelper: ViewDragHelper? = null
private var mIgnoreEvents = false
private var mLastNestedScrollDy = 0
private var mNestedScrolled = false
private var mParentHeight = 0
private var mViewRef: WeakReference<V?>? = null
private var mNestedScrollingChildRef: WeakReference<View?>? = null
private var mCallback: TopSheetCallback? = null
private var mVelocityTracker: VelocityTracker? = null
private var mActivePointerId = 0
private var mInitialY = 0
private var mTouchingScrollingChild = false
/** True if Behavior has a non-null value for the @shapeAppearance attribute */
private var shapeThemingEnabled = false
/** Default Shape Appearance to be used in topsheet */
private var shapeAppearanceModelDefault: ShapeAppearanceModel? = null
private var materialShapeDrawable: MaterialShapeDrawable? = null
private var interpolatorAnimator: ValueAnimator? = null
private var isShapeExpanded = false
var elevation = -1f
var isHideable = false
init {
val a = context.obtainStyledAttributes(
attrs,
R.styleable.BottomSheetBehavior_Layout
)
shapeThemingEnabled =
a.hasValue(R.styleable.BottomSheetBehavior_Layout_shapeAppearance)
createMaterialShapeDrawable(context, attrs!!)
createShapeValueAnimator()
peekHeight = (context.resources.displayMetrics.heightPixels * PEEK_HEIGHT_RATIO).toInt()
isHideable = a.getBoolean(
R.styleable.BottomSheetBehavior_Layout_behavior_hideable,
false
)
skipCollapsed = a.getBoolean(
R.styleable.BottomSheetBehavior_Layout_behavior_skipCollapsed,
false
)
a.recycle()
val configuration =
ViewConfiguration.get(context)
mMaximumVelocity = configuration.scaledMaximumFlingVelocity.toFloat()
}
override fun onSaveInstanceState(parent: CoordinatorLayout, child: V): Parcelable? {
return SavedState(
super.onSaveInstanceState(
parent,
child
), mState
)
}
override fun onRestoreInstanceState(
parent: CoordinatorLayout,
child: V,
state: Parcelable
) {
val ss =
state as SavedState
super.onRestoreInstanceState(parent, child, ss.superState)
// Intermediate states are restored as collapsed state
mState = if (ss.state == STATE_DRAGGING || ss.state == STATE_SETTLING) {
STATE_COLLAPSED
} else {
ss.state
}
}
@Suppress("ComplexMethod")
override fun onLayoutChild(
parent: CoordinatorLayout,
child: V,
layoutDirection: Int
): Boolean {
if (ViewCompat.getFitsSystemWindows(parent) && !ViewCompat.getFitsSystemWindows(child as View)) {
child.fitsSystemWindows = true
}
// Only set MaterialShapeDrawable as background if shapeTheming is enabled, otherwise will
// default to android:background declared in styles or layout.
if (shapeThemingEnabled && materialShapeDrawable != null) {
ViewCompat.setBackground(child as View, materialShapeDrawable)
}
// Set elevation on MaterialShapeDrawable
// Set elevation on MaterialShapeDrawable
if (materialShapeDrawable != null) {
// Use elevation attr if set on topsheet; otherwise, use elevation of child view.
materialShapeDrawable!!.elevation =
if (elevation == -1f) ViewCompat.getElevation(child as View) else elevation
// Update the material shape based on initial state.
isShapeExpanded = state == BottomSheetBehavior.STATE_EXPANDED
materialShapeDrawable!!.interpolation = if (isShapeExpanded) 0f else 1f
}
val savedTop = child!!.top
// First let the parent lay it out
parent.onLayoutChild(child, layoutDirection)
// Offset the top sheet
mParentHeight = parent.height
mMinOffset = (-child.height).coerceAtLeast(-(child.height - mPeekHeight))
mMaxOffset = 0
if (mState == STATE_EXPANDED) {
ViewCompat.offsetTopAndBottom(child, mMaxOffset)
} else if (isHideable && mState == STATE_HIDDEN) {
ViewCompat.offsetTopAndBottom(child, mParentHeight)
} else if (mState == STATE_COLLAPSED) {
ViewCompat.offsetTopAndBottom(child, mMinOffset)
} else if (mState == STATE_DRAGGING || mState == STATE_SETTLING) {
ViewCompat.offsetTopAndBottom(child, savedTop - child.top)
}
if (mViewDragHelper == null) {
mViewDragHelper = ViewDragHelper.create(parent, mDragCallback)
}
mViewRef = WeakReference(child)
mNestedScrollingChildRef =
WeakReference(findScrollingChild(child))
return true
}
@Suppress("ComplexMethod", "ReturnCount")
override fun onInterceptTouchEvent(
parent: CoordinatorLayout,
child: V,
event: MotionEvent
): Boolean {
if (!child!!.isShown) {
return false
}
val action = event.actionMasked
// Record the velocity
if (action == MotionEvent.ACTION_DOWN) {
reset()
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain()
}
mVelocityTracker!!.addMovement(event)
when (action) {
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
mTouchingScrollingChild = false
mActivePointerId = MotionEvent.INVALID_POINTER_ID
// Reset the ignore flag
if (mIgnoreEvents) {
mIgnoreEvents = false
return false
}
}
MotionEvent.ACTION_DOWN -> {
val initialX = event.x.toInt()
mInitialY = event.y.toInt()
val scroll = mNestedScrollingChildRef!!.get()
if (scroll != null && parent.isPointInChildBounds(scroll, initialX, mInitialY)) {
mActivePointerId = event.getPointerId(event.actionIndex)
mTouchingScrollingChild = true
}
mIgnoreEvents = mActivePointerId == MotionEvent.INVALID_POINTER_ID &&
!parent.isPointInChildBounds(child, initialX, mInitialY)
}
}
if (!mIgnoreEvents && mViewDragHelper!!.shouldInterceptTouchEvent(event)) {
return true
}
// We have to handle cases that the ViewDragHelper does not capture the top sheet because
// it is not the top most view of its parent. This is not necessary when the touch event is
// happening over the scrolling content as nested scrolling logic handles that case.
val scroll = mNestedScrollingChildRef!!.get()
return action == MotionEvent.ACTION_MOVE && scroll != null &&
!mIgnoreEvents && mState != STATE_DRAGGING &&
!parent.isPointInChildBounds(
scroll,
event.x.toInt(),
event.y.toInt()
) && abs(mInitialY - event.y) > mViewDragHelper!!.touchSlop
}
override fun onTouchEvent(
parent: CoordinatorLayout,
child: V,
event: MotionEvent
): Boolean {
if (!child!!.isShown) {
return false
}
val action = event.actionMasked
if (mState == STATE_DRAGGING && action == MotionEvent.ACTION_DOWN) {
return true
}
if (mViewDragHelper != null) {
// no crash
mViewDragHelper!!.processTouchEvent(event)
// Record the velocity
if (action == MotionEvent.ACTION_DOWN) {
reset()
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain()
}
mVelocityTracker!!.addMovement(event)
// The ViewDragHelper tries to capture only the top-most View. We have to explicitly tell it
// to capture the top sheet in case it is not captured and the touch slop is passed.
if (action == MotionEvent.ACTION_MOVE && !mIgnoreEvents &&
abs(mInitialY - event.y) > mViewDragHelper!!.touchSlop) {
mViewDragHelper!!.captureChildView(
child,
event.getPointerId(event.actionIndex)
)
}
}
return !mIgnoreEvents
}
override fun onStartNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: V,
directTargetChild: View,
target: View,
nestedScrollAxes: Int
): Boolean {
mLastNestedScrollDy = 0
mNestedScrolled = false
return nestedScrollAxes and ViewCompat.SCROLL_AXIS_VERTICAL != 0
}
override fun onNestedPreScroll(
coordinatorLayout: CoordinatorLayout,
child: V,
target: View,
dx: Int,
dy: Int,
consumed: IntArray
) {
val scrollingChild = mNestedScrollingChildRef!!.get()
if (target !== scrollingChild) {
return
}
val currentTop = child!!.top
val newTop = currentTop - dy
if (dy > 0) { // Upward
if (!target.canScrollVertically(1)) {
if (newTop >= mMinOffset || isHideable) {
consumed[1] = dy
ViewCompat.offsetTopAndBottom(child, -dy)
setStateInternal(STATE_DRAGGING)
} else {
consumed[1] = currentTop - mMinOffset
ViewCompat.offsetTopAndBottom(child, -consumed[1])
setStateInternal(STATE_COLLAPSED)
}
}
} else if (dy < 0) { // Downward
// Negative to check scrolling up, positive to check scrolling down
if (newTop < mMaxOffset) {
consumed[1] = dy
ViewCompat.offsetTopAndBottom(child, -dy)
setStateInternal(STATE_DRAGGING)
} else {
consumed[1] = currentTop - mMaxOffset
ViewCompat.offsetTopAndBottom(child, -consumed[1])
setStateInternal(STATE_EXPANDED)
}
}
dispatchOnSlide(child.top)
mLastNestedScrollDy = dy
mNestedScrolled = true
}
override fun onStopNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: V,
target: View
) {
if (child!!.top == mMaxOffset) {
setStateInternal(STATE_EXPANDED)
return
}
if (target !== mNestedScrollingChildRef!!.get() || !mNestedScrolled) {
return
}
val top: Int
val targetState: Int
if (mLastNestedScrollDy < 0) {
top = mMaxOffset
targetState = STATE_EXPANDED
} else if (isHideable && shouldHide(child, yVelocity)) {
top = -child.height
targetState = STATE_HIDDEN
} else if (mLastNestedScrollDy == 0) {
val currentTop = child.top
if (abs(currentTop - mMinOffset) > abs(currentTop - mMaxOffset)) {
top = mMaxOffset
targetState = STATE_EXPANDED
} else {
top = mMinOffset
targetState = STATE_COLLAPSED
}
} else {
top = mMinOffset
targetState = STATE_COLLAPSED
}
if (mViewDragHelper!!.smoothSlideViewTo(child, child.left, top)) {
setStateInternal(STATE_SETTLING)
ViewCompat.postOnAnimation(
child,
SettleRunnable(
child,
targetState
)
)
} else {
setStateInternal(targetState)
}
mNestedScrolled = false
}
override fun onNestedPreFling(
coordinatorLayout: CoordinatorLayout,
child: V,
target: View,
velocityX: Float,
velocityY: Float
): Boolean {
return target === mNestedScrollingChildRef!!.get() &&
(mState != STATE_EXPANDED ||
super.onNestedPreFling(
coordinatorLayout, child, target,
velocityX, velocityY
))
}
/**
* Sets a callback to be notified of top sheet events.
*
* @param callback The callback to notify when top sheet events occur.
*/
fun setTopSheetCallback(callback: TopSheetCallback?) {
mCallback = callback
}
/**
* Gets/Sets the state of the top sheet. When set, the top sheet will transition to
* that state with animation.
*
* @var state One of [.STATE_EXPANDED], [.STATE_COLLAPSED], [.STATE_DRAGGING],
* and [.STATE_SETTLING].
*/
@get:State
var state: Int
get() = mState
set(state) {
if (state == mState) {
return
}
if (mViewRef == null) {
// The view is not laid out yet; modify mState and let onLayoutChild handle it later
val stateCondition = state == STATE_COLLAPSED || state == STATE_EXPANDED ||
isHideable && state == STATE_HIDDEN
if (stateCondition) {
mState = state
}
return
}
val child = mViewRef!!.get() ?: return
val top: Int
top = if (state == STATE_COLLAPSED) {
mMinOffset
} else if (state == STATE_EXPANDED) {
mMaxOffset
} else if (isHideable && state == STATE_HIDDEN) {
-child.height
} else {
throw IllegalArgumentException("Illegal state argument: $state")
}
setStateInternal(STATE_SETTLING)
if (mViewDragHelper!!.smoothSlideViewTo(child, child.left, top)) {
ViewCompat.postOnAnimation(
child,
SettleRunnable(child, state)
)
}
}
private var oldState = mState
private fun setStateInternal(@State state: Int) {
if (state == STATE_COLLAPSED || state == STATE_EXPANDED) {
oldState = state
}
if (mState == state) {
return
}
mState = state
updateDrawableForTargetState(state)
val topSheet: View? = mViewRef!!.get()
if (topSheet != null && mCallback != null) {
mCallback!!.onStateChanged(topSheet, state)
}
}
@Suppress("NestedBlockDepth")
private fun updateDrawableForTargetState(@BottomSheetBehavior.State state: Int) {
if (state == BottomSheetBehavior.STATE_SETTLING) {
// Special case: we want to know which state we're settling to, so wait for another call.
return
}
val expand = state == BottomSheetBehavior.STATE_EXPANDED
if (isShapeExpanded != expand) {
isShapeExpanded = expand
if (materialShapeDrawable != null && interpolatorAnimator != null) {
if (interpolatorAnimator!!.isRunning) {
interpolatorAnimator!!.reverse()
} else {
val to = if (expand) 0f else 1f
val from = 1f - to
interpolatorAnimator!!.setFloatValues(from, to)
interpolatorAnimator!!.start()
}
}
}
}
private fun reset() {
mActivePointerId = ViewDragHelper.INVALID_POINTER
if (mVelocityTracker != null) {
mVelocityTracker!!.recycle()
mVelocityTracker = null
}
}
private fun shouldHide(child: View, yvel: Float): Boolean {
if (child.top > mMinOffset) {
// It should not hide, but collapse.
return false
}
val newTop = child.top + yvel * HIDE_FRICTION
return abs(newTop - mMinOffset) / mPeekHeight.toFloat() > HIDE_THRESHOLD
}
private fun findScrollingChild(view: View): View? {
if (view is NestedScrollingChild) {
return view
}
if (view is ViewGroup) {
var i = 0
val count = view.childCount
while (i < count) {
val scrollingChild = findScrollingChild(view.getChildAt(i))
if (scrollingChild != null) {
return scrollingChild
}
i++
}
}
return null
}
private val yVelocity: Float
get() {
mVelocityTracker!!.computeCurrentVelocity(VELOCITY_UNITS, mMaximumVelocity)
return mVelocityTracker!!.getYVelocity(mActivePointerId)
}
private val mDragCallback: ViewDragHelper.Callback = object : ViewDragHelper.Callback() {
@Suppress("ReturnCount")
override fun tryCaptureView(
child: View,
pointerId: Int
): Boolean {
if (mState == STATE_DRAGGING) {
return false
}
if (mTouchingScrollingChild) {
return false
}
if (mState == STATE_EXPANDED && mActivePointerId == pointerId) {
val scroll = mNestedScrollingChildRef!!.get()
if (scroll != null && scroll.canScrollVertically(-1)) {
// Let the content scroll up
return false
}
}
return mViewRef != null && mViewRef!!.get() === child
}
override fun onViewPositionChanged(
changedView: View,
left: Int,
top: Int,
dx: Int,
dy: Int
) {
dispatchOnSlide(top)
}
override fun onViewDragStateChanged(state: Int) {
if (state == ViewDragHelper.STATE_DRAGGING) {
setStateInternal(STATE_DRAGGING)
}
}
override fun onViewReleased(
releasedChild: View,
xvel: Float,
yvel: Float
) {
val top: Int
@State val targetState: Int
if (yvel > 0) { // Moving up
top = mMaxOffset
targetState = STATE_EXPANDED
} else if (isHideable && shouldHide(releasedChild, yvel)) {
top = -mViewRef!!.get()!!.height
targetState = STATE_HIDDEN
} else if (yvel == 0f) {
val currentTop = releasedChild.top
if (abs(currentTop - mMinOffset) > abs(currentTop - mMaxOffset)) {
top = mMaxOffset
targetState = STATE_EXPANDED
} else {
top = mMinOffset
targetState = STATE_COLLAPSED
}
} else {
top = mMinOffset
targetState = STATE_COLLAPSED
}
if (mViewDragHelper!!.settleCapturedViewAt(releasedChild.left, top)) {
setStateInternal(STATE_SETTLING)
ViewCompat.postOnAnimation(
releasedChild,
SettleRunnable(
releasedChild,
targetState
)
)
} else {
setStateInternal(targetState)
}
}
override fun clampViewPositionVertical(
child: View,
top: Int,
dy: Int
): Int {
return constrain(
top,
if (isHideable) -child.height else mMinOffset,
mMaxOffset
)
}
override fun clampViewPositionHorizontal(
child: View,
left: Int,
dx: Int
): Int {
return child.left
}
override fun getViewVerticalDragRange(child: View): Int {
return if (isHideable) {
child.height
} else {
mMaxOffset - mMinOffset
}
}
}
private fun createMaterialShapeDrawable(
context: Context,
attrs: AttributeSet
) {
if (shapeThemingEnabled) {
shapeAppearanceModelDefault = ShapeAppearanceModel.builder(
context,
attrs,
R.attr.bottomSheetStyle,
DEF_STYLE_RES
)
.build()
materialShapeDrawable = MaterialShapeDrawable(shapeAppearanceModelDefault!!)
materialShapeDrawable?.initializeElevationOverlay(context)
val defaultColor = TypedValue()
context.theme
.resolveAttribute(android.R.attr.colorBackground, defaultColor, true)
materialShapeDrawable?.setTint(defaultColor.data)
}
}
private fun createShapeValueAnimator() {
interpolatorAnimator = ValueAnimator.ofFloat(0f, 1f)
interpolatorAnimator?.duration = CORNER_ANIMATION_DURATION.toLong()
interpolatorAnimator!!.addUpdateListener { animation ->
val value = animation.animatedValue as Float
if (materialShapeDrawable != null) {
materialShapeDrawable!!.interpolation = value
}
}
}
private fun dispatchOnSlide(top: Int) {
val topSheet: View? = mViewRef!!.get()
if (topSheet != null && mCallback != null) {
val isOpening = oldState == STATE_COLLAPSED
if (top < mMinOffset) {
mCallback!!.onSlide(
topSheet,
(top - mMinOffset).toFloat() / mPeekHeight,
isOpening
)
} else {
mCallback!!.onSlide(
topSheet,
(top - mMinOffset).toFloat() / (mMaxOffset - mMinOffset), isOpening
)
}
}
}
private inner class SettleRunnable internal constructor(
private val mView: View,
@field:State @param:State private val mTargetState: Int
) :
Runnable {
override fun run() {
if (mViewDragHelper != null && mViewDragHelper!!.continueSettling(true)) {
ViewCompat.postOnAnimation(mView, this)
} else {
setStateInternal(mTargetState)
}
}
}
private class SavedState(superState: Parcelable?, @State val state: Int) :
AbsSavedState(superState) {
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeInt(state)
}
}
companion object {
/**
* The top sheet is dragging.
*/
const val STATE_DRAGGING = 1
/**
* The top sheet is settling.
*/
const val STATE_SETTLING = 2
/**
* The top sheet is expanded.
*/
const val STATE_EXPANDED = 3
/**
* The top sheet is collapsed.
*/
const val STATE_COLLAPSED = 4
/**
* The top sheet is hidden.
*/
const val STATE_HIDDEN = 5
private const val HIDE_THRESHOLD = 0.5f
private const val HIDE_FRICTION = 0.1f
/**
* A utility function to get the [TopSheetBehavior] associated with the `view`.
*
* @param view The [View] with [TopSheetBehavior].
* @return The [TopSheetBehavior] associated with the `view`.
*/
@Suppress("UNCHECKED_CAST")
fun <V : View?> from(view: V): TopSheetBehavior<V> {
val params = view!!.layoutParams
require(params is CoordinatorLayout.LayoutParams) { "The view is not a child of CoordinatorLayout" }
val behavior =
params
.behavior
require(behavior is TopSheetBehavior<*>) { "The view is not associated with TopSheetBehavior" }
return behavior as TopSheetBehavior<V>
}
fun constrain(amount: Int, low: Int, high: Int): Int {
return if (amount < low) low else if (amount > high) high else amount
}
private const val CORNER_ANIMATION_DURATION = 500
private val DEF_STYLE_RES = R.style.Widget_Design_BottomSheet_Modal
private const val PEEK_HEIGHT_RATIO = 0.75
private const val VELOCITY_UNITS = 1000
}
}

@ -30,6 +30,7 @@ import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingMa
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingPrivacyNoticeViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingPrivateBrowsingViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingSectionHeaderViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingTabsTrayLayoutViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingThemePickerViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingToolbarPositionPickerViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingTrackingProtectionViewHolder
@ -106,6 +107,7 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) {
object OnboardingFinish : AdapterItem(OnboardingFinishViewHolder.LAYOUT_ID)
object OnboardingToolbarPositionPicker :
AdapterItem(OnboardingToolbarPositionPickerViewHolder.LAYOUT_ID)
object OnboardingTabsTrayLayoutPicker : AdapterItem(OnboardingTabsTrayLayoutViewHolder.LAYOUT_ID)
object OnboardingWhatsNew : AdapterItem(OnboardingWhatsNewViewHolder.LAYOUT_ID)
@ -189,6 +191,7 @@ class SessionControlAdapter(
OnboardingToolbarPositionPickerViewHolder.LAYOUT_ID -> OnboardingToolbarPositionPickerViewHolder(
view
)
OnboardingTabsTrayLayoutViewHolder.LAYOUT_ID -> OnboardingTabsTrayLayoutViewHolder(view)
else -> throw IllegalStateException()
}
}

@ -99,6 +99,7 @@ private fun onboardingAdapterItems(onboardingState: OnboardingState): List<Adapt
AdapterItem.OnboardingThemePicker,
AdapterItem.OnboardingPrivateBrowsing,
AdapterItem.OnboardingToolbarPositionPicker,
AdapterItem.OnboardingTabsTrayLayoutPicker,
AdapterItem.OnboardingPrivacyNotice,
AdapterItem.OnboardingFinish
)

@ -0,0 +1,50 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.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 org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.onboarding_tabs_tray_layout.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.onboarding.OnboardingRadioButton
import org.mozilla.fenix.utils.view.addToRadioGroup
class OnboardingTabsTrayLayoutViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private var fennecStyleTabsScreen: OnboardingRadioButton = view.tabs_tray_fennec_style
private var fenixStyleTabsTray: OnboardingRadioButton = view.tabs_tray_fenix_style
init {
addToRadioGroup(fennecStyleTabsScreen, fenixStyleTabsTray)
fennecStyleTabsScreen.isChecked =
itemView.context.settings().shouldUseFennecStyleTabsScreen
fenixStyleTabsTray.isChecked =
!itemView.context.settings().shouldUseFennecStyleTabsScreen
fennecStyleTabsScreen.onClickListener {
setFennecStyleTabsScreen(true)
}
fenixStyleTabsTray.onClickListener {
setFennecStyleTabsScreen(false)
}
}
private fun setFennecStyleTabsScreen(enabled: Boolean) {
itemView.context.settings().apply {
enableCompactTabs = enabled
useFullScreenTabScreen = enabled
reverseTabOrderInTabsTray = !enabled
useNewTabFloatingActionButton = !enabled
placeNewTabFloatingActionButtonAtTop = false
}
}
companion object {
const val LAYOUT_ID = R.layout.onboarding_tabs_tray_layout
}
}

@ -9,6 +9,9 @@ import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit
import androidx.preference.EditTextPreference
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
@ -26,7 +29,7 @@ import org.mozilla.fenix.utils.view.addToRadioGroup
* Lets the user customize the UI.
*/
@Suppress("TooManyFunctions")
@Suppress("LargeClass", "TooManyFunctions")
class CustomizationFragment : PreferenceFragmentCompat() {
private lateinit var radioLightTheme: RadioButtonPreference
private lateinit var radioDarkTheme: RadioButtonPreference
@ -35,6 +38,12 @@ class CustomizationFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.customization_preferences, rootKey)
requirePreference<SwitchPreference>(R.string.pref_key_strip_url).apply {
isChecked = context.settings().shouldStripUrl
onPreferenceChangeListener = SharedPreferenceUpdater()
}
}
override fun onResume() {
@ -50,8 +59,11 @@ class CustomizationFragment : PreferenceFragmentCompat() {
bindAutoBatteryTheme()
setupRadioGroups()
setupToolbarCategory()
setupTabsTrayCategory()
setupFabCategory()
setupHomeCategory()
setupGesturesCategory()
setupAddonsCustomizationCategory()
}
private fun setupRadioGroups() {
@ -135,10 +147,81 @@ class CustomizationFragment : PreferenceFragmentCompat() {
addToRadioGroup(topPreference, bottomPreference)
}
private fun setupTabsTrayCategory() {
requirePreference<SwitchPreference>(R.string.pref_key_tabs_tray_top_tray).apply {
isChecked = context.settings().useTopTabsTray
onPreferenceChangeListener = SharedPreferenceUpdater()
}
requirePreference<SwitchPreference>(R.string.pref_key_use_fullscreen_tabs_screen).apply {
isChecked = context.settings().useFullScreenTabScreen
onPreferenceChangeListener = SharedPreferenceUpdater()
}
val reverseOrderPref = requirePreference<SwitchPreference>(
R.string.pref_key_tabs_tray_reverse_tab_order).apply {
if (context.settings().enableCompactTabs) {
isChecked = false
isEnabled = false
} else {
isChecked = context.settings().reverseTabOrderInTabsTray
isEnabled = true
}
onPreferenceChangeListener = SharedPreferenceUpdater()
}
requirePreference<SwitchPreference>(R.string.pref_key_tabs_tray_compact_tab).apply {
isChecked = context.settings().enableCompactTabs
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { preference, newValue ->
val newValueBoolean = newValue as Boolean
preference.context.settings().preferences.edit {
putBoolean(preference.key, newValueBoolean)
if (newValueBoolean) {
reverseOrderPref.isChecked = false
putBoolean(getString(R.string.pref_key_tabs_tray_reverse_tab_order), false)
}
reverseOrderPref.isEnabled = !newValueBoolean
}
true
}
}
}
private fun setupFabCategory() {
val fabPositionTop = requirePreference<SwitchPreference>(R.string.pref_key_tabs_tray_fab_top_position).apply {
if (context.settings().useNewTabFloatingActionButton) {
isChecked = context.settings().placeNewTabFloatingActionButtonAtTop
isEnabled = true
} else {
isChecked = false
isEnabled = false
}
onPreferenceChangeListener = SharedPreferenceUpdater()
}
requirePreference<SwitchPreference>(R.string.pref_key_tabs_tray_use_fab).apply {
isChecked = context.settings().useNewTabFloatingActionButton
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { preference, newValue ->
val newValueBoolean = newValue as Boolean
preference.context.settings().preferences.edit {
putBoolean(preference.key, newValueBoolean)
if (!newValueBoolean) {
fabPositionTop.isChecked = false
putBoolean(getString(R.string.pref_key_tabs_tray_fab_top_position), false)
}
fabPositionTop.isEnabled = newValueBoolean
}
true
}
}
}
private fun setupHomeCategory() {
requirePreference<PreferenceCategory>(R.string.pref_home_category).apply {
isVisible = FeatureFlags.topFrecentSite
}
requirePreference<SwitchPreference>(R.string.pref_key_enable_top_frecent_sites).apply {
isVisible = FeatureFlags.topFrecentSite
isChecked = context.settings().showTopFrecentSites
@ -161,4 +244,16 @@ class CustomizationFragment : PreferenceFragmentCompat() {
onPreferenceChangeListener = SharedPreferenceUpdater()
}
}
private fun setupAddonsCustomizationCategory() {
requirePreference<EditTextPreference>(R.string.pref_key_addons_custom_account).apply {
text = context.settings().customAddonsAccount
onPreferenceChangeListener = SharedPreferenceUpdater()
}
requirePreference<EditTextPreference>(R.string.pref_key_addons_custom_collection).apply {
text = context.settings().customAddonsCollection
onPreferenceChangeListener = SharedPreferenceUpdater()
}
}
}

@ -6,6 +6,8 @@ package org.mozilla.fenix.settings
import androidx.core.content.edit
import androidx.preference.Preference
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
/**
@ -15,10 +17,21 @@ import org.mozilla.fenix.ext.settings
open class SharedPreferenceUpdater : Preference.OnPreferenceChangeListener {
override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean {
val newBooleanValue = newValue as? Boolean ?: return false
preference.context.settings().preferences.edit {
putBoolean(preference.key, newBooleanValue)
if (newValue is Boolean) {
preference.context.settings().preferences.edit {
putBoolean(preference.key, newValue)
}
} else if (newValue is String) {
preference.context.settings().preferences.edit {
putString(preference.key, newValue)
}
if (preference.key == preference.context.getString(R.string.pref_key_addons_custom_account) ||
preference.key == preference.context.getString(R.string.pref_key_addons_custom_collection)) {
preference.context.components.updateAddonManager()
}
}
return true
}
}

@ -87,6 +87,8 @@ object SupportUtils {
fun getWhatsNewUrl(context: Context) = if (Config.channel.isFennec) {
getGenericSumoURLForTopic(SumoTopic.UPGRADE_FAQ)
} else if (Config.channel.isFork) {
"https://github.com/fork-maintainers/iceraven-browser/releases"
} else {
getSumoURLForTopic(context, SumoTopic.WHATS_NEW)
}

@ -19,6 +19,7 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.settings
class FenixTabsAdapter(
private val context: Context,
@ -27,7 +28,7 @@ class FenixTabsAdapter(
viewHolderProvider = { parentView ->
TabTrayViewHolder(
LayoutInflater.from(context).inflate(
R.layout.tab_tray_item,
if (context.settings().enableCompactTabs) R.layout.tab_tray_item_compact else R.layout.tab_tray_item,
parentView,
false
),

@ -21,6 +21,7 @@ import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.sync.ListenerDelegate
import org.mozilla.fenix.sync.SyncedTabsAdapter
import org.mozilla.fenix.sync.ext.toAdapterList
@ -50,7 +51,7 @@ class SyncedTabsController(
.collect { mode ->
when (mode) {
is TabTrayDialogFragmentState.Mode.Normal -> {
concatAdapter.addAdapter(0, adapter)
concatAdapter.addAdapter(adapter)
}
is TabTrayDialogFragmentState.Mode.MultiSelect -> {
concatAdapter.removeAdapter(adapter)
@ -63,8 +64,11 @@ class SyncedTabsController(
override fun displaySyncedTabs(syncedTabs: List<SyncedDeviceTabs>) {
scope.launch {
val tabsList = listOf(SyncedTabsAdapter.AdapterItem.Title) + syncedTabs.toAdapterList()
// Reverse layout for TabTrayView which does things backwards.
adapter.submitList(tabsList.reversed())
if (view.context.settings().reverseTabOrderInTabsTray) {
adapter.submitList(tabsList.reversed())
} else {
adapter.submitList(tabsList)
}
}
}

@ -21,8 +21,8 @@ import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.component_tabstray.view.*
import kotlinx.android.synthetic.main.component_tabstray_fab.view.*
import kotlinx.android.synthetic.main.component_tabstray_bottom.view.*
import kotlinx.android.synthetic.main.component_tabstray_fab_bottom.view.*
import kotlinx.android.synthetic.main.fragment_tab_tray_dialog.*
import kotlinx.android.synthetic.main.fragment_tab_tray_dialog.view.*
import kotlinx.coroutines.Dispatchers
@ -159,16 +159,22 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
if (newConfig.orientation != currentOrientation) {
tabTrayView.dismissMenu()
tabTrayView.expand()
tabTrayView.updateTabsTrayLayout()
currentOrientation = newConfig.orientation
}
}
@Suppress("LongMethod")
@OptIn(ExperimentalCoroutinesApi::class)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val activity = activity as HomeActivity
val isPrivate = activity.browsingModeManager.mode.isPrivate
if (requireContext().settings().useTopTabsTray) {
dialog?.window?.attributes?.windowAnimations = R.style.TopTabTrayAnimation
}
val thumbnailLoader = ThumbnailLoader(requireContext().components.core.thumbnailStorage)
val adapter = FenixTabsAdapter(requireContext(), thumbnailLoader)
currentOrientation = resources.configuration.orientation

@ -22,12 +22,24 @@ import androidx.core.view.updatePadding
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.tabs.TabLayout
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.component_tabstray.view.*
import kotlinx.android.synthetic.main.component_tabstray_fab.view.*
import kotlinx.android.synthetic.main.component_tabs_screen_top.view.*
import kotlinx.android.synthetic.main.component_tabstray_bottom.view.collect_multi_select
import kotlinx.android.synthetic.main.component_tabstray_bottom.view.exit_multi_select
import kotlinx.android.synthetic.main.component_tabstray_bottom.view.handle
import kotlinx.android.synthetic.main.component_tabstray_bottom.view.multiselect_title
import kotlinx.android.synthetic.main.component_tabstray_bottom.view.tab_layout
import kotlinx.android.synthetic.main.component_tabstray_bottom.view.tab_tray_empty_view
import kotlinx.android.synthetic.main.component_tabstray_bottom.view.tab_tray_new_tab
import kotlinx.android.synthetic.main.component_tabstray_bottom.view.tab_tray_overflow
import kotlinx.android.synthetic.main.component_tabstray_bottom.view.tab_wrapper
import kotlinx.android.synthetic.main.component_tabstray_bottom.view.tabsTray
import kotlinx.android.synthetic.main.component_tabstray_bottom.view.topBar
import kotlinx.android.synthetic.main.component_tabstray_fab_bottom.view.*
import kotlinx.android.synthetic.main.tabs_tray_tab_counter.*
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.delay
@ -47,11 +59,14 @@ import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.toolbar.TabCounter.Companion.INFINITE_CHAR_PADDING_BOTTOM
import org.mozilla.fenix.components.toolbar.TabCounter.Companion.MAX_VISIBLE_TABS
import org.mozilla.fenix.components.toolbar.TabCounter.Companion.SO_MANY_TABS_OPEN
import org.mozilla.fenix.components.topsheet.TopSheetBehavior
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.tabtray.SaveToCollectionsButtonAdapter.MultiselectModeChange
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentState.Mode
import java.text.NumberFormat
import kotlin.math.max
import kotlin.math.min
import mozilla.components.browser.storage.sync.Tab as SyncTab
/**
@ -69,17 +84,43 @@ class TabTrayView(
private val filterTabs: (Boolean) -> Unit
) : LayoutContainer, TabLayout.OnTabSelectedListener {
val lifecycleScope = lifecycleOwner.lifecycleScope
val fabView = LayoutInflater.from(container.context)
.inflate(R.layout.component_tabstray_fab, container, true)
private val hasAccessibilityEnabled = container.context.settings().accessibilityServicesEnabled
private val useFab = container.context.settings().useNewTabFloatingActionButton
val fabView: View = when (container.context.settings().placeNewTabFloatingActionButtonAtTop) {
true -> LayoutInflater.from(container.context).inflate(R.layout.component_tabstray_fab_top, container, true)
false -> LayoutInflater.from(container.context).inflate(R.layout.component_tabstray_fab_bottom, container, true)
}
val view = LayoutInflater.from(container.context)
.inflate(R.layout.component_tabstray, container, true)
private val enableCompactTabs = container.context.settings().enableCompactTabs
private val reverseTabOrderInTabsTray = container.context.settings().reverseTabOrderInTabsTray
private val isTabsTrayFullScreenMode = container.context.settings().useFullScreenTabScreen
private val hasAccessibilityEnabled = container.context.settings().accessibilityServicesEnabled
private val useTopTabsTray = container.context.settings().useTopTabsTray
val view: View = if (isTabsTrayFullScreenMode) {
when (useTopTabsTray) {
true -> LayoutInflater.from(container.context)
.inflate(R.layout.component_tabs_screen_bottom, container, true)
false -> LayoutInflater.from(container.context)
.inflate(R.layout.component_tabs_screen_top, container, true)
}
} else {
when (useTopTabsTray) {
true -> LayoutInflater.from(container.context)
.inflate(R.layout.component_tabstray_top, container, true)
false -> LayoutInflater.from(container.context)
.inflate(R.layout.component_tabstray_bottom, container, true)
}
}
private val isPrivateModeSelected: Boolean get() = view.tab_layout.selectedTabPosition == PRIVATE_TAB_ID
private val behavior = BottomSheetBehavior.from(view.tab_wrapper)
private val behavior = if (isTabsTrayFullScreenMode) null else {
when (useTopTabsTray) {
true -> TopSheetBehavior.from(view.tab_wrapper)
false -> BottomSheetBehavior.from(view.tab_wrapper)
}
}
private val concatAdapter = ConcatAdapter(tabsAdapter)
private val tabTrayItemMenu: TabTrayItemMenu
@ -103,24 +144,55 @@ class TabTrayView(
toggleFabText(isPrivate)
behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) {
if (interactor.onModeRequested() is Mode.Normal && !hasAccessibilityEnabled) {
if (slideOffset >= SLIDE_OFFSET) {
fabView.new_tab_button.show()
} else {
fabView.new_tab_button.hide()
if (!isTabsTrayFullScreenMode) {
if (useTopTabsTray) {
(behavior as TopSheetBehavior).setTopSheetCallback(object :
TopSheetBehavior.TopSheetCallback {
override fun onSlide(topSheet: View, slideOffset: Float, isOpening: Boolean?) {
if (interactor.onModeRequested() is Mode.Normal && useFab) {
if (slideOffset >= SLIDE_OFFSET) {
fabView.new_tab_button.show()
} else {
fabView.new_tab_button.hide()
}
}
}
}
override fun onStateChanged(topSheet: View, newState: Int) {
if (newState == TopSheetBehavior.STATE_HIDDEN) {
components.analytics.metrics.track(Event.TabsTrayClosed)
interactor.onTabTrayDismissed()
}
}
})
} else {
(behavior as BottomSheetBehavior).addBottomSheetCallback(object :
BottomSheetBehavior.BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) {
if (interactor.onModeRequested() is Mode.Normal && useFab) {
if (slideOffset >= SLIDE_OFFSET) {
fabView.new_tab_button.show()
} else {
fabView.new_tab_button.hide()
}
}
}
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
components.analytics.metrics.track(Event.TabsTrayClosed)
interactor.onTabTrayDismissed()
}
}
})
}
}
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
components.analytics.metrics.track(Event.TabsTrayClosed)
interactor.onTabTrayDismissed()
}
if (isTabsTrayFullScreenMode) {
view.exit_tabs_screen.setOnClickListener {
interactor.onTabTrayDismissed()
}
})
}
val selectedTabIndex = if (!isPrivate) {
DEFAULT_TAB_ID
@ -164,11 +236,8 @@ class TabTrayView(
)
}
updateTabsTrayLayout()
view.tabsTray.apply {
layoutManager = LinearLayoutManager(container.context).apply {
reverseLayout = true
stackFromEnd = true
}
adapter = concatAdapter
tabsTouchHelper = TabsTouchHelper(
@ -181,18 +250,25 @@ class TabTrayView(
tabsAdapter.tabTrayInteractor = interactor
tabsAdapter.onTabsUpdated = {
// Put the 'Add to collections' button after the tabs have loaded.
concatAdapter.addAdapter(0, collectionsButtonAdapter)
// Put the Synced Tabs adapter at the end.
concatAdapter.addAdapter(0, syncedTabsController.adapter)
// And, put the Synced Tabs adapter at the end.
if (reverseTabOrderInTabsTray) {
// Put these at the start when reverse tab order is enabled. Also, we disallow
// reverse tab order for compact tabs in settings.
concatAdapter.addAdapter(0, collectionsButtonAdapter)
concatAdapter.addAdapter(0, syncedTabsController.adapter)
} else {
concatAdapter.addAdapter(collectionsButtonAdapter)
concatAdapter.addAdapter(syncedTabsController.adapter)
}
if (hasAccessibilityEnabled) {
tabsAdapter.notifyItemRangeChanged(0, tabs.size)
}
if (!hasLoaded) {
hasLoaded = true
scrollToTab(view.context.components.core.store.state.selectedTabId)
if (view.context.settings().accessibilityServicesEnabled) {
if (hasAccessibilityEnabled) {
lifecycleScope.launch {
delay(SELECTION_DELAY.toLong())
lifecycleScope.launch(Main) {
@ -238,16 +314,24 @@ class TabTrayView(
}
}
adjustNewTabButtonsForNormalMode()
adjustNewTabButtonForNormalMode()
}
private fun gridViewNumberOfCols(context: Context): Int {
val displayMetrics = context.resources.displayMetrics
val dpWidth = displayMetrics.widthPixels / displayMetrics.density
val columnWidthDp = COLUMN_WIDTH_DP
val columnCount = (dpWidth / columnWidthDp).toInt()
return if (columnCount >= 2) columnCount else 2
}
private fun handleTabClicked(tab: SyncTab) {
interactor.onSyncedTabClicked(tab)
}
private fun adjustNewTabButtonsForNormalMode() {
private fun adjustNewTabButtonForNormalMode() {
view.tab_tray_new_tab.apply {
isVisible = hasAccessibilityEnabled
visibility = if (useFab) View.GONE else View.VISIBLE
setOnClickListener {
sendNewTabEvent(isPrivateModeSelected)
interactor.onNewTabTapped(isPrivateModeSelected)
@ -255,7 +339,7 @@ class TabTrayView(
}
fabView.new_tab_button.apply {
isVisible = !hasAccessibilityEnabled
isVisible = useFab
setOnClickListener {
sendNewTabEvent(isPrivateModeSelected)
interactor.onNewTabTapped(isPrivateModeSelected)
@ -273,8 +357,63 @@ class TabTrayView(
components.analytics.metrics.track(eventToSend)
}
fun updateTabsTrayLayout() {
if (enableCompactTabs) {
setupCompactTabsTrayLayout()
} else {
setupRegularTabsTrayLayout()
}
}
private fun setupCompactTabsTrayLayout() {
view.tabsTray.apply {
val gridLayoutManager = GridLayoutManager(container.context, gridViewNumberOfCols(container.context))
if (useTopTabsTray) {
gridLayoutManager.reverseLayout = true
}
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
val numTabs = tabsAdapter.itemCount
return if (position < numTabs) {
1
} else {
gridViewNumberOfCols(container.context)
}
}
}
layoutManager = gridLayoutManager
}
}
private fun setupRegularTabsTrayLayout() {
view.tabsTray.apply {
val linearLayoutManager = LinearLayoutManager(container.context)
if (useTopTabsTray) {
if (!reverseTabOrderInTabsTray) {
linearLayoutManager.reverseLayout = true
} else {
linearLayoutManager.stackFromEnd = true
}
} else {
if (reverseTabOrderInTabsTray) {
linearLayoutManager.reverseLayout = true
linearLayoutManager.stackFromEnd = true
}
}
layoutManager = linearLayoutManager
}
}
fun expand() {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
if (!isTabsTrayFullScreenMode) {
if (useTopTabsTray) {
(behavior as TopSheetBehavior).state = TopSheetBehavior.STATE_EXPANDED
} else {
(behavior as BottomSheetBehavior).state = BottomSheetBehavior.STATE_EXPANDED
}
}
}
enum class TabChange {
@ -314,7 +453,7 @@ class TabTrayView(
if (oldMode::class != state.mode::class) {
updateTabsForMultiselectModeChanged(state.mode is Mode.MultiSelect)
if (view.context.settings().accessibilityServicesEnabled) {
if (hasAccessibilityEnabled) {
view.announceForAccessibility(
if (state.mode == Mode.Normal) view.context.getString(
R.string.tab_tray_exit_multiselect_content_description
@ -361,7 +500,7 @@ class TabTrayView(
val unselectedItems = oldMode.selectedItems - state.mode.selectedItems
state.mode.selectedItems.union(unselectedItems).forEach { item ->
if (view.context.settings().accessibilityServicesEnabled) {
if (hasAccessibilityEnabled) {
view.announceForAccessibility(
if (unselectedItems.contains(item)) view.context.getString(
R.string.tab_tray_item_unselected_multiselect_content_description,
@ -413,7 +552,7 @@ class TabTrayView(
counter_text.text = updateTabCounter(browserState.normalTabs.size)
updateTabCounterContentDescription(browserState.normalTabs.size)
adjustNewTabButtonsForNormalMode()
adjustNewTabButtonForNormalMode()
}
private fun toggleUIMultiselect(multiselect: Boolean) {
@ -436,13 +575,23 @@ class TabTrayView(
R.dimen.tab_tray_normal_handle_height
}
)
topMargin = view.resources.getDimensionPixelSize(
if (multiselect) {
R.dimen.tab_tray_multiselect_handle_top_margin
} else {
R.dimen.tab_tray_normal_handle_top_margin
}
)
if (useTopTabsTray) {
bottomMargin = view.resources.getDimensionPixelSize(
if (multiselect) {
R.dimen.tab_tray_multiselect_handle_bottom_margin
} else {
R.dimen.tab_tray_normal_handle_bottom_margin
}
)
} else {
topMargin = view.resources.getDimensionPixelSize(
if (multiselect) {
R.dimen.tab_tray_multiselect_handle_top_margin
} else {
R.dimen.tab_tray_normal_handle_top_margin
}
)
}
}
view.tab_wrapper.setChildWPercent(
@ -457,10 +606,12 @@ class TabTrayView(
)
)
if (isTabsTrayFullScreenMode) {
view.exit_tabs_screen.isVisible = !multiselect
}
view.tab_layout.isVisible = !multiselect
view.tab_tray_empty_view.isVisible = !multiselect
view.tab_tray_overflow.isVisible = !multiselect
view.tab_layout.isVisible = !multiselect
}
private fun updateTabsForMultiselectModeChanged(inMultiselectMode: Boolean) {
@ -525,13 +676,17 @@ class TabTrayView(
}
fun setTopOffset(landscape: Boolean) {
val topOffset = if (landscape) {
0
} else {
view.resources.getDimensionPixelSize(R.dimen.tab_tray_top_offset)
}
if (!isTabsTrayFullScreenMode) {
val topOffset = if (landscape) {
0
} else {
view.context.resources.getDimensionPixelSize(R.dimen.tab_tray_top_offset)
}
behavior.setExpandedOffset(topOffset)
if (!useTopTabsTray) {
(behavior as BottomSheetBehavior).setExpandedOffset(topOffset)
}
}
}
fun dismissMenu() {
@ -565,13 +720,17 @@ class TabTrayView(
val selectedBrowserTabIndex = tabs
.indexOfFirst { it.id == sessionId }
// We offset the tab index by the number of items in the other adapters.
// We add the offset, because the layoutManager is initialized with `reverseLayout`.
// We also add 1 to display the tab item above the selected browser tab.
val recyclerViewIndex = selectedBrowserTabIndex +
collectionsButtonAdapter.itemCount +
syncedTabsController.adapter.itemCount +
1
val recyclerViewIndex = if (reverseTabOrderInTabsTray) {
// For reverse tab order and non-compact tabs, we add the items in collections button
// adapter and synced tabs adapter, and offset by 1 to show the tab above the
// current tab, unless current tab is first in reverse order.
min(selectedBrowserTabIndex + 1, tabsAdapter.itemCount - 1) +
collectionsButtonAdapter.itemCount + syncedTabsController.adapter.itemCount
} else {
// We offset index by -1 to show the tab above the current tab, unless current tab
// is the first.
max(0, selectedBrowserTabIndex - 1)
}
layoutManager?.scrollToPosition(recyclerViewIndex)
}
@ -584,6 +743,7 @@ class TabTrayView(
private const val SLIDE_OFFSET = 0
private const val SELECTION_DELAY = 500
private const val NORMAL_HANDLE_PERCENT_WIDTH = 0.1F
private const val COLUMN_WIDTH_DP = 190
}
}

@ -12,6 +12,7 @@ import android.widget.TextView
import androidx.annotation.VisibleForTesting
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.AppCompatImageButton
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import mozilla.components.browser.state.state.MediaState
import mozilla.components.browser.state.store.BrowserStore
@ -34,6 +35,7 @@ import org.mozilla.fenix.ext.getMediaStateForSession
import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.ext.removeAndDisable
import org.mozilla.fenix.ext.removeTouchDelegate
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showAndEnable
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.utils.Do
@ -49,6 +51,8 @@ class TabTrayViewHolder(
private val metrics: MetricController = itemView.context.components.analytics.metrics
) : TabViewHolder(itemView) {
private val iconCard: CardView = itemView.findViewById(R.id.mozac_browser_tabstray_icon_card)
private val iconView: ImageView = itemView.findViewById(R.id.mozac_browser_tabstray_icon)
private val titleView: TextView = itemView.findViewById(R.id.mozac_browser_tabstray_title)
private val closeView: AppCompatImageButton =
itemView.findViewById(R.id.mozac_browser_tabstray_close)
@ -75,6 +79,7 @@ class TabTrayViewHolder(
// Basic text
updateTitle(tab)
updateUrl(tab)
updateIcon(tab)
updateCloseButtonDescription(tab.title)
// Drawables and theme
@ -154,9 +159,25 @@ class TabTrayViewHolder(
// is done in the toolbar and awesomebar:
// https://github.com/mozilla-mobile/fenix/issues/1824
// https://github.com/mozilla-mobile/android-components/issues/6985
urlView?.text = tab.url
.toShortUrl(itemView.context.components.publicSuffixList)
.take(MAX_URI_LENGTH)
urlView?.apply {
text =
if (context.settings().shouldStripUrl) {
tab.url
.toShortUrl(itemView.context.components.publicSuffixList)
.take(MAX_URI_LENGTH)
} else {
tab.url.take(MAX_URI_LENGTH)
}
}
}
private fun updateIcon(tab: Tab) {
if (tab.icon != null) {
iconCard.visibility = View.VISIBLE
iconView.setImageBitmap(tab.icon)
} else {
iconCard.visibility = View.GONE
}
}
@VisibleForTesting

@ -268,17 +268,17 @@ class Settings(private val appContext: Context) : PreferencesHolder {
val isTelemetryEnabled by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_telemetry),
default = true
default = false
)
val isMarketingTelemetryEnabled by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_marketing_telemetry),
default = true
default = false
)
val isExperimentationEnabled by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_experimentation),
default = true
default = false
)
private var trackingProtectionOnboardingShownThisSession = false
@ -546,6 +546,11 @@ class Settings(private val appContext: Context) : PreferencesHolder {
val toolbarPosition: ToolbarPosition
get() = if (shouldUseBottomToolbar) ToolbarPosition.BOTTOM else ToolbarPosition.TOP
var shouldStripUrl by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_strip_url),
default = true
)
/**
* Check each active accessibility service to see if it can perform gestures, if any can,
* then it is *likely* a switch service is enabled. We are assuming this to be the case based on #7486
@ -891,6 +896,49 @@ class Settings(private val appContext: Context) : PreferencesHolder {
0
)
val customAddonsAccount by stringPreference(
appContext.getPreferenceKey(R.string.pref_key_addons_custom_account),
BuildConfig.AMO_ACCOUNT
)
val customAddonsCollection by stringPreference(
appContext.getPreferenceKey(R.string.pref_key_addons_custom_collection),
BuildConfig.AMO_COLLECTION
)
var enableCompactTabs by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_tabs_tray_compact_tab),
default = true
)
val useTopTabsTray by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_tabs_tray_top_tray),
default = false
)
var useFullScreenTabScreen by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_use_fullscreen_tabs_screen),
default = true
)
val shouldUseFennecStyleTabsScreen: Boolean
get() = enableCompactTabs && useFullScreenTabScreen
var reverseTabOrderInTabsTray by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_tabs_tray_reverse_tab_order),
default = false
)
var useNewTabFloatingActionButton by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_tabs_tray_use_fab),
default = false
)
var placeNewTabFloatingActionButtonAtTop by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_tabs_tray_fab_top_position),
default = false
)
private var savedLoginsSortingStrategyString by stringPreference(
appContext.getPreferenceKey(R.string.pref_key_saved_logins_sorting_strategy),
default = SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort.strategyString

@ -0,0 +1,14 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:interpolator="@android:interpolator/decelerate_quad"
android:fromAlpha="0" android:toAlpha="1"
android:duration="150" />
<translate
android:interpolator="@android:interpolator/decelerate_quad"
android:fromYDelta="-7%" android:toYDelta="0%"
android:duration="150"/>
</set>

@ -0,0 +1,14 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:interpolator="@android:interpolator/accelerate_quad"
android:fromAlpha="1" android:toAlpha="0"
android:duration="125" />
<translate
android:interpolator="@android:interpolator/accelerate_quad"
android:fromYDelta="0%" android:toYDelta="-7%"
android:duration="125"/>
</set>

@ -0,0 +1,192 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/tab_wrapper"
style="@style/TopSheetModal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:backgroundTint="@color/foundation_normal_theme">
<View
android:id="@+id/handle"
android:layout_width="0dp"
android:layout_height="3dp"
android:layout_marginBottom="8dp"
android:visibility="gone"
android:background="@color/secondary_text_normal_theme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintWidth_percent="0.1" />
<TextView
android:id="@+id/tab_tray_empty_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center_horizontal"
android:paddingBottom="80dp"
android:text="@string/no_open_tabs_description"
android:textColor="?secondaryText"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@+id/divider" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/topBar"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/foundation_normal_theme"
app:layout_constraintBottom_toTopOf="@id/handle">
<ImageButton
android:id="@+id/exit_multi_select"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="0dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tab_tray_close_multiselect_content_description"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/multiselect_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/multiselect_title"
app:srcCompat="@drawable/ic_close"
app:tint="@color/contrast_text_normal_theme" />
<TextView
android:id="@+id/multiselect_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:textColor="@color/contrast_text_normal_theme"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/collect_multi_select"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@+id/exit_multi_select"
app:layout_constraintTop_toTopOf="parent"
tools:text="3 selected" />
<TextView
android:id="@+id/collect_multi_select"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tab_tray_collection_button_multiselect_content_description"
android:drawablePadding="8dp"
android:gravity="center_vertical"
android:text="@string/tab_tray_save_to_collection"
android:textAllCaps="true"
android:textColor="@color/contrast_text_normal_theme"
android:textSize="14sp"
android:textStyle="bold"
android:visibility="gone"
app:drawableStartCompat="@drawable/ic_tab_collection"
app:drawableTint="@color/contrast_text_normal_theme"
app:fontFamily="@font/metropolis_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/exit_tabs_screen"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="0dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tabs_screen_close_screen_description"
app:layout_constraintBottom_toBottomOf="@+id/tab_layout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/tab_layout"
app:srcCompat="@drawable/mozac_ic_back"
app:tint="@color/primary_text_normal_theme" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="0dp"
android:layout_height="50dp"
android:background="@color/foundation_normal_theme"
app:layout_constraintStart_toEndOf="@id/exit_tabs_screen"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.35"
app:tabGravity="fill"
app:tabIconTint="@color/tab_icon"
app:tabIndicatorColor="@color/accent_normal_theme"
app:tabIndicatorGravity="top"
app:tabRippleColor="@android:color/transparent">
<com.google.android.material.tabs.TabItem
android:id="@+id/default_tab_item"
android:layout_width="0dp"
android:layout_height="match_parent"
android:contentDescription="@string/tab_header_label"
android:layout="@layout/tabs_tray_tab_counter"
app:tabIconTint="@color/tab_icon" />
<com.google.android.material.tabs.TabItem
android:id="@+id/private_tab_item"
android:layout_width="0dp"
android:layout_height="match_parent"
android:contentDescription="@string/tabs_header_private_tabs_title"
android:icon="@drawable/ic_private_browsing" />
</com.google.android.material.tabs.TabLayout>
<ImageButton
android:id="@+id/tab_tray_new_tab"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/add_tab"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="@id/tab_layout"
app:layout_constraintEnd_toStartOf="@id/tab_tray_overflow"
app:layout_constraintTop_toTopOf="@id/tab_layout"
app:tint="@color/primary_text_normal_theme"
app:srcCompat="@drawable/ic_new" />
<ImageButton
android:id="@+id/tab_tray_overflow"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="0dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/open_tabs_menu"
android:visibility="visible"
app:tint="@color/accent_normal_theme"
app:layout_constraintBottom_toBottomOf="@id/tab_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tab_layout"
app:srcCompat="@drawable/ic_menu" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:id="@+id/divider"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="@color/tab_tray_item_divider_normal_theme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@+id/topBar" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/tabsTray"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:scrollbars="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/divider" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,193 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/tab_wrapper"
style="@style/BottomSheetModal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:backgroundTint="@color/foundation_normal_theme">
<View
android:id="@+id/handle"
android:layout_width="0dp"
android:layout_height="3dp"
android:layout_marginTop="8dp"
android:visibility="gone"
android:background="@color/secondary_text_normal_theme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.1" />
<TextView
android:id="@+id/tab_tray_empty_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center_horizontal"
android:paddingTop="80dp"
android:text="@string/no_open_tabs_description"
android:textColor="?secondaryText"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/topBar" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/topBar"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/foundation_normal_theme"
app:layout_constraintTop_toBottomOf="@+id/handle">
<ImageButton
android:id="@+id/exit_multi_select"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="0dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tab_tray_close_multiselect_content_description"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/multiselect_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/multiselect_title"
app:srcCompat="@drawable/ic_close"
app:tint="@color/contrast_text_normal_theme" />
<TextView
android:id="@+id/multiselect_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:textColor="@color/contrast_text_normal_theme"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/collect_multi_select"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@+id/exit_multi_select"
app:layout_constraintTop_toTopOf="parent"
tools:text="3 selected" />
<TextView
android:id="@+id/collect_multi_select"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tab_tray_collection_button_multiselect_content_description"
android:drawablePadding="8dp"
android:gravity="center_vertical"
android:text="@string/tab_tray_save_to_collection"
android:textAllCaps="true"
android:textColor="@color/contrast_text_normal_theme"
android:textSize="14sp"
android:textStyle="bold"
android:visibility="gone"
app:drawableStartCompat="@drawable/ic_tab_collection"
app:drawableTint="@color/contrast_text_normal_theme"
app:fontFamily="@font/metropolis_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/exit_tabs_screen"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="0dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tabs_screen_close_screen_description"
app:layout_constraintBottom_toBottomOf="@+id/tab_layout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/tab_layout"
app:srcCompat="@drawable/mozac_ic_back"
app:tint="@color/primary_text_normal_theme" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="0dp"
android:layout_height="match_parent"
android:background="@color/foundation_normal_theme"
app:layout_constraintStart_toEndOf="@id/exit_tabs_screen"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.35"
app:tabGravity="fill"
app:tabIconTint="@color/tab_icon"
app:tabIndicatorColor="@color/accent_normal_theme"
app:tabRippleColor="@android:color/transparent">
<com.google.android.material.tabs.TabItem
android:id="@+id/default_tab_item"
android:layout_width="0dp"
android:layout_height="match_parent"
android:contentDescription="@string/tab_header_label"
android:layout="@layout/tabs_tray_tab_counter"
app:tabIconTint="@color/tab_icon" />
<com.google.android.material.tabs.TabItem
android:id="@+id/private_tab_item"
android:layout_width="0dp"
android:layout_height="match_parent"
android:contentDescription="@string/tabs_header_private_tabs_title"
android:icon="@drawable/ic_private_browsing" />
</com.google.android.material.tabs.TabLayout>
<ImageButton
android:id="@+id/tab_tray_new_tab"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/add_tab"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="@id/tab_layout"
app:layout_constraintEnd_toStartOf="@id/tab_tray_overflow"
app:layout_constraintTop_toTopOf="@id/tab_layout"
app:tint="@color/primary_text_normal_theme"
app:srcCompat="@drawable/ic_new" />
<ImageButton
android:id="@+id/tab_tray_overflow"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="0dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/open_tabs_menu"
android:visibility="visible"
app:tint="@color/accent_normal_theme"
app:layout_constraintBottom_toBottomOf="@id/tab_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tab_layout"
app:srcCompat="@drawable/ic_menu" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:id="@+id/divider"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="@color/tab_tray_item_divider_normal_theme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/topBar" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/tabsTray"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:paddingBottom="140dp"
android:scrollbars="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/divider" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -4,7 +4,6 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:mozac="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/tab_wrapper"
style="@style/BottomSheetModal"
@ -135,7 +134,7 @@
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/add_tab"
android:visibility="gone"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="@id/tab_layout"
app:layout_constraintEnd_toStartOf="@id/tab_tray_overflow"
app:layout_constraintTop_toTopOf="@id/tab_layout"

@ -19,4 +19,4 @@
app:elevation="99dp"
app:borderWidth="0dp"
app:icon="@drawable/ic_new"
app:iconTint="@color/photonWhite" />
app:iconTint="@color/photonWhite" />

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?><!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/new_tab_button"
style="@style/TabTrayFab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:scrollbars="none"
android:layout_margin="16dp"
android:backgroundTint="@color/accent_normal_theme"
android:contentDescription="@string/add_tab"
android:elevation="99dp"
android:text="@string/tab_drawer_fab_content"
android:textColor="@color/photonWhite"
app:elevation="99dp"
app:borderWidth="0dp"
app:icon="@drawable/ic_new"
app:iconTint="@color/photonWhite" />

@ -0,0 +1,179 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/tab_wrapper"
style="@style/TopSheetModal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:backgroundTint="@color/foundation_normal_theme"
app:layout_behavior="org.mozilla.fenix.components.topsheet.TopSheetBehavior">
<View
android:id="@+id/handle"
android:layout_width="0dp"
android:layout_height="3dp"
android:layout_marginBottom="8dp"
android:background="@color/secondary_text_normal_theme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintWidth_percent="0.1" />
<TextView
android:id="@+id/tab_tray_empty_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center_horizontal"
android:paddingBottom="80dp"
android:text="@string/no_open_tabs_description"
android:textColor="?secondaryText"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@+id/divider" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/topBar"
android:layout_width="match_parent"
android:layout_height="80dp"
android:background="@color/foundation_normal_theme"
app:layout_constraintBottom_toTopOf="@id/handle">
<ImageButton
android:id="@+id/exit_multi_select"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="0dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tab_tray_close_multiselect_content_description"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/multiselect_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/multiselect_title"
app:srcCompat="@drawable/ic_close"
app:tint="@color/contrast_text_normal_theme" />
<TextView
android:id="@+id/multiselect_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:textColor="@color/contrast_text_normal_theme"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/collect_multi_select"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@+id/exit_multi_select"
app:layout_constraintTop_toTopOf="parent"
tools:text="3 selected" />
<TextView
android:id="@+id/collect_multi_select"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tab_tray_collection_button_multiselect_content_description"
android:drawablePadding="8dp"
android:gravity="center_vertical"
android:text="@string/tab_tray_save_to_collection"
android:textAllCaps="true"
android:textColor="@color/contrast_text_normal_theme"
android:textSize="14sp"
android:textStyle="bold"
android:visibility="gone"
app:drawableStartCompat="@drawable/ic_tab_collection"
app:drawableTint="@color/contrast_text_normal_theme"
app:fontFamily="@font/metropolis_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="0dp"
android:layout_height="80dp"
android:background="@color/foundation_normal_theme"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.5"
app:tabGravity="fill"
app:tabIconTint="@color/tab_icon"
app:tabIndicatorColor="@color/accent_normal_theme"
app:tabIndicatorGravity="top"
app:tabRippleColor="@android:color/transparent">
<com.google.android.material.tabs.TabItem
android:id="@+id/default_tab_item"
android:layout_width="0dp"
android:layout_height="match_parent"
android:contentDescription="@string/tab_header_label"
android:layout="@layout/tabs_tray_tab_counter"
app:tabIconTint="@color/tab_icon" />
<com.google.android.material.tabs.TabItem
android:id="@+id/private_tab_item"
android:layout_width="0dp"
android:layout_height="match_parent"
android:contentDescription="@string/tabs_header_private_tabs_title"
android:icon="@drawable/ic_private_browsing" />
</com.google.android.material.tabs.TabLayout>
<ImageButton
android:id="@+id/tab_tray_new_tab"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/add_tab"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="@id/tab_layout"
app:layout_constraintEnd_toStartOf="@id/tab_tray_overflow"
app:layout_constraintTop_toTopOf="@id/tab_layout"
app:tint="@color/primary_text_normal_theme"
app:srcCompat="@drawable/ic_new" />
<ImageButton
android:id="@+id/tab_tray_overflow"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="0dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/open_tabs_menu"
android:visibility="visible"
app:tint="@color/accent_normal_theme"
app:layout_constraintBottom_toBottomOf="@id/tab_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tab_layout"
app:srcCompat="@drawable/ic_menu" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:id="@+id/divider"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="@color/tab_tray_item_divider_normal_theme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@+id/topBar" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/tabsTray"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:scrollbars="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/divider" />
</androidx.constraintlayout.widget.ConstraintLayout>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save