master b11fe80f16e9 cached
105 files
369.1 KB
86.7k tokens
598 symbols
1 requests
Download .txt
Showing preview only (402K chars total). Download the full file or copy to clipboard to get everything.
Repository: matomo-org/matomo-sdk-android
Branch: master
Commit: b11fe80f16e9
Files: 105
Total size: 369.1 KB

Directory structure:
gitextract_u8aen19b/

├── .github/
│   ├── dependabot.yml
│   ├── pull_request_template.md
│   ├── release.yml
│   └── workflows/
│       ├── pull-request-ci.yml
│       ├── release.yml
│       └── update-gradle-wrapper.yml
├── .gitignore
├── LICENSE
├── README.md
├── build.gradle
├── exampleapp/
│   ├── README.md
│   ├── build.gradle
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── org/
│       │           └── matomo/
│       │               └── demo/
│       │                   └── SmokeTest.kt
│       └── main/
│           ├── AndroidManifest.xml
│           ├── java/
│           │   └── org/
│           │       └── matomo/
│           │           └── demo/
│           │               ├── DemoActivity.kt
│           │               ├── DemoApp.kt
│           │               └── SettingsActivity.java
│           └── res/
│               ├── layout/
│               │   ├── activity_demo.xml
│               │   └── activity_settings.xml
│               ├── menu/
│               │   └── demo.xml
│               ├── values/
│               │   ├── dimens.xml
│               │   ├── strings.xml
│               │   └── styles.xml
│               └── values-w820dp/
│                   └── dimens.xml
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── jitpack.yml
├── settings.gradle
└── tracker/
    ├── build.gradle
    ├── lint.xml
    └── src/
        ├── main/
        │   ├── AndroidManifest.xml
        │   └── java/
        │       └── org/
        │           └── matomo/
        │               └── sdk/
        │                   ├── LegacySettingsPorter.kt
        │                   ├── Matomo.kt
        │                   ├── QueryParams.java
        │                   ├── TrackMe.java
        │                   ├── Tracker.java
        │                   ├── TrackerBuilder.java
        │                   ├── dispatcher/
        │                   │   ├── DefaultDispatcher.kt
        │                   │   ├── DefaultDispatcherFactory.kt
        │                   │   ├── DefaultPacketSender.kt
        │                   │   ├── DispatchMode.java
        │                   │   ├── Dispatcher.java
        │                   │   ├── DispatcherFactory.kt
        │                   │   ├── Event.java
        │                   │   ├── EventCache.java
        │                   │   ├── EventDiskCache.java
        │                   │   ├── Packet.java
        │                   │   ├── PacketFactory.java
        │                   │   └── PacketSender.kt
        │                   ├── extra/
        │                   │   ├── CustomDimension.java
        │                   │   ├── CustomVariables.java
        │                   │   ├── DimensionQueue.java
        │                   │   ├── DownloadTracker.java
        │                   │   ├── EcommerceItems.java
        │                   │   ├── InstallReferrerReceiver.java
        │                   │   ├── MatomoApplication.java
        │                   │   ├── MatomoExceptionHandler.java
        │                   │   └── TrackHelper.java
        │                   └── tools/
        │                       ├── ActivityHelper.java
        │                       ├── BuildInfo.java
        │                       ├── Checksum.java
        │                       ├── Connectivity.java
        │                       ├── CurrencyFormatter.java
        │                       ├── DeviceHelper.java
        │                       ├── PropertySource.java
        │                       └── UrlHelper.java
        └── test/
            └── java/
                ├── org/
                │   └── matomo/
                │       └── sdk/
                │           ├── LegacySettingsPorterTest.java
                │           ├── MatomoTest.java
                │           ├── TrackMeTest.java
                │           ├── TrackerBuilderTest.java
                │           ├── TrackerTest.java
                │           ├── dispatcher/
                │           │   ├── DefaultDispatcherTest.java
                │           │   ├── DefaultPacketSenderTest.java
                │           │   ├── EventCacheTest.java
                │           │   ├── EventDiskCacheTest.java
                │           │   ├── EventTest.java
                │           │   ├── PacketFactoryTest.java
                │           │   └── PacketTest.java
                │           ├── extra/
                │           │   ├── CustomDimensionTest.java
                │           │   ├── CustomVariablesTest.java
                │           │   ├── DimensionQueueTest.java
                │           │   ├── DownloadTrackerTest.java
                │           │   ├── EcommerceItemsTest.java
                │           │   ├── InstallReferrerReceiverTest.java
                │           │   ├── MatomoApplicationTest.java
                │           │   └── TrackHelperTest.java
                │           └── tools/
                │               ├── BuildInfoTest.kt
                │               ├── ChecksumTest.kt
                │               ├── ConnectivityTest.kt
                │               ├── CurrencyFormatterTest.kt
                │               ├── DeviceHelperTest.kt
                │               └── PropertySourceTest.kt
                └── testhelpers/
                    ├── BaseTest.kt
                    ├── DefaultTestCase.kt
                    ├── FullEnvTestLifeCycle.kt
                    ├── FullEnvTestRunner.kt
                    ├── JUnitTree.kt
                    ├── MatomoTestApplication.kt
                    ├── QueryHashMap.kt
                    ├── TestActivity.kt
                    ├── TestHelper.kt
                    └── TestPreferences.kt

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates

version: 2
updates:
  - package-ecosystem: "gradle" # See documentation for possible values
    directory: "/" # Location of package manifests
    schedule:
      interval: "daily"
  - package-ecosystem: "github-actions"
    directory: "/" # Location of package manifests
    schedule:
      interval: "weekly"

================================================
FILE: .github/pull_request_template.md
================================================



================================================
FILE: .github/release.yml
================================================
changelog:
  exclude:
    labels:
      - ignore-for-release
    authors:
      - someuser
  categories:
    - title: Breaking Changes 🛠
      labels:
        - breaking-change
    - title: Exciting New Features 🎉
      labels:
        - enhancement
    - title: Dependencies
      labels:
        - dependencies
    - title: Espresso test
      labels:
        - Espresso
    - title: Pipeline
      labels:
        - pipeline
    - title: Other Changes
      labels:
        - "*"

================================================
FILE: .github/workflows/pull-request-ci.yml
================================================
name: PullRequest

on:
  push:
    branches:
      - master
  pull_request:

env:
  BRANCH_NAME: ${{ github.head_ref || github.ref_name }}

jobs:
  env-job:
    runs-on: ubuntu-latest
    outputs:
      modified-branch-name: ${{ steps.env.outputs.MODIFIED_BRANCH_NAME }}
    name: Modify branch name
    steps:
      - name: Sets MODIFIED_BRANCH_NAME
        id: env
        env:
          name: "${{env.BRANCH_NAME}}"
        run: |
          echo "MODIFIED_BRANCH_NAME=${name//\//-}" >> ${GITHUB_OUTPUT}
          cat ${GITHUB_OUTPUT}

  buildTest:
    name: Build & Unit-Tests
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ ubuntu-latest ]
    steps:
      - name: Checkout
        uses: actions/checkout@v6
        with:
          fetch-depth: 0
      - name: Set up JDK
        uses: actions/setup-java@v5
        with:
          distribution: 'adopt'
          java-version: 17
      - uses: gradle/actions/wrapper-validation@v6
      - name: Build project
        run: ./gradlew assembleDebug
      - name: Run tests
        run: ./gradlew test
      - name: Jacoco
        run: ./gradlew :tracker:jacocoTestReport --no-daemon
      - name: Codecov
        run: bash <(curl -s https://codecov.io/bash)
  Check:
    name: Check
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ ubuntu-latest ]
    needs:
      - env-job
    steps:
      - name: Checkout
        uses: actions/checkout@v6
        with:
          fetch-depth: 0
      - name: Install JDK
        uses: actions/setup-java@v5
        with:
          distribution: 'adopt'
          java-version: 17
      - name: Code checks
        run: ./gradlew check
      - name: Archive Lint report
        uses: actions/upload-artifact@v7
        if: ${{ always() }}
        with:
          name: Matomo-Lint-${{ needs.env-job.outputs.modified-branch-name }}
          path: tracker/build/reports/lint-results.html

  Espresso:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ ubuntu-latest ]
        api: [ 30 ]
        abi: [ x86_64 ]
        tag: [ default ]
    needs:
      - env-job
    steps:
      - name: kvm support
        run: |
          egrep -c '(vmx|svm)' /proc/cpuinfo
          id
          sudo adduser $USER kvm
          sudo chown -R $USER /dev/kvm
          id
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0
          submodules: true
      - name: show envs
        run: |
          echo ${{ needs.env-job.outputs.modified-branch-name }}
      - name: set up JDK 17
        uses: actions/setup-java@v5
        with:
          distribution: 'adopt'
          java-version: 17
      - name: Install Android SDK
        uses: hannesa2/action-android/install-sdk@0.1.16.7
      - name: Run instrumentation tests
        uses: hannesa2/action-android/emulator-run-cmd@0.1.16.7
        with:
          cmd: ./gradlew cAT --continue
          api: ${{ matrix.api }}
          tag: ${{ matrix.tag }}
          abi: ${{ matrix.abi }}
          cmdOptions: -noaudio -no-boot-anim -no-window -metrics-collection
      - name: Archive Espresso results
        uses: actions/upload-artifact@v7
        if: ${{ always() }}
        with:
          name: matomo-Espresso-${{ needs.env-job.outputs.modified-branch-name }}
          path: |
            ./**/build/reports/androidTests/connected
            ./**/build/outputs
            !./**/build/outputs/apk


================================================
FILE: .github/workflows/release.yml
================================================
name: Release with changelog

on:
  push:
    tags:
      - '*'

jobs:
  release:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ ubuntu-latest ]
    steps:
      - name: Checkout
        uses: actions/checkout@v6
        with:
          fetch-depth: 0
      - name: Install JDK ${{ matrix.java_version }}
        uses: actions/setup-java@v5
        with:
          distribution: 'adopt'
          java-version: 17
      - name: Build project
        run: ./gradlew assembleRelease
        env:
          VERSION: ${{ github.ref }}
      - name: Get the version
        id: tagger
        uses: jimschubert/query-tag-action@v2
        with:
          skip-unshallow: 'true'
          abbrev: false
          commit-ish: HEAD
      - name: Check pre-release
        run: |
          echo "tag=${{steps.tagger.outputs.tag}}"
          if [[ ${{ steps.tagger.outputs.tag }} == *alpha* || ${{ steps.tagger.outputs.tag }} == *beta* ]]
          then
             prerelease=true
          else
             prerelease=false
          fi
          echo "PRE_RELEASE=$prerelease" >> $GITHUB_ENV
          echo "prerelease=$prerelease"
      - name: Create Release
        uses: softprops/action-gh-release@v3.0.0
        with:
          tag_name: ${{steps.tagger.outputs.tag}}
          name: ${{steps.tagger.outputs.tag}}
          prerelease: ${{ env.PRE_RELEASE }}
          generate_release_notes: true
          files: ./tracker/build/outputs/aar/tracker-release.aar
        env:
          GITHUB_TOKEN: ${{ secrets.FINE_GRAINED_PATN }}


================================================
FILE: .github/workflows/update-gradle-wrapper.yml
================================================
name: Update Gradle Wrapper

on:
  schedule:
    - cron: "0 6 * * MON"

jobs:
  update-gradle-wrapper:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v6
      - name: Install JDK
        uses: actions/setup-java@v5
        with:
          distribution: 'adopt'
          java-version: 17
      - name: Update Gradle Wrapper
        uses: gradle-update/update-gradle-wrapper-action@v2
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}
          set-distribution-checksum: false


================================================
FILE: .gitignore
================================================
# Gradle files
.gradle/
build/

# Local configuration file (sdk path, etc)
local.properties

# Android Studio generated folders
.navigation/
captures/
.externalNativeBuild

# IntelliJ project files
*.iml
.idea/

# Misc
.DS_Store
Thumbs.db

# Keystore files
*.jks


================================================
FILE: LICENSE
================================================
Copyright 2018 Matomo team

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

* Redistributions of source code must retain the above copyright
  notice, this list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright
  notice, this list of conditions and the following disclaimer in the
  documentation and/or other materials provided with the distribution.

* Neither the name of Matomo team nor the names of its contributors
  may be used to endorse or promote products derived from this
  software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


================================================
FILE: README.md
================================================
Matomo SDK for Android
========================

[![](https://jitpack.io/v/matomo-org/matomo-sdk-android.svg)](https://jitpack.io/#matomo-org/matomo-sdk-android)
![Build](https://github.com/matomo-org/matomo-sdk-android/actions/workflows/pull-request-ci.yml/badge.svg)
[![Codecov](https://codecov.io/gh/matomo-org/matomo-sdk-android/branch/master/graph/badge.svg)](https://codecov.io/gh/matomo-org/matomo-sdk-android?branch=master)

Welcome to the [Matomo](http://matomo.org) Tracking SDK for Android. This library helps you send analytics data from Android apps to Matomo instances. Until v4 this library was known as **Piwik** Tracking SDK for Android.

__Features__:
* Caching and offline support
* Graceful reconnection handling
* WIFI-only mode
* Thread-safe support for multiple trackers
* Support for custom connection implementations
* Complete [Matomo HTTP API](https://developer.matomo.org/api-reference/tracking-api) support
    * [Custom dimensions](https://matomo.org/docs/custom-dimensions/)
    * [Event Tracking](https://matomo.org/docs/event-tracking/)
    * [Content Tracking](https://matomo.org/docs/content-tracking/)
    * [Ecommerce](https://matomo.org/docs/ecommerce-analytics/)
* Checksum based app install/upgrade tracking

## Quickstart
For the not so quick start, [see here](https://github.com/matomo-org/matomo-sdk-android/wiki/Getting-started) or look at our [demo app](https://github.com/matomo-org/matomo-sdk-android/tree/master/exampleapp)

* [Setup Matomo](https://matomo.org/docs/installation/) on your server.
* Include the library in your app modules `build.gradle` file
  via [JitPack](https://jitpack.io/#matomo-org/matomo-sdk-android)

```groovy
repositories {
  maven { url 'https://jitpack.io' }
}
dependencies {
  implementation 'com.github.matomo-org:matomo-sdk-android:<latest-version>'
}
```

* Now you need to initialize your `Tracker`. It's recommended to store it as singleton. You can extend `MatomoApplication` or create and store a `Tracker` instance yourself:
```java
import org.matomo.sdk.TrackerBuilder;

public class YourApplication extends Application {
    private Tracker tracker;
    public synchronized Tracker getTracker() {
        if (tracker == null){
            tracker = TrackerBuilder.createDefault("http://domain.tld/matomo.php", 1).build(Matomo.getInstance(this));
        }
        return tracker;
    }
}
```

* The `TrackHelper` class is the easiest way to submit events to your tracker:
```java
// The `Tracker` instance from the previous step
Tracker tracker = ((MatomoApplication) getApplication()).getTracker();
// Track a screen view
TrackHelper.track().screen("/activity_main/activity_settings").title("Settings").with(tracker);
// Monitor your app installs
TrackHelper.track().download().with(tracker);
```

* Something not working? Check [here](https://github.com/matomo-org/matomo-sdk-android/wiki/Troubleshooting).

## License
Android SDK for Matomo is released under the BSD-3 Clause license, see [LICENSE](https://github.com/matomo-org/matomo-sdk-android/blob/master/LICENSE).


================================================
FILE: build.gradle
================================================
buildscript {
    ext.kotlin_version = "2.2.20"
    repositories {
        google()
        maven { url "https://plugins.gradle.org/m2/" }
        maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots" }
    }
    dependencies {
        classpath "com.android.tools.build:gradle:8.13.0"
        classpath "com.mxalbert.gradle:jacoco-android:0.2.1"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

allprojects {
    repositories {
        google()
        maven { url "https://plugins.gradle.org/m2/" }
        maven { url "https://jitpack.io" }
    }
    ext {
        globalMinSdkVersion = 21
        globalTargetSdkVersion = 35
        globalCompileSdkVersion = 35
    }
}


================================================
FILE: exampleapp/README.md
================================================
# Demo Application Matomo Android SDK

## Description

Example of using the Matomo Tracking SDK for Android

```
./gradlew :example:clean :example:installDebug
```

================================================
FILE: exampleapp/build.gradle
================================================
plugins {
    id "com.android.application"
    id "kotlin-android"
}

android {
    namespace "org.matomo.demo"
    defaultConfig {
        applicationId "org.matomo.demo"
        minSdkVersion project.ext.globalMinSdkVersion
        compileSdk project.ext.globalCompileSdkVersion
        targetSdkVersion project.ext.globalTargetSdkVersion
        versionCode 2
        versionName "2.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        testInstrumentationRunnerArguments useTestStorageService: "true"
    }
    buildTypes {
        release {
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_17
        targetCompatibility JavaVersion.VERSION_17
    }
    kotlinOptions {
        jvmTarget = "17"
    }
}

dependencies {
    implementation project(":tracker")
    implementation "androidx.appcompat:appcompat:1.7.1"
    implementation "androidx.legacy:legacy-support-v4:1.0.0"
    implementation "com.github.AppDevNext.Logcat:LogcatCoreLib:3.4"
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"

    androidTestImplementation "androidx.test.ext:junit-ktx:1.3.0"
    androidTestUtil "androidx.test.services:test-services:1.6.0"
    androidTestImplementation "androidx.test.espresso:espresso-core:3.7.0"
}


================================================
FILE: exampleapp/src/androidTest/java/org/matomo/demo/SmokeTest.kt
================================================
package org.matomo.demo

import android.graphics.Bitmap
import androidx.test.core.graphics.writeToTestStorage
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.captureToBitmap
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.rules.activityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestName
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class SmokeTest {

    @get:Rule
    val activityScenarioRule = activityScenarioRule<DemoActivity>()

    @get:Rule
    var nameRule = TestName()

    @Test
    fun testExpand() {
        onView(withId(R.id.trackMainScreenViewButton)).perform(click())
        onView(isRoot())
            .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}-trackMainScreenViewButton") })

        onView(withId(R.id.trackDispatchNow)).perform(click())
        onView(isRoot())
            .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}-trackDispatchNow") })

        onView(withId(R.id.trackCustomVarsButton)).perform(click())
        onView(isRoot())
            .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}-trackCustomVarsButton") })

        onView(withId(R.id.raiseExceptionButton)).perform(click())
        onView(isRoot())
            .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}-raiseExceptionButton") })

        onView(withId(R.id.addEcommerceItemButton)).perform(click())
        onView(isRoot())
            .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}-addEcommerceItemButton") })

        onView(withId(R.id.trackEcommerceCartUpdateButton)).perform(click())
        onView(isRoot())
            .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}-trackEcommerceCartUpdateButton") })

        onView(withId(R.id.completeEcommerceOrderButton)).perform(click())
        onView(isRoot())
            .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}-completeEcommerceOrderButton") })

        onView(withId(R.id.trackGoalButton)).perform(click())
        onView(isRoot())
            .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}-trackGoalButton") })

        onView(withId(R.id.goalTextEditView)).perform(click())
        onView(isRoot())
            .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}-goalTextEditView") })
    }

}


================================================
FILE: exampleapp/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>

    <application
        android:name="org.matomo.demo.DemoApp"
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme">
        <activity
            android:name="org.matomo.demo.DemoActivity"
            android:alwaysRetainTaskState="true"
            android:launchMode="singleInstance"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <activity
            android:name="org.matomo.demo.SettingsActivity"
            android:alwaysRetainTaskState="true"
            android:label="@string/title_activity_settings"
            android:launchMode="singleInstance"
            android:parentActivityName="org.matomo.demo.DemoActivity">
            <meta-data
                android:name="android.support.PARENT_ACTIVITY"
                android:value="org.matomo.demo.DemoActivity"/>
        </activity>
    </application>

</manifest>


================================================
FILE: exampleapp/src/main/java/org/matomo/demo/DemoActivity.kt
================================================
/*
 * Android SDK for Matomo
 *
 * @link https://github.com/matomo-org/matomo-android-sdk
 * @license https://github.com/matomo-org/matomo-sdk-android/blob/master/LICENSE BSD-3 Clause
 */
package org.matomo.demo

import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity
import org.matomo.sdk.QueryParams
import org.matomo.sdk.TrackMe
import org.matomo.sdk.Tracker
import org.matomo.sdk.extra.EcommerceItems
import org.matomo.sdk.extra.MatomoApplication
import org.matomo.sdk.extra.TrackHelper

class DemoActivity : AppCompatActivity() {
    private var cartItems: Int = 0
    private var items: EcommerceItems? = null

    private val tracker: Tracker
        get() = (application as MatomoApplication).tracker


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_demo)
        items = EcommerceItems()

        findViewById<View>(R.id.trackMainScreenViewButton).setOnClickListener { v: View? ->
            TrackHelper.track(TrackMe().set(QueryParams.SESSION_START, 1))
                .screen("/")
                .title("Main screen")
                .with(tracker)
        }

        findViewById<View>(R.id.trackDispatchNow).setOnClickListener { v: View? -> tracker.dispatch() }

        findViewById<View>(R.id.trackCustomVarsButton).setOnClickListener { v: View? ->
            TrackHelper.track()
                .screen("/custom_vars")
                .title("Custom Vars")
                .variable(1, "first", "var")
                .variable(2, "second", "long value")
                .with(tracker)
        }

        findViewById<View>(R.id.raiseExceptionButton).setOnClickListener { v: View? ->
            TrackHelper.track()
                .exception(Exception("OnPurposeException"))
                .description("Crash button")
                .fatal(false)
                .with(tracker)
        }

        findViewById<View>(R.id.trackGoalButton).setOnClickListener { v: View? ->
            var revenue: Float
            try {
                revenue = (findViewById<View>(R.id.goalTextEditView) as EditText).text.toString().toInt().toFloat()
            } catch (e: Exception) {
                TrackHelper.track().exception(e).description("wrong revenue").with(tracker)
                revenue = 0f
            }
            TrackHelper.track().goal(1).revenue(revenue).with(tracker)
        }

        findViewById<View>(R.id.addEcommerceItemButton).setOnClickListener { v: View? ->
            val skus: List<String> = mutableListOf("00001", "00002", "00003", "00004")
            val names: List<String> = mutableListOf("Silly Putty", "Fishing Rod", "Rubber Boots", "Cool Ranch Doritos")
            val categories: List<String> = mutableListOf("Toys & Games", "Hunting & Fishing", "Footwear", "Grocery")
            val prices: List<Int> = mutableListOf(449, 3495, 2450, 250)

            val index = cartItems % 4
            val quantity = (cartItems / 4) + 1

            items!!.addItem(
                EcommerceItems.Item(skus[index])
                    .name(names[index])
                    .category(categories[index])
                    .price(prices[index])
                    .quantity(quantity)
            )
            cartItems++
        }

        findViewById<View>(R.id.trackEcommerceCartUpdateButton).setOnClickListener { v: View? ->
            TrackHelper.track()
                .cartUpdate(8600)
                .items(items)
                .with(tracker)
        }

        findViewById<View>(R.id.completeEcommerceOrderButton).setOnClickListener { v: View? ->
            TrackHelper.track()
                .order((10000 * Math.random()).toString(), 10000)
                .subTotal(1000)
                .tax(2000)
                .shipping(3000)
                .discount(500)
                .items(items)
                .with(tracker)
        }
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        menuInflater.inflate(R.menu.demo, menu)
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        val id = item.itemId
        if (id == R.id.action_settings) {
            val intent = Intent(this, SettingsActivity::class.java)
            intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
            startActivity(intent)
            return true
        }
        return super.onOptionsItemSelected(item)
    }
}


================================================
FILE: exampleapp/src/main/java/org/matomo/demo/DemoApp.kt
================================================
/*
 * Android SDK for Matomo
 *
 * @link https://github.com/matomo-org/matomo-android-sdk
 * @license https://github.com/matomo-org/matomo-sdk-android/blob/master/LICENSE BSD-3 Clause
 */
package org.matomo.demo

import info.hannes.timber.DebugFormatTree
import org.matomo.sdk.TrackMe
import org.matomo.sdk.TrackerBuilder
import org.matomo.sdk.extra.DimensionQueue
import org.matomo.sdk.extra.DownloadTracker.Extra
import org.matomo.sdk.extra.MatomoApplication
import org.matomo.sdk.extra.TrackHelper
import timber.log.Timber
import timber.log.Timber.Forest.plant

class DemoApp : MatomoApplication() {
    override fun onCreateTrackerConfig(): TrackerBuilder {
        return TrackerBuilder.createDefault("https://demo2.matomo.org/matomo.php", 81)
    }

    override fun onCreate() {
        super.onCreate()
        onInitTracker()
    }

    private fun onInitTracker() {
        // Print debug output when working on an app.
        plant(DebugFormatTree())

        // When working on an app we don't want to skew tracking results.
        // getMatomo().setDryRun(BuildConfig.DEBUG);

        // If you want to set a specific userID other than the random UUID token, do it NOW to ensure all future actions use that token.
        // Changing it later will track new events as belonging to a different user.
        // String userEmail = ....preferences....getString
        // getTracker().setUserId(userEmail);

        // Track this app install, this will only trigger once per app version.
        // i.e. "http://org.matomo.demo:1/185DECB5CFE28FDB2F45887022D668B4"
        TrackHelper.track().download().identifier(Extra.ApkChecksum(this)).with(tracker)
        // Alternative:
        // i.e. "http://org.matomo.demo:1/com.android.vending"
        // getTracker().download();
        val dimensionQueue = DimensionQueue(tracker)

        // This will be send the next time something is tracked.
        dimensionQueue.add(0, "test")
        tracker.addTrackingCallback { trackMe: TrackMe? ->
            Timber.i("Tracker.Callback.onTrack(%s)", trackMe)
            trackMe
        }
    }
}

================================================
FILE: exampleapp/src/main/java/org/matomo/demo/SettingsActivity.java
================================================
/*
 * Android SDK for Matomo
 *
 * @link https://github.com/matomo-org/matomo-android-sdk
 * @license https://github.com/matomo-org/matomo-sdk-android/blob/master/LICENSE BSD-3 Clause
 */

package org.matomo.demo;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;

import org.matomo.sdk.extra.MatomoApplication;
import org.matomo.sdk.extra.TrackHelper;

import java.util.ArrayList;
import java.util.Collections;

import timber.log.Timber;


public class SettingsActivity extends Activity {

    private void refreshUI(final Activity settingsActivity) {
        // auto track button
        Button button = findViewById(R.id.bindtoapp);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                TrackHelper.track().screens(getApplication()).with(((MatomoApplication) getApplication()).getTracker());
            }
        });

        // Dry run
        CheckBox dryRun = findViewById(R.id.dryRunCheckbox);
        dryRun.setChecked(((MatomoApplication) getApplication()).getTracker().getDryRunTarget() != null);
        dryRun.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ((MatomoApplication) getApplication()).getTracker().setDryRunTarget(((CheckBox) v).isChecked() ? Collections.synchronizedList(new ArrayList<>()) : null);
            }
        });

        // out out
        CheckBox optOut = findViewById(R.id.optOutCheckbox);
        optOut.setChecked(((MatomoApplication) getApplication()).getTracker().isOptOut());
        optOut.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ((MatomoApplication) getApplication()).getTracker().setOptOut(((CheckBox) v).isChecked());
            }
        });

        // dispatch interval
        EditText input = findViewById(R.id.dispatchIntervallInput);
        input.setText(Long.toString(
                ((MatomoApplication) getApplication()).getTracker().getDispatchInterval()
        ));
        input.addTextChangedListener(
                new TextWatcher() {
                    @Override
                    public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
                        try {
                            int interval = Integer.parseInt(charSequence.toString().trim());
                            ((MatomoApplication) getApplication()).getTracker()
                                    .setDispatchInterval(interval);
                        } catch (NumberFormatException e) {
                            Timber.d("not a number: %s", charSequence.toString());
                        }
                    }

                    @Override
                    public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
                    }

                    @Override
                    public void afterTextChanged(Editable editable) {
                    }
                }

        );

        //session Timeout Input
        input = findViewById(R.id.sessionTimeoutInput);
        input.setText(Long.toString(
                (((MatomoApplication) getApplication()).getTracker().getSessionTimeout() / 60000)
        ));
        input.addTextChangedListener(
                new TextWatcher() {
                    @SuppressLint("SetTextI18n")
                    @Override
                    public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
                        try {
                            int timeoutMin = Integer.parseInt(charSequence.toString().trim());
                            timeoutMin = Math.abs(timeoutMin);
                            ((MatomoApplication) getApplication()).getTracker()
                                    .setSessionTimeout(timeoutMin * 60);
                        } catch (NumberFormatException e) {
                            ((EditText) settingsActivity.findViewById(R.id.sessionTimeoutInput)).setText("30");
                            Timber.d("not a number: %s", charSequence.toString());
                        }
                    }

                    @Override
                    public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
                    }

                    @Override
                    public void afterTextChanged(Editable editable) {
                    }
                }

        );

    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_settings);
        refreshUI(this);
    }

}


================================================
FILE: exampleapp/src/main/res/layout/activity_demo.xml
================================================
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context="org.matomo.demo.DemoActivity">

    <Button
        android:id="@+id/trackMainScreenViewButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/track_screen_vew_button_label" />

    <Button
        android:id="@+id/trackDispatchNow"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/action_dispatch_now" />

    <Button
        android:id="@+id/trackCustomVarsButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/action_track_custom_vars" />

    <Button
        android:id="@+id/raiseExceptionButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/action_divide_by_zero" />

    <Button
        android:id="@+id/addEcommerceItemButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/action_add_ecommerce_item" />

    <Button
        android:id="@+id/trackEcommerceCartUpdateButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/action_track_ecommerce_cart_update" />

    <Button
        android:id="@+id/completeEcommerceOrderButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/action_track_ecommerce_order" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <Button
            android:id="@+id/trackGoalButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/action_track_goal" />

        <EditText
            android:id="@+id/goalTextEditView"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:ems="10"
            android:hint="1"
            android:inputType="number"
            android:text="1"
            tools:ignore="HardcodedText" />

        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="$ Revenue"
            android:textAppearance="?android:attr/textAppearanceMedium"
            tools:ignore="HardcodedText" />
    </LinearLayout>
</LinearLayout>


================================================
FILE: exampleapp/src/main/res/layout/activity_settings.xml
================================================
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context="org.matomo.demo.SettingsActivity"
    tools:ignore="TextViewEdits,HardcodedText,Autofill,LabelFor">

    <TableLayout
        android:id="@+id/tableLayout"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content">

        <TableRow
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="5dp">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_column="0"
                android:inputType="number"
                android:text="Dispatch Interval"
                android:textAppearance="?android:attr/textAppearanceMedium" />

            <EditText
                android:id="@+id/dispatchIntervallInput"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_column="1"
                android:width="60dip"
                android:ems="10"
                android:inputType="numberSigned"
                android:text="5" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_column="2"
                android:text="(sec)"
                android:textAppearance="?android:attr/textAppearanceMedium" />
        </TableRow>

        <TableRow
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="5dp">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_column="0"
                android:inputType="number"
                android:text="Session timeout "
                android:textAppearance="?android:attr/textAppearanceMedium" />

            <EditText
                android:id="@+id/sessionTimeoutInput"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_column="1"
                android:width="60dip"
                android:ems="10"
                android:inputType="numberSigned"
                android:text="30" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_column="2"
                android:text="(min)"
                android:textAppearance="?android:attr/textAppearanceMedium" />
        </TableRow>

        <TableRow
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="5dp">

            <CheckBox
                android:id="@+id/dryRunCheckbox"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_column="0"
                android:layout_span="3"
                android:checked="false"
                android:text="Dry Run" />
        </TableRow>

        <TableRow
            android:id="@+id/tableRow"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="5dp">

            <CheckBox
                android:id="@+id/optOutCheckbox"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_column="0"
                android:layout_span="3"
                android:checked="false"
                android:text="Opt Out" />
        </TableRow>
    </TableLayout>

    <Button
        android:id="@+id/bindtoapp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_column="0"
        android:layout_below="@+id/tableLayout"
        android:layout_alignEnd="@+id/tableLayout"
        android:layout_alignRight="@+id/tableLayout"
        android:layout_alignParentStart="true"
        android:layout_alignParentLeft="true"
        android:layout_gravity="bottom"
        android:text="Auto track activities" />
</RelativeLayout>


================================================
FILE: exampleapp/src/main/res/menu/demo.xml
================================================
<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context="org.matomo.demo.DemoActivity">
    <item android:id="@+id/action_settings"
        android:title="@string/action_settings"
        android:orderInCategory="100"
        app:showAsAction="never" />
</menu>


================================================
FILE: exampleapp/src/main/res/values/dimens.xml
================================================
<resources>
    <!-- Default screen margins, per the Android Design guidelines. -->
    <dimen name="activity_horizontal_margin">16dp</dimen>
    <dimen name="activity_vertical_margin">16dp</dimen>
</resources>


================================================
FILE: exampleapp/src/main/res/values/strings.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string name="app_name">Matomo Example App</string>
    <string name="action_settings">Settings</string>
    <string name="track_screen_vew_button_label">Track Screen View</string>
    <string name="title_activity_settings">Settings</string>
    <string name="action_dispatch_now">Dispatch now</string>
    <string name="action_track_custom_vars">Track Custom Vars</string>
    <string name="action_divide_by_zero">Divide by zero</string>
    <string name="action_add_ecommerce_item">Add Ecommerce Item</string>
    <string name="action_track_ecommerce_cart_update">Track Ecommerce Cart Update</string>
    <string name="action_track_ecommerce_order">Track Ecommerce Order</string>
    <string name="action_track_goal">Track Goal</string>

</resources>


================================================
FILE: exampleapp/src/main/res/values/styles.xml
================================================
<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
    </style>

</resources>


================================================
FILE: exampleapp/src/main/res/values-w820dp/dimens.xml
================================================
<resources>
    <!-- Example customization of dimensions originally defined in res/values/dimens.xml
         (such as screen margins) for screens with more than 820dp of available width. This
         would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
    <dimen name="activity_horizontal_margin">64dp</dimen>
</resources>


================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists


================================================
FILE: gradle.properties
================================================
# Project-wide Gradle settings.

# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.

# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html

# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
android.enableJetifier=true
android.nonFinalResIds=false
android.nonTransitiveRClass=false
android.useAndroidX=true
org.gradle.jvmargs=-Xmx2536m

# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true


================================================
FILE: gradlew
================================================
#!/bin/sh

#
# Copyright © 2015-2021 the original authors.
#
# 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
#
#      https://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.
#
# SPDX-License-Identifier: Apache-2.0
#

##############################################################################
#
#   Gradle start up script for POSIX generated by Gradle.
#
#   Important for running:
#
#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
#       noncompliant, but you have some other compliant shell such as ksh or
#       bash, then to run this script, type that shell name before the whole
#       command line, like:
#
#           ksh Gradle
#
#       Busybox and similar reduced shells will NOT work, because this script
#       requires all of these POSIX shell features:
#         * functions;
#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
#         * compound commands having a testable exit status, especially «case»;
#         * various built-in commands including «command», «set», and «ulimit».
#
#   Important for patching:
#
#   (2) This script targets any POSIX shell, so it avoids extensions provided
#       by Bash, Ksh, etc; in particular arrays are avoided.
#
#       The "traditional" practice of packing multiple parameters into a
#       space-separated string is a well documented source of bugs and security
#       problems, so this is (mostly) avoided, by progressively accumulating
#       options in "$@", and eventually passing that to Java.
#
#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
#       see the in-line comments for details.
#
#       There are tweaks for specific operating systems such as AIX, CygWin,
#       Darwin, MinGW, and NonStop.
#
#   (3) This script is generated from the Groovy template
#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
#       within the Gradle project.
#
#       You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################

# Attempt to set APP_HOME

# Resolve links: $0 may be a link
app_path=$0

# Need this for daisy-chained symlinks.
while
    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
    [ -h "$app_path" ]
do
    ls=$( ls -ld "$app_path" )
    link=${ls#*' -> '}
    case $link in             #(
      /*)   app_path=$link ;; #(
      *)    app_path=$APP_HOME$link ;;
    esac
done

# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum

warn () {
    echo "$*"
} >&2

die () {
    echo
    echo "$*"
    echo
    exit 1
} >&2

# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in                #(
  CYGWIN* )         cygwin=true  ;; #(
  Darwin* )         darwin=true  ;; #(
  MSYS* | MINGW* )  msys=true    ;; #(
  NONSTOP* )        nonstop=true ;;
esac

CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar


# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
        # IBM's JDK on AIX uses strange locations for the executables
        JAVACMD=$JAVA_HOME/jre/sh/java
    else
        JAVACMD=$JAVA_HOME/bin/java
    fi
    if [ ! -x "$JAVACMD" ] ; then
        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
else
    JAVACMD=java
    if ! command -v java >/dev/null 2>&1
    then
        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
fi

# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
    case $MAX_FD in #(
      max*)
        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
        # shellcheck disable=SC2039,SC3045
        MAX_FD=$( ulimit -H -n ) ||
            warn "Could not query maximum file descriptor limit"
    esac
    case $MAX_FD in  #(
      '' | soft) :;; #(
      *)
        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
        # shellcheck disable=SC2039,SC3045
        ulimit -n "$MAX_FD" ||
            warn "Could not set maximum file descriptor limit to $MAX_FD"
    esac
fi

# Collect all arguments for the java command, stacking in reverse order:
#   * args from the command line
#   * the main class name
#   * -classpath
#   * -D...appname settings
#   * --module-path (only if needed)
#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.

# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )

    JAVACMD=$( cygpath --unix "$JAVACMD" )

    # Now convert the arguments - kludge to limit ourselves to /bin/sh
    for arg do
        if
            case $arg in                                #(
              -*)   false ;;                            # don't mess with options #(
              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
                    [ -e "$t" ] ;;                      #(
              *)    false ;;
            esac
        then
            arg=$( cygpath --path --ignore --mixed "$arg" )
        fi
        # Roll the args list around exactly as many times as the number of
        # args, so each arg winds up back in the position where it started, but
        # possibly modified.
        #
        # NB: a `for` loop captures its iteration list before it begins, so
        # changing the positional parameters here affects neither the number of
        # iterations, nor the values presented in `arg`.
        shift                   # remove old arg
        set -- "$@" "$arg"      # push replacement arg
    done
fi


# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'

# Collect all arguments for the java command:
#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
#     and any embedded shellness will be escaped.
#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
#     treated as '${Hostname}' itself on the command line.

set -- \
        "-Dorg.gradle.appname=$APP_BASE_NAME" \
        -classpath "$CLASSPATH" \
        org.gradle.wrapper.GradleWrapperMain \
        "$@"

# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
    die "xargs is not available"
fi

# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
#   set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#

eval "set -- $(
        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
        xargs -n1 |
        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
        tr '\n' ' '
    )" '"$@"'

exec "$JAVACMD" "$@"


================================================
FILE: gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem      https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem

@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem  Gradle startup script for Windows
@rem
@rem ##########################################################################

@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal

set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi

@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"

@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute

echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2

goto fail

:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe

if exist "%JAVA_EXE%" goto execute

echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2

goto fail

:execute
@rem Setup the command line

set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar


@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*

:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd

:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%

:mainEnd
if "%OS%"=="Windows_NT" endlocal

:omega


================================================
FILE: jitpack.yml
================================================
jdk:
  - openjdk17

================================================
FILE: settings.gradle
================================================
include ":exampleapp"
include ":tracker"


================================================
FILE: tracker/build.gradle
================================================
plugins {
    id "com.android.library"
    id "kotlin-android"
    id "maven-publish"
    id "com.mxalbert.gradle.jacoco-android"
}

android {
    namespace "org.matomo.sdk"
    defaultConfig {
        minSdkVersion project.ext.globalMinSdkVersion
        compileSdk project.ext.globalCompileSdkVersion
        targetSdkVersion project.ext.globalTargetSdkVersion
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_17
        targetCompatibility JavaVersion.VERSION_17
    }
    kotlinOptions {
        jvmTarget = "17"
    }
    testOptions.unitTests.includeAndroidResources = true
}

dependencies {
    implementation "androidx.annotation:annotation:1.9.1"
    implementation "com.github.AppDevNext.Logcat:LogcatCoreLib:3.4"
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"

    testImplementation "org.awaitility:awaitility:4.3.0"
    testImplementation "androidx.test:core:1.7.0"
    // Robolectric
    testImplementation "junit:junit:4.13.2"
    testImplementation "org.hamcrest:hamcrest-core:3.0"
    testImplementation "org.hamcrest:hamcrest-library:3.0"
    testImplementation "org.hamcrest:hamcrest-integration:1.3"

    testImplementation "com.squareup.okhttp3:mockwebserver:5.3.2"

    // Mocktio
    testImplementation "org.mockito:mockito-core:5.20.0"
    testImplementation "org.json:json:20250517"
    testImplementation "org.robolectric:robolectric:4.15.1"
}

jacoco {
    toolVersion = "0.8.10"
}

tasks.withType(Test).configureEach {
    jacoco.includeNoLocationClasses = true
    jacoco.excludes = ["jdk.internal.*"]
}

/**
 * Javadoc
 */
android.libraryVariants.configureEach { variant ->
    task("generate${variant.name.capitalize()}Javadoc", type: Javadoc) {
        title = "Documentation for Android $android.defaultConfig.versionName b$android.defaultConfig.versionCode"
        destinationDir = new File("${project.getProjectDir()}/build/docs/javadoc/")
        ext.androidJar = "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar"
        source = variant.javaCompiler.source
        doFirst {
            classpath = files(variant.javaCompile.classpath.files) + files(ext.androidJar)
        }

        description "Generates Javadoc for $variant.name."

        options.memberLevel = JavadocMemberLevel.PRIVATE
        options.links("http://docs.oracle.com/javase/7/docs/api/")
        options.links("http://developer.android.com/reference/reference/")
        exclude "**/BuildConfig.java"
        exclude "**/R.java"
    }
}

publishing {
    publications {
        release(MavenPublication) {
            afterEvaluate {
                from components.release {
                    pom {
                        licenses {
                            license {
                                name = "BSD 3-Clause 'New' or 'Revised' License"
                                url = "https://github.com/matomo-org/matomo-sdk-android/blob/master/LICENSE"
                            }
                        }
                    }
                }
            }
        }
    }
}

@SuppressWarnings("unused")
static def getTag() {
    def process = "git describe --tags".execute()
    return process.text.toString().trim()
}


================================================
FILE: tracker/lint.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<lint>
    <issue id="TimberTagLength" severity="ignore"/>
    <issue id="StringFormatInTimber" severity="ignore"/>
    <issue id="TimberExceptionLogging" severity="ignore"/>
    <issue id="ThrowableNotAtBeginning" severity="ignore"/>
    <issue id="LogNotTimber" severity="ignore"/>
    <issue id="BinaryOperationInTimber" severity="ignore"/>
    <issue id="TimberArgCount" severity="ignore"/>
    <issue id="TimberArgTypes" severity="ignore"/>
</lint>


================================================
FILE: tracker/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:tools="http://schemas.android.com/tools">

    <!-- added suppression for timber since 4.1.2 has minsdk 15 because google doesn't show it in the dashboard -->
    <uses-sdk tools:overrideLibrary="timber.log"/>

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

    <application>
        <!-- Suppress warning about unrestricted access to this receiver this is need to check which app store installed the app -->
        <receiver
            android:name="org.matomo.sdk.extra.InstallReferrerReceiver"
            android:exported="true"
            tools:ignore="ExportedReceiver">
            <intent-filter>
                <action android:name="com.android.vending.INSTALL_REFERRER"/>
            </intent-filter>
        </receiver>
    </application>
</manifest>


================================================
FILE: tracker/src/main/java/org/matomo/sdk/LegacySettingsPorter.kt
================================================
package org.matomo.sdk

import android.content.SharedPreferences
import java.util.UUID

class LegacySettingsPorter(matomo: Matomo) {
    private val mLegacyPrefs: SharedPreferences

    init {
        mLegacyPrefs = matomo.preferences
    }

    fun port(tracker: Tracker) {
        val newSettings = tracker.preferences
        if (mLegacyPrefs.getBoolean(LEGACY_PREF_OPT_OUT, false)) {
            newSettings.edit()
                .putBoolean(Tracker.PREF_KEY_TRACKER_OPTOUT, true)
                .apply()
            mLegacyPrefs.edit().remove(LEGACY_PREF_OPT_OUT).apply()
        }
        if (mLegacyPrefs.contains(LEGACY_PREF_USER_ID)) {
            newSettings.edit()
                .putString(Tracker.PREF_KEY_TRACKER_USERID, mLegacyPrefs.getString(LEGACY_PREF_USER_ID, UUID.randomUUID().toString()))
                .apply()
            mLegacyPrefs.edit().remove(LEGACY_PREF_USER_ID).apply()
        }
        if (mLegacyPrefs.contains(LEGACY_PREF_FIRST_VISIT)) {
            newSettings.edit().putLong(
                Tracker.PREF_KEY_TRACKER_FIRSTVISIT,
                mLegacyPrefs.getLong(LEGACY_PREF_FIRST_VISIT, -1L)
            ).apply()
            mLegacyPrefs.edit().remove(LEGACY_PREF_FIRST_VISIT).apply()
        }
        if (mLegacyPrefs.contains(LEGACY_PREF_VISITCOUNT)) {
            newSettings.edit().putLong(
                Tracker.PREF_KEY_TRACKER_VISITCOUNT,
                mLegacyPrefs.getInt(LEGACY_PREF_VISITCOUNT, 0)
                    .toLong()
            ).apply()
            mLegacyPrefs.edit().remove(LEGACY_PREF_VISITCOUNT).apply()
        }
        if (mLegacyPrefs.contains(LEGACY_PREF_PREV_VISIT)) {
            newSettings.edit().putLong(
                Tracker.PREF_KEY_TRACKER_PREVIOUSVISIT,
                mLegacyPrefs.getLong(LEGACY_PREF_PREV_VISIT, -1)
            ).apply()
            mLegacyPrefs.edit().remove(LEGACY_PREF_PREV_VISIT).apply()
        }
        for ((key) in mLegacyPrefs.all) {
            if (key.startsWith("downloaded:")) {
                newSettings.edit().putBoolean(key, true).apply()
                mLegacyPrefs.edit().remove(key).apply()
            }
        }
    }

    companion object {
        const val LEGACY_PREF_OPT_OUT = "matomo.optout"
        const val LEGACY_PREF_USER_ID = "tracker.userid"
        const val LEGACY_PREF_FIRST_VISIT = "tracker.firstvisit"
        const val LEGACY_PREF_VISITCOUNT = "tracker.visitcount"
        const val LEGACY_PREF_PREV_VISIT = "tracker.previousvisit"
    }
}


================================================
FILE: tracker/src/main/java/org/matomo/sdk/Matomo.kt
================================================
/*
 * Android SDK for Matomo
 *
 * @link https://github.com/matomo-org/matomo-android-sdk
 * @license https://github.com/matomo-org/matomo-sdk-android/blob/master/LICENSE BSD-3 Clause
 */
package org.matomo.sdk

import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import org.matomo.sdk.dispatcher.DefaultDispatcherFactory
import org.matomo.sdk.dispatcher.DispatcherFactory
import org.matomo.sdk.tools.BuildInfo
import org.matomo.sdk.tools.Checksum
import org.matomo.sdk.tools.DeviceHelper
import org.matomo.sdk.tools.PropertySource
import timber.log.Timber

class Matomo private constructor(context: Context) {
    private val preferenceMap: MutableMap<Tracker, SharedPreferences?> = HashMap()
    val context: Context = context.applicationContext

    /**
     * Base preferences, tracker independent.
     */
    val preferences: SharedPreferences = context.getSharedPreferences(BASE_PREFERENCE_FILE, Context.MODE_PRIVATE)

    /**
     * If you want to use your own [org.matomo.sdk.dispatcher.Dispatcher]
     */
    var dispatcherFactory: DispatcherFactory = DefaultDispatcherFactory()

    /**
     * @return Tracker specific settings object
     */
    fun getTrackerPreferences(tracker: Tracker): SharedPreferences? {
        synchronized(preferenceMap) {
            var newPrefs = preferenceMap[tracker]
            if (newPrefs == null) {
                val prefName: String = try {
                    "org.matomo.sdk_" + Checksum.getMD5Checksum(tracker.name)
                } catch (e: Exception) {
                    Timber.e(e)
                    "org.matomo.sdk_" + tracker.name
                }
                newPrefs = context.getSharedPreferences(prefName, Context.MODE_PRIVATE)
                preferenceMap[tracker] = newPrefs
            }
            return newPrefs
        }
    }

    val deviceHelper: DeviceHelper
        get() = DeviceHelper(context, PropertySource(), BuildInfo())

    companion object {
        private const val LOGGER_PREFIX = "MATOMO:"
        private const val BASE_PREFERENCE_FILE = "org.matomo.sdk"

        @SuppressLint("StaticFieldLeak")
        @Volatile
        private var sInstance: Matomo? = null

        @JvmStatic
        fun getInstance(context: Context): Matomo {
            return sInstance ?: synchronized(Matomo::class.java) {
                sInstance ?: Matomo(context).also {
                    sInstance = it
                }
            }
        }

        @JvmStatic
        fun tag(vararg classes: Class<*>): String {
            val tags = arrayOfNulls<String>(classes.size)
            for (i in classes.indices) {
                tags[i] = classes[i].simpleName
            }
            return tag(*tags)
        }

        @JvmStatic
        fun tag(vararg tags: String?): String {
            val sb = StringBuilder(LOGGER_PREFIX)
            for (i in tags.indices) {
                sb.append(tags[i])
                if (i < tags.size - 1) sb.append(":")
            }
            return sb.toString()
        }
    }
}

================================================
FILE: tracker/src/main/java/org/matomo/sdk/QueryParams.java
================================================
/*
 * Android SDK for Matomo
 *
 * @link https://github.com/matomo-org/matomo-android-sdk
 * @license https://github.com/matomo-org/matomo-sdk-android/blob/master/LICENSE BSD-3 Clause
 */

package org.matomo.sdk;

/**
 * Query parameters supported by the tracking HTTP API.
 * See <a href="http://developer.matomo.org/api-reference/tracking-api">Tracking HTTP API</a>
 */
public enum QueryParams {
    //Required parameters
    /**
     * The ID of the website we're tracking a visit/action for.
     * <p>
     * (required)
     */
    SITE_ID("idsite"),
    /**
     * Required for tracking, must be set to one, eg, rec=1.
     * <p>
     * (required)
     */
    RECORD("rec"),
    /**
     * The full URL for the current action.
     * <p>
     * (required)
     */
    URL_PATH("url"),


    //Recommended parameters
    /**
     * The title of the action being tracked.<p>
     * It is possible to <a href="http://matomo.org/faq/how-to/faq_62">use slashes / to set one or several categories for this action.</a>
     * For example, Help / Feedback will create the Action Feedback in the category Help.
     * <p>
     * (recommended)
     */
    ACTION_NAME("action_name"),
    /**
     * The unique visitor ID, must be a 16 characters hexadecimal string.<p>
     * Every unique visitor must be assigned a different ID and this ID must not change after it is assigned.
     * If this value is not set Matomo will still track visits, but the unique visitors metric might be less accurate.
     * <p>
     * (recommended)
     */
    VISITOR_ID("_id"),
    /**
     * Meant to hold a random value that is generated before each request.<p>
     * Using it helps avoid the tracking request being cached by the browser or a proxy.
     * <p>
     * (recommended)
     */
    RANDOM_NUMBER("rand"),
    /**
     * The parameter apiv=1 defines the api version to use (currently always set to 1)
     * <p>
     * (recommended)
     */
    API_VERSION("apiv"),


    // Optional User info
    /**
     * The full HTTP Referrer URL.<p>
     * This value is used to determine how someone got to your website (ie, through a website, search engine or campaign).
     */
    REFERRER("urlref"),
    /**
     * Visit scope <a href="http://matomo.org/docs/custom-variables/">custom variables</a>.<p>
     * This is a JSON encoded string of the custom variable array.
     * @deprecated Consider using <a href="http://matomo.org/docs/custom-dimensions/">Custom Dimensions</a>
     * @see org.matomo.sdk.extra.CustomDimension
     */
    @Deprecated
    VISIT_SCOPE_CUSTOM_VARIABLES("_cvar"),
    /**
     * The current count of visits for this visitor.<p>
     * To set this value correctly, it would be required to store the value for each visitor in your application (using sessions or persisting in a database).
     * Then you would manually increment the counts by one on each new visit or "session", depending on how you choose to define a visit.
     * This value is used to populate the report Visitors &gt; Engagement &gt; Visits by visit number.
     */
    TOTAL_NUMBER_OF_VISITS("_idvc"),
    /**
     * The UNIX timestamp of this visitor's previous visit (seconds since Jan 01 1970. (UTC)).<p>
     * This parameter is used to populate the report Visitors &gt; Engagement &gt; Visits by days since last visit.
     */
    PREVIOUS_VISIT_TIMESTAMP("_viewts"),
    /**
     * The UNIX timestamp of this visitor's first visit (seconds since Jan 01 1970. (UTC)).<p>
     * This could be set to the date where the user first started using your software/app, or when he/she created an account.
     * This parameter is used to populate the Goals &gt; Days to Conversion report.
     */
    FIRST_VISIT_TIMESTAMP("_idts"),
    /**
     * The Campaign name (see <a href="http://matomo.org/docs/tracking-campaigns/">Tracking Campaigns</a>).<p>
     * Used to populate the Referrers &gt; Campaigns report.
     * Note: this parameter will only be used for the first pageview of a visit.
     */
    CAMPAIGN_NAME("_rcn"),
    /**
     * The Campaign Keyword (see <a href="http://matomo.org/docs/tracking-campaigns/">Tracking Campaigns</a>).<p>
     * Used to populate the Referrers &gt; Campaigns report (clicking on a campaign loads all keywords for this campaign).
     * Note: this parameter will only be used for the first pageview of a visit.
     */
    CAMPAIGN_KEYWORD("_rck"),
    /**
     * The resolution of the device the visitor is using, eg 1280x1024.
     */
    SCREEN_RESOLUTION("res"),
    /**
     * The current hour (local time).
     */
    HOURS("h"),
    /**
     * The current minute (local time).
     */
    MINUTES("m"),
    /**
     * The current second (local time).
     */
    SECONDS("s"),
    /**
     * An override value for the User-Agent HTTP header field.<p>
     * The user agent is used to detect the operating system and browser used.
     */
    USER_AGENT("ua"),
    /**
     * An override value for the Accept-Language HTTP header field.<p>
     * This value is used to detect the visitor's country if <a href="http://matomo.org/faq/troubleshooting/faq_65">GeoIP</a> is not enabled.
     */
    LANGUAGE("lang"),
    /**
     * Defines the User ID for this request.<p>
     * User ID is any non empty unique string identifying the user (such as an email address or a username).
     * To access this value, users must be logged-in in your system so you can fetch this user ID from your system, and pass it to Matomo.
     * The User ID appears in the visitor log, the Visitor profile, and you can Segment reports for one or several User ID (userId segment).
     * When specified, the User ID will be "enforced". This means that if there is no recent visit with this User ID, a new one will be created.
     * If a visit is found in the last 30 minutes with your specified User ID, then the new action will be recorded to this existing visit.
     */
    USER_ID("uid"),
    /**
     * If set to 1, will force a new visit to be created for this action.
     */
    SESSION_START("new_visit"),


    // Optional Action info (measure Page view, Outlink, Download, Site search)
    /**
     * Page scope <a href="http://matomo.org/docs/custom-variables/">custom variables</a>.
     * This is a JSON encoded string of the custom variable array.
     * @deprecated Consider using <a href="http://matomo.org/docs/custom-dimensions/">Custom Dimensions</a>
     * @see org.matomo.sdk.extra.CustomDimension
     */
    SCREEN_SCOPE_CUSTOM_VARIABLES("cvar"),
    /**
     * An external URL the user has opened.<p>
     * Used for tracking outlink clicks. We recommend to also set the url parameter to this same value.
     */
    LINK("link"),
    /**
     * URL of a file the user has downloaded.<p>
     * Used for tracking downloads. We recommend to also set the url parameter to this same value.
     */
    DOWNLOAD("download"),
    /**
     * The Site Search keyword.<p>
     * When specified, the request will not be tracked as a normal pageview but will instead be tracked as a <a href="http://matomo.org/docs/site-search/">Site Search</a> request.
     */
    SEARCH_KEYWORD("search"),
    /**
     * When {@link #SEARCH_KEYWORD} is specified, you can optionally specify a search category with this parameter.
     */
    SEARCH_CATEGORY("search_cat"),
    /**
     * When {@link #SEARCH_KEYWORD} is specified, we also recommend to set this to the number of search results.
     */
    SEARCH_NUMBER_OF_HITS("search_count"),
    /**
     * If specified, the tracking request will trigger a conversion for the goal of the website being tracked with this ID.
     */
    GOAL_ID("idgoal"),
    /**
     * A monetary value that was generated as revenue by this goal conversion.<p>
     * Only used if {@link #GOAL_ID} is specified in the request.
     */
    REVENUE("revenue"),
    /**
     * Override for the datetime of the request (normally the current time is used).<p>
     * This can be used to record visits and page views in the past.
     * The expected format is: 2011-04-05 00:11:42 (remember to URL encode the value!).
     * The datetime must be sent in UTC timezone.
     * Events can only be backdated for a maximum time of 24h.
     * Note: if you record data in the past, you will need to <a href="http://matomo.org/faq/how-to/faq_59">force Matomo to re-process reports for the past dates.</a>
     */
    DATETIME_OF_REQUEST("cdt"),


    /**
     * The name of the content. For instance 'Ad Foo Bar'
     *
     * @see <a href="http://matomo.org/docs/content-tracking/">Content Tracking</a>
     */
    CONTENT_NAME("c_n"),
    /**
     * The actual content piece. For instance the path to an image, video, audio, any text
     *
     * @see <a href="http://matomo.org/docs/content-tracking/">Content Tracking</a>
     */
    CONTENT_PIECE("c_p"),
    /**
     * The target of the content. For instance the URL of a landing page
     *
     * @see <a href="http://matomo.org/docs/content-tracking/">Content Tracking</a>
     */
    CONTENT_TARGET("c_t"),
    /**
     * The name of the interaction with the content. For instance a 'click'
     *
     * @see <a href="http://matomo.org/docs/content-tracking/">Content Tracking</a>
     */
    CONTENT_INTERACTION("c_i"),

    /**
     * The event category. Must not be empty. (eg. Videos, Music, Games...)
     *
     * @see <a href="http://matomo.org/docs/event-tracking/">Event Tracking</a>
     */
    EVENT_CATEGORY("e_c"),
    /**
     * The event action. Must not be empty. (eg. Play, Pause, Duration, Add Playlist, Downloaded, Clicked...)
     *
     * @see <a href="http://matomo.org/docs/event-tracking/">Event Tracking</a>
     */
    EVENT_ACTION("e_a"),
    /**
     * The event name. (eg. a Movie name, or Song name, or File name...)
     *
     * @see <a href="http://matomo.org/docs/event-tracking/">Event Tracking</a>
     */
    EVENT_NAME("e_n"),
    /**
     * The event value. Must be a float or integer value (numeric), not a string.
     *
     * @see <a href="http://matomo.org/docs/event-tracking/">Event Tracking</a>
     */
    EVENT_VALUE("e_v"),

    // Ecommerce parameters
    /**
     * Items in your cart or order for ecommerce tracking
     */
    ECOMMERCE_ITEMS("ec_items"),

    /**
     * The amount of tax paid for the order
     */
    TAX("ec_tx"),

    /**
     * The unique identifier for the order
     */
    ORDER_ID("ec_id"),

    /**
     * The amount of shipping paid on the order
     */
    SHIPPING("ec_sh"),

    /**
     * The amount of the discount on the order
     */
    DISCOUNT("ec_dt"),

    /**
     * The sub total amount of the order
     */
    SUBTOTAL("ec_st"),

    // Other parameters
    /**
     * If set to 0 (send_image=0) Matomo will respond with a HTTP 204 response code instead of a GIF image.<p>
     * This improves performance and can fix errors if images are not allowed to be obtained directly (eg Chrome Apps). Available since Matomo 2.10.0
     */
    SEND_IMAGE("send_image");

    private final String value;

    QueryParams(String value) {
        this.value = value;
    }

    public String toString() {
        return value;
    }
}


================================================
FILE: tracker/src/main/java/org/matomo/sdk/TrackMe.java
================================================
/*
 * Android SDK for Matomo
 *
 * @link https://github.com/matomo-org/matomo-android-sdk
 * @license https://github.com/matomo-org/matomo-sdk-android/blob/master/LICENSE BSD-3 Clause
 */

package org.matomo.sdk;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.util.HashMap;
import java.util.Map;

/**
 * This objects represents one query to Matomo.
 * For each event send to Matomo a TrackMe gets created, either explicitly by you or implicitly by the Tracker.
 */
public class TrackMe {
    private static final int DEFAULT_QUERY_CAPACITY = 14;
    private final HashMap<String, String> mQueryParams = new HashMap<>(DEFAULT_QUERY_CAPACITY);

    public TrackMe() { }

    public TrackMe(TrackMe trackMe) {
        mQueryParams.putAll(trackMe.mQueryParams);
    }

    /**
     * Adds TrackMe to this TrackMe, overriding values if necessary.
     */
    public TrackMe putAll(@NonNull TrackMe trackMe) {
        mQueryParams.putAll(trackMe.toMap());
        return this;
    }

    /**
     * Consider using {@link QueryParams} instead of raw strings
     */
    public synchronized TrackMe set(@NonNull String key, String value) {
        if (value == null) mQueryParams.remove(key);
        else if (value.length() > 0) mQueryParams.put(key, value);
        return this;
    }

    /**
     * Consider using {@link QueryParams} instead of raw strings
     */
    @Nullable
    public synchronized String get(@NonNull String queryParams) {
        return mQueryParams.get(queryParams);
    }

    /**
     * You can set any additional Tracking API Parameters within the SDK.
     * This includes for example the local time (parameters h, m and s).
     * <pre>
     * set(QueryParams.HOURS, "10");
     * set(QueryParams.MINUTES, "45");
     * set(QueryParams.SECONDS, "30");
     * </pre>
     *
     * @param key   query params name
     * @param value value
     * @return tracker instance
     */
    public synchronized TrackMe set(@NonNull QueryParams key, String value) {
        set(key.toString(), value);
        return this;
    }

    public synchronized TrackMe set(@NonNull QueryParams key, int value) {
        set(key, Integer.toString(value));
        return this;
    }

    public synchronized TrackMe set(@NonNull QueryParams key, float value) {
        set(key, Float.toString(value));
        return this;
    }

    public synchronized TrackMe set(@NonNull QueryParams key, long value) {
        set(key, Long.toString(value));
        return this;
    }

    public synchronized boolean has(@NonNull QueryParams queryParams) {
        return mQueryParams.containsKey(queryParams.toString());
    }

    /**
     * Only sets the value if it doesn't exist.
     *
     * @param key   type
     * @param value value
     * @return this (for chaining)
     */
    public synchronized TrackMe trySet(@NonNull QueryParams key, int value) {
        return trySet(key, String.valueOf(value));
    }

    /**
     * Only sets the value if it doesn't exist.
     *
     * @param key   type
     * @param value value
     * @return this (for chaining)
     */
    public synchronized TrackMe trySet(@NonNull QueryParams key, float value) {
        return trySet(key, String.valueOf(value));
    }

    public synchronized TrackMe trySet(@NonNull QueryParams key, long value) {
        return trySet(key, String.valueOf(value));
    }

    /**
     * Only sets the value if it doesn't exist.
     *
     * @param key   type
     * @param value value
     * @return this (for chaining)
     */
    public synchronized TrackMe trySet(@NonNull QueryParams key, String value) {
        if (!has(key)) set(key, value);
        return this;
    }

    /**
     * The tracker calls this to get the final data that will be transmitted
     *
     * @return the parameter map, but without the base URL
     */
    public synchronized Map<String, String> toMap() {
        return new HashMap<>(mQueryParams);
    }

    public synchronized String get(@NonNull QueryParams queryParams) {
        return mQueryParams.get(queryParams.toString());
    }

    public synchronized boolean isEmpty() {
        return mQueryParams.isEmpty();
    }
}


================================================
FILE: tracker/src/main/java/org/matomo/sdk/Tracker.java
================================================
/*
 * Android SDK for Matomo
 *
 * @link https://github.com/matomo-org/matomo-android-sdk
 * @license https://github.com/matomo-org/matomo-sdk-android/blob/master/LICENSE BSD-3 Clause
 */

package org.matomo.sdk;

import android.content.SharedPreferences;

import org.matomo.sdk.dispatcher.DispatchMode;
import org.matomo.sdk.dispatcher.Dispatcher;
import org.matomo.sdk.dispatcher.Packet;
import org.matomo.sdk.tools.DeviceHelper;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.UUID;
import java.util.regex.Pattern;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import timber.log.Timber;


/**
 * Main tracking class
 * This class is threadsafe.
 */
@SuppressWarnings("WeakerAccess")
public class Tracker {
    private static final String TAG = Matomo.tag(Tracker.class);

    // Matomo default parameter values
    private static final String DEFAULT_UNKNOWN_VALUE = "unknown";
    private static final String DEFAULT_TRUE_VALUE = "1";
    private static final String DEFAULT_RECORD_VALUE = DEFAULT_TRUE_VALUE;
    private static final String DEFAULT_API_VERSION_VALUE = "1";

    // Sharedpreference keys for persisted values
    protected static final String PREF_KEY_TRACKER_OPTOUT = "tracker.optout";
    protected static final String PREF_KEY_TRACKER_USERID = "tracker.userid";
    protected static final String PREF_KEY_TRACKER_VISITORID = "tracker.visitorid";
    protected static final String PREF_KEY_TRACKER_FIRSTVISIT = "tracker.firstvisit";
    protected static final String PREF_KEY_TRACKER_VISITCOUNT = "tracker.visitcount";
    protected static final String PREF_KEY_TRACKER_PREVIOUSVISIT = "tracker.previousvisit";
    protected static final String PREF_KEY_OFFLINE_CACHE_AGE = "tracker.cache.age";
    protected static final String PREF_KEY_OFFLINE_CACHE_SIZE = "tracker.cache.size";
    protected static final String PREF_KEY_DISPATCHER_MODE = "tracker.dispatcher.mode";

    private static final Pattern VALID_URLS = Pattern.compile("^(\\w+)(?:://)(.+?)$");

    private final Matomo mMatomo;
    private final String mApiUrl;
    private final int mSiteId;
    private final String mDefaultApplicationBaseUrl;
    private final Object mTrackingLock = new Object();
    private final Dispatcher mDispatcher;
    private final String mName;
    private final Random mRandomAntiCachingValue = new Random(new Date().getTime());
    private final TrackMe mDefaultTrackMe = new TrackMe();

    private TrackMe mLastEvent;
    private long mSessionTimeout = 30 * 60 * 1000;
    private long mSessionStartTime = 0;
    private boolean mOptOut;
    private SharedPreferences mPreferences;

    private final LinkedHashSet<Callback> mTrackingCallbacks = new LinkedHashSet<>();
    private DispatchMode mDispatchMode;

    protected Tracker(Matomo matomo, TrackerBuilder config) {
        mMatomo = matomo;
        mApiUrl = config.getApiUrl();
        mSiteId = config.getSiteId();
        mName = config.getTrackerName();
        mDefaultApplicationBaseUrl = config.getApplicationBaseUrl();

        new LegacySettingsPorter(mMatomo).port(this);

        mOptOut = getPreferences().getBoolean(PREF_KEY_TRACKER_OPTOUT, false);

        mDispatcher = mMatomo.getDispatcherFactory().build(this);
        mDispatcher.setDispatchMode(getDispatchMode());

        String userId = getPreferences().getString(PREF_KEY_TRACKER_USERID, null);
        mDefaultTrackMe.set(QueryParams.USER_ID, userId);

        String visitorId = getPreferences().getString(PREF_KEY_TRACKER_VISITORID, null);
        if (visitorId == null) {
            visitorId = makeRandomVisitorId();
            getPreferences().edit().putString(PREF_KEY_TRACKER_VISITORID, visitorId).apply();
        }
        mDefaultTrackMe.set(QueryParams.VISITOR_ID, visitorId);

        mDefaultTrackMe.set(QueryParams.SESSION_START, DEFAULT_TRUE_VALUE);

        DeviceHelper deviceHelper = mMatomo.getDeviceHelper();

        String resolution = DEFAULT_UNKNOWN_VALUE;
        int[] res = deviceHelper.getResolution();
        if (res != null) resolution = String.format("%sx%s", res[0], res[1]);
        mDefaultTrackMe.set(QueryParams.SCREEN_RESOLUTION, resolution);

        mDefaultTrackMe.set(QueryParams.USER_AGENT, deviceHelper.getUserAgent());
        mDefaultTrackMe.set(QueryParams.LANGUAGE, deviceHelper.getUserLanguage());
        mDefaultTrackMe.set(QueryParams.URL_PATH, config.getApplicationBaseUrl());
    }

    public void addTrackingCallback(Callback callback) {
        this.mTrackingCallbacks.add(callback);
    }

    public void removeTrackingCallback(Callback callback) {
        this.mTrackingCallbacks.remove(callback);
    }

    public void reset() {
        dispatch();

        String visitorId = makeRandomVisitorId();

        SharedPreferences prefs = getPreferences();

        //noinspection SynchronizationOnLocalVariableOrMethodParameter
        synchronized (prefs) {
            SharedPreferences.Editor editor = mPreferences.edit();

            editor.remove(PREF_KEY_TRACKER_VISITCOUNT);
            editor.remove(PREF_KEY_TRACKER_PREVIOUSVISIT);
            editor.remove(PREF_KEY_TRACKER_FIRSTVISIT);
            editor.remove(PREF_KEY_TRACKER_USERID);
            editor.remove(PREF_KEY_TRACKER_OPTOUT);

            editor.putString(PREF_KEY_TRACKER_VISITORID, visitorId);

            editor.apply();
        }

        mDefaultTrackMe.set(QueryParams.VISITOR_ID, visitorId);
        mDefaultTrackMe.set(QueryParams.USER_ID, null);
        mDefaultTrackMe.set(QueryParams.FIRST_VISIT_TIMESTAMP, null);
        mDefaultTrackMe.set(QueryParams.TOTAL_NUMBER_OF_VISITS, null);
        mDefaultTrackMe.set(QueryParams.PREVIOUS_VISIT_TIMESTAMP, null);
        mDefaultTrackMe.set(QueryParams.SESSION_START, DEFAULT_TRUE_VALUE);
        mDefaultTrackMe.set(QueryParams.VISIT_SCOPE_CUSTOM_VARIABLES, null);
        mDefaultTrackMe.set(QueryParams.CAMPAIGN_NAME, null);
        mDefaultTrackMe.set(QueryParams.CAMPAIGN_KEYWORD, null);

        startNewSession();
    }

    /**
     * Use this to disable this Tracker, e.g. if the user opted out of tracking.
     * The Tracker will persist the choice and remain disable on next instance creation.<p>
     *
     * @param optOut true to disable reporting
     */
    public void setOptOut(boolean optOut) {
        mOptOut = optOut;
        getPreferences().edit().putBoolean(PREF_KEY_TRACKER_OPTOUT, optOut).apply();
    }

    /**
     * @return true if Matomo is currently disabled
     */
    public boolean isOptOut() {
        return mOptOut;
    }

    public String getName() {
        return mName;
    }

    public Matomo getMatomo() {
        return mMatomo;
    }

    public String getAPIUrl() {
        return mApiUrl;
    }

    protected int getSiteId() {
        return mSiteId;
    }

    /**
     * Matomo will use the content of this object to fill in missing values before any transmission.
     * While you can modify it's values, you can also just set them in your {@link TrackMe} object as already set values will not be overwritten.
     *
     * @return the default TrackMe object
     */
    public TrackMe getDefaultTrackMe() {
        return mDefaultTrackMe;
    }

    public void startNewSession() {
        synchronized (mTrackingLock) {
            mSessionStartTime = 0;
        }
    }

    public void setSessionTimeout(int milliseconds) {
        synchronized (mTrackingLock) {
            mSessionTimeout = milliseconds;
        }
    }

    /**
     * Default is 30min (30*60*1000).
     *
     * @return session timeout value in miliseconds
     */
    public long getSessionTimeout() {
        return mSessionTimeout;
    }

    /**
     * {@link Dispatcher#getConnectionTimeOut()}
     */
    public int getDispatchTimeout() {
        return mDispatcher.getConnectionTimeOut();
    }

    /**
     * {@link Dispatcher#setConnectionTimeOut(int)}
     */
    public void setDispatchTimeout(int timeout) {
        mDispatcher.setConnectionTimeOut(timeout);
    }

    /**
     * Processes all queued events in background thread
     */
    public void dispatch() {
        if (mOptOut) return;
        mDispatcher.forceDispatch();
    }

    /**
     * Process all queued events and block until processing is complete
     */
    public void dispatchBlocking() {
        if (mOptOut) return;
        mDispatcher.forceDispatchBlocking();
    }

    /**
     * Set the interval to 0 to dispatch events as soon as they are queued.
     * If a negative value is used the dispatch timer will never run, a manual dispatch must be used.
     *
     * @param dispatchInterval in milliseconds
     */
    public Tracker setDispatchInterval(long dispatchInterval) {
        mDispatcher.setDispatchInterval(dispatchInterval);
        return this;
    }

    /**
     * Defines if when dispatched, posted JSON must be Gzipped.
     * Need to be handle from web server side with mod_deflate/APACHE lua_zlib/NGINX.
     *
     * @param dispatchGzipped boolean
     */
    public Tracker setDispatchGzipped(boolean dispatchGzipped) {
        mDispatcher.setDispatchGzipped(dispatchGzipped);
        return this;
    }

    /**
     * @return in milliseconds
     */
    public long getDispatchInterval() {
        return mDispatcher.getDispatchInterval();
    }

    /**
     * For how long events should be stored if they could not be send.
     * Events older than the set limit will be discarded on the next dispatch attempt.<br>
     * The Matomo backend accepts backdated events for up to 24 hours by default.
     * <p>
     * &gt;0 = limit in ms<br>
     * 0 = unlimited<br>
     * -1 = disabled offline cache<br>
     *
     * @param age in milliseconds
     */
    public void setOfflineCacheAge(long age) {
        getPreferences().edit().putLong(PREF_KEY_OFFLINE_CACHE_AGE, age).apply();
    }

    /**
     * See {@link #setOfflineCacheAge(long)}
     *
     * @return maximum cache age in milliseconds
     */
    public long getOfflineCacheAge() {
        return getPreferences().getLong(PREF_KEY_OFFLINE_CACHE_AGE, 24 * 60 * 60 * 1000);
    }

    /**
     * How large the offline cache may be.
     * If the limit is reached the oldest files will be deleted first.
     * Events older than the set limit will be discarded on the next dispatch attempt.<br>
     * The Matomo backend accepts backdated events for up to 24 hours by default.
     * <p>
     * &gt;0 = limit in byte<br>
     * 0 = unlimited<br>
     *
     * @param size in byte
     */
    public void setOfflineCacheSize(long size) {
        getPreferences().edit().putLong(PREF_KEY_OFFLINE_CACHE_SIZE, size).apply();
    }

    /**
     * Maximum size the offline cache is allowed to grow to.
     *
     * @return size in byte
     */
    public long getOfflineCacheSize() {
        return getPreferences().getLong(PREF_KEY_OFFLINE_CACHE_SIZE, 4 * 1024 * 1024);
    }

    /**
     * The current dispatch behavior.
     *
     * @see DispatchMode
     */
    public DispatchMode getDispatchMode() {
        if (mDispatchMode == null) {
            String raw = getPreferences().getString(PREF_KEY_DISPATCHER_MODE, null);
            mDispatchMode = DispatchMode.fromString(raw);
            if (mDispatchMode == null) mDispatchMode = DispatchMode.ALWAYS;
        }
        return mDispatchMode;
    }

    /**
     * Sets the dispatch mode.
     *
     * @see DispatchMode
     */
    public void setDispatchMode(DispatchMode mode) {
        mDispatchMode = mode;
        if (mode != DispatchMode.EXCEPTION) {
            getPreferences().edit().putString(PREF_KEY_DISPATCHER_MODE, mode.toString()).apply();
        }
        mDispatcher.setDispatchMode(mode);
    }

    /**
     * Defines the User ID for this request.
     * User ID is any non empty unique string identifying the user (such as an email address or a username).
     * To access this value, users must be logged-in in your system so you can
     * fetch this user ID from your system, and pass it to Matomo.
     * <p>
     * When specified, the User ID will be "enforced".
     * This means that if there is no recent visit with this User ID, a new one will be created.
     * If a visit is found in the last 30 minutes with your specified User ID,
     * then the new action will be recorded to this existing visit.
     *
     * @param userId passing null will delete the current user-id.
     */
    public Tracker setUserId(String userId) {
        mDefaultTrackMe.set(QueryParams.USER_ID, userId);
        getPreferences().edit().putString(PREF_KEY_TRACKER_USERID, userId).apply();
        return this;
    }

    /**
     * @return a user-id string, either the one you set or the one Matomo generated for you.
     */
    public String getUserId() {
        return mDefaultTrackMe.get(QueryParams.USER_ID);
    }

    /**
     * The unique visitor ID, must be a 16 characters hexadecimal string.
     * Every unique visitor must be assigned a different ID and this ID must not change after it is assigned.
     * If this value is not set Matomo will still track visits, but the unique visitors metric might be less accurate.
     */
    public Tracker setVisitorId(String visitorId) throws IllegalArgumentException {
        if (confirmVisitorIdFormat(visitorId)) mDefaultTrackMe.set(QueryParams.VISITOR_ID, visitorId);
        return this;
    }

    public String getVisitorId() {
        return mDefaultTrackMe.get(QueryParams.VISITOR_ID);
    }

    private static final Pattern PATTERN_VISITOR_ID = Pattern.compile("^[0-9a-f]{16}$");

    private boolean confirmVisitorIdFormat(String visitorId) throws IllegalArgumentException {
        if (PATTERN_VISITOR_ID.matcher(visitorId).matches()) return true;

        throw new IllegalArgumentException("VisitorId: " + visitorId + " is not of valid format, " +
                " the format must match the regular expression: " + PATTERN_VISITOR_ID.pattern());
    }

    /**
     * There parameters are only interesting for the very first query.
     */
    private void injectInitialParams(TrackMe trackMe) {
        long firstVisitTime;
        long visitCount;
        long previousVisit;

        SharedPreferences prefs = getPreferences();
        // Protected against Trackers on other threads trying to do the same thing.
        // This works because they would use the same preference object.
        //noinspection SynchronizationOnLocalVariableOrMethodParameter
        synchronized (prefs) {
            SharedPreferences.Editor editor = prefs.edit();
            visitCount = 1 + getPreferences().getLong(PREF_KEY_TRACKER_VISITCOUNT, 0);
            editor.putLong(PREF_KEY_TRACKER_VISITCOUNT, visitCount);

            firstVisitTime = prefs.getLong(PREF_KEY_TRACKER_FIRSTVISIT, -1);
            if (firstVisitTime == -1) {
                firstVisitTime = System.currentTimeMillis() / 1000;
                editor.putLong(PREF_KEY_TRACKER_FIRSTVISIT, firstVisitTime);
            }

            previousVisit = prefs.getLong(PREF_KEY_TRACKER_PREVIOUSVISIT, -1);
            editor.putLong(PREF_KEY_TRACKER_PREVIOUSVISIT, System.currentTimeMillis() / 1000);

            editor.apply();
        }

        // trySet because the developer could have modded these after creating the Tracker
        mDefaultTrackMe.trySet(QueryParams.FIRST_VISIT_TIMESTAMP, firstVisitTime);
        mDefaultTrackMe.trySet(QueryParams.TOTAL_NUMBER_OF_VISITS, visitCount);

        if (previousVisit != -1) mDefaultTrackMe.trySet(QueryParams.PREVIOUS_VISIT_TIMESTAMP, previousVisit);

        trackMe.trySet(QueryParams.SESSION_START, mDefaultTrackMe.get(QueryParams.SESSION_START));
        trackMe.trySet(QueryParams.FIRST_VISIT_TIMESTAMP, mDefaultTrackMe.get(QueryParams.FIRST_VISIT_TIMESTAMP));
        trackMe.trySet(QueryParams.TOTAL_NUMBER_OF_VISITS, mDefaultTrackMe.get(QueryParams.TOTAL_NUMBER_OF_VISITS));
        trackMe.trySet(QueryParams.PREVIOUS_VISIT_TIMESTAMP, mDefaultTrackMe.get(QueryParams.PREVIOUS_VISIT_TIMESTAMP));
    }

    /**
     * These parameters are required for all queries.
     */
    private void injectBaseParams(TrackMe trackMe) {
        trackMe.trySet(QueryParams.SITE_ID, mSiteId);
        trackMe.trySet(QueryParams.RECORD, DEFAULT_RECORD_VALUE);
        trackMe.trySet(QueryParams.API_VERSION, DEFAULT_API_VERSION_VALUE);
        trackMe.trySet(QueryParams.RANDOM_NUMBER, mRandomAntiCachingValue.nextInt(100000));
        trackMe.trySet(QueryParams.DATETIME_OF_REQUEST, new SimpleDateFormat("yyyy-MM-dd HH:mm:ssZ", Locale.US).format(new Date()));
        trackMe.trySet(QueryParams.SEND_IMAGE, "0");

        trackMe.trySet(QueryParams.VISITOR_ID, mDefaultTrackMe.get(QueryParams.VISITOR_ID));
        trackMe.trySet(QueryParams.USER_ID, mDefaultTrackMe.get(QueryParams.USER_ID));

        trackMe.trySet(QueryParams.SCREEN_RESOLUTION, mDefaultTrackMe.get(QueryParams.SCREEN_RESOLUTION));
        trackMe.trySet(QueryParams.USER_AGENT, mDefaultTrackMe.get(QueryParams.USER_AGENT));
        trackMe.trySet(QueryParams.LANGUAGE, mDefaultTrackMe.get(QueryParams.LANGUAGE));

        String urlPath = trackMe.get(QueryParams.URL_PATH);
        if (urlPath == null) {
            urlPath = mDefaultTrackMe.get(QueryParams.URL_PATH);
        } else if (!VALID_URLS.matcher(urlPath).matches()) {
            StringBuilder urlBuilder = new StringBuilder(mDefaultApplicationBaseUrl);
            if (!mDefaultApplicationBaseUrl.endsWith("/") && !urlPath.startsWith("/")) {
                urlBuilder.append("/");
            } else if (mDefaultApplicationBaseUrl.endsWith("/") && urlPath.startsWith("/")) {
                urlPath = urlPath.substring(1);
            }
            urlPath = urlBuilder.append(urlPath).toString();
        }

        // https://github.com/matomo-org/matomo-sdk-android/issues/92
        mDefaultTrackMe.set(QueryParams.URL_PATH, urlPath);
        trackMe.set(QueryParams.URL_PATH, urlPath);
    }

    public Tracker track(TrackMe trackMe) {
        synchronized (mTrackingLock) {
            final boolean newSession = System.currentTimeMillis() - mSessionStartTime > mSessionTimeout;

            if (newSession) {
                mSessionStartTime = System.currentTimeMillis();
                injectInitialParams(trackMe);
            }

            injectBaseParams(trackMe);

            for (Callback callback : mTrackingCallbacks) {
                trackMe = callback.onTrack(trackMe);
                if (trackMe == null) {
                    Timber.tag(TAG).d("Tracking aborted by %s", callback);
                    return this;
                }
            }

            mLastEvent = trackMe;
            if (!mOptOut) {
                mDispatcher.submit(trackMe);
                Timber.tag(TAG).d("Event added to the queue: %s", trackMe);
            } else {
                Timber.tag(TAG).d("Event omitted due to opt out: %s", trackMe);
            }

            return this;
        }
    }

    public static String makeRandomVisitorId() {
        return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 16);
    }


    public SharedPreferences getPreferences() {
        if (mPreferences == null) mPreferences = mMatomo.getTrackerPreferences(this);
        return mPreferences;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Tracker tracker = (Tracker) o;

        if (mSiteId != tracker.mSiteId) return false;
        if (!mApiUrl.equals(tracker.mApiUrl)) return false;
        return mName.equals(tracker.mName);

    }

    @Override
    public int hashCode() {
        int result = mApiUrl.hashCode();
        result = 31 * result + mSiteId;
        result = 31 * result + mName.hashCode();
        return result;
    }

    /**
     * For testing purposes
     *
     * @return query of the event
     */
    @VisibleForTesting
    public TrackMe getLastEventX() {
        return mLastEvent;
    }

    /**
     * Set a data structure here to put the Dispatcher into dry-run-mode.
     * Data will be processed but at the last step just stored instead of transmitted.
     * Set it to null to disable it.
     *
     * @param dryRunTarget a data structure the data should be passed into
     */
    public void setDryRunTarget(List<Packet> dryRunTarget) {
        mDispatcher.setDryRunTarget(dryRunTarget);
    }

    /**
     * If we are in dry-run mode then this will return a datastructure.
     *
     * @return a datastructure or null
     */
    public List<Packet> getDryRunTarget() {
        return mDispatcher.getDryRunTarget();
    }

    public interface Callback {
        /**
         * This method will be called after parameter injection and before transmission within {@link Tracker#track(TrackMe)}.
         * Blocking within this method will block tracking.
         *
         * @param trackMe The `TrackMe` that was passed to {@link Tracker#track(TrackMe)} after all data has been injected.
         * @return The `TrackMe` that will be send, returning NULL here will abort transmission.
         */
        @Nullable
        TrackMe onTrack(TrackMe trackMe);
    }
}


================================================
FILE: tracker/src/main/java/org/matomo/sdk/TrackerBuilder.java
================================================
package org.matomo.sdk;

import java.net.MalformedURLException;
import java.net.URL;

/**
 * Configuration details for a {@link Tracker}
 */
public class TrackerBuilder {
    private final String mApiUrl;
    private final int mSiteId;
    private String mTrackerName;
    private String mApplicationBaseUrl;

    public static TrackerBuilder createDefault(String apiUrl, int siteId) {
        return new TrackerBuilder(apiUrl, siteId, "Default Tracker");
    }

    /**
     * @param apiUrl      Tracking HTTP API endpoint, for example, https://matomo.yourdomain.tld/matomo.php
     * @param siteId      id of your site in the backend
     * @param trackerName name of your tracker, will be used to store configuration data
     */
    public TrackerBuilder(String apiUrl, int siteId, String trackerName) {
        try {
            new URL(apiUrl);
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
        mApiUrl = apiUrl;
        mSiteId = siteId;
        mTrackerName = trackerName;
    }

    public String getApiUrl() {
        return mApiUrl;
    }

    public int getSiteId() {
        return mSiteId;
    }

    /**
     * A unique name for this Tracker. Used to store Tracker settings independent of URL and id changes.
     */
    public TrackerBuilder setTrackerName(String name) {
        mTrackerName = name;
        return this;
    }

    public String getTrackerName() {
        return mTrackerName;
    }

    /**
     * Domain used to build the required parameter url (http://developer.matomo.org/api-reference/tracking-api)
     * Defaults to`https://your.packagename`
     *
     * @param domain your-domain.com
     */
    public TrackerBuilder setApplicationBaseUrl(String domain) {
        mApplicationBaseUrl = domain;
        return this;
    }

    public String getApplicationBaseUrl() {
        return mApplicationBaseUrl;
    }

    public Tracker build(Matomo matomo) {
        if (mApplicationBaseUrl == null) {
            mApplicationBaseUrl = String.format("https://%s/", matomo.getContext().getPackageName());
        }
        return new Tracker(matomo, this);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        TrackerBuilder that = (TrackerBuilder) o;

        return mSiteId == that.mSiteId && mApiUrl.equals(that.mApiUrl) && mTrackerName.equals(that.mTrackerName);
    }

    @Override
    public int hashCode() {
        int result = mApiUrl.hashCode();
        result = 31 * result + mSiteId;
        result = 31 * result + mTrackerName.hashCode();
        return result;
    }

}


================================================
FILE: tracker/src/main/java/org/matomo/sdk/dispatcher/DefaultDispatcher.kt
================================================
/*
 * Android SDK for Matomo
 *
 * @link https://github.com/matomo-org/matomo-android-sdk
 * @license https://github.com/matomo-org/matomo-sdk-android/blob/master/LICENSE BSD-3 Clause
 */
package org.matomo.sdk.dispatcher

import org.matomo.sdk.Matomo.Companion.tag
import org.matomo.sdk.TrackMe
import org.matomo.sdk.tools.Connectivity
import timber.log.Timber
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
import kotlin.concurrent.Volatile
import kotlin.math.min


/**
 * Responsible for transmitting packets to a server
 */
class DefaultDispatcher(
    private val eventCache: EventCache,
    private val connectivity: Connectivity,
    private val packetFactory: PacketFactory,
    private val packetSender: PacketSender
) : Dispatcher {
    private val threadControl = Any()
    private val sleepToken = Semaphore(0)

    @Volatile
    private var timeOut = Dispatcher.DEFAULT_CONNECTION_TIMEOUT

    @Volatile
    private var dispatchInterval = Dispatcher.DEFAULT_DISPATCH_INTERVAL

    @Volatile
    private var retryCounter = 0

    @Volatile
    private var forcedBlocking = false

    private var dispatchGzipped = false

    @Volatile
    private var dispatchMode = DispatchMode.ALWAYS

    @Volatile
    private var running = false

    @Volatile
    private var dispatchThread: Thread? = null
    private var mDryRunTarget: MutableList<Packet>? = null

    init {
        packetSender.setGzipData(dispatchGzipped)
        packetSender.setTimeout(timeOut.toLong())
    }

    /**
     * Connection timeout in milliseconds
     *
     * @return timeout in milliseconds
     */
    override fun getConnectionTimeOut(): Int {
        return timeOut
    }

    /**
     * Timeout when trying to establish connection and when trying to read a response.
     * Values take effect on next dispatch.
     *
     * @param timeOutIn timeout in milliseconds
     */
    override fun setConnectionTimeOut(timeOutIn: Int) {
        timeOut = timeOutIn
        packetSender.setTimeout(timeOut.toLong())
    }

    /**
     * Packets are collected and dispatched in batches, this intervals sets the pause between batches.
     *
     * @param dispatchIntervalIn in milliseconds
     */
    override fun setDispatchInterval(dispatchIntervalIn: Long) {
        dispatchInterval = dispatchIntervalIn
        if (dispatchInterval != -1L)
            launch()
    }

    override fun getDispatchInterval(): Long {
        return dispatchInterval
    }

    /**
     * Packets are collected and dispatched in batches. This boolean sets if post must be
     * gzipped or not. Use of gzip needs mod_deflate/Apache ou lua_zlib/NGINX
     *
     * @param dispatchGzippedIn boolean
     */
    override fun setDispatchGzipped(dispatchGzippedIn: Boolean) {
        dispatchGzipped = dispatchGzippedIn
        packetSender.setGzipData(dispatchGzipped)
    }

    override fun getDispatchGzipped(): Boolean {
        return dispatchGzipped
    }

    override fun setDispatchMode(dispatchModeIn: DispatchMode) {
        this.dispatchMode = dispatchModeIn
    }

    override fun getDispatchMode(): DispatchMode {
        return dispatchMode
    }

    private fun launch(): Boolean {
        synchronized(threadControl) {
            if (!running) {
                running = true
                val thread = Thread(loop)
                thread.priority = Thread.MIN_PRIORITY
                thread.name = "Matomo-default-dispatcher"
                dispatchThread = thread
                thread.start()
                return true
            }
        }
        return false
    }

    /**
     * Starts the dispatcher for one cycle if it is currently not working.
     * If the dispatcher is working it will skip the dispatch interval once.
     */
    override fun forceDispatch(): Boolean {
        if (!launch()) {
            retryCounter = 0
            sleepToken.release()
            return false
        }
        return true
    }

    override fun forceDispatchBlocking() {
        synchronized(threadControl) {
            // force thread to exit after it completes its dispatch loop
            forcedBlocking = true
        }

        if (forceDispatch()) {
            sleepToken.release()
        }

        val dispatchThreadLocal = dispatchThread

        if (dispatchThreadLocal != null) {
            try {
                dispatchThreadLocal.join()
            } catch (e: InterruptedException) {
                Timber.tag(TAG).d("Interrupted while waiting for dispatch thread to complete")
            }
        }

        synchronized(threadControl) {
            // re-enable default behavior
            forcedBlocking = false
        }
    }

    override fun clear() {
        eventCache.clear()
        // Try to exit the loop as the queue is empty
        if (running) forceDispatch()
    }

    override fun submit(trackMe: TrackMe) {
        eventCache.add(Event(trackMe.toMap()))
        if (dispatchInterval != -1L) launch()
    }

    private val loop: Runnable = Runnable {
        retryCounter = 0
        while (running) {
            try {
                var sleepTime = dispatchInterval
                if (retryCounter > 1)
                    sleepTime += min(
                        (retryCounter * dispatchInterval).toDouble(),
                        (5 * dispatchInterval).toDouble()
                    ).toLong()

                // Either we wait the interval or forceDispatch() granted us one free pass
                sleepToken.tryAcquire(sleepTime, TimeUnit.MILLISECONDS)
            } catch (e: InterruptedException) {
                Timber.tag(TAG).e(e)
            }
            if (eventCache.updateState(isOnline)) {
                var count = 0
                val drainedEvents: List<Event> = ArrayList()
                eventCache.drainTo(drainedEvents)
                Timber.tag(TAG).d("Drained %s events.", drainedEvents.size)
                for (packet in packetFactory.buildPackets(drainedEvents)) {
                    var success: Boolean

                    if (mDryRunTarget != null) {
                        Timber.tag(TAG).d("DryRun, stored HttpRequest, now %d.", mDryRunTarget!!.size)
                        success = mDryRunTarget!!.add(packet)
                    } else {
                        success = packetSender.send(packet)
                    }

                    if (success) {
                        count += packet.eventCount
                        retryCounter = 0
                    } else {
                        // On network failure, requeue all un-sent events, but use isOnline to determine if events should be cached in
                        // memory or disk
                        Timber.tag(TAG).d("Failure while trying to send packet")
                        retryCounter++
                        break
                    }

                    // Re-check network connectivity to early exit if we drop offline.  This speeds up how quickly the setOffline method will
                    // take effect
                    if (!isOnline) {
                        Timber.tag(TAG).d("Disconnected during dispatch loop")
                        break
                    }
                }

                Timber.tag(TAG).d("Dispatched %d events.", count)
                if (count < drainedEvents.size) {
                    Timber.tag(TAG).d("Unable to send all events, re-queueing %d events", drainedEvents.size - count)
                    // Requeue events to the event cache that weren't processed (either PacketSender failure or we are now offline).  Once the
                    // events are re-queued we update the event cache state to write the re-queued events to disk or to leave them in memory
                    // depending on the connectivity state of the device.
                    eventCache.requeue(drainedEvents.subList(count, drainedEvents.size))
                    eventCache.updateState(isOnline)
                }
            }

            synchronized(threadControl) {
                // We may be done or this was a forced dispatch.  If we are in a blocking force dispatch we need to exit immediately to ensure
                // the blocking doesn't take too long.
                if (forcedBlocking || eventCache.isEmpty || dispatchInterval < 0) {
                    running = false
                }
            }
        }
    }

    private val isOnline: Boolean
        get() {
            if (!connectivity.isConnected) return false

            return when (dispatchMode) {
                DispatchMode.EXCEPTION -> false
                DispatchMode.ALWAYS -> true
                DispatchMode.WIFI_ONLY -> connectivity.type == Connectivity.Type.WIFI
            }
        }

    override fun setDryRunTarget(dryRunTarget: MutableList<Packet>) {
        mDryRunTarget = dryRunTarget
    }

    override fun getDryRunTarget(): List<Packet> {
        return mDryRunTarget!!
    }

    companion object {
        private val TAG = tag(DefaultDispatcher::class.java)
    }
}


================================================
FILE: tracker/src/main/java/org/matomo/sdk/dispatcher/DefaultDispatcherFactory.kt
================================================
package org.matomo.sdk.dispatcher

import org.matomo.sdk.Tracker
import org.matomo.sdk.tools.Connectivity

open class DefaultDispatcherFactory : DispatcherFactory {
    override fun build(tracker: Tracker): Dispatcher {
        return DefaultDispatcher(
            EventCache(EventDiskCache(tracker)),
            Connectivity(tracker.matomo.context),
            PacketFactory(tracker.apiUrl),
            DefaultPacketSender()
        )
    }
}


================================================
FILE: tracker/src/main/java/org/matomo/sdk/dispatcher/DefaultPacketSender.kt
================================================
package org.matomo.sdk.dispatcher

import org.matomo.sdk.Matomo.Companion.tag
import timber.log.Timber
import java.io.BufferedReader
import java.io.BufferedWriter
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InputStreamReader
import java.io.OutputStream
import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.net.URL
import java.nio.charset.StandardCharsets
import java.util.zip.GZIPOutputStream

class DefaultPacketSender : PacketSender {
    private var mTimeout = Dispatcher.DEFAULT_CONNECTION_TIMEOUT.toLong()
    private var mGzip = false

    override fun send(packet: Packet): Boolean {
        var urlConnection: HttpURLConnection? = null
        try {
            urlConnection = URL(packet.targetURL).openConnection() as HttpURLConnection

            Timber.tag(TAG).v("Connection is open to %s", urlConnection.url.toExternalForm())
            Timber.tag(TAG).v("Sending: %s", packet)

            urlConnection.connectTimeout = mTimeout.toInt()
            urlConnection.readTimeout = mTimeout.toInt()

            // IF there is json data we have to do a post
            if (packet.postData != null) { // POST
                urlConnection.doOutput = true // Forces post
                urlConnection.setRequestProperty("Content-Type", "application/json")
                urlConnection.setRequestProperty("charset", "utf-8")

                val toPost = packet.postData.toString()
                if (mGzip) {
                    urlConnection.addRequestProperty("Content-Encoding", "gzip")
                    val byteArrayOS = ByteArrayOutputStream()

                    GZIPOutputStream(byteArrayOS).use { gzipStream ->
                        gzipStream.write(toPost.toByteArray(StandardCharsets.UTF_8))
                    }
                    // If closing fails we assume the written data to be invalid.
                    // Don't catch the exception and let it abort the `send(Packet)` call.
                    var outputStream: OutputStream? = null
                    try {
                        outputStream = urlConnection.outputStream
                        outputStream.write(byteArrayOS.toByteArray())
                    } finally {
                        if (outputStream != null) {
                            try {
                                outputStream.close()
                            } catch (e: IOException) {
                                // Failing to close the stream is not enough to consider the transmission faulty.
                                Timber.tag(TAG).d(e, "Failed to close output stream after writing gzipped POST data.")
                            }
                        }
                    }
                } else {
                    var writer: BufferedWriter? = null
                    try {
                        writer = BufferedWriter(OutputStreamWriter(urlConnection.outputStream, StandardCharsets.UTF_8))
                        writer.write(toPost)
                    } finally {
                        if (writer != null) {
                            try {
                                writer.close()
                            } catch (e: IOException) {
                                // Failing to close the stream is not enough to consider the transmission faulty.
                                Timber.tag(TAG).d(e, "Failed to close output stream after writing POST data.")
                            }
                        }
                    }
                }
            } else { // GET
                urlConnection.doOutput = false // Defaults to false, but for readability
            }

            val statusCode = urlConnection.responseCode
            Timber.tag(TAG).v("Transmission finished (code=%d).", statusCode)
            val successful = checkResponseCode(statusCode)

            if (successful) {
                // https://github.com/matomo-org/matomo-sdk-android/issues/226

                val `is` = urlConnection.inputStream
                if (`is` != null) {
                    try {
                        `is`.close()
                    } catch (e: IOException) {
                        Timber.tag(TAG).d(e, "Failed to close the error stream.")
                    }
                }
            } else {
                // Consume the error stream (or at least close it) if the status code was non-OK (not 2XX)
                val errorReason = StringBuilder()
                var errorReader: BufferedReader? = null
                try {
                    errorReader = BufferedReader(InputStreamReader(urlConnection.errorStream))
                    var line: String?
                    while ((errorReader.readLine().also { line = it }) != null) errorReason.append(line)
                } finally {
                    if (errorReader != null) {
                        try {
                            errorReader.close()
                        } catch (e: IOException) {
                            Timber.tag(TAG).d(e, "Failed to close the error stream.")
                        }
                    }
                }
                Timber.tag(TAG).w("Transmission failed (code=%d, reason=%s)", statusCode, errorReason.toString())
            }

            return successful
        } catch (e: Exception) {
            Timber.tag(TAG).e(e, "Transmission failed unexpectedly.")
            return false
        } finally {
            urlConnection?.disconnect()
        }
    }

    override fun setTimeout(timeout: Long) {
        mTimeout = timeout
    }

    override fun setGzipData(gzip: Boolean) {
        mGzip = gzip
    }

    companion object {
        private val TAG = tag(DefaultPacketSender::class.java)
        private fun checkResponseCode(code: Int): Boolean {
            return code == HttpURLConnection.HTTP_NO_CONTENT || code == HttpURLConnection.HTTP_OK
        }
    }
}


================================================
FILE: tracker/src/main/java/org/matomo/sdk/dispatcher/DispatchMode.java
================================================
package org.matomo.sdk.dispatcher;

import androidx.annotation.Nullable;


public enum DispatchMode {
    /**
     * Dispatch always (default)
     */
    ALWAYS("always"),
    /**
     * Dispatch only on WIFI
     */
    WIFI_ONLY("wifi_only"),
    /**
     * The dispatcher will assume being offline. This is not persisted and will revert on app restart.
     * Ensures no information is lost when tracking exceptions. See #247
     */
    EXCEPTION("exception");

    private final String key;

    DispatchMode(String key) {this.key = key;}

    @Override
    public String toString() {
        return key;
    }

    @Nullable
    public static DispatchMode fromString(String raw) {
        for (DispatchMode mode : DispatchMode.values()) {
            if (mode.key.equals(raw)) return mode;
        }
        return null;
    }
}


================================================
FILE: tracker/src/main/java/org/matomo/sdk/dispatcher/Dispatcher.java
================================================
/*
 * Android SDK for Matomo
 *
 * @link https://github.com/matomo-org/matomo-android-sdk
 * @license https://github.com/matomo-org/matomo-sdk-android/blob/master/LICENSE BSD-3 Clause
 */

package org.matomo.sdk.dispatcher;

import org.matomo.sdk.TrackMe;

import java.util.List;

/**
 * Responsible for transmitting packets to a server
 */
public interface Dispatcher {
    int DEFAULT_CONNECTION_TIMEOUT = 5 * 1000;  // 5s
    long DEFAULT_DISPATCH_INTERVAL = 120 * 1000; // 120s

    /**
     * Connection timeout in milliseconds
     *
     * @return timeout in milliseconds
     */
    int getConnectionTimeOut();

    /**
     * Timeout when trying to establish connection and when trying to read a response.
     * Values take effect on next dispatch.
     *
     * @param timeOut timeout in milliseconds
     */
    void setConnectionTimeOut(int timeOut);

    /**
     * Packets are collected and dispatched in batches, this intervals sets the pause between batches.
     *
     * @param dispatchInterval in milliseconds
     */
    void setDispatchInterval(long dispatchInterval);

    long getDispatchInterval();

    /**
     * Packets are collected and dispatched in batches. This boolean sets if post must be
     * gzipped or not. Use of gzip needs mod_deflate/Apache ou lua_zlib/NGINX
     *
     * @param dispatchGzipped boolean
     */
    void setDispatchGzipped(boolean dispatchGzipped);

    boolean getDispatchGzipped();

    void setDispatchMode(DispatchMode dispatchMode);

    DispatchMode getDispatchMode();

    /**
     * Starts the dispatcher for one cycle if it is currently not working.
     * If the dispatcher is working it will skip the dispatch interval once.
     */
    boolean forceDispatch();

    /**
     * Dispatch all events in the EventCache and return only after the dispatch is complete.
     *
     * This method may be invoked while the Runtime is being torn down and should not start new threads.
     */
    void forceDispatchBlocking();

    /**
     * To clear the dispatchers queue
     */
    void clear();

    /**
     * Submit for transmission
     */
    void submit(TrackMe trackMe);

    /**
     * For debugging purposes
     * When this is non null then instead of sending data over the network it will be written into this list.
     * Mind thread-safety!
     */
    void setDryRunTarget(List<Packet> dryRunTarget);

    /**
     * For debugging purposes
     * Mind thread-safety!
     */
    List<Packet> getDryRunTarget();
}


================================================
FILE: tracker/src/main/java/org/matomo/sdk/dispatcher/DispatcherFactory.kt
================================================
package org.matomo.sdk.dispatcher

import org.matomo.sdk.Tracker

interface DispatcherFactory {
    fun build(tracker: Tracker): Dispatcher
}



================================================
FILE: tracker/src/main/java/org/matomo/sdk/dispatcher/Event.java
================================================
package org.matomo.sdk.dispatcher;


import org.matomo.sdk.Matomo;

import java.net.URLEncoder;
import java.util.Map;

import timber.log.Timber;

public class Event {
    private static final String TAG = Matomo.tag(Event.class);
    private final long mTimestamp;
    private final String mQuery;

    public Event(Map<String, String> eventData) {
        this(urlEncodeUTF8(eventData));
    }

    public Event(String query) {
        this(System.currentTimeMillis(), query);
    }

    public Event(long timestamp, String query) {
        this.mTimestamp = timestamp;
        this.mQuery = query;
    }

    public long getTimeStamp() {
        return mTimestamp;
    }

    public String getEncodedQuery() {
        return mQuery;
    }

    @Override
    public String toString() {
        return getEncodedQuery();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Event event = (Event) o;

        return mTimestamp == event.mTimestamp && mQuery.equals(event.mQuery);

    }

    @Override
    public int hashCode() {
        int result = (int) (mTimestamp ^ (mTimestamp >>> 32));
        result = 31 * result + mQuery.hashCode();
        return result;
    }

    /**
     * http://stackoverflow.com/q/4737841
     *
     * @param param raw data
     * @return encoded string
     */
    private static String urlEncodeUTF8(String param) {
        try {
            return URLEncoder.encode(param, "UTF-8").replaceAll("\\+", "%20");
        } catch (Exception e) {
            Timber.tag(TAG).e(e, "Cannot encode %s", param);
            return "";
        }
    }

    /**
     * URL encodes a key-value map
     */
    private static String urlEncodeUTF8(Map<String, String> map) {
        StringBuilder sb = new StringBuilder(100);
        sb.append('?');
        for (Map.Entry<String, String> entry : map.entrySet()) {
            sb.append(urlEncodeUTF8(entry.getKey()));
            sb.append('=');
            sb.append(urlEncodeUTF8(entry.getValue()));
            sb.append('&');
        }

        return sb.substring(0, sb.length() - 1);
    }
}


================================================
FILE: tracker/src/main/java/org/matomo/sdk/dispatcher/EventCache.java
================================================
package org.matomo.sdk.dispatcher;


import org.matomo.sdk.Matomo;

import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import java.util.concurrent.LinkedBlockingDeque;

import timber.log.Timber;

public class EventCache {
    private static final String TAG = Matomo.tag(EventCache.class);
    private final LinkedBlockingDeque<Event> mQueue = new LinkedBlockingDeque<>();
    private final EventDiskCache mDiskCache;

    public EventCache(EventDiskCache cache) {
        mDiskCache = cache;
    }

    public void add(Event event) {
        mQueue.add(event);
    }

    public void drainTo(List<Event> drainedEvents) {
        mQueue.drainTo(drainedEvents);
    }

    public void clear() {
        mDiskCache.uncache();
        mQueue.clear();
    }

    public boolean isEmpty() {
        return mQueue.isEmpty() && mDiskCache.isEmpty();
    }

    public boolean updateState(boolean online) {
        if (online) {
            final List<Event> uncache = mDiskCache.uncache();
            ListIterator<Event> it = uncache.listIterator(uncache.size());
            while (it.hasPrevious()) {
                // Anything from  disk cache is older then what the queue could currently contain.
                mQueue.offerFirst(it.previous());
            }
            Timber.tag(TAG).d("Switched state to ONLINE, uncached %d events from disk.", uncache.size());
        } else if (!mQueue.isEmpty()) {
            List<Event> toCache = new ArrayList<>();
            mQueue.drainTo(toCache);
            mDiskCache.cache(toCache);
            Timber.tag(TAG).d("Switched state to OFFLINE, caching %d events to disk.", toCache.size());
        }
        return online && !mQueue.isEmpty();
    }

    public void requeue(List<Event> events) {
        for (Event e : events) {
            mQueue.offerFirst(e);
        }
    }

}


================================================
FILE: tracker/src/main/java/org/matomo/sdk/dispatcher/EventDiskCache.java
================================================
package org.matomo.sdk.dispatcher;


import org.matomo.sdk.Matomo;
import org.matomo.sdk.Tracker;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import timber.log.Timber;

public class EventDiskCache {
    private static final String TAG = Matomo.tag(EventDiskCache.class);
    private static final String CACHE_DIR_NAME = "piwik_cache";
    private static final String VERSION = "1";
    private final LinkedBlockingQueue<File> mEventContainer = new LinkedBlockingQueue<>();
    private final File mCacheDir;
    private final long mMaxAge;
    private final long mMaxSize;
    private long mCurrentSize = 0;
    private boolean mDelayedClear = false;

    public EventDiskCache(Tracker tracker) {
        mMaxAge = tracker.getOfflineCacheAge();
        mMaxSize = tracker.getOfflineCacheSize();
        File baseDir = new File(tracker.getMatomo().getContext().getCacheDir(), CACHE_DIR_NAME);
        try {
            mCacheDir = new File(baseDir, new URL(tracker.getAPIUrl()).getHost());
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
        File[] storedContainers = mCacheDir.listFiles();
        if (storedContainers != null) {
            Arrays.sort(storedContainers);
            for (File container : storedContainers) {
                mCurrentSize += container.length();
                mEventContainer.add(container);
            }
        }
    }

    // Must be called from a synchronized method
    private void checkCacheLimits() {
        long startTime = System.currentTimeMillis();
        if (mMaxAge < 0) {
            Timber.tag(TAG).d("Caching is disabled.");
            while (!mEventContainer.isEmpty()) {
                File head = mEventContainer.poll();
                if (head.delete()) {
                    Timber.tag(TAG).e("Deleted cache container %s", head.getPath());
                }
            }
        } else if (mMaxAge > 0) {
            final Iterator<File> iterator = mEventContainer.iterator();
            while (iterator.hasNext()) {
                File head = iterator.next();
                long timestamp;
                try {
                    final String[] split = head.getName().split("_");
                    timestamp = Long.parseLong(split[1]);
                } catch (Exception e) {
                    Timber.tag(TAG).e(e);
                    timestamp = 0;
                }
                if (timestamp < (System.currentTimeMillis() - mMaxAge)) {
                    if (head.delete()) Timber.tag(TAG).e("Deleted cache container %s", head.getPath());
                    else Timber.tag(TAG).e("Failed to delete cache container %s", head.getPath());
                    iterator.remove();
                } else {
                    // List is sorted by age
                    break;
                }
            }
        }
        if (mMaxSize != 0) {
            final Iterator<File> iterator = mEventContainer.iterator();
            while (iterator.hasNext() && mCurrentSize > mMaxSize) {
                File head = iterator.next();
                mCurrentSize -= head.length();
                iterator.remove();
                if (head.delete()) Timber.tag(TAG).e("Deleted cache container %s", head.getPath());
                else Timber.tag(TAG).e("Failed to delete cache container %s", head.getPath());
            }
        }
        long stopTime = System.currentTimeMillis();
        Timber.tag(TAG).d("Cache check took %dms", (stopTime - startTime));
    }

    private boolean isCachingEnabled() {
        return mMaxAge >= 0;
    }

    public synchronized void cache(@NonNull List<Event> toCache) {
        if (!isCachingEnabled() || toCache.isEmpty()) return;

        checkCacheLimits();

        long startTime = System.currentTimeMillis();

        File container = writeEventFile(toCache);
        if (container != null) {
            mEventContainer.add(container);
            mCurrentSize += container.length();
        }
        long stopTime = System.currentTimeMillis();
        Timber.tag(TAG).d("Caching of %d events took %dms (%s)", toCache.size(), (stopTime - startTime), container);
    }

    @NonNull
    public synchronized List<Event> uncache() {
        List<Event> events = new ArrayList<>();
        if (!isCachingEnabled()) return events;

        long startTime = System.currentTimeMillis();
        while (!mEventContainer.isEmpty()) {
            File head = mEventContainer.poll();
            if (head != null) {
                events.addAll(readEventFile(head));
                if (!head.delete()) Timber.tag(TAG).e("Failed to delete cache container %s", head.getPath());
            }
        }

        checkCacheLimits();

        long stopTime = System.currentTimeMillis();
        Timber.tag(TAG).d("Uncaching of %d events took %dms", events.size(), (stopTime - startTime));
        return events;
    }

    public synchronized boolean isEmpty() {
        if (!mDelayedClear) {
            checkCacheLimits();
            mDelayedClear = true;
        }
        return mEventContainer.isEmpty();
    }

    private List<Event> readEventFile(@NonNull File file) {
        List<Event> events = new ArrayList<>();
        if (!file.exists()) return events;

        InputStream in = null;
        try {
            in = new FileInputStream(file);
            InputStreamReader inputStreamReader = new InputStreamReader(in);
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

            String versionLine = bufferedReader.readLine();
            if (!VERSION.equals(versionLine)) return events;

            final long cutoff = System.currentTimeMillis() - mMaxAge;
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                final int split = line.indexOf(" ");
                if (split == -1) continue;

                try {
                    long timestamp = Long.parseLong(line.substring(0, split));
                    if (mMaxAge > 0 && timestamp < cutoff) continue;

                    String query = line.substring(split + 1);
                    events.add(new Event(timestamp, query));
                } catch (Exception e) { Timber.tag(TAG).e(e); }
            }
        } catch (IOException e) {
            Timber.tag(TAG).e(e);
        } finally {
            if (in != null) {
                try { in.close(); } catch (IOException e) { Timber.tag(TAG).e(e); }
            }
        }

        Timber.tag(TAG).d("Restored %d events from %s", events.size(), file.getPath());
        return events;
    }

    @Nullable
    private File writeEventFile(@NonNull List<Event> events) {
        if (events.isEmpty()) return null;

        if (!mCacheDir.exists() && !mCacheDir.mkdirs())
            Timber.tag(TAG).e("Failed to make disk-cache dir '%s'", mCacheDir);

        File newFile = new File(mCacheDir, "events_" + events.get(events.size() - 1).getTimeStamp());
        FileWriter out = null;
        boolean dataWritten = false;
        try {
            out = new FileWriter(newFile);
            out.append(VERSION).append("\n");

            final long cutoff = System.currentTimeMillis() - mMaxAge;
            for (Event event : events) {
                if (mMaxAge > 0 && event.getTimeStamp() < cutoff) continue;
                out.append(String.valueOf(event.getTimeStamp())).append(" ").append(event.getEncodedQuery()).append("\n");
                dataWritten = true;
            }
        } catch (IOException e) {
            Timber.tag(TAG).e(e);
            //noinspection ResultOfMethodCallIgnored
            newFile.delete();
            return null;
        } finally {
            if (out != null) {
                try { out.close(); } catch (IOException e) { Timber.tag(TAG).e(e); }
            }
        }

        Timber.tag(TAG).d("Saved %d events to %s", events.size(), newFile.getPath());

        // If just version data was written delete the file.
        if (dataWritten) return newFile;
        else {
            //noinspection ResultOfMethodCallIgnored
            newFile.delete();
            return null;
        }
    }

}


================================================
FILE: tracker/src/main/java/org/matomo/sdk/dispatcher/Packet.java
================================================
/*
 * Android SDK for Matomo
 *
 * @link https://github.com/matomo-org/matomo-android-sdk
 * @license https://github.com/matomo-org/matomo-sdk-android/blob/master/LICENSE BSD-3 Clause
 */
package org.matomo.sdk.dispatcher;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.json.JSONObject;

/**
 * Data that can be send to the backend API via the Dispatcher
 */
public class Packet {
    private final String mTargetURL;
    private final JSONObject mPostData;
    private final long mTimeStamp;
    private final int mEventCount;

    /**
     * Constructor for GET requests
     */
    public Packet(String targetURL) {
        this(targetURL, null, 1);
    }

    /**
     * Constructor for POST requests
     *
     * @param targetURL  server
     * @param JSONObject non null if HTTP POST packet
     * @param eventCount number of events in this packet
     */
    public Packet(String targetURL, @Nullable JSONObject JSONObject, int eventCount) {
        mTargetURL = targetURL;
        mPostData = JSONObject;
        mEventCount = eventCount;
        mTimeStamp = System.currentTimeMillis();
    }

    public String getTargetURL() {
        return mTargetURL;
    }

    /**
     * @return may be null if it is a GET request
     */
    @Nullable
    public JSONObject getPostData() {
        return mPostData;
    }

    /**
     * A timestamp to use when replaying offline data
     */
    public long getTimeStamp() {
        return mTimeStamp;
    }

    /**
     * Used to determine the event cache queue positions.
     *
     * @return how many events this packet contains
     */
    public int getEventCount() {
        return mEventCount;
    }

    @NonNull
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("Packet(");
        if (mPostData != null) sb.append("type=POST, data=").append(mPostData);
        else sb.append("type=GET, data=").append(mTargetURL);
        return sb.append(")").toString();
    }
}


================================================
FILE: tracker/src/main/java/org/matomo/sdk/dispatcher/PacketFactory.java
================================================
/*
 * Android SDK for Matomo
 *
 * @link https://github.com/matomo-org/matomo-android-sdk
 * @license https://github.com/matomo-org/matomo-sdk-android/blob/master/LICENSE BSD-3 Clause
 */

package org.matomo.sdk.dispatcher;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import android.text.TextUtils;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.matomo.sdk.Matomo;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import timber.log.Timber;


public class PacketFactory {
    private static final String TAG = Matomo.tag(PacketFactory.class);
    @VisibleForTesting
    public static final int PAGE_SIZE = 20;
    private final String mApiUrl;

    public PacketFactory(final String apiUrl) {
        mApiUrl = apiUrl;
    }

    public List<Packet> buildPackets(final List<Event> events) {
        if (events.isEmpty()) return Collections.emptyList();

        if (events.size() == 1) {
            Packet p = buildPacketForGet(events.get(0));
            if (p == null) return Collections.emptyList();
            else return Collections.singletonList(p);
        }

        int packets = (int) Math.ceil(events.size() * 1.0 / PAGE_SIZE);
        List<Packet> freshPackets = new ArrayList<>(packets);
        for (int i = 0; i < events.size(); i += PAGE_SIZE) {
            List<Event> batch = events.subList(i, Math.min(i + PAGE_SIZE, events.size()));
            final Packet packet;
            if (batch.size() == 1) packet = buildPacketForGet(batch.get(0));
            else packet = buildPacketForPost(batch);
            if (packet != null) freshPackets.add(packet);
        }
        return freshPackets;
    }

    //{
    //    "requests": ["?idsite=1&url=http://example.org&action_name=Test bulk log Pageview&rec=1",
    //    "?idsite=1&url=http://example.net/test.htm&action_name=Another bul k page view&rec=1"]
    //}
    @Nullable
    private Packet buildPacketForPost(List<Event> events) {
        if (events.isEmpty()) return null;
        try {
            JSONObject params = new JSONObject();

            JSONArray jsonArray = new JSONArray();
            for (Event event : events) jsonArray.put(event.getEncodedQuery());
            params.put("requests", jsonArray);
            return new Packet(mApiUrl, params, events.size());
        } catch (JSONException e) {
            Timber.tag(TAG).w(e, "Cannot create json object:\n%s", TextUtils.join(", ", events));
        }
        return null;
    }

    // "http://domain.com/matomo.php?idsite=1&url=http://a.org&action_name=Test bulk log Pageview&rec=1"
    @Nullable
    private Packet buildPacketForGet(@NonNull Event event) {
        if (event.getEncodedQuery().isEmpty()) return null;
        return new Packet(mApiUrl + event);
    }

}


================================================
FILE: tracker/src/main/java/org/matomo/sdk/dispatcher/PacketSender.kt
================================================
package org.matomo.sdk.dispatcher


interface PacketSender {
    /**
     * @return true if successful
     */
    fun send(packet: Packet): Boolean

    /**
     * @param timeout in milliseconds
     */
    fun setTimeout(timeout: Long)

    fun setGzipData(gzip: Boolean)
}


================================================
FILE: tracker/src/main/java/org/matomo/sdk/extra/CustomDimension.java
================================================
package org.matomo.sdk.extra;


import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.matomo.sdk.Matomo;
import org.matomo.sdk.TrackMe;

import timber.log.Timber;

/**
 * Allows you to track Custom Dimensions.
 * In order to use this functionality install and configure
 * https://plugins.matomo.org/CustomDimensions plugin.
 */
public class CustomDimension {
    private static final String TAG = Matomo.tag(CustomDimension.class);
    private final int mId;
    private final String mValue;

    public CustomDimension(int id, String value) {
        mId = id;
        mValue = value;
    }

    public int getId() {
        return mId;
    }

    public String getValue() {
        return mValue;
    }

    /**
     * This method sets a tracking API parameter dimension%dimensionId%=%dimensionValue%.
     * Eg dimension1=foo or dimension2=bar.
     * So the tracking API parameter starts with dimension followed by the set dimensionId.
     * <p>
     * Requires <a href="https://plugins.matomo.org/CustomDimensions">Custom Dimensions</a> plugin (server-side)
     *
     * @param trackMe        into which the data should be inserted
     * @param dimensionId    accepts values greater than 0
     * @param dimensionValue is limited to 255 characters, you can pass null to delete a value
     * @return true if the value was valid
     */
    public static boolean setDimension(@NonNull TrackMe trackMe, int dimensionId, @Nullable String dimensionValue) {
        if (dimensionId < 1) {
            Timber.tag(TAG).e("dimensionId should be great than 0 (arg: %d)", dimensionId);
            return false;
        }
        if (dimensionValue != null && dimensionValue.length() > 255) {
            dimensionValue = dimensionValue.substring(0, 255);
            Timber.tag(TAG).w("dimensionValue was truncated to 255 chars.");
        }
        if (dimensionValue != null && dimensionValue.length() == 0) {
            dimensionValue = null;
        }
        trackMe.set(formatDimensionId(dimensionId), dimensionValue);
        return true;
    }

    public static boolean setDimension(TrackMe trackMe, CustomDimension dimension) {
        return setDimension(trackMe, dimension.getId(), dimension.getValue());
    }

    @Nullable
    public static String getDimension(TrackMe trackMe, int dimensionId) {
        return trackMe.get(formatDimensionId(dimensionId));
    }

    private static String formatDimensionId(int id) {
        return "dimension" + id;
    }
}


================================================
FILE: tracker/src/main/java/org/matomo/sdk/extra/CustomVariables.java
================================================
/*
 * Android SDK for Matomo
 *
 * @link https://github.com/matomo-org/matomo-android-sdk
 * @license https://github.com/matomo-org/matomo-sdk-android/blob/master/LICENSE BSD-3 Clause
 */

package org.matomo.sdk.extra;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.matomo.sdk.Matomo;
import org.matomo.sdk.QueryParams;
import org.matomo.sdk.TrackMe;

import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import timber.log.Timber;

/**
 * A custom variable is a custom name-value pair that you can assign to your users or screen views,
 * and then visualize the reports of how many visits, conversions, etc. for each custom variable.
 * A custom variable is defined by a name — for example,
 * "User status" — and a value – for example, "LoggedIn" or "Anonymous".
 * <p>
 * You can track up to 5 custom variables for each user to your app,
 * and up to 5 custom variables for each screen view.
 * You may configure Matomo to track more custom variables: http://matomo.org/faq/how-to/faq_17931/
 * <p>
 * Desired json output:
 * {
 * "1":["OS","iphone 5.0"],
 * "2":["Matomo Mobile Version","1.6.2"],
 * "3":["Locale","en::en"],
 * "4":["Num Accounts","2"],
 * "5":["Level","over9k"]
 * }
 */
public class CustomVariables {
    private final Map<String, JSONArray> mVars = new ConcurrentHashMap<>();

    private static final String TAG = Matomo.tag(CustomVariables.class);
    protected static final int MAX_LENGTH = 200;

    public CustomVariables() {

    }

    public CustomVariables(@NonNull CustomVariables variables) {
        mVars.putAll(variables.mVars);
    }

    public CustomVariables(@Nullable String json) {
        if (json != null) {
            try {
                JSONObject jsonObject = new JSONObject(json);
                final Iterator<String> it = jsonObject.keys();
                while (it.hasNext()) {
                    String key = it.next();
                    put(key, jsonObject.getJSONArray(key));
                }
            } catch (JSONException e) {Timber.tag(TAG).e(e, "Failed to create CustomVariables from JSON");}
        }
    }

    public CustomVariables putAll(CustomVariables customVariables) {
        mVars.putAll(customVariables.mVars);
        return this;
    }

    /**
     * Custom variable names and values are limited to 200 characters in length each.
     *
     * @param index this Integer accepts values from 1 to 5.
     *              A given custom variable name must always be stored in the same "index" per session.
     *              For example, if you choose to store the variable name = "Gender" in index = 1
     *              and you record another custom variable in index = 1, then the "Gender" variable
     *              will be deleted and replaced with the new custom variable stored in index 1.
     *              You may configure Matomo to track more custom variables than 5.
     *              Read more: http://matomo.org/faq/how-to/faq_17931/
     * @param name  of a specific Custom Variable such as "User type".
     * @param value of a specific Custom Variable such as "Customer".
     * @return super.put result if index in right range and name/value pair aren't null
     */
    public CustomVariables put(int index, String name, String value) {
        if (index > 0 && name != null & value != null) {

            if (name.length() > MAX_LENGTH) {
                Timber.tag(TAG).w("Name is too long %s", name);
                name = name.substring(0, MAX_LENGTH);
            }

            if (value.length() > MAX_LENGTH) {
                Timber.tag(TAG).w("Value is too long %s", value);
                value = value.substring(0, MAX_LENGTH);
            }

            put(Integer.toString(index), new JSONArray(Arrays.asList(name, value)));
        } else Timber.tag(TAG).w("Index is out of range or name/value is null");
        return this;
    }

    /**
     * @param index  index accepts values from 1 to 5.
     * @param values packed key/value pair
     * @return super.put result or null if key is null or value length is not equals 2
     */
    public CustomVariables put(String index, JSONArray values) {
        if (values.length() == 2 && index != null) {
            mVars.put(index, values);
        } else Timber.tag(TAG).w("values.length() should be equal 2");
        return this;
    }

    public String toString() {
        JSONObject json = new JSONObject(mVars);
        return json.length() > 0 ? json.toString() : null;
    }

    public int size() {
        return mVars.size();
    }

    /**
     * Sets the custom variables with scope VISIT to a {@link TrackMe}.
     */
    public TrackMe injectVisitVariables(@NonNull TrackMe trackMe) {
        //noinspection deprecation
        trackMe.set(QueryParams.VISIT_SCOPE_CUSTOM_VARIABLES, this.toString());
        return trackMe;
    }

    @NonNull
    public TrackMe toVisitVariables() {
        return injectVisitVariables(new TrackMe());
    }
}


================================================
FILE: tracker/src/main/java/org/matomo/sdk/extra/DimensionQueue.java
================================================
package org.matomo.sdk.extra;

import org.matomo.sdk.Matomo;
import org.matomo.sdk.TrackMe;
import org.matomo.sdk.Tracker;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import timber.log.Timber;

/**
 * A helper class for custom dimensions. Acts like a queue for dimensions to be send.
 * On each tracking call it will insert as many saved dimensions as it is possible without overwriting existing information.
 */
public class DimensionQueue {
    private static final String TAG = Matomo.tag(DimensionQueue.class);
    private final List<CustomDimension> mOneTimeDimensions = new ArrayList<>();

    public DimensionQueue(Tracker tracker) {
        Tracker.Callback callback = DimensionQueue.this::onTrack;
        tracker.addTrackingCallback(callback);
    }

    /**
     * The added id-value-pair will be injected into the next tracked event,
     * if that events slot for this ID is still empty.
     */
    public void add(int id, String value) {
        mOneTimeDimensions.add(new CustomDimension(id, value));
    }

    private TrackMe onTrack(TrackMe trackMe) {
        for (Iterator<CustomDimension> it = mOneTimeDimensions.iterator(); it.hasNext(); ) {
            CustomDimension dim = it.next();
            String existing = CustomDimension.getDimension(trackMe, dim.getId());
            if (existing != null) {
                Timber.tag(TAG).d("Setting dimension %s to slot %d would overwrite %s, skipping!", dim.getValue(), dim.getId(), existing);
            } else {
                CustomDimension.setDimension(trackMe, dim);
                it.remove();
            }
        }
        return trackMe;
    }
}


================================================
FILE: tracker/src/main/java/org/matomo/sdk/extra/DownloadTracker.java
================================================
package org.matomo.sdk.extra;


import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.matomo.sdk.Matomo;
import org.matomo.sdk.QueryParams;
import org.matomo.sdk.TrackMe;
import org.matomo.sdk.Tracker;
import org.matomo.sdk.tools.Checksum;

import java.io.File;

import timber.log.Timber;

public class DownloadTracker {
    protected static final String TAG = Matomo.tag(DownloadTracker.class);
    private static final String INSTALL_SOURCE_GOOGLE_PLAY = "com.android.vending";
    private final Tracker mTracker;
    private final Object mTrackOnceLock = new Object();
    private final PackageManager mPackMan;
    private final SharedPreferences mPreferences;
    private final boolean mInternalTracking;
    private String mVersion;
    private final PackageInfo mPkgInfo;

    public interface Extra {

        /**
         * Does your {@link Extra} implementation do work intensive stuff?
         * Network? IO?
         *
         * @return true if this should be run async and on a sepperate thread.
         */
        boolean isIntensiveWork();

        /**
         * Example:
         * <br>
         * com.example.pkg:1/ABCDEF01234567
         * <br>
         * "ABCDEF01234567" is the extra identifier here.
         *
         * @return a string that will be used as extra identifier or null
         */
        @Nullable
        String buildExtraIdentifier();

        /**
         * The MD5 checksum of the apk file.
         * com.example.pkg:1/ABCDEF01234567
         */
        class ApkChecksum implements Extra {
            private PackageInfo mPackageInfo;

            public ApkChecksum(Context context) {
                try {
                    mPackageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
                } catch (Exception e) {
                    Timber.tag(TAG).e(e);
                    mPackageInfo = null;
                }
            }

            @Override
            public boolean isIntensiveWork() {
                return true;
            }

            @Nullable
            @Override
            public String buildExtraIdentifier() {
                if (mPackageInfo != null && mPackageInfo.applicationInfo != null && mPackageInfo.applicationInfo.sourceDir != null) {
                    try {
                        return Checksum.getMD5Checksum(new File(mPackageInfo.applicationInfo.sourceDir));
                    } catch (Exception e) { Timber.tag(TAG).e(e); }
                }
                return null;
            }
        }

        /**
         * Custom exta identifier. Supply your own \o/.
         */
        @SuppressWarnings("unused")
        abstract class Custom implements Extra {
        }

        /**
         * No extra identifier.
         * com.example.pkg:1
         */
        class None implements Extra {

            @Override
            public boolean isIntensiveWork() {
                return false;
            }

            @Nullable
            @Override
            public String buildExtraIdentifier() {
                return null;
            }
        }
    }

    public DownloadTracker(Tracker tracker) {
        this(tracker, getOurPackageInfo(tracker.getMatomo().getContext()));
    }

    private static PackageInfo getOurPackageInfo(Context context) {
        try {
            return context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
        } catch (PackageManager.NameNotFoundException e) {
            Timber.tag(TAG).e(e);
            throw new RuntimeException(e);
        }
    }

    public DownloadTracker(Tracker tracker, @NonNull PackageInfo packageInfo) {
        mTracker = tracker;
        Context mContext = tracker.getMatomo().getContext();
        mPreferences = tracker.getPreferences();
        mPackMan = tracker.getMatomo().getContext().getPackageManager();
        mPkgInfo = packageInfo;
        mInternalTracking = mPkgInfo.packageName.equals(mContext.getPackageName());
    }

    public void setVersion(@Nullable String version) {
        mVersion = version;
    }

    public String getVersion() {
        if (mVersion != null) return mVersion;
        return Integer.toString(mPkgInfo.versionCode);
    }

    public void trackOnce(TrackMe baseTrackme, @NonNull Extra extra) {
        String firedKey = "downloaded:" + mPkgInfo.packageName + ":" + getVersion();
        synchronized (mTrackOnceLock) {
            if (!mPreferences.getBoolean(firedKey, false)) {
                mPreferences.edit().putBoolean(firedKey, true).apply();
                trackNewAppDownload(baseTrackme, extra);
            }
        }
    }

    public void trackNewAppDownload(final TrackMe baseTrackme, @NonNull final Extra extra) {
        // We can only get referrer information if we are tracking our own app download.
        final boolean delay = mInternalTracking && INSTALL_SOURCE_GOOGLE_PLAY.equals(mPackMan.getInstallerPackageName(mPkgInfo.packageName));
        if (delay) {
            // Delay tracking incase we were called from within Application.onCreate
            Timber.tag(TAG).d("Google Play is install source, deferring tracking.");
        }
        final Thread trackTask = new Thread(() -> {
            if (delay) try {Thread.sleep(3000);} catch (Exception e) { Timber.tag(ContentValues.TAG).e(e);}
            trackNewAppDownloadInternal(baseTrackme, extra);
        });
        if (!delay && !extra.isIntensiveWork()) trackTask.run();
        else trackTask.start();
    }

    private void trackNewAppDownloadInternal(TrackMe baseTrackMe, @NonNull Extra extra) {
        Timber.tag(TAG).d("Tracking app download...");

        StringBuilder installIdentifier = new StringBuilder();
        installIdentifier.append("http://").append(mPkgInfo.packageName).append(":").append(getVersion());

        String extraIdentifier = extra.buildExtraIdentifier();
        if (extraIdentifier != null) installIdentifier.append("/").append(extraIdentifier);

        // Usual USEFUL values of this field will be: "com.android.vending" or "com.android.browser", i.e. app packagenames.
        // This is not guaranteed, values can also look like: app_process /system/bin com.android.commands.pm.Pm install -r /storage/sdcard0/...
        String referringApp = mPackMan.getInstallerPackageName(mPkgInfo.packageName);
        if (referringApp != null && referringApp.length() > 200) referringApp = referringApp.substring(0, 200);

        if (referringApp != null && referringApp.equals(INSTALL_SOURCE_GOOGLE_PLAY)) {
            // For this type of install source we could have extra referral information
            String referrerExtras = mTracker.getMatomo().getPreferences().getString(InstallReferrerReceiver.PREF_KEY_INSTALL_REFERRER_EXTRAS, null);
            if (referrerExtras != null) referringApp = referringApp + "/?" + referrerExtras;
        }

        if (referringApp != null) referringApp = "http://" + referringApp;

        mTracker.track(baseTrackMe
                .set(QueryParams.EVENT_CATEGORY, "Application")
                .set(QueryParams.EVENT_ACTION, "downloaded")
                .set(QueryParams.ACTION_NAME, "application/downloaded")
                .set(QueryParams.URL_PATH, "/application/downloaded")
                .set(QueryParams.DOWNLOAD, installIdentifier.toString())
                .set(QueryParams.REFERRER, referringApp)); // Can be null in which case the TrackMe removes the REFERRER parameter.

        Timber.tag(TAG).d("... app download tracked.");
    }
}


================================================
FILE: tracker/src/main/java/org/matomo/sdk/extra/EcommerceItems.java
================================================
/*
 * Android SDK for Matomo
 *
 * @link https://github.com/matomo-org/matomo-android-sdk
 * @license https://github.com/matomo-org/matomo-sdk-android/blob/master/LICENSE BSD-3 Clause
 */

package org.matomo.sdk.extra;

import org.json.JSONArray;
import org.matomo.sdk.tools.CurrencyFormatter;

import java.util.HashMap;
import java.util.Map;

public class EcommerceItems {
    private final Map<String, JSONArray> mItems = new HashMap<>();

    /**
     * Adds a product into the ecommerce order. Must be called for each product in the order.
     * If the same sku is used twice, the first item is overwritten.
     */
    public void addItem(Item item) {
        mItems.put(item.mSku, item.toJson());
    }

    public static class Item {
        private final String mSku;
        private String mCategory;
        private Integer mPrice;
        private Integer mQuantity;
        private String mName;

        /**
         * If the same sku is used twice, the first item is overwritten.
         *
         * @param sku Unique identifier for the product
         */
        public Item(String sku) {
            mSku = sku;
        }

        /**
         * @param name Product name
         */
        public Item name(String name) {
            mName = name;
            return this;
        }

        /**
         * @param category Product category
         */
        public Item category(String category) {
            mCategory = category;
            return this;
        }

        /**
         * @param price Price of the product in cents
         */
        public Item price(int price) {
            mPrice = price;
            return this;
        }

        /**
         * @param quantity Quantity
         */
        public Item quantity(int quantity) {
            mQuantity = quantity;
            return this;
        }

        public String getSku() {
            return mSku;
        }

        public String getCategory() {
            return mCategory;
        }

        public Integer getPrice() {
            return mPrice;
        }

        public Integer getQuantity() {
            return mQuantity;
        }

        public String getName() {
            return mName;
        }

        protected JSONArray toJson() {
            JSONArray item = new JSONArray();
            item.put(mSku);
            if (mName != null) item.put(mName);
            if (mCategory != null) item.put(mCategory);
            if (mPrice != null) item.put(CurrencyFormatter.priceString(mPrice));
            if (mQuantity != null) item.put(String.valueOf(mQuantity));
            return item;
        }
    }

    /**
     * Remove a product from an ecommerce order.
     *
     * @param sku unique identifier for the product
     */
    public void remove(String sku) {
        mItems.remove(sku);
    }

    public void remove(Item item) {
        mItems.remove(item.mSku);
    }

    /**
     * Clears all items from the ecommerce order
     */
    public void clear() {
        mItems.clear();
    }

    public String toJson() {
        JSONArray jsonItems = new JSONArray();

        for (JSONArray item : mItems.values()) {
            jsonItems.put(item);
        }
        return jsonItems.toString();
    }
}


================================================
FILE: tracker/src/main/java/org/matomo/sdk/extra/InstallReferrerReceiver.java
================================================
package org.matomo.sdk.extra;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

import org.matomo.sdk.Matomo;

import java.util.Collections;
import java.util.List;

import timber.log.Timber;


public class InstallReferrerReceiver extends BroadcastReceiver {
    private static final String TAG = Matomo.tag(InstallReferrerReceiver.class);

    // Google Play
    static final String REFERRER_SOURCE_GPLAY = "com.android.vending.INSTALL_REFERRER";
    static final String ARG_KEY_GPLAY_REFERRER = "referrer";

    static final String PREF_KEY_INSTALL_REFERRER_EXTRAS = "referrer.extras";
    static final List<String> RESPONSIBILITIES = Collections.singletonList(REFERRER_SOURCE_GPLAY);

    @Override
    public void onReceive(Context context, Intent intent) {
        Timber.tag(TAG).d(intent.toString());
        if (intent.getAction() == null || !RESPONSIBILITIES.contains(intent.getAction())) {
            Timber.tag(TAG).w("Got called outside our responsibilities: %s", intent.getAction());
            return;
        }
        if (intent.getBooleanExtra("forwarded", false)) {
            Timber.tag(TAG).d("Dropping forwarded intent");
            return;
        }
        if (intent.getAction().equals(REFERRER_SOURCE_GPLAY)) {
            String referrer = intent.getStringExtra(ARG_KEY_GPLAY_REFERRER);
            if (referrer != null) {
                final PendingResult result = goAsync();
                new Thread(() -> {
                    Matomo.getInstance(context.getApplicationContext()).getPreferences().edit().putString(PREF_KEY_INSTALL_REFERRER_EXTRAS, referrer).apply();
                    Timber.tag(TAG).d("Stored Google Play referrer extras: %s", referrer);
                    result.finish();
                }).start();
            }
        }
        // Forward to other possible recipients
        intent.setComponent(null);
        intent.setPackage(context.getPackageName());
        intent.putExtra("forwarded", true);
        context.sendBroadcast(intent);
    }
}


================================================
FILE: tracker/src/main/java/org/matomo/sdk/extra/MatomoApplication.java
================================================
/*
 * Android SDK for Matomo
 *
 * @link https://github.com/matomo-org/matomo-android-sdk
 * @license https://github.com/matomo-org/matomo-sdk-android/blob/master/LICENSE BSD-3 Clause
 */

package org.matomo.sdk.extra;

import android.app.Application;

import org.matomo.sdk.Matomo;
import org.matomo.sdk.Tracker;
import org.matomo.sdk.TrackerBuilder;

public abstract class MatomoApplication extends Application {
    private Tracker mMatomoTracker;

    public Matomo getMatomo() {
        return Matomo.getInstance(this);
    }

    /**
     * Gives you an all purpose thread-safe persisted Tracker.
     *
     * @return a shared Tracker
     */
    public synchronized Tracker getTracker() {
        if (mMatomoTracker == null) mMatomoTracker = onCreateTrackerConfig().build(getMatomo());
        return mMatomoTracker;
    }

    /**
     * See {@link TrackerBuilder}.
     * You may be interested in {@link TrackerBuilder#createDefault(String, int)}
     *
     * @return the tracker configuration you want to use.
     */
    public abstract TrackerBuilder onCreateTrackerConfig();

    @Override
    public void onLowMemory() {
        if (mMatomoTracker != null) mMatomoTracker.dispatch();
        super.onLowMemory();
    }

    @Override
    public void onTrimMemory(int level) {
        if ((level == TRIM_MEMORY_UI_HIDDEN || level == TRIM_MEMORY_COMPLETE) && mMatomoTracker != null) {
            mMatomoTracker.dispatch();
        }
        super.onTrimMemory(level);
    }

}


================================================
FILE: tracker/src/main/java/org/matomo/sdk/extra/MatomoExceptionHandler.java
================================================
/*
 * Android SDK for Matomo
 *
 * @link https://github.com/matomo-org/matomo-android-sdk
 * @license https://github.com/matomo-org/matomo-sdk-android/blob/master/LICENSE BSD-3 Clause
 */

package org.matomo.sdk.extra;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.matomo.sdk.Matomo;
import org.matomo.sdk.TrackMe;
import org.matomo.sdk.Tracker;
import org.matomo.sdk.dispatcher.DispatchMode;

import timber.log.Timber;

/**
 * An exception handler that wraps the existing exception handler and dispatches event to a {@link org.matomo.sdk.Tracker}.
 * <p>
 * Also see documentation for {@link TrackHelper#uncaughtExceptions()}
 */
public class MatomoExceptionHandler implements Thread.UncaughtExceptionHandler {
    private static final String TAG = Matomo.tag(MatomoExceptionHandler.class);
    private final Tracker mTracker;
    private final TrackMe mTrackMe;
    private final Thread.UncaughtExceptionHandler mDefaultExceptionHandler;

    public MatomoExceptionHandler(@NonNull Tracker tracker, @Nullable TrackMe trackMe) {
        mTracker = tracker;
        mTrackMe = trackMe;
        mDefaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
    }

    public Tracker getTracker() {
        return mTracker;
    }

    /**
     * This will give you the previous exception handler that is now wrapped.
     */
    public Thread.UncaughtExceptionHandler getDefaultExceptionHandler() {
        return mDefaultExceptionHandler;
    }

    @Override
    public void uncaughtException(@NonNull Thread thread, @NonNull Throwable ex) {
        try {
            String excInfo = ex.getMessage();

            Tracker tracker = getTracker();

            // Force the tracker into offline mode to ensure events are written to disk
            tracker.setDispatchMode(DispatchMode.EXCEPTION);

            TrackHelper.track(mTrackMe).exception(ex).description(excInfo).fatal(true).with(tracker);

            // Immediately dispatch as the app might be dying after rethrowing the exception and block until the dispatch is completed
            tracker.dispatchBlocking();
        } catch (Exception e) {
            Timber.tag(TAG).e(e, "Couldn't track uncaught exception");
        } finally {
            // re-throw critical exception further to the os (important)
            if (getDefaultExceptionHandler() != null && getDefaultExceptionHandler() != this) {
                getDefaultExceptionHandler().uncaughtException(thread, ex);
            }
        }
    }
}


================================================
FILE: tracker/src/main/java/org/matomo/sdk/extra/TrackHelper.java
================================================
package org.matomo.sdk.extra;


import android.app.Activity;
import android.app.Application;
import android.os.Bundle;

import androidx.annotation.Nullable;

import org.matomo.sdk.Matomo;
import org.matomo.sdk.QueryParams;
import org.matomo.sdk.TrackMe;
import org.matomo.sdk.Tracker;
import org.matomo.sdk.tools.ActivityHelper;
import org.matomo.sdk.tools.CurrencyFormatter;

import java.net.URL;
import java.util.HashMap;
import java.util.Map;

import timber.log.Timber;

public class TrackHelper {
    private static final String TAG = Matomo.tag(TrackHelper.class);
    protected final TrackMe mBaseTrackMe;

    private TrackHelper() {
        this(null);
    }

    private TrackHelper(@Nullable TrackMe baseTrackMe) {
        if (baseTrackMe == null) baseTrackMe = new TrackMe();
        mBaseTrackMe = baseTrackMe;
    }

    public static TrackHelper track() {
        return new TrackHelper();
    }

    public static TrackHelper track(@Nullable TrackMe base) {
        return new TrackHelper(base);
    }

    static abstract class BaseEvent {

        private final TrackHelper mBaseBuilder;

        BaseEvent(TrackHelper baseBuilder) {
            mBaseBuilder = baseBuilder;
        }

        TrackMe getBaseTrackMe() {
            return mBaseBuilder.mBaseTrackMe;
        }

        /**
         * May throw an {@link IllegalArgumentException} if the TrackMe was build with incorrect arguments.
         */
        public abstract TrackMe build();

        public void with(MatomoApplication matomoApplication) {
            with(matomoApplication.getTracker());
        }

        public void with(Tracker tracker) {
            TrackMe trackMe = build();
            tracker.track(trackMe);
        }

        public boolean safelyWith(MatomoApplication matomoApplication) {
            return safelyWith(matomoApplication.getTracker());
        }

        /**
         * {@link #build()} can throw an exception on illegal arguments.
         * This can be used to avoid crashes when using dynamic {@link TrackMe} arguments.
         *
         * @return false if an error occured, true if the TrackMe has been submitted to be dispatched.
         */
        public boolean safelyWith(Tracker tracker) {
            try {
                TrackMe trackMe = build();
                tracker.track(trackMe);
            } catch (IllegalArgumentException e) {
                Timber.e(e);
                return false;
            }
            return true;
        }
    }

    /**
     * To track a screenview.
     *
     * @param path Example: "/user/settings/billing"
     * @return an object that allows addition of further details.
     */
    public Screen screen(String path) {
        return new Screen(this, path);
    }

    /**
     * Calls {@link #screen(String)} for an activity.
     * Uses the activity-stack as path and activity title as names.
     *
     * @param activity the activity to track
     */
    public Screen screen(Activity activity) {
        String breadcrumbs = ActivityHelper.getBreadcrumbs(activity);
        return new Screen(this, ActivityHelper.breadcrumbsToPath(breadcrumbs)).title(breadcrumbs);
    }

    public static class Screen extends BaseEvent {
        private final String mPath;
        private final CustomVariables mCustomVariables = new CustomVariables();
        private final Map<Integer, String> mCustomDimensions = new HashMap<>();
        private String mTitle;
        private String mCampaignName;
        private String mCampaignKeyword;

        Screen(TrackHelper baseBuilder, String path) {
            super(baseBuilder);
            mPath = path;
        }

        /**
         * The title of the action being tracked. It is possible to use slashes / to set one or several categories for this action.
         *
         * @param title Example: Help / Feedback will create the Action Feedback in the category Help.
         * @return this object to allow chaining calls
         */
        public Screen title(String title) {
            mTitle = title;
            return this;
        }

        /**
         * Requires <a href="https://plugins.matomo.org/CustomDimensions">Custom Dimensions</a> plugin (server-side)
         *
         * @param index          accepts values greater than 0
         * @param dimensionValue is limited to 255 characters, you can pass null to delete a value
         */
        public Screen dimension(int index, String dimensionValue) {
            mCustomDimensions.put(index, dimensionValue);
            return this;
        }

        /**
         * Custom Variable valid per screen.
         * Only takes effect when setting prior to tracking the screen view.
         *
         * @see org.matomo.sdk.extra.CustomDimension and {@link #dimension(int, String)}
         * @deprecated Consider using <a href="http://matomo.org/docs/custom-dimensions/">Custom Dimensions</a>
         */
        @Deprecated
        public Screen variable(int index, String name, String value) {
            mCustomVariables.put(index, name, value);
            return this;
        }

        /**
         * The marketing campaign for this visit if the user opens the app for example because of an
         * ad or a newsletter. Used to populate the <i>Referrers > Campaigns</i> report.
         *
         * @param name    the name of the campaign
         * @param keyword the keyword of the campaign
         * @return this object to allow chaining calls
         */
        public Screen campaign(String name, String keyword) {
            mCampaignName = name;
            mCampaignKeyword = keyword;
            return this;
        }

        @Override
        public TrackMe build() {
            if (mPath == null) {
                throw new IllegalArgumentException("Screen tracking requires a non-empty path");
            }

            final TrackMe trackMe = new TrackMe(getBaseTrackMe())
                    .set(QueryParams.URL_PATH, mPath)
                    .set(QueryParams.ACTION_NAME, mTitle)
                    .set(QueryParams.CAMPAIGN_NAME, mCampaignName)
                    .set(QueryParams.CAMPAIGN_KEYWORD, mCampaignKeyword);
            if (mCustomVariables.size() > 0) {
                //noinspection deprecation
                trackMe.set(QueryParams.SCREEN_SCOPE_CUSTOM_VARIABLES, mCustomVariables.toString());
            }
            for (Map.Entry<Integer, String> entry : mCustomDimensions.entrySet()) {
                CustomDimension.setDimension(trackMe, entry.getKey(), entry.getValue());
            }
            return trackMe;
        }
    }

    /**
     * Events are a useful way to collect data about a user's interaction with interactive components of your app,
     * like button presses or the use of a particular item in a game.
     *
     * @param category (required) – this String defines the event category.
     *                 You might define event categories based on the class of user actions,
     *                 like clicks or gestures or voice commands, or you might define them based upon the
     *                 features available in your application (play, pause, fast forward, etc.).
     * @param action   (required) this String defines the specific event action within the category specified.
     *                 In the example, we are basically saying that the category of the event is user clicks,
     *                 and the action is a button click.
     * @return an object that allows addition of further details.
     */
    public EventBuilder event(String category, String action) {
        return new EventBuilder(this, category, action);
    }

    public static class EventBuilder extends BaseEvent {
        private final String mCategory;
        private final String mAction;
        private String mPath;
        private String mName;
        private Float mValue;

        EventBuilder(TrackHelper builder, String category, String action) {
            super(builder);
            mCategory = category;
            mAction = action;
        }

        /**
         * The path under which this event occurred.
         * Example: "/user/settings/billing", if you pass NULL, the last path set by #trackScreenView will be used.
         */
        public EventBuilder path(String path) {
            mPath = path;
            return this;
        }

        /**
         * Defines a label associated with the event.
         * For example, if you have multiple Button controls on a screen, you might use the label to specify the specific View control identifier that was clicked.
         */
        public EventBuilder name(String name) {
            mName = name;
            return this;
        }

        /**
         * Defines a numeric value associated with the event.
         * For example, if you were tracking "Buy" button clicks, you might log the number of items being purchased, or their total cost.
         */
        public EventBuilder value(Float value) {
            mValue = value;
            return this;
        }

        @Override
        public TrackMe build() {
            TrackMe trackMe = new TrackMe(getBaseTrackMe())
                    .set(QueryParams.URL_PATH, mPath)
                    .set(QueryParams.EVENT_CATEGORY, mCategory)
                    .set(QueryParams.EVENT_ACTION, mAction)
                    .set(QueryParams.EVENT_NAME, mName);
            if (mValue != null) trackMe.set(QueryParams.EVENT_VALUE, mValue);
            return trackMe;
        }
    }

    /**
     * By default, Goals in Matomo are defined as "matching" parts of the screen path or screen title.
     * In this case a conversion is logged automatically. In some situations, you may want to trigger
     * a conversion manually on other types of actions, for example:
     * when a user submits a form
     * when a user has stayed more than a given amount of time on the page
     * when a user does some interaction in your Android application
     *
     * @param idGoal id of goal as defined in matomo goal settings
     */
    public Goal goal(int idGoal) {
        return new Goal(this, idGoal);
    }

    public static class Goal extends BaseEvent {
        private final int mIdGoal;
        private Float mRevenue;

        Goal(TrackHelper baseBuilder, int idGoal) {
            super(baseBuilder);
            mIdGoal = idGoal;
        }

        /**
         * Tracking request will trigger a conversion for the goal of the website being tracked with this ID
         *
         * @param revenue a monetary value that was generated as revenue by this goal conversion.
         */
        public Goal revenue(Float revenue) {
            mRevenue = revenue;
            return this;
        }

        @Override
        public TrackMe build() {
            if (mIdGoal < 0) {
                throw new IllegalArgumentException("Goal id needs to be >=0");
            }

            TrackMe trackMe = new TrackMe(getBaseTrackMe()).set(QueryParams.GOAL_ID, mIdGoal);
            if (mRevenue != null) trackMe.set(QueryParams.REVENUE, mRevenue);
            return trackMe;
        }
    }

    /**
     * Tracks an  <a href="http://matomo.org/faq/new-to-matomo/faq_71/">Outlink</a>
     *
     * @param url HTTPS, HTTP and FTPare valid
     * @return this Tracker for chaining
     */
    public Outlink outlink(URL url) {
        return new Outlink(this, url);
    }

    public static class Outlink extends BaseEvent {
        private final URL mURL;

        Outlink(TrackHelper baseBuilder, URL url) {
            super(baseBuilder);
            mURL = url;
        }

        @Override
        public TrackMe build() {
            if (mURL == null || mURL.toExternalForm().length() == 0) {
                throw new IllegalArgumentException("Outlink tracking requires a non-empty URL");
            }
            if (!mURL.getProtocol().equals("http") && !mURL.getProtocol().equals("https") && !mURL.getProtocol().equals("ftp")) {
                throw new IllegalArgumentException("Only http|https|ftp is supported for outlinks");
            }

            return new TrackMe(getBaseTrackMe())
                    .set(QueryParams.LINK, mURL.toExternalForm())
                    .set(QueryParams.URL_PATH, mURL.toExternalForm());
        }
    }

    /**
     * Tracks an  <a href="http://matomo.org/docs/site-search/">site search</a>
     *
     * @param keyword Searched query in the app
     * @return this Tracker for chaining
     */
    public Search search(String keyword) {
        return new Search(this, keyword);
    }

    public static class Search extends BaseEvent {
        private final String mKeyword;
        private String mCategory;
        private Integer mCount;

        Search(TrackHelper baseBuilder, String keyword) {
            super(baseBuilder);
            mKeyword = keyword;
        }

        /**
         * You can optionally specify a search category with this parameter.
         *
         * @return this object, to chain calls.
         */
        public Search category(String category) {
            mCategory = category;
            return this;
        }

        /**
         * We recommend to set the search count to the number of search results displayed on the results page.
         * When keywords are tracked with a count of 0, they will appear in the "No Result Search Keyword" report.
         *
         * @return this object, to chain calls.
         */
        public Search count(Integer count) {
            mCount = count;
            return this;
        }

        @Override
        public TrackMe build() {
            TrackMe trackMe = new TrackMe(getBaseTrackMe())
                    .set(QueryParams.SEARCH_KEYWORD, mKeyword)
                    .set(QueryParams.SEARCH_CATEGORY, mCategory);
            if (mCount != null) trackMe.set(QueryParams.SEARCH_NUMBER_OF_HITS, mCount);
            return trackMe;
        }
    }

    /**
     * Sends a download event for this app.
     * This only triggers an event once per app version unless you force it.<p>
     * {@link Download#force()}
     * <p class="note">
     * Resulting download url:<p>
     * Case {@link org.matomo.sdk.extra.DownloadTracker.Extra.ApkChecksum}:<br>
     * http://packageName:versionCode/apk-md5-checksum<br>
     * <p>
     * Case {@link org.matomo.sdk.extra.DownloadTracker.Extra.None}:<br>
     * http://packageName:versionCode<p>
     *
     * @return this object, to chain calls.
     */
    public Download download(DownloadTracker downloadTracker) {
        return new Download(downloadTracker, this);
    }

    public Download download() {
        return new Download(null, this);
    }

    public static class Download {
        private DownloadTracker mDownloadTracker;
        private final TrackHelper mBaseBuilder;
        private DownloadTracker.Extra mExtra = new DownloadTracker.Extra.None();
        private boolean mForced = false;
        private String mVersion;

        Download(DownloadTracker downloadTracker, TrackHelper baseBuilder) {
            mDownloadTracker = downloadTracker;
            mBaseBuilder = baseBuilder;
        }

        /**
         * Sets the identifier type for this download
         *
         * @param identifier {@link org.matomo.sdk.extra.DownloadTracker.Extra.ApkChecksum} or {@link org.matomo.sdk.extra.DownloadTracker.Extra.None}
         * @return this object, to chain calls.
         */
        public Download identifier(DownloadTracker.Extra identifier) {
            mExtra = identifier;
            return this;
        }

        /**
         * Normally a download event is only fired once per app version.
         * If the download has already been tracked for this version, nothing happens.
         * Calling this will force this download to be tracked.
         *
         * @return this object, to chain calls.
         */
        public Download force() {
            mForced = true;
            return this;
        }

        /**
         * To track specific app versions. Useful if the app can change without the apk being updated (e.g. hybrid apps/web apps).
         *
         * @param version by default {@link android.content.pm.PackageInfo#versionCode} is used.
         * @return this object, to chain calls.
         */
        public Download version(String version) {
            mVersion = version;
            return this;
        }

        public void with(Tracker tracker) {
            if (mDownloadTracker == null) mDownloadTracker = new DownloadTracker(tracker);
            if (mVersion != null) mDownloadTracker.setVersion(mVersion);
            if (mForced) mDownloadTracker.trackNewAppDownload(mBaseBuilder.mBaseTrackMe, mExtra);
            else mDownloadTracker.trackOnce(mBaseBuilder.mBaseTrackMe, mExtra);
        }
    }

    /**
     * Tracking the impressions
     *
     * @param contentName The name of the content. For instance 'Ad Foo Bar'
     */
    public ContentImpression impression(String contentName) {
        return new ContentImpression(this, contentName);
    }

    public static class ContentImpression extends BaseEvent {
        private final String mContentName;
        private String mContentPiece;
        private String mContentTarget;

        ContentImpression(TrackHelper baseBuilder, String contentName) {
            super(baseBuilder);
            mContentName = contentName;
        }

        /**
         * @param contentPiece The actual content. For instance the path to an image, video, audio, any text
         */
        public ContentImpression piece(String contentPiece) {
            mContentPiece = contentPiece;
            return this;
        }

        /**
         * @param contentTarget The target of the content. For instance the URL of a landing page.
         */
        public ContentImpression target(String contentTarget) {
            mContentTarget = contentTarget;
            return this;
        }

        @Override
        public TrackMe build() {
            if (mContentName == null || mContentName.length() == 0) {
                throw new IllegalArgumentException("Tracking content impressions requires a non-empty content-name");
            }
            return new TrackMe(getBaseTrackMe())
                    .set(QueryParams.CONTENT_NAME, mContentName)
                    .set(QueryParams.CONTENT_PIECE, mContentPiece)
                    .set(QueryParams.CONTENT_TARGET, mContentTarget);
        }
    }

    /**
     * Tracking the interactions<p>
     * To map an interaction to an impression make sure to set the same value for contentName and contentPiece as
     * the impression has.
     *
     * @param contentInteraction The name of the interaction with the content. For instance a 'click'
     * @param contentName        The name of the content. For instance 'Ad Foo Bar'
     */
    public ContentInteraction interaction(String contentName, String contentInteraction) {
        return new ContentInteraction(this, contentName, contentInteraction);
    }

    public static class ContentInteraction extends BaseEvent {
        private final String mContentName;
        private final String mInteraction;
        private String mContentPiece;
        private String mContentTarget;

        ContentInteraction(TrackHelper baseBuilder, String contentName, String interaction) {
            super(baseBuilder);
            mContentName = contentName;
            mInteraction = interaction;
        }

        /**
         * @param contentPiece The actual content. For instance the path to an image, video, audio, any text
         */
        public ContentInteraction piece(String contentPiece) {
            mContentPiece = contentPiece;
            return this;
        }

        /**
         * @param contentTarget The target the content leading to when an interaction occurs. For instance the URL of a landing page.
         */
        public ContentInteraction target(String contentTarget) {
            mContentTarget = contentTarget;
            return this;
        }

        @Override
        public TrackMe build() {
            if (mContentName == null || mContentName.length() == 0) {
                throw new IllegalArgumentException("Content name needs to be non-empty");
            }
            if (mInteraction == null || mInteraction.length() == 0) {
                throw new IllegalArgumentException("Interaction name needs to be non-empty");
            }

            return new TrackMe(getBaseTrackMe())
                    .set(QueryParams.CONTENT_NAME, mContentName)
                    .set(QueryParams.CONTENT_PIECE, mContentPiece)
                    .set(QueryParams.CONTENT_TARGET, mContentTarget)
                    .set(QueryParams.CONTENT_INTERACTION, mInteraction);
        }
    }


    /**
     * Tracks a shopping cart. Call this javascript function every time a user is adding, updating
     * or deleting a product from the cart.
     *
     * @param grandTotal total value of items in cart
     */
    public CartUpdate cartUpdate(int grandTotal) {
        return new CartUpdate(this, grandTotal);
    }

    public static class CartUpdate extends BaseEvent {
        private final int mGrandTotal;
        private EcommerceItems mEcommerceItems;

        CartUpdate(TrackHelper baseBuilder, int grandTotal) {
            super(baseBuilder);
            mGrandTotal = grandTotal;
        }

        /**
         * @param items Items included in the cart
         */
        public CartUpdate items(EcommerceItems items) {
            mEcommerceItems = items;
            return this;
        }

        @Override
        public TrackMe build() {
            if (mEcommerceItems == null) mEcommerceItems = new EcommerceItems();
            return new TrackMe(getBaseTrackMe())
                    .set(QueryParams.GOAL_ID, 0)
                    .set(QueryParams.REVENUE, CurrencyFormatter.priceString(mGrandTotal))
                    .set(QueryParams.ECOMMERCE_ITEMS, mEcommerceItems.toJson());
        }
    }

    /**
     * Tracks an Ecommerce order, including any ecommerce item previously added to the order.  All
     * monetary values should be passed as an integer number of cents (or the smallest integer unit
     * for your currency)
     *
     * @param orderId    (required) A unique string identifying the order
     * @param grandTotal (required) total amount of the order, in cents
     */
    public Order order(String orderId, int grandTotal) {
        return new Order(this, orderId, grandTotal);
    }

    public static class Order extends BaseEvent {
        private final String mOrderId;
        private final int mGrandTotal;
        private EcommerceItems mEcommerceItems;
        private Integer mDiscount;
        private Integer mShipping;
        private Integer mTax;
        private Integer mSubTotal;

        Order(TrackHelper baseBuilder, String orderId, int grandTotal) {
            super(baseBuilder);
            mOrderId = orderId;
            mGrandTotal = grandTotal;
        }

        /**
         * @param subTotal the subTotal for the order, in cents
         */
        public Order subTotal(Integer subTotal) {
            mSubTotal = subTotal;
            return this;
        }

        /**
         * @param tax the tax for the order, in cents
         */
        public Order tax(Integer tax) {
            mTax = tax;
            return this;
        }

        /**
         * @param shipping the shipping for the order, in cents
         */
        public Order shipping(Integer shipping) {
            mShipping = shipping;
            return this;
        }

        /**
         * @param discount the discount for the order, in cents
         */
        public Order discount(Integer discount) {
            mDiscount = discount;
            return this;
        }

        /**
         * @param items the items included in the order
         */
        public Order items(EcommerceItems items) {
            mEcommerceItems = items;
            return this;
        }

        @Override
        public TrackMe build() {
            if (mEcommerceItems == null) mEcommerceItems = new EcommerceItems();
            return new TrackMe(getBaseTrackMe())
                    .set(QueryParams.GOAL_ID, 0)
                    .set(QueryParams.ORDER_ID, mOrderId)
                    .set(QueryParams.REVENUE, CurrencyFormatter.priceString(mGrandTotal))
                    .set(QueryParams.ECOMMERCE_ITEMS, mEcommerceItems.toJson())
                    .set(QueryParams.SUBTOTAL, CurrencyFormatter.priceString(mSubTotal))
                    .set(QueryParams.TAX, CurrencyFormatter.priceString(mTax))
                    .set(QueryParams.SHIPPING, CurrencyFormatter.priceString(mShipping))
                    .set(QueryParams.DISCOUNT, CurrencyFormatter.priceString(mDiscount));
        }
    }

    /**
     * Caught exceptions are errors in your app for which you've defined exception handling code,
     * such as the occasional timeout of a network connection during a request for data.
     * <p>
     * This is just a different way to define an event.
     * Keep in mind Matomo is not a crash tracker, use this sparingly.
     * <p>
     * For this to be useful you should ensure that proguard does not remove all classnames and line numbers.
     * Also note that if this is used across different app versions and obfuscation is used, the same exception might be mapped to different obfuscated names by proguard.
     * This would mean the same exception (
Download .txt
gitextract_u8aen19b/

├── .github/
│   ├── dependabot.yml
│   ├── pull_request_template.md
│   ├── release.yml
│   └── workflows/
│       ├── pull-request-ci.yml
│       ├── release.yml
│       └── update-gradle-wrapper.yml
├── .gitignore
├── LICENSE
├── README.md
├── build.gradle
├── exampleapp/
│   ├── README.md
│   ├── build.gradle
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── org/
│       │           └── matomo/
│       │               └── demo/
│       │                   └── SmokeTest.kt
│       └── main/
│           ├── AndroidManifest.xml
│           ├── java/
│           │   └── org/
│           │       └── matomo/
│           │           └── demo/
│           │               ├── DemoActivity.kt
│           │               ├── DemoApp.kt
│           │               └── SettingsActivity.java
│           └── res/
│               ├── layout/
│               │   ├── activity_demo.xml
│               │   └── activity_settings.xml
│               ├── menu/
│               │   └── demo.xml
│               ├── values/
│               │   ├── dimens.xml
│               │   ├── strings.xml
│               │   └── styles.xml
│               └── values-w820dp/
│                   └── dimens.xml
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── jitpack.yml
├── settings.gradle
└── tracker/
    ├── build.gradle
    ├── lint.xml
    └── src/
        ├── main/
        │   ├── AndroidManifest.xml
        │   └── java/
        │       └── org/
        │           └── matomo/
        │               └── sdk/
        │                   ├── LegacySettingsPorter.kt
        │                   ├── Matomo.kt
        │                   ├── QueryParams.java
        │                   ├── TrackMe.java
        │                   ├── Tracker.java
        │                   ├── TrackerBuilder.java
        │                   ├── dispatcher/
        │                   │   ├── DefaultDispatcher.kt
        │                   │   ├── DefaultDispatcherFactory.kt
        │                   │   ├── DefaultPacketSender.kt
        │                   │   ├── DispatchMode.java
        │                   │   ├── Dispatcher.java
        │                   │   ├── DispatcherFactory.kt
        │                   │   ├── Event.java
        │                   │   ├── EventCache.java
        │                   │   ├── EventDiskCache.java
        │                   │   ├── Packet.java
        │                   │   ├── PacketFactory.java
        │                   │   └── PacketSender.kt
        │                   ├── extra/
        │                   │   ├── CustomDimension.java
        │                   │   ├── CustomVariables.java
        │                   │   ├── DimensionQueue.java
        │                   │   ├── DownloadTracker.java
        │                   │   ├── EcommerceItems.java
        │                   │   ├── InstallReferrerReceiver.java
        │                   │   ├── MatomoApplication.java
        │                   │   ├── MatomoExceptionHandler.java
        │                   │   └── TrackHelper.java
        │                   └── tools/
        │                       ├── ActivityHelper.java
        │                       ├── BuildInfo.java
        │                       ├── Checksum.java
        │                       ├── Connectivity.java
        │                       ├── CurrencyFormatter.java
        │                       ├── DeviceHelper.java
        │                       ├── PropertySource.java
        │                       └── UrlHelper.java
        └── test/
            └── java/
                ├── org/
                │   └── matomo/
                │       └── sdk/
                │           ├── LegacySettingsPorterTest.java
                │           ├── MatomoTest.java
                │           ├── TrackMeTest.java
                │           ├── TrackerBuilderTest.java
                │           ├── TrackerTest.java
                │           ├── dispatcher/
                │           │   ├── DefaultDispatcherTest.java
                │           │   ├── DefaultPacketSenderTest.java
                │           │   ├── EventCacheTest.java
                │           │   ├── EventDiskCacheTest.java
                │           │   ├── EventTest.java
                │           │   ├── PacketFactoryTest.java
                │           │   └── PacketTest.java
                │           ├── extra/
                │           │   ├── CustomDimensionTest.java
                │           │   ├── CustomVariablesTest.java
                │           │   ├── DimensionQueueTest.java
                │           │   ├── DownloadTrackerTest.java
                │           │   ├── EcommerceItemsTest.java
                │           │   ├── InstallReferrerReceiverTest.java
                │           │   ├── MatomoApplicationTest.java
                │           │   └── TrackHelperTest.java
                │           └── tools/
                │               ├── BuildInfoTest.kt
                │               ├── ChecksumTest.kt
                │               ├── ConnectivityTest.kt
                │               ├── CurrencyFormatterTest.kt
                │               ├── DeviceHelperTest.kt
                │               └── PropertySourceTest.kt
                └── testhelpers/
                    ├── BaseTest.kt
                    ├── DefaultTestCase.kt
                    ├── FullEnvTestLifeCycle.kt
                    ├── FullEnvTestRunner.kt
                    ├── JUnitTree.kt
                    ├── MatomoTestApplication.kt
                    ├── QueryHashMap.kt
                    ├── TestActivity.kt
                    ├── TestHelper.kt
                    └── TestPreferences.kt
Download .txt
SYMBOL INDEX (598 symbols across 49 files)

FILE: exampleapp/src/main/java/org/matomo/demo/SettingsActivity.java
  class SettingsActivity (line 29) | public class SettingsActivity extends Activity {
    method refreshUI (line 31) | private void refreshUI(final Activity settingsActivity) {
    method onCreate (line 124) | @Override

FILE: tracker/src/main/java/org/matomo/sdk/QueryParams.java
  type QueryParams (line 14) | public enum QueryParams {
    method QueryParams (line 294) | QueryParams(String value) {
    method toString (line 298) | public String toString() {

FILE: tracker/src/main/java/org/matomo/sdk/TrackMe.java
  class TrackMe (line 20) | public class TrackMe {
    method TrackMe (line 24) | public TrackMe() { }
    method TrackMe (line 26) | public TrackMe(TrackMe trackMe) {
    method putAll (line 33) | public TrackMe putAll(@NonNull TrackMe trackMe) {
    method set (line 41) | public synchronized TrackMe set(@NonNull String key, String value) {
    method get (line 50) | @Nullable
    method set (line 68) | public synchronized TrackMe set(@NonNull QueryParams key, String value) {
    method set (line 73) | public synchronized TrackMe set(@NonNull QueryParams key, int value) {
    method set (line 78) | public synchronized TrackMe set(@NonNull QueryParams key, float value) {
    method set (line 83) | public synchronized TrackMe set(@NonNull QueryParams key, long value) {
    method has (line 88) | public synchronized boolean has(@NonNull QueryParams queryParams) {
    method trySet (line 99) | public synchronized TrackMe trySet(@NonNull QueryParams key, int value) {
    method trySet (line 110) | public synchronized TrackMe trySet(@NonNull QueryParams key, float val...
    method trySet (line 114) | public synchronized TrackMe trySet(@NonNull QueryParams key, long valu...
    method trySet (line 125) | public synchronized TrackMe trySet(@NonNull QueryParams key, String va...
    method toMap (line 135) | public synchronized Map<String, String> toMap() {
    method get (line 139) | public synchronized String get(@NonNull QueryParams queryParams) {
    method isEmpty (line 143) | public synchronized boolean isEmpty() {

FILE: tracker/src/main/java/org/matomo/sdk/Tracker.java
  class Tracker (line 35) | @SuppressWarnings("WeakerAccess")
    method Tracker (line 77) | protected Tracker(Matomo matomo, TrackerBuilder config) {
    method addTrackingCallback (line 115) | public void addTrackingCallback(Callback callback) {
    method removeTrackingCallback (line 119) | public void removeTrackingCallback(Callback callback) {
    method reset (line 123) | public void reset() {
    method setOptOut (line 164) | public void setOptOut(boolean optOut) {
    method isOptOut (line 172) | public boolean isOptOut() {
    method getName (line 176) | public String getName() {
    method getMatomo (line 180) | public Matomo getMatomo() {
    method getAPIUrl (line 184) | public String getAPIUrl() {
    method getSiteId (line 188) | protected int getSiteId() {
    method getDefaultTrackMe (line 198) | public TrackMe getDefaultTrackMe() {
    method startNewSession (line 202) | public void startNewSession() {
    method setSessionTimeout (line 208) | public void setSessionTimeout(int milliseconds) {
    method getSessionTimeout (line 219) | public long getSessionTimeout() {
    method getDispatchTimeout (line 226) | public int getDispatchTimeout() {
    method setDispatchTimeout (line 233) | public void setDispatchTimeout(int timeout) {
    method dispatch (line 240) | public void dispatch() {
    method dispatchBlocking (line 248) | public void dispatchBlocking() {
    method setDispatchInterval (line 259) | public Tracker setDispatchInterval(long dispatchInterval) {
    method setDispatchGzipped (line 270) | public Tracker setDispatchGzipped(boolean dispatchGzipped) {
    method getDispatchInterval (line 278) | public long getDispatchInterval() {
    method setOfflineCacheAge (line 293) | public void setOfflineCacheAge(long age) {
    method getOfflineCacheAge (line 302) | public long getOfflineCacheAge() {
    method setOfflineCacheSize (line 317) | public void setOfflineCacheSize(long size) {
    method getOfflineCacheSize (line 326) | public long getOfflineCacheSize() {
    method getDispatchMode (line 335) | public DispatchMode getDispatchMode() {
    method setDispatchMode (line 349) | public void setDispatchMode(DispatchMode mode) {
    method setUserId (line 370) | public Tracker setUserId(String userId) {
    method getUserId (line 379) | public String getUserId() {
    method setVisitorId (line 388) | public Tracker setVisitorId(String visitorId) throws IllegalArgumentEx...
    method getVisitorId (line 393) | public String getVisitorId() {
    method confirmVisitorIdFormat (line 399) | private boolean confirmVisitorIdFormat(String visitorId) throws Illega...
    method injectInitialParams (line 409) | private void injectInitialParams(TrackMe trackMe) {
    method injectBaseParams (line 450) | private void injectBaseParams(TrackMe trackMe) {
    method track (line 483) | public Tracker track(TrackMe trackMe) {
    method makeRandomVisitorId (line 514) | public static String makeRandomVisitorId() {
    method getPreferences (line 519) | public SharedPreferences getPreferences() {
    method equals (line 524) | @Override
    method hashCode (line 537) | @Override
    method getLastEventX (line 550) | @VisibleForTesting
    method setDryRunTarget (line 562) | public void setDryRunTarget(List<Packet> dryRunTarget) {
    method getDryRunTarget (line 571) | public List<Packet> getDryRunTarget() {
    type Callback (line 575) | public interface Callback {
      method onTrack (line 583) | @Nullable

FILE: tracker/src/main/java/org/matomo/sdk/TrackerBuilder.java
  class TrackerBuilder (line 9) | public class TrackerBuilder {
    method createDefault (line 15) | public static TrackerBuilder createDefault(String apiUrl, int siteId) {
    method TrackerBuilder (line 24) | public TrackerBuilder(String apiUrl, int siteId, String trackerName) {
    method getApiUrl (line 35) | public String getApiUrl() {
    method getSiteId (line 39) | public int getSiteId() {
    method setTrackerName (line 46) | public TrackerBuilder setTrackerName(String name) {
    method getTrackerName (line 51) | public String getTrackerName() {
    method setApplicationBaseUrl (line 61) | public TrackerBuilder setApplicationBaseUrl(String domain) {
    method getApplicationBaseUrl (line 66) | public String getApplicationBaseUrl() {
    method build (line 70) | public Tracker build(Matomo matomo) {
    method equals (line 77) | @Override
    method hashCode (line 87) | @Override

FILE: tracker/src/main/java/org/matomo/sdk/dispatcher/DispatchMode.java
  type DispatchMode (line 6) | public enum DispatchMode {
    method DispatchMode (line 23) | DispatchMode(String key) {this.key = key;}
    method toString (line 25) | @Override
    method fromString (line 30) | @Nullable

FILE: tracker/src/main/java/org/matomo/sdk/dispatcher/Dispatcher.java
  type Dispatcher (line 17) | public interface Dispatcher {
    method getConnectionTimeOut (line 26) | int getConnectionTimeOut();
    method setConnectionTimeOut (line 34) | void setConnectionTimeOut(int timeOut);
    method setDispatchInterval (line 41) | void setDispatchInterval(long dispatchInterval);
    method getDispatchInterval (line 43) | long getDispatchInterval();
    method setDispatchGzipped (line 51) | void setDispatchGzipped(boolean dispatchGzipped);
    method getDispatchGzipped (line 53) | boolean getDispatchGzipped();
    method setDispatchMode (line 55) | void setDispatchMode(DispatchMode dispatchMode);
    method getDispatchMode (line 57) | DispatchMode getDispatchMode();
    method forceDispatch (line 63) | boolean forceDispatch();
    method forceDispatchBlocking (line 70) | void forceDispatchBlocking();
    method clear (line 75) | void clear();
    method submit (line 80) | void submit(TrackMe trackMe);
    method setDryRunTarget (line 87) | void setDryRunTarget(List<Packet> dryRunTarget);
    method getDryRunTarget (line 93) | List<Packet> getDryRunTarget();

FILE: tracker/src/main/java/org/matomo/sdk/dispatcher/Event.java
  class Event (line 11) | public class Event {
    method Event (line 16) | public Event(Map<String, String> eventData) {
    method Event (line 20) | public Event(String query) {
    method Event (line 24) | public Event(long timestamp, String query) {
    method getTimeStamp (line 29) | public long getTimeStamp() {
    method getEncodedQuery (line 33) | public String getEncodedQuery() {
    method toString (line 37) | @Override
    method equals (line 42) | @Override
    method hashCode (line 53) | @Override
    method urlEncodeUTF8 (line 66) | private static String urlEncodeUTF8(String param) {
    method urlEncodeUTF8 (line 78) | private static String urlEncodeUTF8(Map<String, String> map) {

FILE: tracker/src/main/java/org/matomo/sdk/dispatcher/EventCache.java
  class EventCache (line 13) | public class EventCache {
    method EventCache (line 18) | public EventCache(EventDiskCache cache) {
    method add (line 22) | public void add(Event event) {
    method drainTo (line 26) | public void drainTo(List<Event> drainedEvents) {
    method clear (line 30) | public void clear() {
    method isEmpty (line 35) | public boolean isEmpty() {
    method updateState (line 39) | public boolean updateState(boolean online) {
    method requeue (line 57) | public void requeue(List<Event> events) {

FILE: tracker/src/main/java/org/matomo/sdk/dispatcher/EventDiskCache.java
  class EventDiskCache (line 26) | public class EventDiskCache {
    method EventDiskCache (line 37) | public EventDiskCache(Tracker tracker) {
    method checkCacheLimits (line 57) | private void checkCacheLimits() {
    method isCachingEnabled (line 103) | private boolean isCachingEnabled() {
    method cache (line 107) | public synchronized void cache(@NonNull List<Event> toCache) {
    method uncache (line 123) | @NonNull
    method isEmpty (line 144) | public synchronized boolean isEmpty() {
    method readEventFile (line 152) | private List<Event> readEventFile(@NonNull File file) {
    method writeEventFile (line 191) | @Nullable

FILE: tracker/src/main/java/org/matomo/sdk/dispatcher/Packet.java
  class Packet (line 17) | public class Packet {
    method Packet (line 26) | public Packet(String targetURL) {
    method Packet (line 37) | public Packet(String targetURL, @Nullable JSONObject JSONObject, int e...
    method getTargetURL (line 44) | public String getTargetURL() {
    method getPostData (line 51) | @Nullable
    method getTimeStamp (line 59) | public long getTimeStamp() {
    method getEventCount (line 68) | public int getEventCount() {
    method toString (line 72) | @NonNull

FILE: tracker/src/main/java/org/matomo/sdk/dispatcher/PacketFactory.java
  class PacketFactory (line 27) | public class PacketFactory {
    method PacketFactory (line 33) | public PacketFactory(final String apiUrl) {
    method buildPackets (line 37) | public List<Packet> buildPackets(final List<Event> events) {
    method buildPacketForPost (line 62) | @Nullable
    method buildPacketForGet (line 79) | @Nullable

FILE: tracker/src/main/java/org/matomo/sdk/extra/CustomDimension.java
  class CustomDimension (line 17) | public class CustomDimension {
    method CustomDimension (line 22) | public CustomDimension(int id, String value) {
    method getId (line 27) | public int getId() {
    method getValue (line 31) | public String getValue() {
    method setDimension (line 47) | public static boolean setDimension(@NonNull TrackMe trackMe, int dimen...
    method setDimension (line 63) | public static boolean setDimension(TrackMe trackMe, CustomDimension di...
    method getDimension (line 67) | @Nullable
    method formatDimensionId (line 72) | private static String formatDimensionId(int id) {

FILE: tracker/src/main/java/org/matomo/sdk/extra/CustomVariables.java
  class CustomVariables (line 46) | public class CustomVariables {
    method CustomVariables (line 52) | public CustomVariables() {
    method CustomVariables (line 56) | public CustomVariables(@NonNull CustomVariables variables) {
    method CustomVariables (line 60) | public CustomVariables(@Nullable String json) {
    method putAll (line 73) | public CustomVariables putAll(CustomVariables customVariables) {
    method put (line 92) | public CustomVariables put(int index, String name, String value) {
    method put (line 115) | public CustomVariables put(String index, JSONArray values) {
    method toString (line 122) | public String toString() {
    method size (line 127) | public int size() {
    method injectVisitVariables (line 134) | public TrackMe injectVisitVariables(@NonNull TrackMe trackMe) {
    method toVisitVariables (line 140) | @NonNull

FILE: tracker/src/main/java/org/matomo/sdk/extra/DimensionQueue.java
  class DimensionQueue (line 17) | public class DimensionQueue {
    method DimensionQueue (line 21) | public DimensionQueue(Tracker tracker) {
    method add (line 30) | public void add(int id, String value) {
    method onTrack (line 34) | private TrackMe onTrack(TrackMe trackMe) {

FILE: tracker/src/main/java/org/matomo/sdk/extra/DownloadTracker.java
  class DownloadTracker (line 22) | public class DownloadTracker {
    type Extra (line 33) | public interface Extra {
      method isIntensiveWork (line 41) | boolean isIntensiveWork();
      method buildExtraIdentifier (line 52) | @Nullable
      class ApkChecksum (line 59) | class ApkChecksum implements Extra {
        method ApkChecksum (line 62) | public ApkChecksum(Context context) {
        method isIntensiveWork (line 71) | @Override
        method buildExtraIdentifier (line 76) | @Nullable
      class Custom (line 91) | @SuppressWarnings("unused")
      class None (line 99) | class None implements Extra {
        method isIntensiveWork (line 101) | @Override
        method buildExtraIdentifier (line 106) | @Nullable
    method DownloadTracker (line 114) | public DownloadTracker(Tracker tracker) {
    method getOurPackageInfo (line 118) | private static PackageInfo getOurPackageInfo(Context context) {
    method DownloadTracker (line 127) | public DownloadTracker(Tracker tracker, @NonNull PackageInfo packageIn...
    method setVersion (line 136) | public void setVersion(@Nullable String version) {
    method getVersion (line 140) | public String getVersion() {
    method trackOnce (line 145) | public void trackOnce(TrackMe baseTrackme, @NonNull Extra extra) {
    method trackNewAppDownload (line 155) | public void trackNewAppDownload(final TrackMe baseTrackme, @NonNull fi...
    method trackNewAppDownloadInternal (line 170) | private void trackNewAppDownloadInternal(TrackMe baseTrackMe, @NonNull...

FILE: tracker/src/main/java/org/matomo/sdk/extra/EcommerceItems.java
  class EcommerceItems (line 16) | public class EcommerceItems {
    method addItem (line 23) | public void addItem(Item item) {
    class Item (line 27) | public static class Item {
      method Item (line 39) | public Item(String sku) {
      method name (line 46) | public Item name(String name) {
      method category (line 54) | public Item category(String category) {
      method price (line 62) | public Item price(int price) {
      method quantity (line 70) | public Item quantity(int quantity) {
      method getSku (line 75) | public String getSku() {
      method getCategory (line 79) | public String getCategory() {
      method getPrice (line 83) | public Integer getPrice() {
      method getQuantity (line 87) | public Integer getQuantity() {
      method getName (line 91) | public String getName() {
      method toJson (line 95) | protected JSONArray toJson() {
    method remove (line 111) | public void remove(String sku) {
    method remove (line 115) | public void remove(Item item) {
    method clear (line 122) | public void clear() {
    method toJson (line 126) | public String toJson() {

FILE: tracker/src/main/java/org/matomo/sdk/extra/InstallReferrerReceiver.java
  class InstallReferrerReceiver (line 15) | public class InstallReferrerReceiver extends BroadcastReceiver {
    method onReceive (line 25) | @Override

FILE: tracker/src/main/java/org/matomo/sdk/extra/MatomoApplication.java
  class MatomoApplication (line 16) | public abstract class MatomoApplication extends Application {
    method getMatomo (line 19) | public Matomo getMatomo() {
    method getTracker (line 28) | public synchronized Tracker getTracker() {
    method onCreateTrackerConfig (line 39) | public abstract TrackerBuilder onCreateTrackerConfig();
    method onLowMemory (line 41) | @Override
    method onTrimMemory (line 47) | @Override

FILE: tracker/src/main/java/org/matomo/sdk/extra/MatomoExceptionHandler.java
  class MatomoExceptionHandler (line 25) | public class MatomoExceptionHandler implements Thread.UncaughtExceptionH...
    method MatomoExceptionHandler (line 31) | public MatomoExceptionHandler(@NonNull Tracker tracker, @Nullable Trac...
    method getTracker (line 37) | public Tracker getTracker() {
    method getDefaultExceptionHandler (line 44) | public Thread.UncaughtExceptionHandler getDefaultExceptionHandler() {
    method uncaughtException (line 48) | @Override

FILE: tracker/src/main/java/org/matomo/sdk/extra/TrackHelper.java
  class TrackHelper (line 23) | public class TrackHelper {
    method TrackHelper (line 27) | private TrackHelper() {
    method TrackHelper (line 31) | private TrackHelper(@Nullable TrackMe baseTrackMe) {
    method track (line 36) | public static TrackHelper track() {
    method track (line 40) | public static TrackHelper track(@Nullable TrackMe base) {
    class BaseEvent (line 44) | static abstract class BaseEvent {
      method BaseEvent (line 48) | BaseEvent(TrackHelper baseBuilder) {
      method getBaseTrackMe (line 52) | TrackMe getBaseTrackMe() {
      method build (line 59) | public abstract TrackMe build();
      method with (line 61) | public void with(MatomoApplication matomoApplication) {
      method with (line 65) | public void with(Tracker tracker) {
      method safelyWith (line 70) | public boolean safelyWith(MatomoApplication matomoApplication) {
      method safelyWith (line 80) | public boolean safelyWith(Tracker tracker) {
    method screen (line 98) | public Screen screen(String path) {
    method screen (line 108) | public Screen screen(Activity activity) {
    class Screen (line 113) | public static class Screen extends BaseEvent {
      method Screen (line 121) | Screen(TrackHelper baseBuilder, String path) {
      method title (line 132) | public Screen title(String title) {
      method dimension (line 143) | public Screen dimension(int index, String dimensionValue) {
      method variable (line 155) | @Deprecated
      method campaign (line 169) | public Screen campaign(String name, String keyword) {
      method build (line 175) | @Override
    method event (line 210) | public EventBuilder event(String category, String action) {
    class EventBuilder (line 214) | public static class EventBuilder extends BaseEvent {
      method EventBuilder (line 221) | EventBuilder(TrackHelper builder, String category, String action) {
      method path (line 231) | public EventBuilder path(String path) {
      method name (line 240) | public EventBuilder name(String name) {
      method value (line 249) | public EventBuilder value(Float value) {
      method build (line 254) | @Override
    method goal (line 276) | public Goal goal(int idGoal) {
    class Goal (line 280) | public static class Goal extends BaseEvent {
      method Goal (line 284) | Goal(TrackHelper baseBuilder, int idGoal) {
      method revenue (line 294) | public Goal revenue(Float revenue) {
      method build (line 299) | @Override
    method outlink (line 317) | public Outlink outlink(URL url) {
    class Outlink (line 321) | public static class Outlink extends BaseEvent {
      method Outlink (line 324) | Outlink(TrackHelper baseBuilder, URL url) {
      method build (line 329) | @Override
    method search (line 350) | public Search search(String keyword) {
    class Search (line 354) | public static class Search extends BaseEvent {
      method Search (line 359) | Search(TrackHelper baseBuilder, String keyword) {
      method category (line 369) | public Search category(String category) {
      method count (line 380) | public Search count(Integer count) {
      method build (line 385) | @Override
    method download (line 409) | public Download download(DownloadTracker downloadTracker) {
    method download (line 413) | public Download download() {
    class Download (line 417) | public static class Download {
      method Download (line 424) | Download(DownloadTracker downloadTracker, TrackHelper baseBuilder) {
      method identifier (line 435) | public Download identifier(DownloadTracker.Extra identifier) {
      method force (line 447) | public Download force() {
      method version (line 458) | public Download version(String version) {
      method with (line 463) | public void with(Tracker tracker) {
    method impression (line 476) | public ContentImpression impression(String contentName) {
    class ContentImpression (line 480) | public static class ContentImpression extends BaseEvent {
      method ContentImpression (line 485) | ContentImpression(TrackHelper baseBuilder, String contentName) {
      method piece (line 493) | public ContentImpression piece(String contentPiece) {
      method target (line 501) | public ContentImpression target(String contentTarget) {
      method build (line 506) | @Override
    method interaction (line 526) | public ContentInteraction interaction(String contentName, String conte...
    class ContentInteraction (line 530) | public static class ContentInteraction extends BaseEvent {
      method ContentInteraction (line 536) | ContentInteraction(TrackHelper baseBuilder, String contentName, Stri...
      method piece (line 545) | public ContentInteraction piece(String contentPiece) {
      method target (line 553) | public ContentInteraction target(String contentTarget) {
      method build (line 558) | @Override
    method cartUpdate (line 582) | public CartUpdate cartUpdate(int grandTotal) {
    class CartUpdate (line 586) | public static class CartUpdate extends BaseEvent {
      method CartUpdate (line 590) | CartUpdate(TrackHelper baseBuilder, int grandTotal) {
      method items (line 598) | public CartUpdate items(EcommerceItems items) {
      method build (line 603) | @Override
    method order (line 621) | public Order order(String orderId, int grandTotal) {
    class Order (line 625) | public static class Order extends BaseEvent {
      method Order (line 634) | Order(TrackHelper baseBuilder, String orderId, int grandTotal) {
      method subTotal (line 643) | public Order subTotal(Integer subTotal) {
      method tax (line 651) | public Order tax(Integer tax) {
      method shipping (line 659) | public Order shipping(Integer shipping) {
      method discount (line 667) | public Order discount(Integer discount) {
      method items (line 675) | public Order items(EcommerceItems items) {
      method build (line 680) | @Override
    method exception (line 708) | public Exception exception(Throwable throwable) {
    class Exception (line 712) | public static class Exception extends BaseEvent {
      method Exception (line 717) | Exception(TrackHelper baseBuilder, Throwable throwable) {
      method description (line 725) | public Exception description(String description) {
      method fatal (line 733) | public Exception fatal(boolean isFatal) {
      method build (line 738) | @Override
    method uncaughtExceptions (line 767) | public UncaughtExceptions uncaughtExceptions() {
    class UncaughtExceptions (line 771) | public static class UncaughtExceptions {
      method UncaughtExceptions (line 774) | UncaughtExceptions(TrackHelper baseBuilder) {
      method with (line 782) | public Thread.UncaughtExceptionHandler with(Tracker tracker) {
    method screens (line 799) | public AppTracking screens(Application app) {
    class AppTracking (line 803) | public static class AppTracking {
      method AppTracking (line 807) | public AppTracking(TrackHelper baseBuilder, Application application) {
      method with (line 816) | public Application.ActivityLifecycleCallbacks with(final Tracker tra...
    method dimension (line 860) | public Dimension dimension(int id, String value) {
    class Dimension (line 864) | public static class Dimension extends TrackHelper {
      method Dimension (line 866) | Dimension(TrackMe base) {
      method dimension (line 870) | @Override
    method visitVariables (line 884) | @Deprecated
    method visitVariables (line 896) | @Deprecated
    class VisitVariables (line 901) | @SuppressWarnings("deprecation")
      method VisitVariables (line 904) | public VisitVariables(TrackHelper baseBuilder, CustomVariables custo...
      method visitVariables (line 914) | public VisitVariables visitVariables(int id, String name, String val...

FILE: tracker/src/main/java/org/matomo/sdk/tools/ActivityHelper.java
  class ActivityHelper (line 10) | public class ActivityHelper {
    method getBreadcrumbs (line 12) | public static String getBreadcrumbs(final Activity activity) {
    method joinSlash (line 23) | public static String joinSlash(List<String> sequence) {
    method breadcrumbsToPath (line 30) | public static String breadcrumbsToPath(String breadcrumbs) {

FILE: tracker/src/main/java/org/matomo/sdk/tools/BuildInfo.java
  class BuildInfo (line 6) | public class BuildInfo {
    method getRelease (line 7) | public String getRelease() {
    method getModel (line 11) | public String getModel() {
    method getBuildId (line 15) | public String getBuildId() {

FILE: tracker/src/main/java/org/matomo/sdk/tools/Checksum.java
  class Checksum (line 18) | public class Checksum {
    method getHex (line 24) | public static String getHex(byte[] raw) {
    method getMD5Checksum (line 36) | public static String getMD5Checksum(String string) throws Exception {
    method getMD5Checksum (line 46) | public static String getMD5Checksum(File file) throws Exception {

FILE: tracker/src/main/java/org/matomo/sdk/tools/Connectivity.java
  class Connectivity (line 12) | public class Connectivity {
    method Connectivity (line 15) | public Connectivity(Context context) {
    method isConnected (line 19) | public boolean isConnected() {
    type Type (line 24) | public enum Type {
    method getType (line 28) | public Type getType() {

FILE: tracker/src/main/java/org/matomo/sdk/tools/CurrencyFormatter.java
  class CurrencyFormatter (line 14) | public class CurrencyFormatter {
    method priceString (line 15) | @Nullable

FILE: tracker/src/main/java/org/matomo/sdk/tools/DeviceHelper.java
  class DeviceHelper (line 23) | public class DeviceHelper {
    method DeviceHelper (line 29) | public DeviceHelper(Context context, PropertySource propertySource, Bu...
    method getUserLanguage (line 40) | public String getUserLanguage() {
    method getUserAgent (line 49) | public String getUserAgent() {
    method getResolution (line 71) | public int[] getResolution() {

FILE: tracker/src/main/java/org/matomo/sdk/tools/PropertySource.java
  class PropertySource (line 6) | public class PropertySource {
    method getHttpAgent (line 7) | @Nullable
    method getJVMVersion (line 12) | @Nullable
    method getSystemProperty (line 17) | @Nullable

FILE: tracker/src/main/java/org/matomo/sdk/tools/UrlHelper.java
  class UrlHelper (line 27) | public class UrlHelper {
    method parse (line 34) | public static List<Pair<String, String>> parse(@NonNull final URI uri,...
    method parse (line 44) | public static void parse(@NonNull final List<Pair<String, String>> par...
    method decode (line 60) | private static String decode(@NonNull final String content, @Nullable ...

FILE: tracker/src/test/java/org/matomo/sdk/LegacySettingsPorterTest.java
  class LegacySettingsPorterTest (line 27) | @SuppressLint("CommitPrefEdits")
    method setup (line 39) | @Before
    method testPort_optOut_empty (line 54) | @Test
    method testPort_optOut_exists (line 63) | @Test
    method testPort_userId_empty (line 74) | @Test
    method testPort_userId_exists (line 83) | @Test
    method testPort_firstVisit_empty (line 95) | @Test
    method testPort_firstVisit_exists (line 104) | @Test
    method testPort_visitCount_empty (line 116) | @Test
    method testPort_visitCount_exists (line 125) | @Test
    method testPort_previousVisit_empty (line 137) | @Test
    method testPort_previousVisit_exists (line 146) | @Test
    method testDownloadMapping_empty (line 158) | @Test
    method testDownloadMapping_exists (line 168) | @Test

FILE: tracker/src/test/java/org/matomo/sdk/MatomoTest.java
  class MatomoTest (line 49) | @Config(sdk = 28, manifest = Config.NONE, application = MatomoTestApplic...
    method testNewTracker (line 53) | @Test
    method testNormalTracker (line 62) | @Test
    method testTrackerNaming (line 70) | @Test
    method testLowMemoryDispatch (line 76) | @SuppressLint("InlinedApi")
    method testGetSettings (line 108) | @Test
    method testSetDispatcherFactory (line 123) | @Test

FILE: tracker/src/test/java/org/matomo/sdk/TrackMeTest.java
  class TrackMeTest (line 20) | @RunWith(MockitoJUnitRunner.class)
    method testSourcingFromOtherTrackMe (line 22) | @Test
    method testAdd_overwrite (line 36) | @Test
    method testSet (line 56) | @Test
    method testTrySet (line 79) | @Test
    method testSetAll (line 107) | @Test

FILE: tracker/src/test/java/org/matomo/sdk/TrackerBuilderTest.java
  class TrackerBuilderTest (line 17) | @RunWith(MockitoJUnitRunner.class)
    method testApplicationDomain (line 21) | @Test
    method testSiteId (line 38) | @Test
    method testGetName (line 44) | @Test
    method testEquals (line 53) | @Test
    method testHashCode (line 62) | @Test

FILE: tracker/src/test/java/org/matomo/sdk/TrackerTest.java
  class TrackerTest (line 56) | @SuppressWarnings("PointlessArithmeticExpression")
    method setup (line 68) | @Before
    method testGetPreferences (line 93) | @Test
    method testLastScreenUrl (line 103) | @Test
    method testSetDispatchInterval (line 128) | @Test
    method testSetDispatchTimeout (line 137) | @Test
    method testGetOfflineCacheAge_defaultValue (line 147) | @Test
    method testSetOfflineCacheAge (line 153) | @Test
    method testGetOfflineCacheSize_defaultValue (line 160) | @Test
    method testSetOfflineCacheSize (line 166) | @Test
    method testDispatchMode_default (line 173) | @Test
    method testDispatchMode_change (line 181) | @Test
    method testDispatchMode_fallback (line 189) | @Test
    method testSetDispatchMode_propagation (line 197) | @Test
    method testSetDispatchMode_propagation_change (line 204) | @Test
    method testSetDispatchMode_exception (line 215) | @Test
    method testsetDispatchGzip (line 227) | @Test
    method testOptOut_set (line 234) | @Test
    method testOptOut_init (line 243) | @Test
    method testDispatch (line 253) | @Test
    method testDispatch_optOut (line 262) | @Test
    method testGetSiteId (line 273) | @Test
    method testGetMatomo (line 279) | @Test
    method testSetURL (line 285) | @Test
    method testApplicationDomain (line 304) | @Test
    method testVisitorId_invalid_short (line 315) | @Test(expected = IllegalArgumentException.class)
    method testVisitorId_invalid_long (line 323) | @Test(expected = IllegalArgumentException.class)
    method testVisitorId_invalid_charset (line 331) | @Test(expected = IllegalArgumentException.class)
    method testVisitorId_init (line 339) | @Test
    method testVisitorId_restore (line 345) | @Test
    method testVisitorId_dispatch (line 355) | @Test
    method testUserID_init (line 371) | @Test
    method testUserID_restore (line 378) | @Test
    method testUserID_invalid (line 390) | @Test
    method testUserID_dispatch (line 410) | @Test
    method testGetResolution (line 425) | @Test
    method testSetNewSession (line 434) | @Test
    method testSetNewSessionRaceCondition (line 448) | @Test
    method testSetSessionTimeout (line 475) | @Test
    method testCheckSessionTimeout (line 497) | @Test
    method testReset (line 514) | @Test
    method testTrackerEquals (line 560) | @Test
    method testTrackerHashCode (line 580) | @Test
    method testUrlPathCorrection (line 585) | @Test
    method testSetUserAgent (line 598) | @Test
    method testFirstVisitTimeStamp (line 619) | @Test
    method testTotalVisitCount (line 639) | @Test
    method testVisitCountMultipleThreads (line 658) | @Test
    method testSessionStartRaceCondition (line 674) | @Test
    method testFirstVisitMultipleThreads (line 717) | @Test
    method testPreviousVisits (line 736) | @Test
    method testPreviousVisit (line 758) | @Test
    method testTrackingCallback (line 804) | @Test
    method testTrackingCallbacks (line 836) | @Test
    method validateDefaultQuery (line 861) | private static void validateDefaultQuery(TrackMe params) {
    method testCustomDispatcherFactory (line 870) | @Test

FILE: tracker/src/test/java/org/matomo/sdk/dispatcher/DefaultDispatcherTest.java
  class DefaultDispatcherTest (line 50) | public class DefaultDispatcherTest extends BaseTest {
    method setup (line 60) | @Before
    method testClear (line 90) | @Test
    method testClear_cleanExit (line 96) | @Test
    method testGetDispatchMode (line 122) | @Test
    method testDispatchMode_wifiOnly (line 129) | @Test
    method testConnectivityChange (line 150) | @Test
    method testGetDispatchGzipped (line 172) | @Test
    method testDefaultConnectionTimeout (line 180) | @Test
    method testSetConnectionTimeout (line 185) | @Test
    method testDefaultDispatchInterval (line 192) | @Test
    method testForceDispatchTwice (line 197) | @Test
    method testMultiThreadDispatch (line 207) | @Test
    method testForceDispatch (line 221) | @Test
    method testBatchDispatch (line 239) | @Test
    method testBlockingDispatch (line 257) | @Test
    method testBlockingDispatchInFlight (line 278) | @Test
    method testBlockingDispatchCollision (line 299) | @Test
    method testBlockingDispatchExceptionMode (line 333) | @Test
    method testDispatchRetryWithBackoff (line 364) | @Test
    method testDispatchInterval (line 381) | @Test
    method testRandomDispatchIntervals (line 391) | @Test
    method checkForMIAs (line 413) | public static void checkForMIAs(int expectedEvents, List<String> creat...
    method launchTestThreads (line 451) | public static void launchTestThreads(final String apiUrl, final Dispat...
    method getFlattenedQueries (line 473) | public static List<String> getFlattenedQueries(List<Packet> packets) t...
    method getTestEvent (line 489) | public static TrackMe getTestEvent() {

FILE: tracker/src/test/java/org/matomo/sdk/dispatcher/DefaultPacketSenderTest.java
  class DefaultPacketSenderTest (line 26) | @RunWith(MockitoJUnitRunner.class)
    method setup (line 32) | @Before
    method tearDown (line 39) | @After
    method testDispatch (line 45) | @Test
    method testGzip (line 66) | @Test
    method testTimeout (line 87) | @Test

FILE: tracker/src/test/java/org/matomo/sdk/dispatcher/EventCacheTest.java
  class EventCacheTest (line 27) | @RunWith(MockitoJUnitRunner.class)
    method setup (line 33) | @Before
    method testClear (line 40) | @Test
    method testDrain_simple (line 48) | @Test
    method testDrain_empty (line 59) | @Test
    method testDrain_diskCache_empty (line 66) | @Test
    method testDrain_diskCache_nonempty (line 74) | @Test
    method testDrain_diskCache_first (line 84) | @Test
    method testUpdateState_online (line 98) | @Test
    method testUpdateState_offline (line 106) | @Test
    method testUpdateState_offline_ordering (line 121) | @Test

FILE: tracker/src/test/java/org/matomo/sdk/dispatcher/EventDiskCacheTest.java
  class EventDiskCacheTest (line 29) | @RunWith(MockitoJUnitRunner.class)
    method setup (line 39) | @Before
    method tearDown (line 57) | @SuppressWarnings("ResultOfMethodCallIgnored")
    method testIsEmpty (line 70) | @Test
    method testCachePath (line 77) | @Test
    method testCacheFileName (line 86) | @Test
    method testCaching (line 95) | @Test
    method testCaching_empty (line 106) | @Test
    method testOrder (line 111) | @Test
    method testMaxAge_positive_allStale (line 127) | @Test
    method testMaxAge_positive_singleContainer (line 139) | @Test
    method testMaxAge_positive_multipleContainer (line 153) | @Test
    method testMaxAge_unlimited (line 169) | @Test
    method testMaxAge_negative_cachingDisabled (line 186) | @Test
    method testClearDataOnceEvenIfDisabled (line 199) | @Test
    method testMaxSize_limited (line 212) | @Test
    method testMaxSize_disabled (line 237) | @Test
    method stressTest_singles (line 254) | @Test
    method stressTest_multi (line 273) | @Test
    method testOfflineMode_issue_271 (line 295) | @Test

FILE: tracker/src/test/java/org/matomo/sdk/dispatcher/EventTest.java
  class EventTest (line 28) | @RunWith(MockitoJUnitRunner.class)
    method testhashCode (line 30) | @Test
    method testEncoding_escaping (line 36) | @Test
    method testBncoding_empty (line 45) | @Test
    method testEncondingSingles (line 52) | @Test
    method testEncodingMultiples (line 62) | @Test
    method parseEncoding (line 77) | private static Map<String, String> parseEncoding(String url) throws Ex...

FILE: tracker/src/test/java/org/matomo/sdk/dispatcher/PacketFactoryTest.java
  class PacketFactoryTest (line 19) | @RunWith(MockitoJUnitRunner.class)
    method testPOST_apiUrl (line 22) | @Test
    method testPOST_data (line 32) | @Test
    method testGET_apiUrl (line 40) | @Test
    method testGET_badUrl (line 48) | @Test
    method testEmptyEvents (line 54) | @Test
    method testPacking_rest (line 60) | @Test
    method testPacking_notfull (line 78) | @Test
    method testPacking_even (line 96) | @Test

FILE: tracker/src/test/java/org/matomo/sdk/dispatcher/PacketTest.java
  class PacketTest (line 13) | @RunWith(MockitoJUnitRunner.class)
    method testEventCount (line 16) | @Test
    method testTimeStamp (line 22) | @Test

FILE: tracker/src/test/java/org/matomo/sdk/extra/CustomDimensionTest.java
  class CustomDimensionTest (line 17) | @RunWith(MockitoJUnitRunner.class)
    method testSetCustomDimensions (line 20) | @Test
    method testSet_truncate (line 38) | @Test
    method testSet_badId (line 45) | @Test
    method testSet_removal (line 52) | @Test
    method testSet_empty (line 61) | @Test

FILE: tracker/src/test/java/org/matomo/sdk/extra/CustomVariablesTest.java
  class CustomVariablesTest (line 20) | @SuppressWarnings("deprecation")
    method testPutAll (line 24) | @Test
    method testInherit (line 41) | @Test
    method testToString (line 54) | @Test
    method testToStringJSON (line 67) | @Test
    method testTrimLongValue (line 78) | @Test
    method testWrongIndex (line 89) | @Test
    method testWrongValueSize (line 101) | @Test
    method testInject (line 111) | @Test
    method testToTrackMe (line 120) | @Test
    method testVisitCustomVariables (line 128) | @Test

FILE: tracker/src/test/java/org/matomo/sdk/extra/DimensionQueueTest.java
  class DimensionQueueTest (line 17) | @RunWith(MockitoJUnitRunner.class)
    method testEmpty (line 22) | @Test
    method testCallback (line 33) | @Test
    method testCollision (line 48) | @Test
    method testOverwriting (line 62) | @Test

FILE: tracker/src/test/java/org/matomo/sdk/extra/DownloadTrackerTest.java
  class DownloadTrackerTest (line 39) | @RunWith(MockitoJUnitRunner.class)
    method setup (line 49) | @Before
    method testTrackAppDownload (line 65) | @Test
    method testTrackIdentifier (line 77) | @Test
    method testTrackReferrer (line 123) | @Test
    method testTrackNewAppDownloadWithVersion (line 151) | @Test
    method checkNewAppDownload (line 179) | private void checkNewAppDownload(TrackMe trackMe) {

FILE: tracker/src/test/java/org/matomo/sdk/extra/EcommerceItemsTest.java
  class EcommerceItemsTest (line 15) | @RunWith(MockitoJUnitRunner.class)
    method testEmptyItems (line 18) | @Test
    method testAddItems (line 24) | @Test
    method testRemoveItem (line 38) | @Test
    method testRemoveAllItems (line 53) | @Test
    method testItem (line 63) | @Test

FILE: tracker/src/test/java/org/matomo/sdk/extra/InstallReferrerReceiverTest.java
  class InstallReferrerReceiverTest (line 15) | public class InstallReferrerReceiverTest extends DefaultTestCase {
    method testReceiveGooglePlay (line 18) | @Test
    method testGracefulFailure (line 49) | @Test

FILE: tracker/src/test/java/org/matomo/sdk/extra/MatomoApplicationTest.java
  class MatomoApplicationTest (line 26) | @Config(sdk = 28, manifest = Config.NONE, application = MatomoTestApplic...
    method testAutoBindActivities (line 30) | @Test
    method testApplicationGetTracker (line 44) | @Test
    method testApplication (line 50) | @Test

FILE: tracker/src/test/java/org/matomo/sdk/extra/TrackHelperTest.java
  class TrackHelperTest (line 43) | @SuppressWarnings("deprecation")
    method setup (line 52) | @Before
    method testBaseEvent (line 66) | @Test
    method testBaseEvent_track_safely (line 73) | @Test
    method testOutlink (line 97) | @Test
    method testOutlink_invalid_url (line 118) | @Test(expected = IllegalArgumentException.class)
    method testDownloadTrackChecksum (line 123) | @Test
    method testDownloadTrackForced (line 130) | @Test
    method testDownloadCustomVersion (line 137) | @Test
    method testVisitCustomVariables_merge_base (line 147) | @Test
    method testVisitCustomVariables_merge_singles (line 163) | @Test
    method testVisitCustomVariables_add (line 180) | @Test
    method testSetScreenCustomVariable (line 197) | @Test
    method testSetScreenCustomDimension (line 208) | @Test
    method testSetScreem_empty_path (line 226) | @Test(expected = IllegalArgumentException.class)
    method testCustomDimension_trackHelperAny (line 231) | @Test
    method testCustomDimension_override (line 246) | @Test
    method testTrackScreenView (line 261) | @Test
    method testTrackScreenWithTitleView (line 268) | @Test
    method testTrackScreenWithCampaignView (line 276) | @Test
    method testTrackEvent (line 285) | @Test
    method testTrackEventName (line 294) | @Test
    method testTrackEventNameAndValue (line 305) | @Test
    method testTrackEventNameAndValueWithpath (line 317) | @Test
    method testTrackGoal (line 329) | @Test
    method testTrackGoal_invalid_id (line 338) | @Test(expected = IllegalArgumentException.class)
    method testTrackSiteSearch (line 343) | @Test
    method testTrackGoalRevenue (line 360) | @Test
    method testTrackContentImpression (line 369) | @Test
    method testTrackContentImpression_invalid_name_empty (line 380) | @Test(expected = IllegalArgumentException.class)
    method testTrackContentImpression_invalid_name_null (line 385) | @Test(expected = IllegalArgumentException.class)
    method testTrackContentInteraction_invalid_name_empty (line 390) | @Test
    method testTrackContentInteraction_invalid_name_null (line 405) | @Test
    method testTrackEcommerceCartUpdate (line 420) | @Test
    method testTrackEcommerceOrder (line 440) | @Test
    method testTrackException (line 465) | @Test
    method testExceptionHandler (line 483) | @SuppressWarnings({"divzero", "NumericOverflow"})
Condensed preview — 105 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (400K chars).
[
  {
    "path": ".github/dependabot.yml",
    "chars": 633,
    "preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
  },
  {
    "path": ".github/pull_request_template.md",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": ".github/release.yml",
    "chars": 482,
    "preview": "changelog:\n  exclude:\n    labels:\n      - ignore-for-release\n    authors:\n      - someuser\n  categories:\n    - title: Br"
  },
  {
    "path": ".github/workflows/pull-request-ci.yml",
    "chars": 3493,
    "preview": "name: PullRequest\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n\nenv:\n  BRANCH_NAME: ${{ github.head_ref || "
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 1582,
    "preview": "name: Release with changelog\n\non:\n  push:\n    tags:\n      - '*'\n\njobs:\n  release:\n    runs-on: ${{ matrix.os }}\n    stra"
  },
  {
    "path": ".github/workflows/update-gradle-wrapper.yml",
    "chars": 514,
    "preview": "name: Update Gradle Wrapper\n\non:\n  schedule:\n    - cron: \"0 6 * * MON\"\n\njobs:\n  update-gradle-wrapper:\n    runs-on: ubun"
  },
  {
    "path": ".gitignore",
    "chars": 263,
    "preview": "# Gradle files\n.gradle/\nbuild/\n\n# Local configuration file (sdk path, etc)\nlocal.properties\n\n# Android Studio generated "
  },
  {
    "path": "LICENSE",
    "chars": 1475,
    "preview": "Copyright 2018 Matomo team\n\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmod"
  },
  {
    "path": "README.md",
    "chars": 3062,
    "preview": "Matomo SDK for Android\n========================\n\n[![](https://jitpack.io/v/matomo-org/matomo-sdk-android.svg)](https://j"
  },
  {
    "path": "build.gradle",
    "chars": 732,
    "preview": "buildscript {\n    ext.kotlin_version = \"2.2.20\"\n    repositories {\n        google()\n        maven { url \"https://plugins"
  },
  {
    "path": "exampleapp/README.md",
    "chars": 163,
    "preview": "# Demo Application Matomo Android SDK\n\n## Description\n\nExample of using the Matomo Tracking SDK for Android\n\n```\n./gradl"
  },
  {
    "path": "exampleapp/build.gradle",
    "chars": 1288,
    "preview": "plugins {\n    id \"com.android.application\"\n    id \"kotlin-android\"\n}\n\nandroid {\n    namespace \"org.matomo.demo\"\n    defa"
  },
  {
    "path": "exampleapp/src/androidTest/java/org/matomo/demo/SmokeTest.kt",
    "chars": 3107,
    "preview": "package org.matomo.demo\n\nimport android.graphics.Bitmap\nimport androidx.test.core.graphics.writeToTestStorage\nimport and"
  },
  {
    "path": "exampleapp/src/main/AndroidManifest.xml",
    "chars": 1493,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <uses-"
  },
  {
    "path": "exampleapp/src/main/java/org/matomo/demo/DemoActivity.kt",
    "chars": 4596,
    "preview": "/*\n * Android SDK for Matomo\n *\n * @link https://github.com/matomo-org/matomo-android-sdk\n * @license https://github.com"
  },
  {
    "path": "exampleapp/src/main/java/org/matomo/demo/DemoApp.kt",
    "chars": 2103,
    "preview": "/*\n * Android SDK for Matomo\n *\n * @link https://github.com/matomo-org/matomo-android-sdk\n * @license https://github.com"
  },
  {
    "path": "exampleapp/src/main/java/org/matomo/demo/SettingsActivity.java",
    "chars": 4935,
    "preview": "/*\n * Android SDK for Matomo\n *\n * @link https://github.com/matomo-org/matomo-android-sdk\n * @license https://github.com"
  },
  {
    "path": "exampleapp/src/main/res/layout/activity_demo.xml",
    "chars": 3090,
    "preview": "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/too"
  },
  {
    "path": "exampleapp/src/main/res/layout/activity_settings.xml",
    "chars": 4717,
    "preview": "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/t"
  },
  {
    "path": "exampleapp/src/main/res/menu/demo.xml",
    "chars": 402,
    "preview": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n      xmlns:app=\"http://schemas.android.com/apk/res-aut"
  },
  {
    "path": "exampleapp/src/main/res/values/dimens.xml",
    "chars": 211,
    "preview": "<resources>\n    <!-- Default screen margins, per the Android Design guidelines. -->\n    <dimen name=\"activity_horizontal"
  },
  {
    "path": "exampleapp/src/main/res/values/strings.xml",
    "chars": 809,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <string name=\"app_name\">Matomo Example App</string>\n    <string "
  },
  {
    "path": "exampleapp/src/main/res/values/styles.xml",
    "chars": 194,
    "preview": "<resources>\n\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.DarkActionBar"
  },
  {
    "path": "exampleapp/src/main/res/values-w820dp/dimens.xml",
    "chars": 358,
    "preview": "<resources>\n    <!-- Example customization of dimensions originally defined in res/values/dimens.xml\n         (such as s"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "chars": 251,
    "preview": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributi"
  },
  {
    "path": "gradle.properties",
    "chars": 846,
    "preview": "# Project-wide Gradle settings.\n\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will o"
  },
  {
    "path": "gradlew",
    "chars": 8729,
    "preview": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Lice"
  },
  {
    "path": "gradlew.bat",
    "chars": 2966,
    "preview": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (th"
  },
  {
    "path": "jitpack.yml",
    "chars": 18,
    "preview": "jdk:\n  - openjdk17"
  },
  {
    "path": "settings.gradle",
    "chars": 41,
    "preview": "include \":exampleapp\"\ninclude \":tracker\"\n"
  },
  {
    "path": "tracker/build.gradle",
    "chars": 3405,
    "preview": "plugins {\n    id \"com.android.library\"\n    id \"kotlin-android\"\n    id \"maven-publish\"\n    id \"com.mxalbert.gradle.jacoco"
  },
  {
    "path": "tracker/lint.xml",
    "chars": 493,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<lint>\n    <issue id=\"TimberTagLength\" severity=\"ignore\"/>\n    <issue id=\"StringF"
  },
  {
    "path": "tracker/src/main/AndroidManifest.xml",
    "chars": 988,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n          xm"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/LegacySettingsPorter.kt",
    "chars": 2500,
    "preview": "package org.matomo.sdk\n\nimport android.content.SharedPreferences\nimport java.util.UUID\n\nclass LegacySettingsPorter(matom"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/Matomo.kt",
    "chars": 3074,
    "preview": "/*\n * Android SDK for Matomo\n *\n * @link https://github.com/matomo-org/matomo-android-sdk\n * @license https://github.com"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/QueryParams.java",
    "chars": 11123,
    "preview": "/*\n * Android SDK for Matomo\n *\n * @link https://github.com/matomo-org/matomo-android-sdk\n * @license https://github.com"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/TrackMe.java",
    "chars": 4185,
    "preview": "/*\n * Android SDK for Matomo\n *\n * @link https://github.com/matomo-org/matomo-android-sdk\n * @license https://github.com"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/Tracker.java",
    "chars": 21271,
    "preview": "/*\n * Android SDK for Matomo\n *\n * @link https://github.com/matomo-org/matomo-android-sdk\n * @license https://github.com"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/TrackerBuilder.java",
    "chars": 2693,
    "preview": "package org.matomo.sdk;\n\nimport java.net.MalformedURLException;\nimport java.net.URL;\n\n/**\n * Configuration details for a"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/dispatcher/DefaultDispatcher.kt",
    "chars": 9048,
    "preview": "/*\n * Android SDK for Matomo\n *\n * @link https://github.com/matomo-org/matomo-android-sdk\n * @license https://github.com"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/dispatcher/DefaultDispatcherFactory.kt",
    "chars": 448,
    "preview": "package org.matomo.sdk.dispatcher\n\nimport org.matomo.sdk.Tracker\nimport org.matomo.sdk.tools.Connectivity\n\nopen class De"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/dispatcher/DefaultPacketSender.kt",
    "chars": 5929,
    "preview": "package org.matomo.sdk.dispatcher\n\nimport org.matomo.sdk.Matomo.Companion.tag\nimport timber.log.Timber\nimport java.io.Bu"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/dispatcher/DispatchMode.java",
    "chars": 836,
    "preview": "package org.matomo.sdk.dispatcher;\n\nimport androidx.annotation.Nullable;\n\n\npublic enum DispatchMode {\n    /**\n     * Dis"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/dispatcher/Dispatcher.java",
    "chars": 2492,
    "preview": "/*\n * Android SDK for Matomo\n *\n * @link https://github.com/matomo-org/matomo-android-sdk\n * @license https://github.com"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/dispatcher/DispatcherFactory.kt",
    "chars": 143,
    "preview": "package org.matomo.sdk.dispatcher\n\nimport org.matomo.sdk.Tracker\n\ninterface DispatcherFactory {\n    fun build(tracker: T"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/dispatcher/Event.java",
    "chars": 2187,
    "preview": "package org.matomo.sdk.dispatcher;\n\n\nimport org.matomo.sdk.Matomo;\n\nimport java.net.URLEncoder;\nimport java.util.Map;\n\ni"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/dispatcher/EventCache.java",
    "chars": 1863,
    "preview": "package org.matomo.sdk.dispatcher;\n\n\nimport org.matomo.sdk.Matomo;\n\nimport java.util.ArrayList;\nimport java.util.List;\ni"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/dispatcher/EventDiskCache.java",
    "chars": 8589,
    "preview": "package org.matomo.sdk.dispatcher;\n\n\nimport org.matomo.sdk.Matomo;\nimport org.matomo.sdk.Tracker;\n\nimport java.io.Buffer"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/dispatcher/Packet.java",
    "chars": 2008,
    "preview": "/*\n * Android SDK for Matomo\n *\n * @link https://github.com/matomo-org/matomo-android-sdk\n * @license https://github.com"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/dispatcher/PacketFactory.java",
    "chars": 2882,
    "preview": "/*\n * Android SDK for Matomo\n *\n * @link https://github.com/matomo-org/matomo-android-sdk\n * @license https://github.com"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/dispatcher/PacketSender.kt",
    "chars": 276,
    "preview": "package org.matomo.sdk.dispatcher\n\n\ninterface PacketSender {\n    /**\n     * @return true if successful\n     */\n    fun s"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/extra/CustomDimension.java",
    "chars": 2506,
    "preview": "package org.matomo.sdk.extra;\n\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport org.mat"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/extra/CustomVariables.java",
    "chars": 5134,
    "preview": "/*\n * Android SDK for Matomo\n *\n * @link https://github.com/matomo-org/matomo-android-sdk\n * @license https://github.com"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/extra/DimensionQueue.java",
    "chars": 1668,
    "preview": "package org.matomo.sdk.extra;\n\nimport org.matomo.sdk.Matomo;\nimport org.matomo.sdk.TrackMe;\nimport org.matomo.sdk.Tracke"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/extra/DownloadTracker.java",
    "chars": 7727,
    "preview": "package org.matomo.sdk.extra;\n\n\nimport android.content.ContentValues;\nimport android.content.Context;\nimport android.con"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/extra/EcommerceItems.java",
    "chars": 3240,
    "preview": "/*\n * Android SDK for Matomo\n *\n * @link https://github.com/matomo-org/matomo-android-sdk\n * @license https://github.com"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/extra/InstallReferrerReceiver.java",
    "chars": 2066,
    "preview": "package org.matomo.sdk.extra;\n\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android."
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/extra/MatomoApplication.java",
    "chars": 1492,
    "preview": "/*\n * Android SDK for Matomo\n *\n * @link https://github.com/matomo-org/matomo-android-sdk\n * @license https://github.com"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/extra/MatomoExceptionHandler.java",
    "chars": 2526,
    "preview": "/*\n * Android SDK for Matomo\n *\n * @link https://github.com/matomo-org/matomo-android-sdk\n * @license https://github.com"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/extra/TrackHelper.java",
    "chars": 33442,
    "preview": "package org.matomo.sdk.extra;\n\n\nimport android.app.Activity;\nimport android.app.Application;\nimport android.os.Bundle;\n\n"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/tools/ActivityHelper.java",
    "chars": 897,
    "preview": "package org.matomo.sdk.tools;\n\nimport android.app.Activity;\nimport android.text.TextUtils;\n\nimport java.util.ArrayList;\n"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/tools/BuildInfo.java",
    "chars": 293,
    "preview": "package org.matomo.sdk.tools;\n\n\nimport android.os.Build;\n\npublic class BuildInfo {\n    public String getRelease() {\n    "
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/tools/Checksum.java",
    "chars": 1781,
    "preview": "/*\n * Android SDK for Matomo\n *\n * @link https://github.com/matomo-org/matomo-android-sdk\n * @license https://github.com"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/tools/Connectivity.java",
    "chars": 1070,
    "preview": "package org.matomo.sdk.tools;\n\n\nimport android.content.Context;\nimport android.net.ConnectivityManager;\nimport android.n"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/tools/CurrencyFormatter.java",
    "chars": 518,
    "preview": "/*\n * Android SDK for Matomo\n *\n * @link https://github.com/matomo-org/matomo-android-sdk\n * @license https://github.com"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/tools/DeviceHelper.java",
    "chars": 3212,
    "preview": "/*\n * Android SDK for Matomo\n *\n * @link https://github.com/matomo-org/matomo-android-sdk\n * @license https://github.com"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/tools/PropertySource.java",
    "chars": 426,
    "preview": "package org.matomo.sdk.tools;\n\n\nimport androidx.annotation.Nullable;\n\npublic class PropertySource {\n    @Nullable\n    pu"
  },
  {
    "path": "tracker/src/main/java/org/matomo/sdk/tools/UrlHelper.java",
    "chars": 2364,
    "preview": "/*\n *\n *  * Android SDK for Matomo\n *  *\n *  * @link https://github.com/matomo-org/matomo-android-sdk\n *  * @license htt"
  },
  {
    "path": "tracker/src/test/java/org/matomo/sdk/LegacySettingsPorterTest.java",
    "chars": 7173,
    "preview": "package org.matomo.sdk;\n\n\nimport android.annotation.SuppressLint;\nimport android.content.SharedPreferences;\n\nimport org."
  },
  {
    "path": "tracker/src/test/java/org/matomo/sdk/MatomoTest.java",
    "chars": 5453,
    "preview": "/*\n * Android SDK for Matomo\n *\n * @link https://github.com/matomo-org/matomo-android-sdk\n * @license https://github.com"
  },
  {
    "path": "tracker/src/test/java/org/matomo/sdk/TrackMeTest.java",
    "chars": 4348,
    "preview": "package org.matomo.sdk;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnitRu"
  },
  {
    "path": "tracker/src/test/java/org/matomo/sdk/TrackerBuilderTest.java",
    "chars": 2503,
    "preview": "package org.matomo.sdk;\n\nimport android.content.Context;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport"
  },
  {
    "path": "tracker/src/test/java/org/matomo/sdk/TrackerTest.java",
    "chars": 37471,
    "preview": "package org.matomo.sdk;\n\nimport android.content.Context;\nimport android.content.SharedPreferences;\n\nimport org.junit.Bef"
  },
  {
    "path": "tracker/src/test/java/org/matomo/sdk/dispatcher/DefaultDispatcherTest.java",
    "chars": 19182,
    "preview": "/*\n * Android SDK for Matomo\n *\n * @link https://github.com/matomo-org/matomo-android-sdk\n * @license https://github.com"
  },
  {
    "path": "tracker/src/test/java/org/matomo/sdk/dispatcher/DefaultPacketSenderTest.java",
    "chars": 3799,
    "preview": "package org.matomo.sdk.dispatcher;\n\nimport org.json.JSONObject;\nimport org.junit.After;\nimport org.junit.Before;\nimport "
  },
  {
    "path": "tracker/src/test/java/org/matomo/sdk/dispatcher/EventCacheTest.java",
    "chars": 4495,
    "preview": "package org.matomo.sdk.dispatcher;\n\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nim"
  },
  {
    "path": "tracker/src/test/java/org/matomo/sdk/dispatcher/EventDiskCacheTest.java",
    "chars": 11923,
    "preview": "package org.matomo.sdk.dispatcher;\n\nimport android.content.Context;\n\nimport org.junit.After;\nimport org.junit.Before;\nim"
  },
  {
    "path": "tracker/src/test/java/org/matomo/sdk/dispatcher/EventTest.java",
    "chars": 2821,
    "preview": "/*\n * Android SDK for Matomo\n *\n * @link https://github.com/matomo-org/matomo-android-sdk\n * @license https://github.com"
  },
  {
    "path": "tracker/src/test/java/org/matomo/sdk/dispatcher/PacketFactoryTest.java",
    "chars": 4427,
    "preview": "package org.matomo.sdk.dispatcher;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.Moc"
  },
  {
    "path": "tracker/src/test/java/org/matomo/sdk/dispatcher/PacketTest.java",
    "chars": 710,
    "preview": "package org.matomo.sdk.dispatcher;\n\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.Mo"
  },
  {
    "path": "tracker/src/test/java/org/matomo/sdk/extra/CustomDimensionTest.java",
    "chars": 2241,
    "preview": "package org.matomo.sdk.extra;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.matomo.sdk.TrackMe;\nim"
  },
  {
    "path": "tracker/src/test/java/org/matomo/sdk/extra/CustomVariablesTest.java",
    "chars": 4863,
    "preview": "package org.matomo.sdk.extra;\n\nimport org.json.JSONArray;\nimport org.json.JSONObject;\nimport org.junit.Test;\nimport org."
  },
  {
    "path": "tracker/src/test/java/org/matomo/sdk/extra/DimensionQueueTest.java",
    "chars": 2981,
    "preview": "package org.matomo.sdk.extra;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.matomo.sdk.TrackMe;\nim"
  },
  {
    "path": "tracker/src/test/java/org/matomo/sdk/extra/DownloadTrackerTest.java",
    "chars": 8391,
    "preview": "package org.matomo.sdk.extra;\n\nimport android.content.Context;\nimport android.content.SharedPreferences;\nimport android."
  },
  {
    "path": "tracker/src/test/java/org/matomo/sdk/extra/EcommerceItemsTest.java",
    "chars": 3020,
    "preview": "package org.matomo.sdk.extra;\n\nimport org.json.JSONArray;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport"
  },
  {
    "path": "tracker/src/test/java/org/matomo/sdk/extra/InstallReferrerReceiverTest.java",
    "chars": 4124,
    "preview": "package org.matomo.sdk.extra;\n\nimport android.content.Intent;\n\nimport org.junit.Test;\n\nimport androidx.test.core.app.App"
  },
  {
    "path": "tracker/src/test/java/org/matomo/sdk/extra/MatomoApplicationTest.java",
    "chars": 1944,
    "preview": "package org.matomo.sdk.extra;\n\nimport android.app.Application;\n\nimport org.junit.Assert;\nimport org.junit.Test;\nimport o"
  },
  {
    "path": "tracker/src/test/java/org/matomo/sdk/extra/TrackHelperTest.java",
    "chars": 22160,
    "preview": "package org.matomo.sdk.extra;\n\nimport android.content.Context;\nimport android.content.pm.PackageInfo;\nimport android.con"
  },
  {
    "path": "tracker/src/test/java/org/matomo/sdk/tools/BuildInfoTest.kt",
    "chars": 798,
    "preview": "package org.matomo.sdk.tools\n\nimport android.os.Build\nimport org.junit.Assert\nimport org.junit.Before\nimport org.junit.T"
  },
  {
    "path": "tracker/src/test/java/org/matomo/sdk/tools/ChecksumTest.kt",
    "chars": 757,
    "preview": "package org.matomo.sdk.tools\n\nimport org.junit.Assert\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.m"
  },
  {
    "path": "tracker/src/test/java/org/matomo/sdk/tools/ConnectivityTest.kt",
    "chars": 1791,
    "preview": "package org.matomo.sdk.tools\n\nimport android.content.Context\nimport android.net.ConnectivityManager\nimport android.net.N"
  },
  {
    "path": "tracker/src/test/java/org/matomo/sdk/tools/CurrencyFormatterTest.kt",
    "chars": 765,
    "preview": "package org.matomo.sdk.tools\n\nimport org.junit.Assert\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.m"
  },
  {
    "path": "tracker/src/test/java/org/matomo/sdk/tools/DeviceHelperTest.kt",
    "chars": 1842,
    "preview": "package org.matomo.sdk.tools\n\nimport android.content.Context\nimport org.junit.Assert\nimport org.junit.Before\nimport org."
  },
  {
    "path": "tracker/src/test/java/org/matomo/sdk/tools/PropertySourceTest.kt",
    "chars": 810,
    "preview": "package org.matomo.sdk.tools\n\nimport org.junit.Assert\nimport org.junit.Test\nimport org.mockito.Mockito\nimport testhelper"
  },
  {
    "path": "tracker/src/test/java/testhelpers/BaseTest.kt",
    "chars": 355,
    "preview": "package testhelpers\n\nimport org.junit.After\nimport org.junit.Before\nimport timber.log.Timber\nimport timber.log.Timber.Fo"
  },
  {
    "path": "tracker/src/test/java/testhelpers/DefaultTestCase.kt",
    "chars": 793,
    "preview": "package testhelpers\n\nimport androidx.test.core.app.ApplicationProvider\nimport org.junit.runner.RunWith\nimport org.matomo"
  },
  {
    "path": "tracker/src/test/java/testhelpers/FullEnvTestLifeCycle.kt",
    "chars": 388,
    "preview": "/*\n * Android SDK for Matomo\n *\n * @link https://github.com/matomo-org/matomo-android-sdk\n * @license https://github.com"
  },
  {
    "path": "tracker/src/test/java/testhelpers/FullEnvTestRunner.kt",
    "chars": 775,
    "preview": "/*\n * Android SDK for Matomo\n *\n * @link https://github.com/matomo-org/matomo-android-sdk\n * @license https://github.com"
  },
  {
    "path": "tracker/src/test/java/testhelpers/JUnitTree.kt",
    "chars": 769,
    "preview": "package testhelpers\n\nimport android.util.Log\nimport timber.log.Timber\n\nclass JUnitTree : Timber.DebugTree() {\n    privat"
  },
  {
    "path": "tracker/src/test/java/testhelpers/MatomoTestApplication.kt",
    "chars": 767,
    "preview": "package testhelpers\n\nimport org.matomo.sdk.TrackerBuilder\nimport org.matomo.sdk.extra.MatomoApplication\nimport org.robol"
  },
  {
    "path": "tracker/src/test/java/testhelpers/QueryHashMap.kt",
    "chars": 254,
    "preview": "package testhelpers\n\nimport org.matomo.sdk.QueryParams\nimport org.matomo.sdk.TrackMe\n\nclass QueryHashMap(trackMe: TrackM"
  },
  {
    "path": "tracker/src/test/java/testhelpers/TestActivity.kt",
    "chars": 365,
    "preview": "package testhelpers\n\nimport android.app.Activity\nimport android.os.Bundle\n\nclass TestActivity : Activity() {\n    public "
  },
  {
    "path": "tracker/src/test/java/testhelpers/TestHelper.kt",
    "chars": 245,
    "preview": "package testhelpers\n\nimport timber.log.Timber\n\nobject TestHelper {\n    @JvmStatic\n    fun sleep(millis: Long) {\n        "
  },
  {
    "path": "tracker/src/test/java/testhelpers/TestPreferences.kt",
    "chars": 3075,
    "preview": "package testhelpers\n\nimport android.content.SharedPreferences\nimport android.content.SharedPreferences.OnSharedPreferenc"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the matomo-org/matomo-sdk-android GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 105 files (369.1 KB), approximately 86.7k tokens, and a symbol index with 598 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!