Do all of Iceraven in one commit
@ -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: 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: 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,106 @@
|
|||||||
|
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: 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
|
||||||
|
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' >>temp_changelog.md
|
||||||
|
echo " " >>temp_changelog.md
|
||||||
|
echo " " >>temp_changelog.md
|
||||||
|
echo "</details>" >>temp_changelog.md
|
||||||
|
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,374 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
@file:Suppress("TooManyFunctions")
|
||||||
|
|
||||||
|
package io.github.forkmaintainers.iceraven.components
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AtomicFile
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import mozilla.components.concept.fetch.Client
|
||||||
|
import mozilla.components.concept.fetch.Request
|
||||||
|
import mozilla.components.concept.fetch.isSuccess
|
||||||
|
import mozilla.components.feature.addons.Addon
|
||||||
|
import mozilla.components.feature.addons.AddonsProvider
|
||||||
|
import mozilla.components.support.base.log.logger.Logger
|
||||||
|
import mozilla.components.support.ktx.kotlin.sanitizeURL
|
||||||
|
import mozilla.components.support.ktx.util.readAndDeserialize
|
||||||
|
import mozilla.components.support.ktx.util.writeString
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
internal const val API_VERSION = "api/v4"
|
||||||
|
internal const val DEFAULT_SERVER_URL = "https://addons.mozilla.org"
|
||||||
|
internal const val DEFAULT_COLLECTION_ACCOUNT = "mozilla"
|
||||||
|
internal const val DEFAULT_COLLECTION_NAME = "7e8d6dc651b54ab385fb8791bf9dac"
|
||||||
|
internal const val COLLECTION_FILE_NAME = "%s_components_addon_collection_%s.json"
|
||||||
|
internal const val MINUTE_IN_MS = 60 * 1000
|
||||||
|
internal const val DEFAULT_READ_TIMEOUT_IN_SECONDS = 20L
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide access to the collections AMO API.
|
||||||
|
* https://addons-server.readthedocs.io/en/latest/topics/api/collections.html
|
||||||
|
*
|
||||||
|
* Unlike the android-components version, supports multiple-page responses and
|
||||||
|
* custom collection accounts.
|
||||||
|
*
|
||||||
|
* Needs to extend AddonCollectionProvider because AddonsManagerAdapter won't
|
||||||
|
* take just any AddonsProvider.
|
||||||
|
*
|
||||||
|
* @property serverURL The url of the endpoint to interact with e.g production, staging
|
||||||
|
* or testing. Defaults to [DEFAULT_SERVER_URL].
|
||||||
|
* @property collectionAccount The account owning the collection to access, defaults
|
||||||
|
* to [DEFAULT_COLLECTION_ACCOUNT].
|
||||||
|
* @property collectionName The name of the collection to access, defaults
|
||||||
|
* to [DEFAULT_COLLECTION_NAME].
|
||||||
|
* @property maxCacheAgeInMinutes maximum time (in minutes) the collection cache
|
||||||
|
* should remain valid. Defaults to -1, meaning no cache is being used by default.
|
||||||
|
* @property client A reference of [Client] for interacting with the AMO HTTP api.
|
||||||
|
*/
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
class PagedAddonCollectionProvider(
|
||||||
|
private val context: Context,
|
||||||
|
private val client: Client,
|
||||||
|
private val serverURL: String = DEFAULT_SERVER_URL,
|
||||||
|
private var collectionAccount: String = DEFAULT_COLLECTION_ACCOUNT,
|
||||||
|
private var collectionName: String = DEFAULT_COLLECTION_NAME,
|
||||||
|
private val maxCacheAgeInMinutes: Long = -1
|
||||||
|
) : AddonsProvider {
|
||||||
|
|
||||||
|
private val logger = Logger("PagedAddonCollectionProvider")
|
||||||
|
|
||||||
|
private val diskCacheLock = Any()
|
||||||
|
|
||||||
|
fun setCollectionAccount(account: String) {
|
||||||
|
collectionAccount = account
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCollectionName(collection: String) {
|
||||||
|
collectionName = collection
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interacts with the collections endpoint to provide a list of available
|
||||||
|
* add-ons. May return a cached response, if available, not expired (see
|
||||||
|
* [maxCacheAgeInMinutes]) and allowed (see [allowCache]).
|
||||||
|
*
|
||||||
|
* @param allowCache whether or not the result may be provided
|
||||||
|
* from a previously cached response, defaults to true.
|
||||||
|
* @param readTimeoutInSeconds optional timeout in seconds to use when fetching
|
||||||
|
* available add-ons from a remote endpoint. If not specified [DEFAULT_READ_TIMEOUT_IN_SECONDS]
|
||||||
|
* will be used.
|
||||||
|
* @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
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cachedAddons != null) {
|
||||||
|
return cachedAddons
|
||||||
|
} else {
|
||||||
|
return getAllPages(listOf(
|
||||||
|
serverURL,
|
||||||
|
API_VERSION,
|
||||||
|
"accounts/account",
|
||||||
|
collectionAccount,
|
||||||
|
"collections",
|
||||||
|
collectionName,
|
||||||
|
"addons"
|
||||||
|
).joinToString("/"), readTimeoutInSeconds ?: DEFAULT_READ_TIMEOUT_IN_SECONDS).also {
|
||||||
|
// Cache the JSON object before we parse out the addons
|
||||||
|
if (maxCacheAgeInMinutes > 0) {
|
||||||
|
writeToDiskCache(it.toString())
|
||||||
|
}
|
||||||
|
}.getAddons()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all pages of add-ons from the given URL (following the "next"
|
||||||
|
* field in the returned JSON) and combines the "results" arrays into that
|
||||||
|
* of the first page. Returns that coalesced object.
|
||||||
|
*
|
||||||
|
* @param url URL of the first page to fetch
|
||||||
|
* @param readTimeoutInSeconds timeout in seconds to use when fetching each page.
|
||||||
|
* @throws IOException if the request failed, or could not be executed due to cancellation,
|
||||||
|
* a connectivity problem or a timeout.
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
suspend fun getAllPages(url: String, readTimeoutInSeconds: Long): JSONObject {
|
||||||
|
// Fetch and compile all the pages into one object we can return
|
||||||
|
var compiledResponse: JSONObject? = null
|
||||||
|
// Each page tells us where to get the next page, if there is one
|
||||||
|
var nextURL: String? = url
|
||||||
|
while (nextURL != null) {
|
||||||
|
client.fetch(
|
||||||
|
Request(
|
||||||
|
url = nextURL,
|
||||||
|
readTimeout = Pair(readTimeoutInSeconds, TimeUnit.SECONDS)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.use { response ->
|
||||||
|
if (!response.isSuccess) {
|
||||||
|
val errorMessage = "Failed to fetch addon collection. Status code: ${response.status}"
|
||||||
|
logger.error(errorMessage)
|
||||||
|
throw IOException(errorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentResponse = try {
|
||||||
|
JSONObject(response.body.string(Charsets.UTF_8))
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
throw IOException(e)
|
||||||
|
}
|
||||||
|
if (compiledResponse == null) {
|
||||||
|
compiledResponse = currentResponse
|
||||||
|
} else {
|
||||||
|
// Write the addons into the first response
|
||||||
|
compiledResponse!!.getJSONArray("results").concat(currentResponse.getJSONArray("results"))
|
||||||
|
}
|
||||||
|
nextURL = if (currentResponse.isNull("next")) null else currentResponse.getString("next")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return compiledResponse!!
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches given Addon icon from the url and returns a decoded Bitmap
|
||||||
|
* @throws IOException if the request could not be executed due to cancellation,
|
||||||
|
* a connectivity problem or a timeout.
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
suspend fun getAddonIconBitmap(addon: Addon): Bitmap? {
|
||||||
|
var bitmap: Bitmap? = null
|
||||||
|
if (addon.iconUrl != "") {
|
||||||
|
client.fetch(
|
||||||
|
Request(url = addon.iconUrl.sanitizeURL())
|
||||||
|
).use { response ->
|
||||||
|
if (response.isSuccess) {
|
||||||
|
response.body.useStream {
|
||||||
|
bitmap = BitmapFactory.decodeStream(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun writeToDiskCache(collectionResponse: String) {
|
||||||
|
synchronized(diskCacheLock) {
|
||||||
|
getCacheFile(context).writeString { collectionResponse }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun readFromDiskCache(): List<Addon>? {
|
||||||
|
synchronized(diskCacheLock) {
|
||||||
|
return getCacheFile(context).readAndDeserialize {
|
||||||
|
JSONObject(it).getAddons()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun cacheExpired(context: Context): Boolean {
|
||||||
|
return getCacheLastUpdated(context) < Date().time - maxCacheAgeInMinutes * MINUTE_IN_MS
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun getCacheLastUpdated(context: Context): Long {
|
||||||
|
val file = getBaseCacheFile(context)
|
||||||
|
return if (file.exists()) file.lastModified() else -1
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCacheFile(context: Context): AtomicFile {
|
||||||
|
return AtomicFile(getBaseCacheFile(context))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBaseCacheFile(context: Context): File {
|
||||||
|
return File(context.filesDir, COLLECTION_FILE_NAME.format(collectionAccount, collectionName))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteCacheFile(context: Context): Boolean {
|
||||||
|
val file = getBaseCacheFile(context)
|
||||||
|
return if (file.exists()) file.delete() else false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun JSONObject.getAddons(): List<Addon> {
|
||||||
|
val addonsJson = getJSONArray("results")
|
||||||
|
return (0 until addonsJson.length()).map { index ->
|
||||||
|
addonsJson.getJSONObject(index).toAddons()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun JSONObject.toAddons(): Addon {
|
||||||
|
return with(getJSONObject("addon")) {
|
||||||
|
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,436 @@
|
|||||||
|
/* 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.
|
||||||
|
* @property excludedAddonIDs A list of add-on IDs we could exclude. Currently ignored.
|
||||||
|
*/
|
||||||
|
@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,
|
||||||
|
private val excludedAddonIDs: List<String> = emptyList()
|
||||||
|
) : 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,827 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
/* Added the Mozilla Public License above to avoid failing detekt rule */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2015 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.mozilla.fenix.components.topsheet
|
||||||
|
|
||||||
|
import android.animation.ValueAnimator
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.AbsSavedState
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.VelocityTracker
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewConfiguration
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.annotation.IntDef
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.core.view.NestedScrollingChild
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.customview.widget.ViewDragHelper
|
||||||
|
import com.google.android.material.R
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
|
import com.google.android.material.shape.ShapeAppearanceModel
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interaction behavior plugin for a child view of [CoordinatorLayout] to make it work as
|
||||||
|
* a top sheet.
|
||||||
|
*/
|
||||||
|
@Suppress("TooManyFunctions", "LargeClass")
|
||||||
|
class TopSheetBehavior<V : View?>
|
||||||
|
/**
|
||||||
|
* Default constructor for inflating TopSheetBehaviors from layout.
|
||||||
|
*
|
||||||
|
* @param context The [Context].
|
||||||
|
* @param attrs The [AttributeSet].
|
||||||
|
*/(context: Context, attrs: AttributeSet?) : CoordinatorLayout.Behavior<V>(context, attrs) {
|
||||||
|
/**
|
||||||
|
* Callback for monitoring events about top sheets.
|
||||||
|
*/
|
||||||
|
interface TopSheetCallback {
|
||||||
|
/**
|
||||||
|
* Called when the top sheet changes its state.
|
||||||
|
*
|
||||||
|
* @param topSheet The top sheet view.
|
||||||
|
* @param newState The new state. This will be one of [.STATE_DRAGGING],
|
||||||
|
* [.STATE_SETTLING], [.STATE_EXPANDED],
|
||||||
|
* [.STATE_COLLAPSED], or [.STATE_HIDDEN].
|
||||||
|
*/
|
||||||
|
fun onStateChanged(topSheet: View, @State newState: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the top sheet is being dragged.
|
||||||
|
*
|
||||||
|
* @param topSheet The top sheet view.
|
||||||
|
* @param slideOffset The new offset of this top sheet within its range, from 0 to 1
|
||||||
|
* when it is moving upward, and from 0 to -1 when it moving downward.
|
||||||
|
* @param isOpening detect showing
|
||||||
|
*/
|
||||||
|
fun onSlide(topSheet: View, slideOffset: Float, isOpening: Boolean?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
@IntDef(
|
||||||
|
STATE_EXPANDED,
|
||||||
|
STATE_COLLAPSED,
|
||||||
|
STATE_DRAGGING,
|
||||||
|
STATE_SETTLING,
|
||||||
|
STATE_HIDDEN
|
||||||
|
)
|
||||||
|
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
|
||||||
|
annotation class State
|
||||||
|
|
||||||
|
private var mMaximumVelocity = 0f
|
||||||
|
private var mPeekHeight = 0
|
||||||
|
private var mMinOffset = 0
|
||||||
|
private var mMaxOffset = 0
|
||||||
|
private var skipCollapsed = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets/Sets the height of the top sheet when it is collapsed.
|
||||||
|
*
|
||||||
|
* @var peekHeight The height of the collapsed top sheet in pixels.
|
||||||
|
* @attr ref com.google.android.material.R.styleable#TopSheetBehavior_Params_behavior_peekHeight
|
||||||
|
*/
|
||||||
|
private var peekHeight: Int
|
||||||
|
get() = mPeekHeight
|
||||||
|
set(peekHeight) {
|
||||||
|
mPeekHeight = 0.coerceAtLeast(peekHeight)
|
||||||
|
if (mViewRef != null && mViewRef!!.get() != null) {
|
||||||
|
mMinOffset =
|
||||||
|
(-mViewRef!!.get()!!.height).coerceAtLeast(
|
||||||
|
-(mViewRef!!.get()!!.height - mPeekHeight))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var mState = STATE_COLLAPSED
|
||||||
|
private var mViewDragHelper: ViewDragHelper? = null
|
||||||
|
private var mIgnoreEvents = false
|
||||||
|
private var mLastNestedScrollDy = 0
|
||||||
|
private var mNestedScrolled = false
|
||||||
|
private var mParentHeight = 0
|
||||||
|
private var mViewRef: WeakReference<V?>? = null
|
||||||
|
private var mNestedScrollingChildRef: WeakReference<View?>? = null
|
||||||
|
private var mCallback: TopSheetCallback? = null
|
||||||
|
private var mVelocityTracker: VelocityTracker? = null
|
||||||
|
private var mActivePointerId = 0
|
||||||
|
private var mInitialY = 0
|
||||||
|
private var mTouchingScrollingChild = false
|
||||||
|
|
||||||
|
/** True if Behavior has a non-null value for the @shapeAppearance attribute */
|
||||||
|
private var shapeThemingEnabled = false
|
||||||
|
|
||||||
|
/** Default Shape Appearance to be used in topsheet */
|
||||||
|
private var shapeAppearanceModelDefault: ShapeAppearanceModel? = null
|
||||||
|
private var materialShapeDrawable: MaterialShapeDrawable? = null
|
||||||
|
private var interpolatorAnimator: ValueAnimator? = null
|
||||||
|
private var isShapeExpanded = false
|
||||||
|
|
||||||
|
var elevation = -1f
|
||||||
|
var isHideable = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
val a = context.obtainStyledAttributes(
|
||||||
|
attrs,
|
||||||
|
R.styleable.BottomSheetBehavior_Layout
|
||||||
|
)
|
||||||
|
shapeThemingEnabled =
|
||||||
|
a.hasValue(R.styleable.BottomSheetBehavior_Layout_shapeAppearance)
|
||||||
|
createMaterialShapeDrawable(context, attrs!!)
|
||||||
|
createShapeValueAnimator()
|
||||||
|
peekHeight = (context.resources.displayMetrics.heightPixels * PEEK_HEIGHT_RATIO).toInt()
|
||||||
|
isHideable = a.getBoolean(
|
||||||
|
R.styleable.BottomSheetBehavior_Layout_behavior_hideable,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
skipCollapsed = a.getBoolean(
|
||||||
|
R.styleable.BottomSheetBehavior_Layout_behavior_skipCollapsed,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
a.recycle()
|
||||||
|
val configuration =
|
||||||
|
ViewConfiguration.get(context)
|
||||||
|
mMaximumVelocity = configuration.scaledMaximumFlingVelocity.toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(parent: CoordinatorLayout, child: V): Parcelable? {
|
||||||
|
return SavedState(
|
||||||
|
super.onSaveInstanceState(
|
||||||
|
parent,
|
||||||
|
child!!
|
||||||
|
), mState
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRestoreInstanceState(
|
||||||
|
parent: CoordinatorLayout,
|
||||||
|
child: V,
|
||||||
|
state: Parcelable
|
||||||
|
) {
|
||||||
|
val ss =
|
||||||
|
state as SavedState
|
||||||
|
super.onRestoreInstanceState(parent, child!!, ss.superState)
|
||||||
|
// Intermediate states are restored as collapsed state
|
||||||
|
mState = if (ss.state == STATE_DRAGGING || ss.state == STATE_SETTLING) {
|
||||||
|
STATE_COLLAPSED
|
||||||
|
} else {
|
||||||
|
ss.state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("ComplexMethod")
|
||||||
|
override fun onLayoutChild(
|
||||||
|
parent: CoordinatorLayout,
|
||||||
|
child: V,
|
||||||
|
layoutDirection: Int
|
||||||
|
): Boolean {
|
||||||
|
if (ViewCompat.getFitsSystemWindows(parent) && !ViewCompat.getFitsSystemWindows(child as View)) {
|
||||||
|
child.fitsSystemWindows = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only set MaterialShapeDrawable as background if shapeTheming is enabled, otherwise will
|
||||||
|
// default to android:background declared in styles or layout.
|
||||||
|
if (shapeThemingEnabled && materialShapeDrawable != null) {
|
||||||
|
ViewCompat.setBackground(child as View, materialShapeDrawable)
|
||||||
|
}
|
||||||
|
// Set elevation on MaterialShapeDrawable
|
||||||
|
// Set elevation on MaterialShapeDrawable
|
||||||
|
if (materialShapeDrawable != null) {
|
||||||
|
// Use elevation attr if set on topsheet; otherwise, use elevation of child view.
|
||||||
|
materialShapeDrawable!!.elevation =
|
||||||
|
if (elevation == -1f) ViewCompat.getElevation(child as View) else elevation
|
||||||
|
// Update the material shape based on initial state.
|
||||||
|
isShapeExpanded = state == BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
materialShapeDrawable!!.interpolation = if (isShapeExpanded) 0f else 1f
|
||||||
|
}
|
||||||
|
|
||||||
|
val savedTop = child!!.top
|
||||||
|
// First let the parent lay it out
|
||||||
|
parent.onLayoutChild(child, layoutDirection)
|
||||||
|
// Offset the top sheet
|
||||||
|
mParentHeight = parent.height
|
||||||
|
mMinOffset = (-child.height).coerceAtLeast(-(child.height - mPeekHeight))
|
||||||
|
mMaxOffset = 0
|
||||||
|
if (mState == STATE_EXPANDED) {
|
||||||
|
ViewCompat.offsetTopAndBottom(child, mMaxOffset)
|
||||||
|
} else if (isHideable && mState == STATE_HIDDEN) {
|
||||||
|
ViewCompat.offsetTopAndBottom(child, mParentHeight)
|
||||||
|
} else if (mState == STATE_COLLAPSED) {
|
||||||
|
ViewCompat.offsetTopAndBottom(child, mMinOffset)
|
||||||
|
} else if (mState == STATE_DRAGGING || mState == STATE_SETTLING) {
|
||||||
|
ViewCompat.offsetTopAndBottom(child, savedTop - child.top)
|
||||||
|
}
|
||||||
|
if (mViewDragHelper == null) {
|
||||||
|
mViewDragHelper = ViewDragHelper.create(parent, mDragCallback)
|
||||||
|
}
|
||||||
|
mViewRef = WeakReference(child)
|
||||||
|
mNestedScrollingChildRef =
|
||||||
|
WeakReference(findScrollingChild(child))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("ComplexMethod", "ReturnCount")
|
||||||
|
override fun onInterceptTouchEvent(
|
||||||
|
parent: CoordinatorLayout,
|
||||||
|
child: V,
|
||||||
|
event: MotionEvent
|
||||||
|
): Boolean {
|
||||||
|
if (!child!!.isShown) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val action = event.actionMasked
|
||||||
|
// Record the velocity
|
||||||
|
if (action == MotionEvent.ACTION_DOWN) {
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
if (mVelocityTracker == null) {
|
||||||
|
mVelocityTracker = VelocityTracker.obtain()
|
||||||
|
}
|
||||||
|
mVelocityTracker!!.addMovement(event)
|
||||||
|
when (action) {
|
||||||
|
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||||
|
mTouchingScrollingChild = false
|
||||||
|
mActivePointerId = MotionEvent.INVALID_POINTER_ID
|
||||||
|
// Reset the ignore flag
|
||||||
|
if (mIgnoreEvents) {
|
||||||
|
mIgnoreEvents = false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MotionEvent.ACTION_DOWN -> {
|
||||||
|
val initialX = event.x.toInt()
|
||||||
|
mInitialY = event.y.toInt()
|
||||||
|
val scroll = mNestedScrollingChildRef!!.get()
|
||||||
|
if (scroll != null && parent.isPointInChildBounds(scroll, initialX, mInitialY)) {
|
||||||
|
mActivePointerId = event.getPointerId(event.actionIndex)
|
||||||
|
mTouchingScrollingChild = true
|
||||||
|
}
|
||||||
|
mIgnoreEvents = mActivePointerId == MotionEvent.INVALID_POINTER_ID &&
|
||||||
|
!parent.isPointInChildBounds(child, initialX, mInitialY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!mIgnoreEvents && mViewDragHelper!!.shouldInterceptTouchEvent(event)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// We have to handle cases that the ViewDragHelper does not capture the top sheet because
|
||||||
|
// it is not the top most view of its parent. This is not necessary when the touch event is
|
||||||
|
// happening over the scrolling content as nested scrolling logic handles that case.
|
||||||
|
val scroll = mNestedScrollingChildRef!!.get()
|
||||||
|
return action == MotionEvent.ACTION_MOVE && scroll != null &&
|
||||||
|
!mIgnoreEvents && mState != STATE_DRAGGING &&
|
||||||
|
!parent.isPointInChildBounds(
|
||||||
|
scroll,
|
||||||
|
event.x.toInt(),
|
||||||
|
event.y.toInt()
|
||||||
|
) && abs(mInitialY - event.y) > mViewDragHelper!!.touchSlop
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTouchEvent(
|
||||||
|
parent: CoordinatorLayout,
|
||||||
|
child: V,
|
||||||
|
event: MotionEvent
|
||||||
|
): Boolean {
|
||||||
|
if (!child!!.isShown) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val action = event.actionMasked
|
||||||
|
if (mState == STATE_DRAGGING && action == MotionEvent.ACTION_DOWN) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (mViewDragHelper != null) {
|
||||||
|
// no crash
|
||||||
|
mViewDragHelper!!.processTouchEvent(event)
|
||||||
|
// Record the velocity
|
||||||
|
if (action == MotionEvent.ACTION_DOWN) {
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
if (mVelocityTracker == null) {
|
||||||
|
mVelocityTracker = VelocityTracker.obtain()
|
||||||
|
}
|
||||||
|
mVelocityTracker!!.addMovement(event)
|
||||||
|
// The ViewDragHelper tries to capture only the top-most View. We have to explicitly tell it
|
||||||
|
// to capture the top sheet in case it is not captured and the touch slop is passed.
|
||||||
|
if (action == MotionEvent.ACTION_MOVE && !mIgnoreEvents &&
|
||||||
|
abs(mInitialY - event.y) > mViewDragHelper!!.touchSlop) {
|
||||||
|
mViewDragHelper!!.captureChildView(
|
||||||
|
child,
|
||||||
|
event.getPointerId(event.actionIndex)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !mIgnoreEvents
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartNestedScroll(
|
||||||
|
coordinatorLayout: CoordinatorLayout,
|
||||||
|
child: V,
|
||||||
|
directTargetChild: View,
|
||||||
|
target: View,
|
||||||
|
nestedScrollAxes: Int
|
||||||
|
): Boolean {
|
||||||
|
mLastNestedScrollDy = 0
|
||||||
|
mNestedScrolled = false
|
||||||
|
return nestedScrollAxes and ViewCompat.SCROLL_AXIS_VERTICAL != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNestedPreScroll(
|
||||||
|
coordinatorLayout: CoordinatorLayout,
|
||||||
|
child: V,
|
||||||
|
target: View,
|
||||||
|
dx: Int,
|
||||||
|
dy: Int,
|
||||||
|
consumed: IntArray
|
||||||
|
) {
|
||||||
|
val scrollingChild = mNestedScrollingChildRef!!.get()
|
||||||
|
if (target !== scrollingChild) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val currentTop = child!!.top
|
||||||
|
val newTop = currentTop - dy
|
||||||
|
if (dy > 0) { // Upward
|
||||||
|
if (!target.canScrollVertically(1)) {
|
||||||
|
if (newTop >= mMinOffset || isHideable) {
|
||||||
|
consumed[1] = dy
|
||||||
|
ViewCompat.offsetTopAndBottom(child, -dy)
|
||||||
|
setStateInternal(STATE_DRAGGING)
|
||||||
|
} else {
|
||||||
|
consumed[1] = currentTop - mMinOffset
|
||||||
|
ViewCompat.offsetTopAndBottom(child, -consumed[1])
|
||||||
|
setStateInternal(STATE_COLLAPSED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (dy < 0) { // Downward
|
||||||
|
// Negative to check scrolling up, positive to check scrolling down
|
||||||
|
if (newTop < mMaxOffset) {
|
||||||
|
consumed[1] = dy
|
||||||
|
ViewCompat.offsetTopAndBottom(child, -dy)
|
||||||
|
setStateInternal(STATE_DRAGGING)
|
||||||
|
} else {
|
||||||
|
consumed[1] = currentTop - mMaxOffset
|
||||||
|
ViewCompat.offsetTopAndBottom(child, -consumed[1])
|
||||||
|
setStateInternal(STATE_EXPANDED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dispatchOnSlide(child.top)
|
||||||
|
mLastNestedScrollDy = dy
|
||||||
|
mNestedScrolled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopNestedScroll(
|
||||||
|
coordinatorLayout: CoordinatorLayout,
|
||||||
|
child: V,
|
||||||
|
target: View
|
||||||
|
) {
|
||||||
|
if (child!!.top == mMaxOffset) {
|
||||||
|
setStateInternal(STATE_EXPANDED)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (target !== mNestedScrollingChildRef!!.get() || !mNestedScrolled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val top: Int
|
||||||
|
val targetState: Int
|
||||||
|
if (mLastNestedScrollDy < 0) {
|
||||||
|
top = mMaxOffset
|
||||||
|
targetState = STATE_EXPANDED
|
||||||
|
} else if (isHideable && shouldHide(child, yVelocity)) {
|
||||||
|
top = -child.height
|
||||||
|
targetState = STATE_HIDDEN
|
||||||
|
} else if (mLastNestedScrollDy == 0) {
|
||||||
|
val currentTop = child.top
|
||||||
|
if (abs(currentTop - mMinOffset) > abs(currentTop - mMaxOffset)) {
|
||||||
|
top = mMaxOffset
|
||||||
|
targetState = STATE_EXPANDED
|
||||||
|
} else {
|
||||||
|
top = mMinOffset
|
||||||
|
targetState = STATE_COLLAPSED
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
top = mMinOffset
|
||||||
|
targetState = STATE_COLLAPSED
|
||||||
|
}
|
||||||
|
if (mViewDragHelper!!.smoothSlideViewTo(child, child.left, top)) {
|
||||||
|
setStateInternal(STATE_SETTLING)
|
||||||
|
ViewCompat.postOnAnimation(
|
||||||
|
child,
|
||||||
|
SettleRunnable(
|
||||||
|
child,
|
||||||
|
targetState
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setStateInternal(targetState)
|
||||||
|
}
|
||||||
|
mNestedScrolled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNestedPreFling(
|
||||||
|
coordinatorLayout: CoordinatorLayout,
|
||||||
|
child: V,
|
||||||
|
target: View,
|
||||||
|
velocityX: Float,
|
||||||
|
velocityY: Float
|
||||||
|
): Boolean {
|
||||||
|
return target === mNestedScrollingChildRef!!.get() &&
|
||||||
|
(mState != STATE_EXPANDED ||
|
||||||
|
super.onNestedPreFling(
|
||||||
|
coordinatorLayout, child!!, target,
|
||||||
|
velocityX, velocityY
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a callback to be notified of top sheet events.
|
||||||
|
*
|
||||||
|
* @param callback The callback to notify when top sheet events occur.
|
||||||
|
*/
|
||||||
|
fun setTopSheetCallback(callback: TopSheetCallback?) {
|
||||||
|
mCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets/Sets the state of the top sheet. When set, the top sheet will transition to
|
||||||
|
* that state with animation.
|
||||||
|
*
|
||||||
|
* @var state One of [.STATE_EXPANDED], [.STATE_COLLAPSED], [.STATE_DRAGGING],
|
||||||
|
* and [.STATE_SETTLING].
|
||||||
|
*/
|
||||||
|
@get:State
|
||||||
|
var state: Int
|
||||||
|
get() = mState
|
||||||
|
set(state) {
|
||||||
|
if (state == mState) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (mViewRef == null) {
|
||||||
|
// The view is not laid out yet; modify mState and let onLayoutChild handle it later
|
||||||
|
val stateCondition = state == STATE_COLLAPSED || state == STATE_EXPANDED ||
|
||||||
|
isHideable && state == STATE_HIDDEN
|
||||||
|
if (stateCondition) {
|
||||||
|
mState = state
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val child = mViewRef!!.get() ?: return
|
||||||
|
val top: Int
|
||||||
|
top = if (state == STATE_COLLAPSED) {
|
||||||
|
mMinOffset
|
||||||
|
} else if (state == STATE_EXPANDED) {
|
||||||
|
mMaxOffset
|
||||||
|
} else if (isHideable && state == STATE_HIDDEN) {
|
||||||
|
-child.height
|
||||||
|
} else {
|
||||||
|
throw IllegalArgumentException("Illegal state argument: $state")
|
||||||
|
}
|
||||||
|
setStateInternal(STATE_SETTLING)
|
||||||
|
if (mViewDragHelper!!.smoothSlideViewTo(child, child.left, top)) {
|
||||||
|
ViewCompat.postOnAnimation(
|
||||||
|
child,
|
||||||
|
SettleRunnable(child, state)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var oldState = mState
|
||||||
|
private fun setStateInternal(@State state: Int) {
|
||||||
|
if (state == STATE_COLLAPSED || state == STATE_EXPANDED) {
|
||||||
|
oldState = state
|
||||||
|
}
|
||||||
|
if (mState == state) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mState = state
|
||||||
|
updateDrawableForTargetState(state)
|
||||||
|
val topSheet: View? = mViewRef!!.get()
|
||||||
|
if (topSheet != null && mCallback != null) {
|
||||||
|
mCallback!!.onStateChanged(topSheet, state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("NestedBlockDepth")
|
||||||
|
private fun updateDrawableForTargetState(@BottomSheetBehavior.State state: Int) {
|
||||||
|
if (state == BottomSheetBehavior.STATE_SETTLING) {
|
||||||
|
// Special case: we want to know which state we're settling to, so wait for another call.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val expand = state == BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
if (isShapeExpanded != expand) {
|
||||||
|
isShapeExpanded = expand
|
||||||
|
if (materialShapeDrawable != null && interpolatorAnimator != null) {
|
||||||
|
if (interpolatorAnimator!!.isRunning) {
|
||||||
|
interpolatorAnimator!!.reverse()
|
||||||
|
} else {
|
||||||
|
val to = if (expand) 0f else 1f
|
||||||
|
val from = 1f - to
|
||||||
|
interpolatorAnimator!!.setFloatValues(from, to)
|
||||||
|
interpolatorAnimator!!.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reset() {
|
||||||
|
mActivePointerId = ViewDragHelper.INVALID_POINTER
|
||||||
|
if (mVelocityTracker != null) {
|
||||||
|
mVelocityTracker!!.recycle()
|
||||||
|
mVelocityTracker = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shouldHide(child: View, yvel: Float): Boolean {
|
||||||
|
if (child.top > mMinOffset) {
|
||||||
|
// It should not hide, but collapse.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val newTop = child.top + yvel * HIDE_FRICTION
|
||||||
|
return abs(newTop - mMinOffset) / mPeekHeight.toFloat() > HIDE_THRESHOLD
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findScrollingChild(view: View): View? {
|
||||||
|
if (view is NestedScrollingChild) {
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
if (view is ViewGroup) {
|
||||||
|
var i = 0
|
||||||
|
val count = view.childCount
|
||||||
|
while (i < count) {
|
||||||
|
val scrollingChild = findScrollingChild(view.getChildAt(i))
|
||||||
|
if (scrollingChild != null) {
|
||||||
|
return scrollingChild
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private val yVelocity: Float
|
||||||
|
get() {
|
||||||
|
mVelocityTracker!!.computeCurrentVelocity(VELOCITY_UNITS, mMaximumVelocity)
|
||||||
|
return mVelocityTracker!!.getYVelocity(mActivePointerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val mDragCallback: ViewDragHelper.Callback = object : ViewDragHelper.Callback() {
|
||||||
|
@Suppress("ReturnCount")
|
||||||
|
override fun tryCaptureView(
|
||||||
|
child: View,
|
||||||
|
pointerId: Int
|
||||||
|
): Boolean {
|
||||||
|
if (mState == STATE_DRAGGING) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (mTouchingScrollingChild) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (mState == STATE_EXPANDED && mActivePointerId == pointerId) {
|
||||||
|
val scroll = mNestedScrollingChildRef!!.get()
|
||||||
|
if (scroll != null && scroll.canScrollVertically(-1)) {
|
||||||
|
// Let the content scroll up
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mViewRef != null && mViewRef!!.get() === child
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewPositionChanged(
|
||||||
|
changedView: View,
|
||||||
|
left: Int,
|
||||||
|
top: Int,
|
||||||
|
dx: Int,
|
||||||
|
dy: Int
|
||||||
|
) {
|
||||||
|
dispatchOnSlide(top)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewDragStateChanged(state: Int) {
|
||||||
|
if (state == ViewDragHelper.STATE_DRAGGING) {
|
||||||
|
setStateInternal(STATE_DRAGGING)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewReleased(
|
||||||
|
releasedChild: View,
|
||||||
|
xvel: Float,
|
||||||
|
yvel: Float
|
||||||
|
) {
|
||||||
|
val top: Int
|
||||||
|
@State val targetState: Int
|
||||||
|
if (yvel > 0) { // Moving up
|
||||||
|
top = mMaxOffset
|
||||||
|
targetState = STATE_EXPANDED
|
||||||
|
} else if (isHideable && shouldHide(releasedChild, yvel)) {
|
||||||
|
top = -mViewRef!!.get()!!.height
|
||||||
|
targetState = STATE_HIDDEN
|
||||||
|
} else if (yvel == 0f) {
|
||||||
|
val currentTop = releasedChild.top
|
||||||
|
if (abs(currentTop - mMinOffset) > abs(currentTop - mMaxOffset)) {
|
||||||
|
top = mMaxOffset
|
||||||
|
targetState = STATE_EXPANDED
|
||||||
|
} else {
|
||||||
|
top = mMinOffset
|
||||||
|
targetState = STATE_COLLAPSED
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
top = mMinOffset
|
||||||
|
targetState = STATE_COLLAPSED
|
||||||
|
}
|
||||||
|
if (mViewDragHelper!!.settleCapturedViewAt(releasedChild.left, top)) {
|
||||||
|
setStateInternal(STATE_SETTLING)
|
||||||
|
ViewCompat.postOnAnimation(
|
||||||
|
releasedChild,
|
||||||
|
SettleRunnable(
|
||||||
|
releasedChild,
|
||||||
|
targetState
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setStateInternal(targetState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clampViewPositionVertical(
|
||||||
|
child: View,
|
||||||
|
top: Int,
|
||||||
|
dy: Int
|
||||||
|
): Int {
|
||||||
|
return constrain(
|
||||||
|
top,
|
||||||
|
if (isHideable) -child.height else mMinOffset,
|
||||||
|
mMaxOffset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clampViewPositionHorizontal(
|
||||||
|
child: View,
|
||||||
|
left: Int,
|
||||||
|
dx: Int
|
||||||
|
): Int {
|
||||||
|
return child.left
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getViewVerticalDragRange(child: View): Int {
|
||||||
|
return if (isHideable) {
|
||||||
|
child.height
|
||||||
|
} else {
|
||||||
|
mMaxOffset - mMinOffset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createMaterialShapeDrawable(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet
|
||||||
|
) {
|
||||||
|
if (shapeThemingEnabled) {
|
||||||
|
shapeAppearanceModelDefault = ShapeAppearanceModel.builder(
|
||||||
|
context,
|
||||||
|
attrs,
|
||||||
|
R.attr.bottomSheetStyle,
|
||||||
|
DEF_STYLE_RES
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
materialShapeDrawable = MaterialShapeDrawable(shapeAppearanceModelDefault!!)
|
||||||
|
materialShapeDrawable?.initializeElevationOverlay(context)
|
||||||
|
val defaultColor = TypedValue()
|
||||||
|
context.theme
|
||||||
|
.resolveAttribute(android.R.attr.colorBackground, defaultColor, true)
|
||||||
|
materialShapeDrawable?.setTint(defaultColor.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createShapeValueAnimator() {
|
||||||
|
interpolatorAnimator = ValueAnimator.ofFloat(0f, 1f)
|
||||||
|
interpolatorAnimator?.duration = CORNER_ANIMATION_DURATION.toLong()
|
||||||
|
interpolatorAnimator!!.addUpdateListener { animation ->
|
||||||
|
val value = animation.animatedValue as Float
|
||||||
|
if (materialShapeDrawable != null) {
|
||||||
|
materialShapeDrawable!!.interpolation = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dispatchOnSlide(top: Int) {
|
||||||
|
val topSheet: View? = mViewRef!!.get()
|
||||||
|
if (topSheet != null && mCallback != null) {
|
||||||
|
val isOpening = oldState == STATE_COLLAPSED
|
||||||
|
if (top < mMinOffset) {
|
||||||
|
mCallback!!.onSlide(
|
||||||
|
topSheet,
|
||||||
|
(top - mMinOffset).toFloat() / mPeekHeight,
|
||||||
|
isOpening
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
mCallback!!.onSlide(
|
||||||
|
topSheet,
|
||||||
|
(top - mMinOffset).toFloat() / (mMaxOffset - mMinOffset), isOpening
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class SettleRunnable internal constructor(
|
||||||
|
private val mView: View,
|
||||||
|
@field:State @param:State private val mTargetState: Int
|
||||||
|
) :
|
||||||
|
Runnable {
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
if (mViewDragHelper != null && mViewDragHelper!!.continueSettling(true)) {
|
||||||
|
ViewCompat.postOnAnimation(mView, this)
|
||||||
|
} else {
|
||||||
|
setStateInternal(mTargetState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SavedState(superState: Parcelable?, @State val state: Int) :
|
||||||
|
AbsSavedState(superState) {
|
||||||
|
|
||||||
|
override fun writeToParcel(out: Parcel, flags: Int) {
|
||||||
|
super.writeToParcel(out, flags)
|
||||||
|
out.writeInt(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* The top sheet is dragging.
|
||||||
|
*/
|
||||||
|
const val STATE_DRAGGING = 1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The top sheet is settling.
|
||||||
|
*/
|
||||||
|
const val STATE_SETTLING = 2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The top sheet is expanded.
|
||||||
|
*/
|
||||||
|
const val STATE_EXPANDED = 3
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The top sheet is collapsed.
|
||||||
|
*/
|
||||||
|
const val STATE_COLLAPSED = 4
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The top sheet is hidden.
|
||||||
|
*/
|
||||||
|
const val STATE_HIDDEN = 5
|
||||||
|
private const val HIDE_THRESHOLD = 0.5f
|
||||||
|
private const val HIDE_FRICTION = 0.1f
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility function to get the [TopSheetBehavior] associated with the `view`.
|
||||||
|
*
|
||||||
|
* @param view The [View] with [TopSheetBehavior].
|
||||||
|
* @return The [TopSheetBehavior] associated with the `view`.
|
||||||
|
*/
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun <V : View?> from(view: V): TopSheetBehavior<V> {
|
||||||
|
val params = view!!.layoutParams
|
||||||
|
require(params is CoordinatorLayout.LayoutParams) { "The view is not a child of CoordinatorLayout" }
|
||||||
|
val behavior =
|
||||||
|
params
|
||||||
|
.behavior
|
||||||
|
require(behavior is TopSheetBehavior<*>) { "The view is not associated with TopSheetBehavior" }
|
||||||
|
return behavior as TopSheetBehavior<V>
|
||||||
|
}
|
||||||
|
|
||||||
|
fun constrain(amount: Int, low: Int, high: Int): Int {
|
||||||
|
return if (amount < low) low else if (amount > high) high else amount
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val CORNER_ANIMATION_DURATION = 500
|
||||||
|
private val DEF_STYLE_RES = R.style.Widget_Design_BottomSheet_Modal
|
||||||
|
|
||||||
|
private const val PEEK_HEIGHT_RATIO = 0.75
|
||||||
|
private const val VELOCITY_UNITS = 1000
|
||||||
|
}
|
||||||
|
}
|
@ -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,44 @@
|
|||||||
|
<?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/. -->
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/private_session_description_wrapper"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="@dimen/home_item_horizontal_margin"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/private_session_description"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="none"
|
||||||
|
android:lineSpacingExtra="6dp"
|
||||||
|
android:paddingHorizontal="4dp"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:scrollHorizontally="false"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:textColor="?primaryText"
|
||||||
|
android:textDirection="locale"
|
||||||
|
android:textSize="14sp"
|
||||||
|
tools:text="@string/private_browsing_placeholder_description_2" />
|
||||||
|
|
||||||
|
<org.mozilla.fenix.utils.LinkTextView
|
||||||
|
android:id="@+id/private_session_common_myths"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="none"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:lineSpacingExtra="6dp"
|
||||||
|
android:paddingHorizontal="4dp"
|
||||||
|
android:paddingTop="10dp"
|
||||||
|
android:paddingBottom="19dp"
|
||||||
|
android:scrollHorizontally="false"
|
||||||
|
android:text="@string/private_browsing_common_myths"
|
||||||
|
android:textColor="?primaryText"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
</LinearLayout>
|
@ -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="?primaryText"
|
||||||
|
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||||
|
android:contentDescription="@string/addons_search_hint"
|
||||||
|
app:showAsAction="ifRoom|collapseActionView" />
|
||||||
|
</menu>
|
@ -1,7 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
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 |