Do all of Iceraven in one commit
Bump top site limit under its new name Fix unresilved conflict Allow Nimbus to generate code that allows the fork flavors Drop private browsing page again Use the new text color name Point at paged style again Bring back Sentry so we don't have to change the code that would call it Note we need to build with app: now for some reason Also prefix Github build commands Adapt to settings moving Parse Iceraven versions Discover the linter's formatter Get rid of some Detekt complaints Handle more weird version strings Finish README Stop auto-release changelogs from being too huge Fix #440 by reattaching settingspull/543/head
@ -0,0 +1,24 @@
|
||||
name: PR comment
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
branches:
|
||||
- fork
|
||||
jobs: # Disabled because we cannot build changes from fork PRs using this repo's secrets due to Github limitations. So, the built apk will be from wrong code, so this is pointless.
|
||||
comment-on-pr:
|
||||
runs-on: ubuntu-latest
|
||||
if: "! contains(toJSON(github.event.pull_request.title), '[skip ci]')"
|
||||
steps:
|
||||
- name: Comment on PR with link to checks page
|
||||
uses: mshick/add-pr-comment@v1
|
||||
with:
|
||||
message: |
|
||||
### Download the built apks
|
||||
You can download the apks built by Github actions **after** the CI checks pass.
|
||||
Please go to the <a href="${{ github.event.pull_request.html_url }}/checks">checks page for this PR</a> to find the zipped apk files under the artifacts drop-down, as seen in the example screenshot below.
|
||||
|
||||
Note that you will have to click on the "Android build PR" tab on the left side to see the artifacts.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/fork-maintainers/iceraven-browser/fork/.github/imgs/download-artifacts-screenshot.png" />
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
allow-repeats: false
|
After Width: | Height: | Size: 99 KiB |
@ -0,0 +1,115 @@
|
||||
name: Android build PR
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- fork
|
||||
jobs:
|
||||
run-build:
|
||||
runs-on: ubuntu-latest
|
||||
if: "! contains(toJSON(github.event.pull_request.title), '[skip ci]')"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
- name: Install Android SDK with pieces Gradle skips
|
||||
run: ./automation/iceraven/install-sdk.sh
|
||||
- name: Create version name
|
||||
run: echo "VERSION_NAME=$(git describe --tags HEAD)" >> $GITHUB_ENV
|
||||
- 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: app:assembleForkRelease -PversionName=${{ env.VERSION_NAME }}
|
||||
|
||||
|
||||
run-testDebug:
|
||||
runs-on: ubuntu-latest
|
||||
if: "! contains(toJSON(github.event.pull_request.title), '[skip ci]')"
|
||||
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
|
||||
if: "! contains(toJSON(github.event.pull_request.title), '[skip ci]')"
|
||||
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
|
||||
if: "! contains(toJSON(github.event.pull_request.title), '[skip ci]')"
|
||||
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
|
||||
if: "! contains(toJSON(github.event.pull_request.title), '[skip ci]')"
|
||||
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,143 @@
|
||||
name: Android build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- fork
|
||||
jobs:
|
||||
run-build:
|
||||
runs-on: ubuntu-latest
|
||||
if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
- name: Install Android SDK with pieces Gradle skips
|
||||
run: ./automation/iceraven/install-sdk.sh
|
||||
- name: Create version name
|
||||
run: echo "VERSION_NAME=$(git describe --tags HEAD)" >> $GITHUB_ENV
|
||||
- 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: app:assembleForkRelease -PversionName=${{ env.VERSION_NAME }}
|
||||
- name: Create signed APKs
|
||||
uses: abhijitvalluri/sign-apks@v0.8
|
||||
with:
|
||||
releaseDirectory: app/build/outputs/apk/forkRelease/
|
||||
signingKeyBase64: ${{ secrets.DEBUG_SIGNING_KEY }}
|
||||
alias: ${{ secrets.DEBUG_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.DEBUG_KEY_STORE_PASSWORD }}
|
||||
keyPassword: ${{ secrets.DEBUG_KEY_PASSWORD }}
|
||||
- name: Archive arm64 apk
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: app-arm64-v8a-forkRelease.apk
|
||||
path: app/build/outputs/apk/forkRelease/app-arm64-v8a-forkRelease.apk
|
||||
- name: Archive armeabi apk
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: app-armeabi-v7a-forkRelease.apk
|
||||
path: app/build/outputs/apk/forkRelease/app-armeabi-v7a-forkRelease.apk
|
||||
- name: Archive x86 apk
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: app-x86-forkRelease.apk
|
||||
path: app/build/outputs/apk/forkRelease/app-x86-forkRelease.apk
|
||||
- name: Archive x86_64 apk
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: app-x86_64-forkRelease.apk
|
||||
path: app/build/outputs/apk/forkRelease/app-x86_64-forkRelease.apk
|
||||
|
||||
|
||||
run-testDebug:
|
||||
runs-on: ubuntu-latest
|
||||
if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"
|
||||
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
|
||||
if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"
|
||||
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
|
||||
if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"
|
||||
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
|
||||
if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"
|
||||
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,111 @@
|
||||
name: Release Automation
|
||||
on:
|
||||
create:
|
||||
jobs:
|
||||
release-automation:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
if: "contains(toJSON(github.event.ref_type), 'tag') && contains(toJSON(github.event.ref), 'iceraven')"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
- name: Install Android SDK with pieces Gradle skips
|
||||
run: ./automation/iceraven/install-sdk.sh
|
||||
- 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: app:assembleForkRelease -PversionName=${{ github.event.ref }}
|
||||
- name: Create signed APKs
|
||||
uses: abhijitvalluri/sign-apks@v0.8
|
||||
with:
|
||||
releaseDirectory: app/build/outputs/apk/forkRelease/
|
||||
signingKeyBase64: ${{ secrets.DEBUG_SIGNING_KEY }}
|
||||
alias: ${{ secrets.DEBUG_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.DEBUG_KEY_STORE_PASSWORD }}
|
||||
keyPassword: ${{ secrets.DEBUG_KEY_PASSWORD }}
|
||||
- name: Create changelog
|
||||
run: |
|
||||
PREVIOUS_RELEASE_TAG=$(git tag --list iceraven-* --sort=-creatordate | tail -n+2 | head -n 1)
|
||||
|
||||
echo "## Automated release of version ${{ github.event.ref }} browser" >>temp_changelog.md
|
||||
if [[ "$(git log | grep $PREVIOUS_RELEASE_TAG | wc -l)" != "0" ]] ; then
|
||||
# There's a path from the previous release to now
|
||||
echo "<details>" >>temp_changelog.md
|
||||
echo "<summary>Click to expand</summary>" >>temp_changelog.md
|
||||
echo " " >>temp_changelog.md
|
||||
echo "This is an automated release, consisting of the following changes:" >>temp_changelog.md
|
||||
echo "### Change log (commit history since previous release)" >>temp_changelog.md
|
||||
echo " " >>temp_changelog.md
|
||||
git log ${{ github.event.ref }}...$PREVIOUS_RELEASE_TAG --pretty='format:%C(auto)%h (%as) %s' | head -n 1000 >>temp_changelog.md
|
||||
echo " " >>temp_changelog.md
|
||||
echo " " >>temp_changelog.md
|
||||
echo "</details>" >>temp_changelog.md
|
||||
else
|
||||
echo "This is an automated release, not directly descended from the previous release." >>temp_changelog.md
|
||||
fi
|
||||
echo "**NOTE**: @fork-maintainers, you can edit these auto-generated release notes with a more user-friendly summary of the key changes, if needed." >>temp_changelog.md
|
||||
echo " " >>temp_changelog.md
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.event.ref }}
|
||||
release_name: "Version ${{ github.event.ref }}"
|
||||
draft: false
|
||||
prerelease: false
|
||||
body_path: temp_changelog.md
|
||||
|
||||
- name: Upload arm64 apk
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/forkRelease/app-arm64-v8a-forkRelease.apk
|
||||
asset_name: ${{ github.event.ref }}-browser-arm64-v8a-forkRelease.apk
|
||||
asset_content_type: application/vnd.android.package-archive
|
||||
|
||||
|
||||
- name: Upload armeabi apk
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/forkRelease/app-armeabi-v7a-forkRelease.apk
|
||||
asset_name: ${{ github.event.ref }}-browser-armeabi-v7a-forkRelease.apk
|
||||
asset_content_type: application/vnd.android.package-archive
|
||||
|
||||
|
||||
- name: Upload x86 apk
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/forkRelease/app-x86-forkRelease.apk
|
||||
asset_name: ${{ github.event.ref }}-browser-x86-forkRelease.apk
|
||||
asset_content_type: application/vnd.android.package-archive
|
||||
|
||||
|
||||
- name: Upload x86_64 apk
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/forkRelease/app-x86_64-forkRelease.apk
|
||||
asset_name: ${{ github.event.ref }}-browser-x86_64-forkRelease.apk
|
||||
asset_content_type: application/vnd.android.package-archive
|
@ -0,0 +1,16 @@
|
||||
language: android
|
||||
dist: trusty
|
||||
script:
|
||||
# Prepare SDK
|
||||
- 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
|
||||
# The build needs this but Gradle refuses to fetch it.
|
||||
- sdkmanager "ndk;21.0.6113669"
|
||||
# Run tests
|
||||
- ./gradlew -q testDebug 2>&1
|
||||
# Make sure a release build builds
|
||||
- ./gradlew assembleForkRelease -PversionName="$(git describe --tags HEAD)"
|
||||
|
@ -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>
|
After Width: | Height: | Size: 38 KiB |
@ -0,0 +1,27 @@
|
||||
/* 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
|
||||
|
||||
class DebugFenixApplication : FenixApplication() {
|
||||
|
||||
override fun setupLeakCanary() {
|
||||
val isEnabled = components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
|
||||
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>
|
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 7.2 KiB |
After Width: | Height: | Size: 8.4 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 12 KiB |
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>
|
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 68 KiB |
After Width: | Height: | Size: 56 KiB |
After Width: | Height: | Size: 64 KiB |
After Width: | Height: | Size: 58 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>
|
@ -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>
|
@ -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">#F6F6F6</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>
|
After Width: | Height: | Size: 45 KiB |
@ -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;
|
||||
|
||||
public class Adjust {
|
||||
public static void onCreate(AdjustConfig adjustConfig) {
|
||||
}
|
||||
|
||||
public static void onResume() {
|
||||
}
|
||||
|
||||
public static void onPause() {
|
||||
}
|
||||
|
||||
public static void setEnabled(boolean enabled) {
|
||||
}
|
||||
|
||||
public static void gdprForgetMe(Object ignored) {
|
||||
}
|
||||
}
|
@ -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,39 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package com.google.android.gms.ads.identifier;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
|
||||
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) {
|
||||
return new Info();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
package com.google.android.play.core.review
|
||||
class ReviewManager {
|
||||
|
||||
class FakeReviewFlowTaskResult {
|
||||
val isSuccessful: Boolean = false
|
||||
val result: Any = false
|
||||
}
|
||||
class FakeReviewFlowTask {
|
||||
@Suppress("UNUSED_PARAMETER", "UNUSED_EXPRESSION")
|
||||
fun addOnCompleteListener(ignored: (FakeReviewFlowTaskResult) -> Unit) {
|
||||
1
|
||||
}
|
||||
}
|
||||
fun requestReviewFlow(): FakeReviewFlowTask {
|
||||
return FakeReviewFlowTask()
|
||||
}
|
||||
@Suppress("UNUSED_PARAMETER", "UNUSED_EXPRESSION")
|
||||
fun launchReviewFlow(ignored1: Any, ignored2: Any) {
|
||||
1
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
/* 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.play.core.review;
|
||||
|
||||
import android.content.Context;
|
||||
import com.google.android.play.core.review.ReviewManager;
|
||||
|
||||
|
||||
public class ReviewManagerFactory {
|
||||
|
||||
public static ReviewManager create(Context context) {
|
||||
return new ReviewManager();
|
||||
}
|
||||
|
||||
}
|
@ -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,95 @@
|
||||
/*
|
||||
* 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) {
|
||||
}
|
||||
|
||||
public static String getDeviceId() { return "stub"; }
|
||||
|
||||
public static String getUserId() { return "stub"; }
|
||||
}
|
@ -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,403 @@
|
||||
/* 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 org.mozilla.fenix.Config
|
||||
import org.mozilla.fenix.ext.settings
|
||||
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 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 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 val maxCacheAgeInMinutes: Long = -1
|
||||
) : AddonsProvider {
|
||||
|
||||
private val logger = Logger("PagedAddonCollectionProvider")
|
||||
|
||||
private val diskCacheLock = Any()
|
||||
|
||||
/**
|
||||
* Get the account we should be fetching addons from.
|
||||
*/
|
||||
private fun getCollectionAccount(): String {
|
||||
var result = context.settings().customAddonsAccount
|
||||
if (Config.channel.isNightlyOrDebug && context.settings().amoCollectionOverrideConfigured()) {
|
||||
result = context.settings().overrideAmoUser
|
||||
}
|
||||
|
||||
logger.info("Determined collection account: ${result}")
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the collection name we should be fetching addons from.
|
||||
*/
|
||||
private fun getCollectionName(): String {
|
||||
var result = context.settings().customAddonsCollection
|
||||
if (Config.channel.isNightlyOrDebug && context.settings().amoCollectionOverrideConfigured()) {
|
||||
result = context.settings().overrideAmoCollection
|
||||
}
|
||||
|
||||
logger.info("Determined collection name: ${result}")
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param language optional language that will be ignored.
|
||||
* @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?,
|
||||
language: String?
|
||||
): List<Addon> {
|
||||
val cachedAddons = if (allowCache && !cacheExpired(context)) {
|
||||
readFromDiskCache()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val collectionAccount = getCollectionAccount()
|
||||
val collectionName = getCollectionName()
|
||||
|
||||
if (cachedAddons != null) {
|
||||
logger.info("Providing cached list of addons for ${collectionAccount} collection ${collectionName}")
|
||||
return cachedAddons
|
||||
} else {
|
||||
logger.info("Fetching fresh list of addons for ${collectionAccount} collection ${collectionName}")
|
||||
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) {
|
||||
logger.info("Storing cache file")
|
||||
synchronized(diskCacheLock) {
|
||||
getCacheFile(context).writeString { collectionResponse }
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun readFromDiskCache(): List<Addon>? {
|
||||
logger.info("Loading cache file")
|
||||
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 {
|
||||
val collectionAccount = getCollectionAccount()
|
||||
val collectionName = getCollectionName()
|
||||
return File(context.filesDir, COLLECTION_FILE_NAME.format(collectionAccount, collectionName))
|
||||
}
|
||||
|
||||
fun deleteCacheFile(context: Context): Boolean {
|
||||
logger.info("Clearing cache file")
|
||||
synchronized(diskCacheLock) {
|
||||
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")) {
|
||||
val download = getDownload()
|
||||
Addon(
|
||||
id = getSafeString("guid"),
|
||||
authors = getAuthors(),
|
||||
categories = getCategories(),
|
||||
createdAt = getSafeString("created"),
|
||||
updatedAt = getSafeString("last_updated"),
|
||||
downloadId = download?.getDownloadId() ?: "",
|
||||
downloadUrl = download?.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.getDownload(): JSONObject? {
|
||||
return (
|
||||
getJSONObject("current_version")
|
||||
.optJSONArray("files")
|
||||
?.getJSONObject(0)
|
||||
)
|
||||
}
|
||||
|
||||
internal fun JSONObject.getDownloadId(): String {
|
||||
return getSafeString("id")
|
||||
}
|
||||
|
||||
internal fun JSONObject.getDownloadUrl(): String {
|
||||
return 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,309 @@
|
||||
/* 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.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.databinding.MozacFeatureAddonsFragmentDialogAddonInstalledBinding
|
||||
import mozilla.components.feature.addons.ui.translateName
|
||||
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.
|
||||
*/
|
||||
// We have an extra "Lint" Android Studio linter pass that Android Components
|
||||
// where the original code came from doesn't. So we tell it to ignore us. Make
|
||||
// sure to keep up with changes in Android Components though.
|
||||
@SuppressLint("all")
|
||||
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
|
||||
)
|
||||
|
||||
val binding = MozacFeatureAddonsFragmentDialogAddonInstalledBinding.bind(rootView)
|
||||
|
||||
rootView.findViewById<TextView>(R.id.title).text =
|
||||
requireContext().getString(
|
||||
R.string.mozac_feature_addons_installed_dialog_title,
|
||||
addon.translateName(requireContext()),
|
||||
requireContext().appName
|
||||
)
|
||||
|
||||
val icon = safeArguments.getParcelable<Bitmap>(KEY_ICON)
|
||||
if (icon != null) {
|
||||
binding.icon.setImageDrawable(BitmapDrawable(resources, icon))
|
||||
} else {
|
||||
iconJob = fetchIcon(addon, binding.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(
|
||||
ContextCompat.getDrawable(context, 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,434 @@
|
||||
/* 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.translateName
|
||||
import mozilla.components.feature.addons.ui.translateSummary
|
||||
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")
|
||||
// We have an extra "Lint" Android Studio linter pass that Android Components
|
||||
// where the original code came from doesn't. So we tell it to ignore us. Make
|
||||
// sure to keep up with changes in Android Components though.
|
||||
@SuppressLint("all")
|
||||
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)
|
||||
val divider = view.findViewById<View>(R.id.divider)
|
||||
return SectionViewHolder(view, titleView, divider)
|
||||
}
|
||||
|
||||
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.translateName(context)
|
||||
} else {
|
||||
addon.id
|
||||
}
|
||||
|
||||
if (addon.translatableSummary.isNotEmpty()) {
|
||||
holder.summaryView.text = addon.translateSummary(context)
|
||||
} 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
|
||||
}
|
||||
}
|
@ -0,0 +1,835 @@
|
||||
/* 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>
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrain a number between limits.
|
||||
*/
|
||||
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
|
||||
}
|
||||
}
|
@ -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,14 @@
|
||||
<?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/. -->
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/search"
|
||||
android:icon="@drawable/ic_search"
|
||||
android:title="@string/addons_search_hint"
|
||||
app:iconTint="?textPrimary"
|
||||
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||
android:contentDescription="@string/addons_search_hint"
|
||||
app:showAsAction="ifRoom|collapseActionView" />
|
||||
</menu>
|
@ -1,8 +1,5 @@
|
||||
<?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/. -->
|
||||
<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>
|
||||
</adaptive-icon>
|
@ -1,8 +1,5 @@
|
||||
<?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/. -->
|
||||
<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>
|
||||
</adaptive-icon>
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 19 KiB |