Full Code of amplitude/Amplitude-Android for AI

main 089265eb1ff7 cached
78 files
691.8 KB
157.1k tokens
1019 symbols
1 requests
Download .txt
Showing preview only (725K chars total). Download the full file or copy to clipboard to get everything.
Repository: amplitude/Amplitude-Android
Branch: main
Commit: 089265eb1ff7
Files: 78
Total size: 691.8 KB

Directory structure:
gitextract_d5k27ryi/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── Bug_report.md
│   │   ├── Feature_request.md
│   │   └── Question.md
│   └── workflows/
│       ├── codeql-analysis.yml
│       ├── jira-issue-create.yml
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── build.gradle
├── gradle.properties
├── gradlew
├── gradlew.bat
├── package.json
├── release.config.js
├── settings.gradle
└── src/
    ├── main/
    │   ├── AndroidManifest.xml
    │   ├── java/
    │   │   └── com/
    │   │       └── amplitude/
    │   │           ├── api/
    │   │           │   ├── Amplitude.java
    │   │           │   ├── AmplitudeCallbacks.java
    │   │           │   ├── AmplitudeClient.java
    │   │           │   ├── AmplitudeDeviceIdCallback.java
    │   │           │   ├── AmplitudeLog.java
    │   │           │   ├── AmplitudeLogCallback.java
    │   │           │   ├── AmplitudeServerZone.java
    │   │           │   ├── ConfigManager.java
    │   │           │   ├── Constants.java
    │   │           │   ├── CursorWindowAllocationException.java
    │   │           │   ├── DatabaseHelper.java
    │   │           │   ├── DatabaseResetListener.java
    │   │           │   ├── DeviceInfo.java
    │   │           │   ├── Identify.java
    │   │           │   ├── IdentifyInterceptor.java
    │   │           │   ├── IngestionMetadata.java
    │   │           │   ├── Middleware.java
    │   │           │   ├── MiddlewareExtended.java
    │   │           │   ├── MiddlewareExtra.java
    │   │           │   ├── MiddlewareNext.java
    │   │           │   ├── MiddlewarePayload.java
    │   │           │   ├── MiddlewareRunner.java
    │   │           │   ├── PinnedAmplitudeClient.java
    │   │           │   ├── Plan.java
    │   │           │   ├── Revenue.java
    │   │           │   ├── TrackingOptions.java
    │   │           │   ├── Utils.java
    │   │           │   └── WorkerThread.java
    │   │           ├── eventexplorer/
    │   │           │   ├── EventExplorer.java
    │   │           │   ├── EventExplorerInfoActivity.java
    │   │           │   └── EventExplorerTouchHandler.java
    │   │           ├── unity/
    │   │           │   └── plugins/
    │   │           │       └── AmplitudePlugin.java
    │   │           └── util/
    │   │               ├── DoubleCheck.java
    │   │               └── Provider.java
    │   └── res/
    │       ├── drawable/
    │       │   └── amp_button_bg.xml
    │       ├── layout/
    │       │   ├── amp_activity_eventexplorer_info.xml
    │       │   └── amp_bubble_view.xml
    │       └── values/
    │           ├── colors.xml
    │           └── strings.xml
    └── test/
        └── java/
            └── com/
                └── amplitude/
                    └── api/
                        ├── AmplitudeClientTest.java
                        ├── AmplitudeServerZoneTest.java
                        ├── AmplitudeTest.java
                        ├── BaseTest.java
                        ├── ConfigManagerTest.java
                        ├── DatabaseHelperTest.java
                        ├── DatabaseRecoveryTest.java
                        ├── DeviceInfoTest.java
                        ├── IdentifyTest.java
                        ├── IngestionMetadataTest.java
                        ├── InitializeTest.java
                        ├── MiddlewareRunnerTest.java
                        ├── MockGeocoder.java
                        ├── PinningTest.java
                        ├── PlanTest.java
                        ├── RevenueTest.java
                        ├── SessionTest.java
                        ├── TrackingOptionsTest.java
                        └── util/
                            ├── MockHttpURLConnectionHelper.java
                            └── MockURLStreamHandler.java

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

================================================
FILE: .github/ISSUE_TEMPLATE/Bug_report.md
================================================
---
name: Bug report 🐛
about: You're having technical issues
labels: 'bug'
---

<!--- Please fill out the template to the best of your ability -->

## Expected Behavior
<!--- What should have happened? -->

## Current Behavior
<!--- What went wrong? -->

## Possible Solution
<!--- (Not obligatory) Suggest a fix/reason -->

## Steps to Reproduce
<!--- Please provide a clear sequence of steps to reproduce this bug --> 
<!--- Include code and images, if relevant -->
1.
2.
3.
4.

## Environment
- SDK Version: <!-- E.g. v2.28.0 -->
- Android API Level: <!-- E.g. 8.1.0 -->
- Device: <!--- E.g. Samsung Galaxy S20-->


================================================
FILE: .github/ISSUE_TEMPLATE/Feature_request.md
================================================
---
name: Feature Request 🚀
about: You'd like something added to the SDK
labels: 'feature request'
---

<!--- Please fill out the template to the best of your ability -->

## Summary

<!-- Please describe what feature you would like added -->

## Motivations

<!-- Please explain what value this feature would add. E.g. what problem does it solve -->


================================================
FILE: .github/ISSUE_TEMPLATE/Question.md
================================================
---
name: Question ❓
about: Ask a question
labels: 'question'
---

## Summary

<!-- What do you need help with? -->


================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"

on:
  push:
    branches: [ main ]
  pull_request:
    # The branches below must be a subset of the branches above
    branches: [ main ]

jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-latest
    permissions:
      actions: read
      contents: read
      security-events: write

    strategy:
      fail-fast: false
      matrix:
        language: [ 'java' ]
        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
        # Learn more:
        # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed

    steps:
    - name: Checkout repository
      uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3

    # Initializes the CodeQL tools for scanning.
    - name: Initialize CodeQL
      uses: github/codeql-action/init@b8d3b6e8af63cde30bdc382c0bc28114f4346c88 # v2
      with:
        languages: ${{ matrix.language }}
        # If you wish to specify custom queries, you can do so here or in a config file.
        # By default, queries listed here will override any specified in a config file.
        # Prefix the list here with "+" to use these queries and those in the config file.
        # queries: ./path/to/local/query, your-org/your-repo/queries@main

    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
    # If this step fails, then you should remove it and run the build manually (see below)
    - name: Autobuild
      uses: github/codeql-action/autobuild@b8d3b6e8af63cde30bdc382c0bc28114f4346c88 # v2

    # ℹ️ Command-line programs to run using the OS shell.
    # 📚 https://git.io/JvXDl

    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
    #    and modify them (or add more) to build your code if your project
    #    uses a compiled language

    #- run: |
    #   make bootstrap
    #   make release

    - name: Perform CodeQL Analysis
      uses: github/codeql-action/analyze@b8d3b6e8af63cde30bdc382c0bc28114f4346c88 # v2


================================================
FILE: .github/workflows/jira-issue-create.yml
================================================
# Creates jira tickets for new github issues to help triage
name: Jira Issue Creator For Android

on:
  issues:
    types: [opened]
  workflow_call:
    inputs:
      label:
        type: string

jobs:
  call-workflow-passing-data:
    uses: amplitude/Amplitude-TypeScript/.github/workflows/jira-issue-create-template.yml@8dadabbe62161729e3aa83c0d664e106b748c8cc # @amplitude/plugin-session-replay-react-native@0.4.9
    with:
      label: "Android"
      subcomponent: "dx_legacy_android_sdk"
    secrets:
      JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
      JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
      JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
      JIRA_PROJECT: ${{ secrets.JIRA_PROJECT }}


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

on:
  workflow_dispatch:
    inputs:
      dryRun:
        description: 'Do a dry run to preview instead of a real release'
        required: true
        default: 'true'

jobs:
  authorize:
    name: Authorize
    runs-on: ubuntu-latest
    steps:
      - name: ${{ github.actor }} permission check to do a release
        uses: "lannonbr/repo-permission-check-action@2bb8c89ba8bf115c4bfab344d6a6f442b24c9a1f" # 2.0.2
        with:
          permission: "write"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  release:
    name: Release
    runs-on: ubuntu-latest
    needs: [authorize]
    steps:
      - name: Checkout
        uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

      - name: Set up JDK 8
        uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4
        with:
          java-version: '8'
          distribution: 'zulu'

      - name: Build
        run: ./gradlew build

      - name: Test
        run: ./gradlew test --info

      - name: Configure GPG
        env:
          GPG_KEY_CONTENTS: ${{ secrets.GPG_KEY_CONTENTS }}
          SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }}
        run: |
          sudo bash -c "echo '$GPG_KEY_CONTENTS' | base64 -d > '$SIGNING_SECRET_KEY_RING_FILE'"

      - name: Configure Sonatype
        env:
          GRADLE_PROP_FILE: local.properties
        run: |
          echo "sonatypeUsername=${{ secrets.OSSRH_USERNAME }}" >> $GRADLE_PROP_FILE
          echo "sonatypePassword=${{ secrets.OSSRH_PASSWORD }}" >> $GRADLE_PROP_FILE
          echo "sonatypeStagingProfileId=${{ secrets.SONATYPE_STAGING_PROFILE_ID }}" >> $GRADLE_PROP_FILE
          echo "signing.keyId=${{ secrets.SIGNING_KEY_ID }}" >> $GRADLE_PROP_FILE
          echo "signing.password=${{ secrets.SIGNING_PASSWORD }}" >> $GRADLE_PROP_FILE
          echo "signing.secretKeyRingFile=${{ secrets.SIGNING_SECRET_KEY_RING_FILE }}" >> $GRADLE_PROP_FILE

      - name: Semantic Release --dry-run
        if: ${{ github.event.inputs.dryRun == 'true'}}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GIT_AUTHOR_NAME: amplitude-sdk-bot
          GIT_AUTHOR_EMAIL: amplitude-sdk-bot@users.noreply.github.com
          GIT_COMMITTER_NAME: amplitude-sdk-bot
          GIT_COMMITTER_EMAIL: amplitude-sdk-bot@users.noreply.github.com
        run: |
          npm ci
          npm exec semantic-release -- --dry-run

      - name: Semantic Release
        if: ${{ github.event.inputs.dryRun == 'false'}}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GIT_AUTHOR_NAME: amplitude-sdk-bot
          GIT_AUTHOR_EMAIL: amplitude-sdk-bot@users.noreply.github.com
          GIT_COMMITTER_NAME: amplitude-sdk-bot
          GIT_COMMITTER_EMAIL: amplitude-sdk-bot@users.noreply.github.com
        run: |
          npm ci
          npm exec semantic-release


================================================
FILE: .github/workflows/test.yml
================================================
name: Build and Test

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  test:
    name: Build and Test
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

      - name: Set up JDK 8
        uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4
        with:
          java-version: '8'
          distribution: 'zulu'

      - name: Build
        run: ./gradlew build

      - name: Test
        run: ./gradlew test --info


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

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

# Android Studio generated folders
.navigation/
captures/
.externalNativeBuild
caches/
daemon/
native/
wrapper/

# IntelliJ project files
*.iml
.idea/

# Java binary files
*.class

# Misc
.DS_Store
*.log
*.asc
*.bak
changes.txt
release.sh
node_modules/


================================================
FILE: CHANGELOG.md
================================================
## [2.40.3](https://github.com/amplitude/Amplitude-Android/compare/v2.40.2...v2.40.3) (2025-04-29)


### Bug Fixes

* default location listening to false ([#407](https://github.com/amplitude/Amplitude-Android/issues/407)) ([27d3758](https://github.com/amplitude/Amplitude-Android/commit/27d3758fd8427d44e15813356ada2e3234ad62b8))

## [2.40.2](https://github.com/amplitude/Amplitude-Android/compare/v2.40.1...v2.40.2) (2024-06-12)


### Bug Fixes

* catch all exceptions when trying to intercept identifies ([#402](https://github.com/amplitude/Amplitude-Android/issues/402)) ([f0350bf](https://github.com/amplitude/Amplitude-Android/commit/f0350bf5dff450fe34c1a9095b16082f2d6c5353))

## [2.40.1](https://github.com/amplitude/Amplitude-Android/compare/v2.40.0...v2.40.1) (2024-04-17)


### Bug Fixes

* always run middleware flush on updateServer ([#400](https://github.com/amplitude/Amplitude-Android/issues/400)) ([fbee357](https://github.com/amplitude/Amplitude-Android/commit/fbee357d6ba9eb540101cf09393f7eebafdfd49d))

# [2.40.0](https://github.com/amplitude/Amplitude-Android/compare/v2.39.9...v2.40.0) (2024-04-17)


### Features

* middleware session replay integration ([#399](https://github.com/amplitude/Amplitude-Android/issues/399)) ([28bbbe6](https://github.com/amplitude/Amplitude-Android/commit/28bbbe6ad2d0a0fe6424645e6105ecb8c2be7a4f))

## [2.39.9](https://github.com/amplitude/Amplitude-Android/compare/v2.39.8...v2.39.9) (2024-02-27)


### Bug Fixes

* move identifyInterceptor before eventBridge receiver is set ([#392](https://github.com/amplitude/Amplitude-Android/issues/392)) ([a6d4c9d](https://github.com/amplitude/Amplitude-Android/commit/a6d4c9d1ad8cfc99420e92ed39164feaf3e5eac1))

## [2.39.8](https://github.com/amplitude/Amplitude-Android/compare/v2.39.7...v2.39.8) (2023-07-21)


### Bug Fixes

* try to correctly handle session change for race condition ([#382](https://github.com/amplitude/Amplitude-Android/issues/382)) ([b0f4fea](https://github.com/amplitude/Amplitude-Android/commit/b0f4fea58d808129c0bd46a7624f8a1e541bc6c6))

## [2.39.7](https://github.com/amplitude/Amplitude-Android/compare/v2.39.6...v2.39.7) (2023-07-07)


### Bug Fixes

* try to reduce sqlite cursor memory usage ([#375](https://github.com/amplitude/Amplitude-Android/issues/375)) ([77e508d](https://github.com/amplitude/Amplitude-Android/commit/77e508d9b8cbaf36328f3176e89c04f502c606c6))

## [2.39.6](https://github.com/amplitude/Amplitude-Android/compare/v2.39.5...v2.39.6) (2023-07-06)


### Bug Fixes

* remove md5 usage ([#372](https://github.com/amplitude/Amplitude-Android/issues/372)) ([c849590](https://github.com/amplitude/Amplitude-Android/commit/c84959086f9a645f87a3175caaec3022154ee6bb))

## [2.39.5](https://github.com/amplitude/Amplitude-Android/compare/v2.39.4...v2.39.5) (2023-06-13)


### Bug Fixes

* do not fetch advertising Id if adid tracking is disabled ([#366](https://github.com/amplitude/Amplitude-Android/issues/366)) ([a1f8cc8](https://github.com/amplitude/Amplitude-Android/commit/a1f8cc80af0bbaf2b7edc3b47b87c3f518ea8433))

## [2.39.4](https://github.com/amplitude/Amplitude-Android/compare/v2.39.3...v2.39.4) (2023-06-01)


### Bug Fixes

* move inForeground reads/writes to main thread ([#362](https://github.com/amplitude/Amplitude-Android/issues/362)) ([15b4b35](https://github.com/amplitude/Amplitude-Android/commit/15b4b35187da132adeeb35e6726e5b76bc48f75f))

## [2.39.3](https://github.com/amplitude/Amplitude-Android/compare/v2.39.2...v2.39.3) (2023-04-29)


### Bug Fixes

* filter null value in identify user properties ([#356](https://github.com/amplitude/Amplitude-Android/issues/356)) ([779b6b3](https://github.com/amplitude/Amplitude-Android/commit/779b6b3246e01b0c408ecae7437b7f40c7067f22))
* update identify interceptor to identify only ([#357](https://github.com/amplitude/Amplitude-Android/issues/357)) ([afd3251](https://github.com/amplitude/Amplitude-Android/commit/afd3251f75c30b130a42805e49b31673916cedf1))

### As of September 21, 2020 CHANGELOG.md is no longer manually updated. 
Please check the [releases page](https://github.com/amplitude/Amplitude-Android/releases) for up to date changes.

## 2.28.2 (Sep 13, 2020)
* Add `setMinTimeBetweenSessionsMillis` in plugin for Unity Plugin to use.

## 2.28.1 (Aug 26, 2020)
* Add `setOffline` in plugin for Unity Plugin to use.

## 2.28.0 (Aug 10, 2020)

* Introducing useDynamicConfig flag!! Turning this flag on will find the best server url automatically based on users' geo location.
* Note 1. If you have your own proxy server and use setServerUrl API, please leave this OFF.
* Note 2. If you have users in China Mainland, we suggest you turn this on.
* Note 3. By default, this feature is OFF. So you need to explicitly set it to ON to use it.

## 2.27.0 (Jul 14, 2020)

* Added setServerUrl to `AmplitudePlugin` to enable it for Unity SDK too.
* Fix an issue during location fetching.

## 2.26.1 (Jun 15, 2020)

* Fix the incorrect behavior of `disableLocationListening`. If you want to disable location listening over LocationManager. Please call called before initialization.

## 2.26.0 (Jun 2, 2020)

* Remove ComodoRSA certificate for SSL pinning.

## 2.25.2 (May 13, 2020)

* Add 3 APIs to `AmplitudePlugin` (`uploadEvents`, `useAdvertisingIdForDeviceId`, `setDeviceId`)

## 2.25.1 (Apr 3, 2020)

* Remove the declaration of location related permissions in manifest file.

## 2.25.0 (Mar 17, 2020)

* Added APIs to `AmplitudeClient` to let users set library name and version. This should be only used when you develop your own library which wraps Amplitude Android SDK.

## 2.24.2 (Feb 5, 2020)

* Now you can set auth token! Use `AmplitudeClient#setBearerToken(String token)` please!

## 2.24.1 (Jan 28, 2020)

* Fix the issue that `version` property shows old version.

## 2.24.0 (Jan 28, 2020)

* Now you can enable or disable COPPA (Children's Online Privacy Protection Act) restrictions on ADID, city, IP address and location tracking. `AmplitudeClient#enableCoppaControl()` and `AmplitudeClient#disableCoppaControl()`

## 2.23.2 (Aug 05, 2019)

* Catch exceptions when fetching most recent location.

## 2.23.1 (Jul 19, 2019)

* Handle SQLite database crashes caused by fetching events that exceed 2MB (max size of cursor window).

## 2.23.0 (Apr 22, 2019)

* Make `startNewSessionIfNeeded` a public method. Only call this if you know what you are doing. This may trigger a new session to start.

## 2.22.1 (Mar 21, 2019)

* Store deviceId in SharedPreferences as backup in case SQLite database fails or becomes corrupted.

## 2.22.0 (Jan 18, 2019)

* Add ability to set a custom server URL for uploading events using `setServerUrl`.

## 2.21.0 (Dec 05, 2018)

* Update SDK to better handle when the SQLite database file gets corrupted between interactions.
* Add optional diagnostic logging that tracks exceptions thrown in the SDK and sends to Amplitude.

## 2.20.0 (Oct 15, 2018)

* Add ability to set group properties via a new `groupIdentify` method that takes in an `Identify` object as well as a group type and group name.

## 2.19.1 (Aug 14, 2018)

* Update SDK to better handle SQLite Exceptions.

## 2.19.0 (Jul 24, 2018)

* Add `TrackingOptions` interface to customize the automatic tracking of user properties in the SDK (such as language, ip_address, platform, etc). See [Help Center Documentation](https://amplitude.zendesk.com/hc/en-us/articles/115002935588#disable-automatic-tracking-of-properties) for instructions on setting up this configuration.

## 2.18.2 (Jul 24, 2018)

* Use randomly generated device id if user has limitAdTracking enabled.

## 2.18.1 (May 07, 2018)

* Updating to [OkHttp 3.10.0](https://github.com/square/okhttp/blob/master/CHANGELOG.md#version-3100)
* Lowering event upload max batch size from 100 to 50. This should help to avoid out of memory issues on Android devices with low memory.

## 2.18.0 (Apr 19, 2018)

* Added a `setUserId` method with optional boolean argument `startNewSession`, which when `true` starts a new session after changing the userId.

## 2.17.0 (Feb 05, 2018)

* Add ability to specify a custom `platform` value during initialization as an input argument. If the value is `null` or an empty string then `platform` will default to `Android`.

## 2.16.0 (Nov 27, 2017)

* Expose a public `getUserPropertiesOperations` method on the `Identify` class.
* Handle exceptions when the LocationManager is not available for fetching location.

## 2.15.0 (Oct 04, 2017)

* Updating to latest version of OkHttp3 ([3.9.0](https://github.com/square/okhttp/blob/master/CHANGELOG.md#version-390))

## 2.14.1 (Jul 27, 2017)

* Switch to an internal implementation of `isEmptyString` instead of Android TextUtils.

## 2.14.0 (Jul 05, 2017)

* Add support for logging events to multiple Amplitude apps. See our [Help Center Documentation](https://amplitude.zendesk.com/hc/en-us/articles/115002935588#logging-events-to-multiple-projects) for details.

## 2.13.4 (May 09, 2017)

* Handle exceptions when fetching device carrier information. Thanks to @fkam-tt for the pull request.
* Copy userProperties on main thread in `setUserProperties` to prevent ConcurrentModificationExceptions.
* Migrating setup instructions and SDK documentation in the README file to Zendesk articles.

## 2.13.3 (Mar 13, 2017)

* Handle exceptions when reading from database. Only affects certain Fairphone and LG devices.
* Handle exceptions when building request to upload event data. Only affects certain Lenovo devices.

## 2.13.2 (Dec 22, 2016)

* Fix crash when pulling null unsent event strings during upload.
* Fix bug where unserializable events were being saved to unsent events table.
* Added more logging around JSON serialization errors when logging events.

## 2.13.1 (Dec 15, 2016)

* Fix bug where `regenerateDeviceId` was not being run on background thread. DeviceInfo.generateUUID() should be a static method.

## 2.13.0 (Dec 05, 2016)

* Add helper method to regenerate a new random deviceId. This can be used in conjunction with `setUserId(null)` to anonymize a user after they log out. Note this is not recommended unless you know what you are doing. See [Readme](https://github.com/amplitude/Amplitude-Android#logging-out-and-anonymous-users) for more information.

## 2.12.0 (Nov 07, 2016)

* Allow `logEvent` with a custom timestamp (milliseconds since epoch). See [documentation](https://rawgit.com/amplitude/Amplitude-Android/master/javadoc/com/amplitude/api/AmplitudeClient.html#logEvent-java.lang.String-org.json.JSONObject-org.json.JSONObject-org.json.JSONObject-org.json.JSONObject-long-boolean-) for more details.

## 2.11.0 (Oct 26, 2016)

* Add ability to log identify events outOfSession, this is useful for updating user properties without triggering session-handling logic. See [Readme](https://github.com/amplitude/Amplitude-Android#tracking-sessions) for more information.

## 2.10.0 (Oct 12, 2016)

* Catch and handle `CursorWindowAllocationException` thrown when the SDK is querying from the SQLite DB when app memory is low. If the exception is caught during `initialize`, then it is treated as if `initialize` was never called. If the exception is caught during the uploading of unsent events, then the upload is deferred to a later time.
* Block event property and user property dictionaries that have more than 1000 items. This is to block properties that are set unintentionally (for example in a loop). A single call to `logEvent` should not have more than 1000 event properties. Similarly a single call to `setUserProperties` should not have more than 1000 user properties.
* Handle IllegalArgumentException thrown by Android Geocoder for bad lat / lon values.

## 2.9.2 (Jul 14, 2016)

* Fix bug where `enableLocationListening` and `disableLocationListening` were not being run on background thread. Thanks to @elevenfive for PR.
* Update `Revenue` class to expose public `equals` and `hashCode` methods.

## 2.9.1 (Jul 11, 2016)

* Fix bug where `setOptOut` was not being run on background thread.
* `productId` is no longer a required field for `Revenue` logged via `logRevenueV2`.
* Fix bug where receipt and receiptSignature were being truncated if they were too long (exceeded 1024 characters).

## 2.9.0 (Jul 07, 2016)

* Add automatic flushing of unsent events on app close/minimize (through the Activity Lifecycle `onPause` callback). This only works if you call `Amplitude.getInstance().enableForegroundTracking(getApplication());`, which is recommended in the README by default for Setup. To disable you can call `Amplitude.getInstance().setFlushEventsOnClose(false);`

## 2.8.0 (Jun 29, 2016)

* Run the `initialize` logic on the background thread so that the SQLite database operations do not delay the main thread.
* Add support for Amazon Advertising ID (use in place of Google Advertising ID on Amazon devices). Thanks to @jcomo for the pull request.

## 2.7.2 (May 24, 2016)

* Add documentation for SDK functions. You can take a look [here](https://rawgit.com/amplitude/Amplitude-Android/master/javadoc/index.html). A link has also been added to the Readme.
* Fix bug where fetching the user's location on select devices throws a SecurityException, causing a crash.

## 2.7.1 (Apr 19, 2016)

* RevenueProperties is a confusing name and should actually be eventProperties. Deprecating Revenue.setRevenueProperties and replacing it with Revenue.setEventProperties, and clarified in Readme.

## 2.7.0 (Apr 19, 2016)

* Add support setting groups for users and events. See [Readme](https://github.com/amplitude/Amplitude-Android#setting-groups) for more information.
* Add helper method `getSessionId` to expose the current sessionId value.
* Add `logRevenueV2` and new `Revenue` class to support logging revenue events with properties, revenue type, and verified. See [Readme](https://github.com/amplitude/Amplitude-Android#tracking-revenue) for more info.
* Fix crash when trying to enableForegroundTracking with the PinnedAmplitudeClient. AmplitudeClient methods should be using `this` instead of static `instance` variable.

## 2.6.0 (Mar 29, 2016)

* Update to OKHttp v3.0.1.
* Add support for prepend user property operation.
* Fix bug where merging events for upload causes array index out of bounds exception.
* Migrate shared preferences (userId and event meta data) to Sqlite db to support apps with multiple processes.

## 2.5.1 (Mar 14, 2016)

* Fix bug where updateServer sets the wrong batchLimit when limit is false.

## 2.5.0 (Jan 15, 2016)

* Add ability to clear all user properties.
* Check that SDK is initialized when user calls enableForegroundTracking, identify, setUserProperties.

## 2.4.0 (Dec 15, 2015)

* Add support for append user property operation.

## 2.3.0 (Nov 30, 2015)

* Log if Google Play Services is enabled for the application.

## 2.2.0 (Oct 20, 2015)

* Removed all references to Apache HTTPClient to support Android M.
* Handle exceptions when fetching last known location from LocationManager.
* Add ability to set custom deviceId.
* Handle exception when cloning JSON object.
* Maintain only one instance of OKHttpClient.
* Add AmplitudeLog helper class that supports enabling and disabling of logging as well as setting of the log level.
* Fix bug where event and identify queues are not truncated if eventMaxCount is less than eventRemoveBatchSize.

## 2.1.0 (Oct 04, 2015)

* Add support for user properties operations (set, setOnce, add, unset).
* Fix bug where end session event was not being sent upon app reopen.

## 2.0.4 (Sep 23, 2015)

* Fix bug where deviceInfo was trying to use Geocoder if none present.

## 2.0.3 (Sep 22, 2015)

* Fix bug where deviceId was being fetched on main thread.

## 2.0.2 (Aug 24, 2015)

* Fix Maven jar, fixed build file.

## 2.0.1 (Aug 21, 2015)

* Catch all exceptions thrown by Android TelephonyManager and NullPointerExceptions thrown by geocoder during country lookup.

## 2.0.0 (Aug 20, 2015)

* Expose user ID with getUserId.
* Simplified session tracking. No longer need to call startSession and endSession. No longer send start/end session events by default. Added foreground tracking for sessions that uses Android activity lifecycles.
* The minimum supported API level is 9. API level 14 is required for foreground tracking.
* Always track Android advertising ID (ADID) regardless of limit ad tracking enabled.
* Track if limit ad tracking enabled as an API property for each logged event.
* Database upgraded to version 2: added a new store table for key value pairs.
* Device ID is now saved to and reloaded from the SQLite database (instead of SharedPrefs because SharedPrefs currently does not support multiple processes).
* MessageDigest.getInstance(String) is not threadsafe (known Android issue). Replaced with alternate MD5 implementation from http://org.rodage.com/pub/java/security/MD5.java.
* Create a copy of input userProperties JSONObject in setUserProperties to try and prevent ConcurrentModificationException.

## 1.7.0 (May 29, 2015)

* Enable configuration of eventUploadThreshold, eventMaxCount,
  eventUploadMaxBatchSize, eventUploadPeriodSeconds, minTimeBetweenSessionsMillis,
  and sessionTimeoutMillis.

## 1.6.3 (May 06, 2015)

* Add offline mode to turn off server uploading for a time.
* Add synchronous logging. Logs events to the DB synchronously to guarantee event persistence.

## 1.6.2 (Apr 17, 2015)

* Change protection on AmplitudeClient to public.

## 1.6.1 (Apr 13, 2015)

* Fix double class inclusion in jar distribution

## 1.6.0 (Apr 08, 2015)

* Fix crash under aggressive proguard optimizations.
* Fix device id being lost occasionally on app update.
* Fix exception when calling logEvent with empty JSONObject.
* Log a DEBUG message on each event.

## 1.5.0 (Mar 24, 2015)

* Add PinnedAmplitudeClient to support SSL pinning.
* Deprecate static methods on Amplitude. Switch to using Amplitude.getInstance().
* Upgrade HTTP client to okhttp.

## 1.4.6 (Mar 16, 2015)

* Fix bug when initializing with user id. Api key was not set properly.

## 1.4.4 (Mar 11, 2015)

* Expose setUserProperties(JSONObject, boolean) as a static
* Handle null edge cases in location request
* Add user opt out support
* Merge user properties in setUserProperties by default
* Refactor Amplitude to be a singleton to support tests
* Add option to disable fine-grained location tracking
* Fix crash: ConcurrentModificationException in HashMap
* Fix crash: CursorWindowAllocationException in SQLite

## 1.4.3 (Nov 13, 2014)

* Update field names, split platform and os, and send library information

## 1.4.2 (Nov 7, 2014)

* Don't log end session event if session isn't open
* Fix creating a new session id when the previous session id is invalid or non existant

## 1.4.1 (Jul 16, 2014)

* Hotfix extra class file in jar.

## 1.4.0 (Jul 1, 2014)

* Send androidADID with events
* Use Google Play Advertising ID instead of Android ID, if set. Default / fall back on using a random UUID
* Pull country from reverse geocode, then telephony network country, then locale

## 1.3.0 (Jun 4, 2014)

* Add getDeviceId to unity plugin
* Add additional logRevenue methods for receipt validation
* Make device ID public
* Fix bug where first event was getting skipped from upload
* Catch SQLiteExceptions
* Catch exceptions through by Apache HTTPClient

## 1.0.0 (May 1, 2014)

* Initial packaged release


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2014 Amplitude Analytics

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
<p align="center">
  <a href="https://amplitude.com" target="_blank" align="center">
    <img src="https://static.amplitude.com/lightning/46c85bfd91905de8047f1ee65c7c93d6fa9ee6ea/static/media/amplitude-logo-with-text.4fb9e463.svg" width="280">
  </a>
  <br />
</p>

<div align="center">

[![Legacy SDK](https://img.shields.io/badge/state-legacy-yellow)](https://github.com/amplitude/Amplitude-Kotlin)
[![Maven Central](https://img.shields.io/maven-central/v/com.amplitude/android-sdk?versionPrefix=2)](https://mvnrepository.com/artifact/com.amplitude/android-sdk/latest)

</div>

# Announcement 📣

Amplitude is introducing a new [Android Kotlin SDK](https://github.com/amplitude/Amplitude-Kotlin). This new SDK provides improved developer experience, helps users instrument data more seamlessly and provide more control over data being instrumented using custom plugins.

To learn more about the new SDK, here are some useful links:

* Maven Central: https://search.maven.org/artifact/com.amplitude/analytics-android
* GitHub: https://github.com/amplitude/Amplitude-Kotlin
* Documentation: https://www.docs.developers.amplitude.com/data/sdks/android-kotlin

# Official Amplitude Android SDK

##### _February 17, 2023_ - [v2.39.2](https://github.com/amplitude/Amplitude-Android/releases/tag/v2.39.2)

## Amplitude and Ampli SDK
[Ampli SDK](https://www.docs.developers.amplitude.com/data/ampli) is autogenerated library based on your pre-defined [tracking plan](https://help.amplitude.com/hc/en-us/articles/5078731378203-Create-a-tracking-plan). The Ampli SDK, is a lightweight wrapper over the Amplitude SDK that provides type-safety, supports linting, and enables features like input validation. The code replicates the spec in the Tracking Plan and enforces its rules and requirements. This repository is about **Amplitude SDK**. To learn more about Ampli SDK, please refer to the [Ampli Android](https://www.docs.developers.amplitude.com/data/sdks/android-ampli/) and [examples](https://github.com/amplitude/ampli-examples).

## Installation and Quick Start
Please visit our :100:[Developer Center](https://www.docs.developers.amplitude.com/data/sdks/android/) for instructions on installing and using our the SDK.

## Javadoc
See our [Android SDK Reference](http://amplitude.github.io/Amplitude-Android/) for a list and description of all available SDK methods.

## Demo Applications
* A [demo application](https://github.com/amplitude/Android-Demo) showing the integration of our SDK using Gradle.
* A [demo application](https://github.com/amplitude/Segment-Android-Demo) showing the integration of our SDK using [Segment's](https://segment.com) Android SDK.
* A [demo application](https://github.com/amplitude/GTM-Android-Demo) demonstrating a potential integration with Google Tag Manager.

## Changelog
Click [here](https://github.com/amplitude/Amplitude-Android/wiki/Changelog) to view the Android SDK Changelog.

## Need Help?
If you have any problems or issues over our SDK, feel free to [create a github issue](https://github.com/amplitude/Amplitude-Android/issues/new) or submit a request on [Amplitude Help](https://help.amplitude.com/hc/en-us/requests/new).


================================================
FILE: build.gradle
================================================
group = ARTIFACT_GROUP
version = ARTIFACT_VERSION

buildscript {
    repositories {
        google()
        jcenter()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.2'
    }
}

apply plugin: 'com.android.library'
apply plugin: 'maven'
apply plugin: 'signing'

ext {
    artifactId = 'amplitude-android-sdk'
}

repositories {
    // The order in which you list these repositories matter.
    google()
    jcenter()
}

android {
    compileSdkVersion 28
    buildToolsVersion '28.0.3'

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    defaultConfig {
        minSdkVersion 14
        // Note: Can't target to the latest 29 now, since running Robolectric tests on 29 will
        // require Java 9 above. However, Android Studio will error out when setting up Java to 9
        // above.
        targetSdkVersion 28

        buildConfigField "String", "AMPLITUDE_VERSION", "\"${version}\""

        testInstrumentationRunner 'androidx.test.ext.junit.runners.AndroidJUnit4'

        // The following argument makes the Android Test Orchestrator run its
        // "pm clear" command after each test invocation. This command ensures
        // that the app's state is completely cleared between tests.
        testInstrumentationRunnerArguments clearPackageData: 'true'
    }

    lintOptions {
        abortOnError true
        textReport true
        warningsAsErrors false
    }

    testOptions {
        unitTests.includeAndroidResources = true
    }
}

dependencies {
    implementation 'com.amplitude:analytics-connector:1.0.0'
    implementation 'com.squareup.okhttp3:okhttp:4.2.2'
    testImplementation 'com.squareup.okhttp3:mockwebserver:4.2.2'
    testImplementation 'org.robolectric:robolectric:4.3.1'
    testImplementation 'org.robolectric:shadows-maps:3.4-rc2'
    testImplementation 'org.powermock:powermock-module-junit4:1.6.6'
    testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.6'
    testImplementation 'org.powermock:powermock-api-mockito:1.6.6'
    testImplementation 'org.powermock:powermock-classloading-xstream:1.6.6'
    testImplementation 'com.google.android:support-v4:r6'
    testImplementation 'com.google.android.gms:play-services-ads:18.3.0'
    testImplementation 'com.google.android.gms:play-services-base:17.1.0'
    testImplementation 'org.json:json:20140107'

    testImplementation "junit:junit:4.12"

    // Core library
    testImplementation 'androidx.test:core:1.2.0'

    // AndroidJUnitRunner and JUnit Rules
    testImplementation 'androidx.test:runner:1.2.0'
    testImplementation 'androidx.test:rules:1.2.0'

    // Assertions
    testImplementation 'androidx.test.ext:junit:1.1.1'
}

File secretPropsFile = project.rootProject.file('local.properties')
if (secretPropsFile.exists()) {
    // Read local.properties file first if it exists
    Properties p = new Properties()
    new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) }
    p.each { name, value -> ext[name] = value }

}

// ======== For SDK Releases ========
uploadArchives {
    repositories.mavenDeployer {
        beforeDeployment {
            MavenDeployment deployment -> signing.signPom(deployment)
        }

        pom.groupId = ARTIFACT_GROUP
        pom.version = ARTIFACT_VERSION

        pom.project {
            name project.name
            version ARTIFACT_VERSION
            packaging POM_PACKAGING
            description POM_DESCRIPTION
            url POM_URL

            scm {
                url POM_SCM_URL
                connection POM_SCM_CONNECTION
                developerConnection POM_SCM_DEV_CONNECTION
            }

            licenses {
                license {
                    name POM_LICENCE_NAME
                    url POM_LICENCE_URL
                    distribution POM_LICENCE_DIST
                }
            }
            developers {
                developer {
                    id POM_DEVELOPER_ID
                    name POM_DEVELOPER_NAME
                    email POM_DEVELOPER_EMAIL
                    organization POM_DEVELOPER_ORG
                    organizationUrl POM_DEVELOPER_ORG_URL
                }
            }
        }

        pom.whenConfigured { pom ->
            pom.dependencies*.optional = true
            pom.dependencies.find { dep ->
                dep.groupId == 'com.amplitude' && dep.artifactId == 'analytics-connector'
            }.optional = false
        }

        repository(url: RELEASE_REPOSITORY_URL) {
            authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
        }
        snapshotRepository(url: SNAPSHOT_REPOSITORY_URL) {
            authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
        }
    }
}

task install(type: Upload, dependsOn: assemble) {
    repositories.mavenInstaller {
        configuration = configurations.archives

        pom.groupId = ARTIFACT_GROUP
        pom.version = ARTIFACT_VERSION

        pom.project {
            name project.name
            version ARTIFACT_VERSION
            packaging POM_PACKAGING
            description POM_DESCRIPTION
            url POM_URL

            scm {
                url POM_SCM_URL
                connection POM_SCM_CONNECTION
                developerConnection POM_SCM_DEV_CONNECTION
            }

            licenses {
                license {
                    name POM_LICENCE_NAME
                    url POM_LICENCE_URL
                    distribution POM_LICENCE_DIST
                }
            }
            developers {
                developer {
                    id POM_DEVELOPER_ID
                    name POM_DEVELOPER_NAME
                    email POM_DEVELOPER_EMAIL
                    organization POM_DEVELOPER_ORG
                    organizationUrl POM_DEVELOPER_ORG_URL
                }
            }
        }
    }
}

task androidJavadocs(type: Javadoc, dependsOn: ':generateReleaseBuildConfig') {
    source = android.sourceSets.main.java.srcDirs
    classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
    classpath += project.files('build/generated/source/buildConfig/release')

    exclude(
            '**/R.*',
            '**/security/**',
            '**/unity/**',
            '**/api/AmplitudeLog.java',
            '**/api/Constants.java',
            '**/api/DeviceInfo.java',
            '**/api/Utils.java',
            '**/api/WorkerThread.java',
            '**/api/CursorWindowAllocationException.java'
    )
    options {
        encoding = 'UTF-8'
        docEncoding = 'UTF-8'
        charSet = 'UTF-8'
    }
    failOnError false
}

task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) {
    classifier = 'javadoc'
    from androidJavadocs.destinationDir
}

task androidSourcesJar(type: Jar) {
    classifier = 'sources'
    from android.sourceSets.main.java.srcDirs
}

signing {
    required { isReleaseBuild() && gradle.taskGraph.hasTask(":uploadArchives") }
    sign configurations.archives
}

artifacts {
    archives androidSourcesJar
    archives androidJavadocsJar
}

def isReleaseBuild() {
    return version.contains("SNAPSHOT") == false
}

def getRepositoryUsername() {
    return hasProperty('sonatypeUsername') ? sonatypeUsername : ""
}

def getRepositoryPassword() {
    return hasProperty('sonatypePassword') ? sonatypePassword : ""
}


================================================
FILE: gradle.properties
================================================
ARTIFACT_VERSION=2.40.3
ARTIFACT_GROUP=com.amplitude

POM_PACKAGING=aar
POM_DESCRIPTION=Amplitude Android SDK

POM_URL=https://github.com/amplitude/Amplitude-Android
POM_SCM_URL=https://github.com/amplitude/Amplitude-Android
POM_SCM_CONNECTION=scm:git:http://github.com/amplitude/Amplitude-Android
POM_SCM_DEV_CONNECTION=scm:git:git@github.com:amplitude/Amplitude-Android.git
POM_LICENCE_NAME=The MIT License
POM_LICENCE_URL=http://www.opensource.org/licenses/mit-license.php
POM_LICENCE_DIST=repo
POM_DEVELOPER_ID=amplitude_sdk_dev
POM_DEVELOPER_NAME=Amplitude SDK Developers
POM_DEVELOPER_EMAIL=sdk.dev@amplitude.com
POM_DEVELOPER_ORG=Amplitude
POM_DEVELOPER_ORG_URL=https://amplitude.com/

RELEASE_REPOSITORY_URL=https://oss.sonatype.org/service/local/staging/deploy/maven2/
SNAPSHOT_REPOSITORY_URL=https://oss.sonatype.org/content/repositories/snapshots/


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

#
# Copyright 2015 the original author or 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.
#

##############################################################################
##
##  Gradle start up script for UN*X
##
##############################################################################

# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
    ls=`ls -ld "$PRG"`
    link=`expr "$ls" : '.*-> \(.*\)$'`
    if expr "$link" : '/.*' > /dev/null; then
        PRG="$link"
    else
        PRG=`dirname "$PRG"`"/$link"
    fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null

APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`

# 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"'

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

warn () {
    echo "$*"
}

die () {
    echo
    echo "$*"
    echo
    exit 1
}

# 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
    ;;
  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"
    which java >/dev/null 2>&1 || 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

# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
    MAX_FD_LIMIT=`ulimit -H -n`
    if [ $? -eq 0 ] ; then
        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
            MAX_FD="$MAX_FD_LIMIT"
        fi
        ulimit -n $MAX_FD
        if [ $? -ne 0 ] ; then
            warn "Could not set maximum file descriptor limit: $MAX_FD"
        fi
    else
        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
    fi
fi

# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi

# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
    JAVACMD=`cygpath --unix "$JAVACMD"`

    # We build the pattern for arguments to be converted via cygpath
    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
    SEP=""
    for dir in $ROOTDIRSRAW ; do
        ROOTDIRS="$ROOTDIRS$SEP$dir"
        SEP="|"
    done
    OURCYGPATTERN="(^($ROOTDIRS))"
    # Add a user-defined pattern to the cygpath arguments
    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
    fi
    # Now convert the arguments - kludge to limit ourselves to /bin/sh
    i=0
    for arg in "$@" ; do
        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option

        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
        else
            eval `echo args$i`="\"$arg\""
        fi
        i=`expr $i + 1`
    done
    case $i in
        0) set -- ;;
        1) set -- "$args0" ;;
        2) set -- "$args0" "$args1" ;;
        3) set -- "$args0" "$args1" "$args2" ;;
        4) set -- "$args0" "$args1" "$args2" "$args3" ;;
        5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
        6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
        7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
        8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
        9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
    esac
fi

# Escape application args
save () {
    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
    echo " "
}
APP_ARGS=`save "$@"`

# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"

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

@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=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

@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%" == "0" goto init

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

goto fail

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

if exist "%JAVA_EXE%" goto init

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

goto fail

:init
@rem Get command-line arguments, handling Windows variants

if not "%OS%" == "Windows_NT" goto win9xME_args

:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2

:win9xME_args_slurp
if "x%~1" == "x" goto execute

set CMD_LINE_ARGS=%*

: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 %CMD_LINE_ARGS%

:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="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!
if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1

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

:omega


================================================
FILE: package.json
================================================
{
  "private": true,
  "dependencies": {
    "lodash": "4.17.21",
    "semantic-release": "17.4.7",
    "@semantic-release/changelog": "5.0.1",
    "@semantic-release/git": "9.0.1",
    "@google/semantic-release-replace-plugin": "1.2.0",
    "@semantic-release/exec": "5.0.0"
  }
}


================================================
FILE: release.config.js
================================================
module.exports = {
  "branches": [
    {name: 'beta', prerelease: true},
    "main"
  ],
  "tagFormat": ["v${version}"],
  "plugins": [
    ["@semantic-release/commit-analyzer", {
      "preset": "angular",
      "parserOpts": {
        "noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES", "BREAKING"]
      }
    }],
    ["@semantic-release/release-notes-generator", {
      "preset": "angular",
    }],
    ["@semantic-release/changelog", {
      "changelogFile": "CHANGELOG.md"
    }],
    "@semantic-release/github",
    [
      "@google/semantic-release-replace-plugin",
      {
        "replacements": [
          {
            "files": ["gradle.properties"],
            "from": "ARTIFACT_VERSION=.*",
            "to": "ARTIFACT_VERSION=${nextRelease.version}",
            "results": [
              {
                "file": "gradle.properties",
                "hasChanged": true,
                "numMatches": 1,
                "numReplacements": 1
              }
            ],
            "countMatches": true
          },
        ]
      }
    ],
    ["@semantic-release/git", {
      "assets": ["gradle.properties", "CHANGELOG.md"],
      "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
    }],
    ["@semantic-release/exec", {
      "publishCmd": "./gradlew uploadArchives",
    }],
  ],
}


================================================
FILE: settings.gradle
================================================
rootProject.name = 'android-sdk'


================================================
FILE: src/main/AndroidManifest.xml
================================================
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.amplitude">

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

</manifest>


================================================
FILE: src/main/java/com/amplitude/api/Amplitude.java
================================================
package com.amplitude.api;

import android.content.Context;

import org.json.JSONObject;

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


/**
 * <h1>Amplitude</h1>
 * This is the main Amplitude class that manages SDK instances. <br><br>
 * <b>NOTE:</b> All of the methods except {@code getInstance()} have been deprecated.
 * Please call those methods on the AmplitudeClient instance instead, for example:
 * {@code Amplitude.getInstance().logEvent();}
 *
 * @see com.amplitude.api.AmplitudeClient AmplitudeClient
 */
public class Amplitude {

    static final Map<String, AmplitudeClient> instances = new HashMap<String, AmplitudeClient>();

    /**
     * Gets the default instance.
     *
     * @return the default instance
     */
    public static AmplitudeClient getInstance() {
        return getInstance(null);
    }

    /**
     * Gets the specified instance. If instance is null or empty string, fetches the default
     * instance instead.
     *
     * @param instance name to get "ex app 1"
     * @return the specified instance
     */
    public static synchronized AmplitudeClient getInstance(String instance) {
        instance = Utils.normalizeInstanceName(instance);
        AmplitudeClient client = instances.get(instance);
        if (client == null) {
            client = new AmplitudeClient(instance);
            instances.put(instance, client);
        }
        return client;
    }

    /**
     * Initialize the SDK with the Android app context and Amplitude API key.
     * Initializing is required before calling other methods such as {@code logEvent();}.
     *
     * @param context the context
     * @param apiKey  the api key
     */
    @Deprecated
    public static void initialize(Context context, String apiKey) {
        getInstance().initialize(context, apiKey);
    }

    /**
     * Initialize the SDK with the Android app context, Amplitude API key, and a user Id.
     * Initializing is required before calling other methods such as {@code logEvent();}.
     *
     * @param context the context
     * @param apiKey  the api key
     * @param userId  the user id
     */
    @Deprecated
    public static void initialize(Context context, String apiKey, String userId) {
        getInstance().initialize(context, apiKey, userId);
    }

    /**
     * Enable new device id per install.
     *
     * @param newDeviceIdPerInstall the new device id per install
     */
    @Deprecated
    public static void enableNewDeviceIdPerInstall(boolean newDeviceIdPerInstall) {
        getInstance().enableNewDeviceIdPerInstall(newDeviceIdPerInstall);
    }

    /**
     * Use advertising id for device id.
     */
    @Deprecated
    public static void useAdvertisingIdForDeviceId() {
        getInstance().useAdvertisingIdForDeviceId();
    }

    /**
     * Enable location listening.
     */
    @Deprecated
    public static void enableLocationListening() {
        getInstance().enableLocationListening();
    }

    /**
     * Disable location listening.
     */
    @Deprecated
    public static void disableLocationListening() {
        getInstance().disableLocationListening();
    }

    /**
     * Sets session timeout millis.
     *
     * @param sessionTimeoutMillis the session timeout millis
     */
    @Deprecated
    public static void setSessionTimeoutMillis(long sessionTimeoutMillis) {
        getInstance().setSessionTimeoutMillis(sessionTimeoutMillis);
    }

    /**
     * Sets opt out.
     *
     * @param optOut the opt out
     */
    @Deprecated
    public static void setOptOut(boolean optOut) {
        getInstance().setOptOut(optOut);
    }

    /**
     * Log event.
     *
     * @param eventType the event type
     */
    @Deprecated
    public static void logEvent(String eventType) {
        getInstance().logEvent(eventType);
    }

    /**
     * Log event.
     *
     * @param eventType       the event type
     * @param eventProperties the event properties
     */
    @Deprecated
    public static void logEvent(String eventType, JSONObject eventProperties) {
        getInstance().logEvent(eventType, eventProperties);
    }

    /**
     * Upload events.
     */
    @Deprecated
    public static void uploadEvents() {
        getInstance().uploadEvents();
    }

    /**
     * Start session.
     */
    @Deprecated
    public static void startSession() { return; }

    /**
     * End session.
     */
    @Deprecated
    public static void endSession() { return; }

    /**
     * Log revenue.
     *
     * @param amount the amount
     */
    @Deprecated
    public static void logRevenue(double amount) {
        getInstance().logRevenue(amount);
    }

    /**
     * Log revenue.
     *
     * @param productId the product id
     * @param quantity  the quantity
     * @param price     the price
     */
    @Deprecated
    public static void logRevenue(String productId, int quantity, double price) {
        getInstance().logRevenue(productId, quantity, price);
    }

    /**
     * Log revenue.
     *
     * @param productId        the product id
     * @param quantity         the quantity
     * @param price            the price
     * @param receipt          the receipt
     * @param receiptSignature the receipt signature
     */
    @Deprecated
    public static void logRevenue(String productId, int quantity, double price, String receipt,
            String receiptSignature) {
        getInstance().logRevenue(productId, quantity, price, receipt, receiptSignature);
    }

    /**
     * Sets user properties.
     *
     * @param userProperties the user properties
     */
    @Deprecated
    public static void setUserProperties(JSONObject userProperties) {
        getInstance().setUserProperties(userProperties);
    }

    /**
     * Sets user properties.
     *
     * @param userProperties the user properties
     * @param replace        the replace
     */
    @Deprecated
    public static void setUserProperties(JSONObject userProperties, boolean replace) {
        getInstance().setUserProperties(userProperties, replace);
    }

    /**
     * Sets user id.
     *
     * @param userId the user id
     */
    @Deprecated
    public static void setUserId(String userId) {
        getInstance().setUserId(userId);
    }

    /**
     * Gets device id.
     *
     * @return the device id
     */
    @Deprecated
    public static String getDeviceId() {
        return getInstance().getDeviceId();
    }
}


================================================
FILE: src/main/java/com/amplitude/api/AmplitudeCallbacks.java
================================================
package com.amplitude.api;

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

class AmplitudeCallbacks implements Application.ActivityLifecycleCallbacks {

    private static final String TAG = AmplitudeCallbacks.class.getName();
    private static final String NULLMSG = "Need to initialize AmplitudeCallbacks with AmplitudeClient instance";

    private AmplitudeClient clientInstance = null;
    private static AmplitudeLog logger = AmplitudeLog.getLogger();

    public AmplitudeCallbacks(AmplitudeClient clientInstance) {
        if (clientInstance == null) {
            logger.e(TAG, NULLMSG);
            return;
        }

        this.clientInstance = clientInstance;
        clientInstance.useForegroundTracking();
    }

    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {}

    @Override
    public void onActivityDestroyed(Activity activity) {}

    @Override
    public void onActivityPaused(Activity activity) {
        if (clientInstance == null) {
            logger.e(TAG, NULLMSG);
            return;
        }

        clientInstance.onExitForeground(getCurrentTimeMillis());
    }

    @Override
    public void onActivityResumed(Activity activity) {
        if (clientInstance == null) {
            logger.e(TAG, NULLMSG);
            return;
        }

        clientInstance.onEnterForeground(getCurrentTimeMillis());
    }

    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle outstate) {}

    @Override
    public void onActivityStarted(Activity activity) {}

    @Override
    public void onActivityStopped(Activity activity) {}

    protected long getCurrentTimeMillis() {
        return System.currentTimeMillis();
    }
}


================================================
FILE: src/main/java/com/amplitude/api/AmplitudeClient.java
================================================
package com.amplitude.api;

import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.location.Location;
import android.os.Build;
import android.util.Pair;


import com.amplitude.analytics.connector.AnalyticsConnector;
import com.amplitude.analytics.connector.Identity;
import com.amplitude.analytics.connector.util.JSONUtil;
import com.amplitude.eventexplorer.EventExplorer;
import com.amplitude.util.DoubleCheck;
import com.amplitude.util.Provider;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;

import kotlin.Unit;
import okhttp3.Call;
import okhttp3.FormBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

/**
 * <h1>AmplitudeClient</h1>
 * This is the SDK instance class that contains all of the SDK functionality.<br><br>
 * <b>Note:</b> call the methods on the default shared instance in the Amplitude class,
 * for example: {@code Amplitude.getInstance().logEvent();}<br><br>
 * Many of the SDK functions return the SDK instance back, allowing you to chain multiple method
 * calls together, for example: {@code Amplitude.getInstance().initialize(this, "APIKEY").enableForegroundTracking(getApplication())}
 */
public class AmplitudeClient {

    /**
     * The class identifier tag used in logging. TAG = {@code "com.amplitude.api.AmplitudeClient";}
     */
    private static final String TAG = AmplitudeClient.class.getName();

    /**
     * The event type for start session events.
     */
    public static final String START_SESSION_EVENT = "session_start";
    /**
     * The event type for end session events.
     */
    public static final String END_SESSION_EVENT = "session_end";

    /**
     * The pref/database key for the device ID value.
     */
    public static final String DEVICE_ID_KEY = "device_id";
    /**
     * The pref/database key for the user ID value.
     */
    public static final String USER_ID_KEY = "user_id";
    /**
     * The pref/database key for the opt out flag.
     */
    public static final String OPT_OUT_KEY = "opt_out";
    /**
     * The pref/database key for the sequence number.
     */
    public static final String SEQUENCE_NUMBER_KEY = "sequence_number";
    /**
     * The pref/database key for the last event time.
     */
    public static final String LAST_EVENT_TIME_KEY = "last_event_time";
    /**
     * The pref/database key for the last event ID value.
     */
    public static final String LAST_EVENT_ID_KEY = "last_event_id";
    /**
     * The pref/database key for the last identify ID value.
     */
    public static final String LAST_IDENTIFY_ID_KEY = "last_identify_id";
    /**
     * The pref/database key for the previous session ID value.
     */
    public static final String PREVIOUS_SESSION_ID_KEY = "previous_session_id";

    private static final AmplitudeLog logger = AmplitudeLog.getLogger();

    /**
     * The Android App Context.
     */
    protected Context context;
    /**
     * The shared OkHTTPClient instance.
     */
    protected Call.Factory callFactory;
    /**
     * The shared Amplitude database helper instance.
     */
    protected DatabaseHelper dbHelper;
    /**
     * The Amplitude App API key.
     */
    protected String apiKey;
    /**
     * The name for this instance of AmplitudeClient.
     */
    protected String instanceName;
    /**
     * The user's ID value.
     */
    protected String userId;
    /**
     * The user's Device ID value.
     */
    protected String deviceId;
    private boolean newDeviceIdPerInstall = false;
    private boolean useAdvertisingIdForDeviceId = false;
    private boolean useAppSetIdForDeviceId = false;
    protected boolean initialized = false;
    private AmplitudeDeviceIdCallback deviceIdCallback;
    private boolean optOut = false;
    private boolean offline = false;
    TrackingOptions inputTrackingOptions = new TrackingOptions();
    TrackingOptions appliedTrackingOptions = TrackingOptions.copyOf(inputTrackingOptions);
    JSONObject apiPropertiesTrackingOptions = appliedTrackingOptions.getApiPropertiesTrackingOptions();
    private boolean coppaControlEnabled = false;
    private boolean locationListening = false;
    private EventExplorer eventExplorer;
    private Plan plan;
    private IdentifyInterceptor identifyInterceptor;

    /**
     * The ingestion metadata.
     */
    private IngestionMetadata ingestionMetadata;

    /**
     * Amplitude Server Zone
     */
    private AmplitudeServerZone serverZone = AmplitudeServerZone.US;

    /**
     * The device's Platform value.
     */
    protected String platform;

    /**
     * Event metadata
     */
    long sessionId = -1;
    long sequenceNumber = 0;
    long lastEventId = -1;
    long lastIdentifyId = -1;
    long lastEventTime = -1;
    long previousSessionId = -1;

    protected DeviceInfo deviceInfo;

    /**
     * The current session ID value.
     */
    private int eventUploadThreshold = Constants.EVENT_UPLOAD_THRESHOLD;
    private int eventUploadMaxBatchSize = Constants.EVENT_UPLOAD_MAX_BATCH_SIZE;
    private int eventMaxCount = Constants.EVENT_MAX_COUNT;
    private long eventUploadPeriodMillis = Constants.EVENT_UPLOAD_PERIOD_MILLIS;
    private long minTimeBetweenSessionsMillis = Constants.MIN_TIME_BETWEEN_SESSIONS_MILLIS;
    private long identifyBatchIntervalMillis= Constants.IDENTIFY_BATCH_INTERVAL_MILLIS;
    private long sessionTimeoutMillis = Constants.SESSION_TIMEOUT_MILLIS;
    private boolean backoffUpload = false;
    private int backoffUploadBatchSize = eventUploadMaxBatchSize;
    private boolean usingForegroundTracking = false;
    private boolean trackingSessionEvents = false;
    private boolean inForeground = false;
    private boolean isEnteringForeground = false;
    private boolean flushEventsOnClose = true;
    private String libraryName = Constants.LIBRARY;
    private String libraryVersion = Constants.VERSION;
    private boolean useDynamicConfig = false;

    private AtomicBoolean updateScheduled = new AtomicBoolean(false);
    /**
     * Whether or not the SDK is in the process of uploading events.
     */
    AtomicBoolean uploadingCurrently = new AtomicBoolean(false);

    /**
     * The last SDK error - used for testing.
     */
    Throwable lastError;
    /**
     * The url for Amplitude API endpoint
     */
    String url = Constants.EVENT_LOG_URL;
    /**
     * The Bearer Token for authentication
     */
    String bearerToken = null;
    /**
     * The background event logging worker thread instance.
     */
    WorkerThread logThread = new WorkerThread("logThread");
    /**
     * The background event uploading worker thread instance.
     */
    WorkerThread httpThread = new WorkerThread("httpThread");
    /**
     * The core package for integrating with the Experiment SDK.
     */
    final AnalyticsConnector connector;
    /**
     * The runner for middleware
     */
    MiddlewareRunner middlewareRunner = new MiddlewareRunner();

    /**
     * Instantiates a new default instance AmplitudeClient and starts worker threads.
     */
    public AmplitudeClient() {
        this(null);
    }

    /**
     * Instantiates a new AmplitudeClient with instance name and starts worker threads.
     * @param instance
     */
    public AmplitudeClient(String instance) {
        this.instanceName = Utils.normalizeInstanceName(instance);
        logThread.start();
        httpThread.start();
        this.connector = AnalyticsConnector.getInstance(this.instanceName);
    }

    /**
     * Initialize the Amplitude SDK with the Android application context and your Amplitude
     * App API key. <b>Note:</b> initialization is required before you log events and modify
     * user properties.
     *
     * @param context the Android application context
     * @param apiKey  your Amplitude App API key
     * @return the AmplitudeClient
     */
    public AmplitudeClient initialize(Context context, String apiKey) {
        return initialize(context, apiKey, null);
    }

    /**
     * Initialize the Amplitude SDK with the Android application context, your Amplitude App API
     * key, and a user ID for the current user. <b>Note:</b> initialization is required before
     * you log events and modify user properties.
     *
     * @param context the Android application context
     * @param apiKey  your Amplitude App API key
     * @param userId  the user id to set
     * @return the AmplitudeClient
     */
    public AmplitudeClient initialize(Context context, String apiKey, String userId) {
        return initialize(context, apiKey, userId, null, false);
    }

    /**
     * Initialize the Amplitude SDK with the Android application context, your Amplitude App API
     * key, a user ID for the current user, and a custom platform value.
     * <b>Note:</b> initialization is required before you log events and modify user properties.
     *
     * @param context the Android application context
     * @param apiKey  your Amplitude App API key
     * @param userId  the user id to set
     * @param
     * @return the AmplitudeClient
     */
    public synchronized AmplitudeClient initialize(
            final Context context,
            final String apiKey,
            final String userId,
            final String platform,
            final boolean enableDiagnosticLogging
    ) {
        return this.initializeInternal(
                context,
                apiKey,
                userId,
                platform,
                enableDiagnosticLogging,
                null);
    }

    /**
     * Initialize the Amplitude SDK with the Android application context, your Amplitude App API
     * key, a user ID for the current user, and a custom platform value.
     * <b>Note:</b> initialization is required before you log events and modify user properties.
     *
     * @param context the Android application context
     * @param apiKey  your Amplitude App API key
     * @param userId  the user id to set
     * @param callFactory the call factory that used by Amplitude to make http request
     * @return the AmplitudeClient
     */
    public synchronized AmplitudeClient initialize(
            final Context context,
            final String apiKey,
            final String userId,
            final String platform,
            final boolean enableDiagnosticLogging,
            final Call.Factory callFactory
    ) {
        return this.initializeInternal(
                context,
                apiKey,
                userId,
                platform,
                enableDiagnosticLogging,
                callFactory);
    }

    /**
     * Initialize the Amplitude SDK with the Android application context, your Amplitude App API
     * key, a user ID for the current user, and a custom platform value.
     * <b>Note:</b> initialization is required before you log events and modify user properties.
     *
     * @param context the Android application context
     * @param apiKey  your Amplitude App API key
     * @param userId  the user id to set
     * @param
     * @return the AmplitudeClient
     */
    public synchronized AmplitudeClient initializeInternal(
            final Context context,
            final String apiKey,
            final String userId,
            final String platform,
            final boolean enableDiagnosticLogging,
            final Call.Factory callFactory
    ) {
        if (context == null) {
            logger.e(TAG, "Argument context cannot be null in initialize()");
            return this;
        }

        if (Utils.isEmptyString(apiKey)) {
            logger.e(TAG, "Argument apiKey cannot be null or blank in initialize()");
            return this;
        }

        this.context = context.getApplicationContext();
        this.apiKey = apiKey;
        this.dbHelper = DatabaseHelper.getDatabaseHelper(this.context, this.instanceName);
        this.platform = Utils.isEmptyString(platform) ? Constants.PLATFORM : platform;

        final AmplitudeClient client = this;
        runOnLogThread(() -> {
            if (!initialized) {
                // this try block is idempotent, so it's safe to retry initialize if failed
                try {
                    if (callFactory == null) {
                        // defer OkHttp client to first call
                        final Provider<Call.Factory> callProvider
                                = DoubleCheck.provider(OkHttpClient::new);
                        this.callFactory = request -> callProvider.get().newCall(request);
                    } else {
                        this.callFactory = callFactory;
                    }

                    if (useDynamicConfig) {
                        ConfigManager.getInstance().refresh(new ConfigManager.RefreshListener() {
                            @Override
                            public void onFinished() {
                                url = ConfigManager.getInstance().getIngestionEndpoint();
                            }
                        }, serverZone);
                    }

                    deviceInfo = initializeDeviceInfo();
                    deviceId = initializeDeviceId();
                    if (this.deviceIdCallback != null) {
                        this.deviceIdCallback.onDeviceIdReady(deviceId);
                    }

                    if (userId != null) {
                        client.userId = userId;
                        dbHelper.insertOrReplaceKeyValue(USER_ID_KEY, userId);
                    } else {
                        client.userId = dbHelper.getValue(USER_ID_KEY);
                    }

                    identifyInterceptor = new IdentifyInterceptor(dbHelper, logThread, identifyBatchIntervalMillis, this);

                    // set up listener to core package to receive exposure events from Experiment
                    connector.getEventBridge().setEventReceiver(analyticsEvent -> {
                        String eventType = analyticsEvent.getEventType();
                        JSONObject eventProperties = JSONUtil.toJSONObject(analyticsEvent.getEventProperties());
                        JSONObject userProperties = JSONUtil.toJSONObject(analyticsEvent.getUserProperties());
                        logEventAsync(eventType, eventProperties, null, userProperties,
                            null, null, getCurrentTimeMillis(), false);
                        return Unit.INSTANCE;
                    });

                    // Set user ID and device ID in core identity store for use in Experiment SDK
                    connector.getIdentityStore().setIdentity(new Identity(userId, deviceId, new HashMap<>()));

                    // May take some time...
                    deviceInfo.prefetch();

                    final Long optOutLong = dbHelper.getLongValue(OPT_OUT_KEY);
                    optOut = optOutLong != null && optOutLong == 1;

                    // try to restore previous session id
                    previousSessionId = getLongvalue(PREVIOUS_SESSION_ID_KEY, -1);
                    if (previousSessionId >= 0) {
                        sessionId = previousSessionId;
                    }

                    // reload event meta data
                    sequenceNumber = getLongvalue(SEQUENCE_NUMBER_KEY, 0);
                    lastEventId = getLongvalue(LAST_EVENT_ID_KEY, -1);
                    lastIdentifyId = getLongvalue(LAST_IDENTIFY_ID_KEY, -1);
                    lastEventTime = getLongvalue(LAST_EVENT_TIME_KEY, -1);

                    // install database reset listener to re-insert metadata in memory
                    dbHelper.setDatabaseResetListener(new DatabaseResetListener() {
                        @Override
                        public void onDatabaseReset(SQLiteDatabase db) {
                            dbHelper.insertOrReplaceKeyValueToTable(db, DatabaseHelper.STORE_TABLE_NAME, DEVICE_ID_KEY, client.deviceId);
                            dbHelper.insertOrReplaceKeyValueToTable(db, DatabaseHelper.STORE_TABLE_NAME, USER_ID_KEY, client.userId);
                            dbHelper.insertOrReplaceKeyValueToTable(db, DatabaseHelper.LONG_STORE_TABLE_NAME, OPT_OUT_KEY, client.optOut ? 1L : 0L);
                            dbHelper.insertOrReplaceKeyValueToTable(db, DatabaseHelper.LONG_STORE_TABLE_NAME, PREVIOUS_SESSION_ID_KEY, client.sessionId);
                            dbHelper.insertOrReplaceKeyValueToTable(db, DatabaseHelper.LONG_STORE_TABLE_NAME, LAST_EVENT_TIME_KEY, client.lastEventTime);
                        }
                    });

                    initialized = true;

                } catch (CursorWindowAllocationException e) {  // treat as uninitialized SDK
                    logger.e(TAG, String.format(
                            "Failed to initialize Amplitude SDK due to: %s", e.getMessage()
                    ));
                    client.apiKey = null;
                }
            }
        });

        return this;
    }

    /**
     * Enable foreground tracking for the SDK. This is <b>HIGHLY RECOMMENDED</b>, and will allow
     * for accurate session tracking.
     *
     * @param app the Android application
     * @return the AmplitudeClient
     * @see <a href="https://github.com/amplitude/Amplitude-Android#tracking-sessions">
     *     Tracking Sessions</a>
     */
    public AmplitudeClient enableForegroundTracking(Application app) {
        if (usingForegroundTracking || !contextAndApiKeySet("enableForegroundTracking()")) {
            return this;
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            app.registerActivityLifecycleCallbacks(new AmplitudeCallbacks(this));
        }

        return this;
    }

    /**
     * @deprecated - We removed Diagnostics class and this function has no-op.
     * Will completely remove it in the near future.
     */
    public AmplitudeClient enableDiagnosticLogging() {
        return this;
    }

    /**
     * @deprecated - We removed Diagnostics class and this function has no-op.
     * Will completely remove it in the near future.
     */
    public AmplitudeClient disableDiagnosticLogging() {
        return this;
    }

    /**
     * @deprecated - We removed Diagnostics class and this function has no-op.
     * Will completely remove it in the near future.
     */
    public AmplitudeClient setDiagnosticEventMaxCount(int eventMaxCount) {
        return this;
    }

    /**
     * Whether to set a new device ID per install. If true, then the SDK will always generate a new
     * device ID on app install (as opposed to re-using an existing value like ADID).
     *
     * @param newDeviceIdPerInstall whether to set a new device ID on app install.
     * @return the AmplitudeClient
     * @deprecated
     */
    public AmplitudeClient enableNewDeviceIdPerInstall(boolean newDeviceIdPerInstall) {
        this.newDeviceIdPerInstall = newDeviceIdPerInstall;
        return this;
    }

    /**
     * Whether to use the Android advertising ID (ADID) as the user's device ID.
     *
     * @return the AmplitudeClient
     */
    public AmplitudeClient useAdvertisingIdForDeviceId() {
        useAdvertisingIdForDeviceId = true;
        return this;
    }

    /**
     * Use Android app set id as the user's device ID.
     *
     * @return the AmplitudeClient
     */
    public AmplitudeClient useAppSetIdForDeviceId() {
        useAppSetIdForDeviceId = true;
        return this;
    }

    /**
     * Enable location listening in the SDK. This will add the user's current lat/lon coordinates
     * to every event logged.
     *
     * This function should be called before SDK initialization, e.g. {@link #initialize(Context, String)}.
     *
     * @return the AmplitudeClient
     */
    public AmplitudeClient enableLocationListening() {
        this.locationListening = true;
        if (this.deviceInfo != null) {
            this.deviceInfo.setLocationListening(true);
        }
        return this;
    }

    /**
     * Disable location listening in the SDK. This will stop the sending of the user's current
     * lat/lon coordinates.
     *
     * This function should be called before SDK initialization, e.g. {@link #initialize(Context, String)}.
     *
     * @return the AmplitudeClient
     */
    public AmplitudeClient disableLocationListening() {
        this.locationListening = false;
        if (this.deviceInfo != null) {
            this.deviceInfo.setLocationListening(false);
        }
        return this;
    }

    /**
     * Sets event upload threshold. The SDK will attempt to batch upload unsent events
     * every eventUploadPeriodMillis milliseconds, or if the unsent event count exceeds the
     * event upload threshold.
     *
     * @param eventUploadThreshold the event upload threshold
     * @return the AmplitudeClient
     */
    public AmplitudeClient setEventUploadThreshold(int eventUploadThreshold) {
        this.eventUploadThreshold = eventUploadThreshold;
        return this;
    }

    /**
     * Sets event upload max batch size. This controls the maximum number of events sent with
     * each upload request.
     *
     * @param eventUploadMaxBatchSize the event upload max batch size
     * @return the AmplitudeClient
     */
    public AmplitudeClient setEventUploadMaxBatchSize(int eventUploadMaxBatchSize) {
        this.eventUploadMaxBatchSize = eventUploadMaxBatchSize;
        this.backoffUploadBatchSize = eventUploadMaxBatchSize;
        return this;
    }

    /**
     * Sets event max count. This is the maximum number of unsent events to keep on the device
     * (for example if the device does not have internet connectivity and cannot upload events).
     * If the number of unsent events exceeds the max count, then the SDK begins dropping events,
     * starting from the earliest logged.
     *
     * @param eventMaxCount the event max count
     * @return the AmplitudeClient
     */
    public AmplitudeClient setEventMaxCount(int eventMaxCount) {
        this.eventMaxCount = eventMaxCount;
        return this;
    }

    /**
     * Sets event upload period millis. The SDK will attempt to batch upload unsent events
     * every eventUploadPeriodMillis milliseconds, or if the unsent event count exceeds the
     * event upload threshold.
     *
     * @param eventUploadPeriodMillis the event upload period millis
     * @return the AmplitudeClient
     */
    public AmplitudeClient setEventUploadPeriodMillis(int eventUploadPeriodMillis) {
        this.eventUploadPeriodMillis = eventUploadPeriodMillis;
        return this;
    }

    /**
     * Sets min time between sessions millis.
     *
     * @param minTimeBetweenSessionsMillis the min time between sessions millis
     * @return the min time between sessions millis
     */
    public AmplitudeClient setMinTimeBetweenSessionsMillis(long minTimeBetweenSessionsMillis) {
        this.minTimeBetweenSessionsMillis = minTimeBetweenSessionsMillis;
        return this;
    }

    /**
     * Sets min time for identify batch millis.
     *
     * @param identifyBatchIntervalMillis the time interval for identify batch interval
     * @return the AmplitudeClient
     */
    public AmplitudeClient setIdentifyBatchIntervalMillis(long identifyBatchIntervalMillis) {
        if (identifyBatchIntervalMillis < eventUploadPeriodMillis) {
            logger.w(TAG, "Warning: minimum batch interval is event upload period.");
            return this;
        }
        this.identifyBatchIntervalMillis = identifyBatchIntervalMillis;
        if (this.identifyInterceptor != null) {
            identifyInterceptor.setIdentifyBatchIntervalMillis(identifyBatchIntervalMillis);
        }
        return this;
    }

    /**
     * Sets a custom server url for event upload.
     *
     * We now have a new method setServerZone. To send data to Amplitude's EU servers, recommend to
     * use setServerZone method like client.setServerZone(AmplitudeServerZone.EU);
     *
     * @param serverUrl - a string url for event upload.
     * @return the AmplitudeClient
     */
    public AmplitudeClient setServerUrl(String serverUrl) {
        if (!Utils.isEmptyString(serverUrl)) {
            url = serverUrl;
        }
        return this;
    }

    /**
     * Set Bearer Token to be included in request header.
     * @param token
     * @return the AmplitudeClient
     */
    public AmplitudeClient setBearerToken(String token) {
        this.bearerToken = token;
        return this;
    }

    /**
     * Sets session timeout millis. If foreground tracking has not been enabled with
     * @{code enableForegroundTracking()}, then new sessions will be started after
     * sessionTimeoutMillis milliseconds have passed since the last event logged.
     *
     * @param sessionTimeoutMillis the session timeout millis
     * @return the AmplitudeClient
     */
    public AmplitudeClient setSessionTimeoutMillis(long sessionTimeoutMillis) {
        this.sessionTimeoutMillis = sessionTimeoutMillis;
        return this;
    }

    public AmplitudeClient setTrackingOptions(TrackingOptions trackingOptions) {
        inputTrackingOptions = trackingOptions;
        appliedTrackingOptions = TrackingOptions.copyOf(inputTrackingOptions);
        if (coppaControlEnabled) {
            appliedTrackingOptions.mergeIn(TrackingOptions.forCoppaControl());
        }
        apiPropertiesTrackingOptions = appliedTrackingOptions.getApiPropertiesTrackingOptions();
        return this;
    }

    /**
     * Enable COPPA (Children's Online Privacy Protection Act) restrictions on ADID, city, IP address and location tracking.
     * This can be used by any customer that does not want to collect ADID, city, IP address and location tracking.
     */
    public AmplitudeClient enableCoppaControl() {
        coppaControlEnabled = true;
        appliedTrackingOptions.mergeIn(TrackingOptions.forCoppaControl());
        apiPropertiesTrackingOptions = appliedTrackingOptions.getApiPropertiesTrackingOptions();
        return this;
    }

    /**
     * Disable COPPA (Children's Online Privacy Protection Act) restrictions on ADID, city, IP address and location tracking.
     */
    public AmplitudeClient disableCoppaControl() {
        coppaControlEnabled = false;
        appliedTrackingOptions = TrackingOptions.copyOf(inputTrackingOptions);
        apiPropertiesTrackingOptions = appliedTrackingOptions.getApiPropertiesTrackingOptions();
        return this;
    }

    /**
     * Sets opt out. If true then the SDK does not track any events for the user.
     *
     * @param optOut whether or not to opt the user out of tracking
     * @return the AmplitudeClient
     */
    public AmplitudeClient setOptOut(final boolean optOut) {
        if (!contextAndApiKeySet("setOptOut()")) {
            return this;
        }

        final AmplitudeClient client = this;
        runOnLogThread(new Runnable() {
            @Override
            public void run() {
                if (Utils.isEmptyString(apiKey)) { // in case initialization failed
                    return;
                }
                client.optOut = optOut;
                dbHelper.insertOrReplaceKeyLongValue(OPT_OUT_KEY, optOut ? 1L : 0L);
            }
        });
        return this;
    }

    public Boolean getOptOut() {
        return optOut;
    }

    /**
     * Library name is default as `amplitude-android`.
     * Notice: You will only want to set it when following conditions are met.
     * 1. You develop your own library which bridges Amplitude Android native library.
     * 2. You want to track your library as one of the data sources.
     */
    public AmplitudeClient setLibraryName(final String libraryName) {
        this.libraryName = libraryName;
        return this;
    }

    /**
     * Library version is default as the latest Amplitude Android SDK version.
     * Notice: You will only want to set it when following conditions are met.
     * 1. You develop your own library which bridges Amplitude Android native library.
     * 2. You want to track your library as one of the data sources.
     */
    public AmplitudeClient setLibraryVersion(final String libraryVersion) {
        this.libraryVersion = libraryVersion;
        return this;
    }

    /**
     * Returns whether or not the user is opted out of tracking.
     *
     * @return the optOut flag value
     */
    public boolean isOptedOut() {
        return optOut;
    }

    /**
     * Enable/disable message logging by the SDK.
     *
     * @param enableLogging whether to enable message logging by the SDK.
     * @return the AmplitudeClient
     */
    public AmplitudeClient enableLogging(boolean enableLogging) {
        logger.setEnableLogging(enableLogging);
        return this;
    }

    /**
     * Sets the logging level. Logging messages will only appear if they are the same severity
     * level or higher than the set log level.
     *
     * @param logLevel the log level
     * @return the AmplitudeClient
     */
    public AmplitudeClient setLogLevel(int logLevel) {
        logger.setLogLevel(logLevel);
        return this;
    }

    /**
     * Set log callback, it can help read and collect error message from sdk
     *
     * @param callback
     * @return the AmplitudeClient
     */
    public AmplitudeClient setLogCallback(AmplitudeLogCallback callback) {
        logger.setAmplitudeLogCallback(callback);
        return this;
    }

    /**
     * Sets offline. If offline is true, then the SDK will not upload events to Amplitude servers;
     * however, it will still log events.
     *
     * @param offline whether or not the SDK should be offline
     * @return the AmplitudeClient
     */
    public AmplitudeClient setOffline(boolean offline) {
        this.offline = offline;

        // Try to update to the server once offline mode is disabled.
        if (!offline) {
            uploadEvents();
        }

        return this;
    }

    /**
     * Enable/disable flushing of unsent events on app close (enabled by default).
     *
     * @param flushEventsOnClose whether to flush unsent events on app close
     * @return the AmplitudeClient
     */
    public AmplitudeClient setFlushEventsOnClose(boolean flushEventsOnClose) {
        this.flushEventsOnClose = flushEventsOnClose;
        return this;
    }

    /**
     * Track session events amplitude client. If enabled then the SDK will automatically send
     * start and end session events to mark the start and end of the user's sessions.
     *
     * @param trackingSessionEvents whether to enable tracking of session events
     * @return the AmplitudeClient
     */
    public AmplitudeClient trackSessionEvents(boolean trackingSessionEvents) {
        this.trackingSessionEvents = trackingSessionEvents;
        return this;
    }

    /**
     * Turning this flag on will find the best server url automatically based on users' geo location.
     * Note:
     * 1. If you have your own proxy server and use `setServerUrl` API, please leave this off.
     * 2. If you have users in China Mainland, we suggest you turn this on.
     *
     * @param useDynamicConfig whether to enable dynamic config
     * @return the AmplitudeClient
     */
    public AmplitudeClient setUseDynamicConfig(boolean useDynamicConfig) {
        this.useDynamicConfig = useDynamicConfig;
        return this;
    }

    /**
     * Show Amplitude Event Explorer for the given activity.
     *
     * @param activity root activity
     */
    public void showEventExplorer(Activity activity) {
        if (this.eventExplorer == null) {
            this.eventExplorer = new EventExplorer(this.instanceName);
        }
        this.eventExplorer.show(activity);
    }

    /**
     * Set foreground tracking to true.
     */
    void useForegroundTracking() {
        usingForegroundTracking = true;
    }

    /**
     * Whether foreground tracking is enabled.
     *
     * @return whether foreground tracking is enabled
     */
    boolean isUsingForegroundTracking() { return usingForegroundTracking; }

    /**
     * Add middleware to the middleware runner
     */
    public void addEventMiddleware(Middleware middleware) {
        middlewareRunner.add(middleware);
    }

    /**
     * Whether app is in the foreground.
     *
     * @return whether app is in the foreground
     */
    boolean isInForeground() { return inForeground; }

    /**
     * Log an event with the specified event type.
     * <b>Note:</b> this is asynchronous and happens on a background thread.
     *
     * @param eventType the event type
     */
    public void logEvent(String eventType) {
        logEvent(eventType, null);
    }

    /**
     * Log an event with the specified event type and event properties.
     * <b>Note:</b> this is asynchronous and happens on a background thread.
     *
     * @param eventType       the event type
     * @param eventProperties the event properties
     */
    public void logEvent(String eventType, JSONObject eventProperties) {
        logEvent(eventType, eventProperties, false);
    }

    /**
     * Log an event with the specified event type, event properties, with optional out of session
     * flag. If out of session is true, then the sessionId will be -1 for the event, indicating
     * that it is not part of the current session. Note: this might be useful when logging events
     * for notifications received.
     * <b>Note:</b> this is asynchronous and happens on a background thread.
     *
     * @param eventType       the event type
     * @param eventProperties the event properties
     * @param extra           the extra unstructured data for middleware
     */
    public void logEvent(String eventType, JSONObject eventProperties, MiddlewareExtra extra) {
        logEvent(eventType, eventProperties, null, getCurrentTimeMillis(), false, extra);
    }

    /**
     * Log an event with the specified event type, event properties, with optional out of session
     * flag. If out of session is true, then the sessionId will be -1 for the event, indicating
     * that it is not part of the current session. Note: this might be useful when logging events
     * for notifications received.
     * <b>Note:</b> this is asynchronous and happens on a background thread.
     *
     * @param eventType       the event type
     * @param eventProperties the event properties
     * @param outOfSession    the out of session
     */
    public void logEvent(String eventType, JSONObject eventProperties, boolean outOfSession) {
        logEvent(eventType, eventProperties, null, outOfSession);
    }

    /**
     * Log an event with the specified event type, event properties, and groups. Use this to set
     * event-level groups, meaning the group(s) set only apply for this specific event and does
     * not persist on the user.
     * <b>Note:</b> this is asynchronous and happens on a background thread.
     *
     * @param eventType       the event type
     * @param eventProperties the event properties
     * @param groups          the groups
     */
    public void logEvent(String eventType, JSONObject eventProperties, JSONObject groups) {
        logEvent(eventType, eventProperties, groups, false);
    }

    /**
     * Log event with the specified event type, event properties, groups, with optional out of
     * session flag. If out of session is true, then the sessionId will be -1 for the event,
     * indicating that it is not part of the current session. Note: this might be useful when
     * logging events for notifications received.
     * <b>Note:</b> this is asynchronous and happens on a background thread.
     *
     * @param eventType       the event type
     * @param eventProperties the event properties
     * @param groups          the groups
     * @param outOfSession    the out of session
     */
    public void logEvent(String eventType, JSONObject eventProperties, JSONObject groups, boolean outOfSession) {
        logEvent(eventType, eventProperties, groups, getCurrentTimeMillis(), outOfSession);
    }

    /**
     * Log event with the specified event type, event properties, groups, timestamp, with optional
     * out of session flag. If out of session is true, then the sessionId will be -1 for the event,
     * indicating that it is not part of the current session. Note: this might be useful when
     * logging events for notifications received.
     * <b>Note:</b> this is asynchronous and happens on a background thread.
     *
     * @param eventType       the event type
     * @param eventProperties the event properties
     * @param groups          the groups
     * @param timestamp       the timestamp in millisecond since epoch
     * @param outOfSession    the out of session
     * @see <a href="https://github.com/amplitude/Amplitude-Android#setting-event-properties">
     *     Setting Event Properties</a>
     * @see <a href="https://github.com/amplitude/Amplitude-Android#setting-groups">
     *     Setting Groups</a>
     * @see <a href="https://github.com/amplitude/Amplitude-Android#tracking-sessions">
     *     Tracking Sessions</a>
     */
    public void logEvent(String eventType, JSONObject eventProperties, JSONObject groups, long timestamp, boolean outOfSession) {
        logEvent(eventType, eventProperties, groups,
                timestamp, outOfSession, null);
    }

    public void logEvent(String eventType, JSONObject eventProperties, JSONObject groups, long timestamp, boolean outOfSession, MiddlewareExtra extra) {
        if (validateLogEvent(eventType)) {
            logEventAsync(
                eventType, eventProperties, null, null, groups, null,
                timestamp, outOfSession, extra);
        }
    }

    /**
     * Log an event with the specified event type.
     * <b>Note:</b> this is version is synchronous and blocks the main thread until done.
     *
     * @param eventType the event type
     */
    public void logEventSync(String eventType) {
        logEventSync(eventType, null);
    }

    /**
     * Log an event with the specified event type and event properties.
     * <b>Note:</b> this is version is synchronous and blocks the main thread until done.
     *
     * @param eventType       the event type
     * @param eventProperties the event properties
     * @see <a href="https://github.com/amplitude/Amplitude-Android#setting-event-properties">
     *     Setting Event Properties</a>
     */
    public void logEventSync(String eventType, JSONObject eventProperties) {
        logEventSync(eventType, eventProperties, false);
    }

    /**
     * Log an event with the specified event type, event properties, with optional out of session
     * flag. If out of session is true, then the sessionId will be -1 for the event, indicating
     * that it is not part of the current session. Note: this might be useful when logging events
     * for notifications received.
     * <b>Note:</b> this is version is synchronous and blocks the main thread until done.
     *
     * @param eventType       the event type
     * @param eventProperties the event properties
     * @param outOfSession    the out of session
     */
    public void logEventSync(String eventType, JSONObject eventProperties, boolean outOfSession) {
        logEventSync(eventType, eventProperties, null, outOfSession);
    }

    /**
     * Log an event with the specified event type, event properties, and groups. Use this to set
     * event-level groups, meaning the group(s) set only apply for this specific event and does
     * not persist on the user.
     * <b>Note:</b> this is version is synchronous and blocks the main thread until done.
     *
     * @param eventType       the event type
     * @param eventProperties the event properties
     * @param groups          the groups
     */
    public void logEventSync(String eventType, JSONObject eventProperties, JSONObject groups) {
        logEventSync(eventType, eventProperties, groups, false);
    }

    /**
     * Log event with the specified event type, event properties, groups, with optional out of
     * session flag. If out of session is true, then the sessionId will be -1 for the event,
     * indicating that it is not part of the current session. Note: this might be useful when
     * logging events for notifications received.
     * <b>Note:</b> this is version is synchronous and blocks the main thread until done.
     *
     * @param eventType       the event type
     * @param eventProperties the event properties
     * @param groups          the groups
     * @param outOfSession    the out of session
     * @see <a href="https://github.com/amplitude/Amplitude-Android#setting-event-properties">
     *     Setting Event Properties</a>
     * @see <a href="https://github.com/amplitude/Amplitude-Android#setting-groups">
     *     Setting Groups</a>
     * @see <a href="https://github.com/amplitude/Amplitude-Android#tracking-sessions">
     *     Tracking Sessions</a>
     */
    public void logEventSync(String eventType, JSONObject eventProperties, JSONObject groups, boolean outOfSession) {
        logEventSync(eventType, eventProperties, groups, getCurrentTimeMillis(), outOfSession);
    }

    /**
     * Log event with the specified event type, event properties, groups, timestamp,  with optional
     * sout of ession flag. If out of session is true, then the sessionId will be -1 for the event,
     * indicating that it is not part of the current session. Note: this might be useful when
     * logging events for notifications received.
     * <b>Note:</b> this is version is synchronous and blocks the main thread until done.
     *
     * @param eventType       the event type
     * @param eventProperties the event properties
     * @param groups          the groups
     * @param timestamp       the timestamp in milliseconds since epoch
     * @param outOfSession    the out of session
     */
    public void logEventSync(String eventType, JSONObject eventProperties, JSONObject groups, long timestamp, boolean outOfSession) {
        if (validateLogEvent(eventType)) {
            logEvent(eventType, eventProperties, null, null, groups, null, timestamp, outOfSession, this.inForeground);
        }
    }

    /**
     * Validate the event type being logged. Also verifies that the context and API key
     * have been set already with an initialize call.
     *
     * @param eventType the event type
     * @return true if the event type is valid
     */
    protected boolean validateLogEvent(String eventType) {
        if (Utils.isEmptyString(eventType)) {
            logger.e(TAG, "Argument eventType cannot be null or blank in logEvent()");
            return false;
        }

        return contextAndApiKeySet("logEvent()");
    }

    /**
     * Log event async. Internal method to handle the synchronous logging of events.
     *
     * @param eventType       the event type
     * @param eventProperties the event properties
     * @param apiProperties   the api properties
     * @param userProperties  the user properties
     * @param groups          the groups
     * @param timestamp       the timestamp
     * @param outOfSession    the out of session
     */
    protected void logEventAsync(final String eventType, JSONObject eventProperties,
           JSONObject apiProperties, JSONObject userProperties, JSONObject groups,
           JSONObject groupProperties, final long timestamp, final boolean outOfSession) {
        logEventAsync(eventType,eventProperties, apiProperties, userProperties, groups,groupProperties, timestamp, outOfSession, null);
    }

    protected void logEventAsync(final String eventType, JSONObject eventProperties,
            JSONObject apiProperties, JSONObject userProperties, JSONObject groups,
            JSONObject groupProperties, final long timestamp, final boolean outOfSession, MiddlewareExtra extra) {
        // Clone the incoming eventProperties object before sending over
        // to the log thread. Helps avoid ConcurrentModificationException
        // if the caller starts mutating the object they passed in.
        // Only does a shallow copy, so it's still possible, though unlikely,
        // to hit concurrent access if the caller mutates deep in the object.
        if (eventProperties != null) {
            eventProperties = Utils.cloneJSONObject(eventProperties);
        }

        if (apiProperties != null) {
            apiProperties = Utils.cloneJSONObject(apiProperties);
        }

        if (userProperties != null) {
            userProperties = Utils.cloneJSONObject(userProperties);
        }

        if (groups != null) {
            groups = Utils.cloneJSONObject(groups);
        }

        if (groupProperties != null) {
            groupProperties = Utils.cloneJSONObject(groupProperties);
        }

        final JSONObject copyEventProperties = eventProperties;
        final JSONObject copyApiProperties = apiProperties;
        final JSONObject copyUserProperties = userProperties;
        final JSONObject copyGroups = groups;
        final JSONObject copyGroupProperties = groupProperties;
        final boolean isForeground = this.inForeground;
        runOnLogThread(new Runnable() {
            @Override
            public void run() {
                if (Utils.isEmptyString(apiKey)) {  // in case initialization failed
                    return;
                }
                logEvent(
                    eventType, copyEventProperties, copyApiProperties,
                    copyUserProperties, copyGroups, copyGroupProperties, timestamp, outOfSession, extra,
                    isForeground
                );
            }
        });
    }

    /**
     * Log event. Internal method to handle the asynchronous logging of events on background
     * thread.
     *
     * @param eventType       the event type
     * @param eventProperties the event properties
     * @param apiProperties   the api properties
     * @param userProperties  the user properties
     * @param groups          the groups
     * @param timestamp       the timestamp
     * @param outOfSession    the out of session
     * @param inForeground    in foreground
     * @return the event ID if succeeded, else -1.
     */
    protected long logEvent(String eventType, JSONObject eventProperties, JSONObject apiProperties,
                            JSONObject userProperties, JSONObject groups, JSONObject groupProperties,
                            long timestamp, boolean outOfSession, boolean inForeground) {
        return logEvent(eventType, eventProperties, apiProperties, userProperties, groups, groupProperties, timestamp, outOfSession, null, inForeground);
    }

    protected long logEvent(String eventType, JSONObject eventProperties, JSONObject apiProperties,
            JSONObject userProperties, JSONObject groups, JSONObject groupProperties,
            long timestamp, boolean outOfSession, MiddlewareExtra extra, boolean inForeground) {

        logger.d(TAG, "Logged event to Amplitude: " + eventType);

        if (optOut) {
            return -1;
        }

        // skip session check if logging start_session or end_session events
        boolean loggingSessionEvent = trackingSessionEvents &&
                (eventType.equals(START_SESSION_EVENT) || eventType.equals(END_SESSION_EVENT));

        if (!loggingSessionEvent && !outOfSession) {
            // default case + corner case when async logEvent between onPause and onResume
            if (!inForeground || isEnteringForeground){
                isEnteringForeground = false;
                startNewSessionIfNeeded(timestamp);
            } else {
                refreshSessionTime(timestamp);
            }
        }

        long result = -1;
        JSONObject event = new JSONObject();
        try {
            event.put("event_type", replaceWithJSONNull(eventType));
            event.put("timestamp", timestamp);
            event.put("user_id", replaceWithJSONNull(userId));
            event.put("device_id", replaceWithJSONNull(deviceId));
            event.put("session_id", outOfSession ? -1 : sessionId);
            event.put("uuid", UUID.randomUUID().toString());
            event.put("sequence_number", getNextSequenceNumber());

            if (appliedTrackingOptions.shouldTrackVersionName()) {
                event.put("version_name", replaceWithJSONNull(deviceInfo.getVersionName()));
            }
            if (appliedTrackingOptions.shouldTrackOsName()) {
                event.put("os_name", replaceWithJSONNull(deviceInfo.getOsName()));
            }
            if (appliedTrackingOptions.shouldTrackOsVersion()) {
                event.put("os_version", replaceWithJSONNull(deviceInfo.getOsVersion()));
            }
            if (appliedTrackingOptions.shouldTrackApiLevel()) {
                event.put("api_level", replaceWithJSONNull(Build.VERSION.SDK_INT));
            }
            if (appliedTrackingOptions.shouldTrackDeviceBrand()) {
                event.put("device_brand", replaceWithJSONNull(deviceInfo.getBrand()));
            }
            if (appliedTrackingOptions.shouldTrackDeviceManufacturer()) {
                event.put("device_manufacturer", replaceWithJSONNull(deviceInfo.getManufacturer()));
            }
            if (appliedTrackingOptions.shouldTrackDeviceModel()) {
                event.put("device_model", replaceWithJSONNull(deviceInfo.getModel()));
            }
            if (appliedTrackingOptions.shouldTrackCarrier()) {
                event.put("carrier", replaceWithJSONNull(deviceInfo.getCarrier()));
            }
            if (appliedTrackingOptions.shouldTrackCountry()) {
                event.put("country", replaceWithJSONNull(deviceInfo.getCountry()));
            }
            if (appliedTrackingOptions.shouldTrackLanguage()) {
                event.put("language", replaceWithJSONNull(deviceInfo.getLanguage()));
            }
            if (appliedTrackingOptions.shouldTrackPlatform()) {
                event.put("platform", platform);
            }

            JSONObject library = new JSONObject();
            library.put("name", this.libraryName == null ? Constants.LIBRARY_UNKNOWN : this.libraryName);
            library.put("version", this.libraryVersion == null ? Constants.VERSION_UNKNOWN : this.libraryVersion);
            event.put("library", library);

            if (plan != null) {
                event.put("plan", plan.toJSONObject());
            }

            if (ingestionMetadata != null) {
                event.put("ingestion_metadata", ingestionMetadata.toJSONObject());
            }

            apiProperties = (apiProperties == null) ? new JSONObject() : apiProperties;
            if (apiPropertiesTrackingOptions != null && apiPropertiesTrackingOptions.length() > 0) {
                apiProperties.put("tracking_options", apiPropertiesTrackingOptions);
            }

            if (appliedTrackingOptions.shouldTrackLatLng()) {
                Location location = deviceInfo.getMostRecentLocation();
                if (location != null) {
                    JSONObject locationJSON = new JSONObject();
                    locationJSON.put("lat", location.getLatitude());
                    locationJSON.put("lng", location.getLongitude());
                    apiProperties.put("location", locationJSON);
                }
            }
            if (appliedTrackingOptions.shouldTrackAdid() && deviceInfo.getAdvertisingId() != null) {
                apiProperties.put("androidADID", deviceInfo.getAdvertisingId());
            }
            if (appliedTrackingOptions.shouldTrackAppSetId() && deviceInfo.getAppSetId() != null) {
                apiProperties.put("android_app_set_id", deviceInfo.getAppSetId());
            }
            apiProperties.put("limit_ad_tracking", deviceInfo.isLimitAdTrackingEnabled());
            apiProperties.put("gps_enabled", deviceInfo.isGooglePlayServicesEnabled());

            event.put("api_properties", apiProperties);
            event.put("event_properties", (eventProperties == null) ? new JSONObject()
                : truncate(eventProperties));
            event.put("user_properties", (userProperties == null) ? new JSONObject()
                : truncate(userProperties));
            event.put("groups", (groups == null) ? new JSONObject() : truncate(groups));
            event.put("group_properties", (groupProperties == null) ? new JSONObject()
                : truncate(groupProperties));
            result = saveEvent(eventType, event, extra);

            // If the the event is an identify, update the user properties to the core identity
            // for experiment SDK to consume.
            if (eventType.equals(Constants.IDENTIFY_EVENT) && userProperties != null) {
                connector.getIdentityStore().editIdentity()
                    .updateUserProperties(JSONUtil.toUpdateUserPropertiesMap(userProperties))
                    .commit();
            }
        } catch (JSONException e) {
            logger.e(TAG, String.format(
                "JSON Serialization of event type %s failed, skipping: %s", eventType, e.toString()
            ));
        }

        return result;
    }

    /**
     * Save event long. Internal method to save an event to the database.
     *
     * @param eventType the event type
     * @param event     the event
     * @param extra     the extra unstructured data for middleware
     * @return the event ID if succeeded, else -1
     */
    protected long saveEvent(String eventType, JSONObject event, MiddlewareExtra extra) {
        if (!middlewareRunner.run(new MiddlewarePayload(event, extra))) return -1;

        if (Utils.isEmptyString(event.toString())) {
            logger.e(TAG, String.format(
                "Detected empty event string for event type %s, skipping", eventType
            ));
            return -1;
        }

        // Intercept event
        event = identifyInterceptor.intercept(eventType, event);
        if (event == null) {
            return -1;
        }

        return saveEvent(eventType, event);
    }

    /**
     * Save event. Internal method to save an event.
     *
     * @param eventType the event type
     * @param event     the event
     * @return the event ID if succeeded, else -1
     */
    protected long saveEvent(String eventType, JSONObject event) {
        String eventString = event.toString();

        if (eventType.equals(Constants.IDENTIFY_EVENT) || eventType.equals(Constants.GROUP_IDENTIFY_EVENT)) {
            lastIdentifyId = dbHelper.addIdentify(eventString);
            setLastIdentifyId(lastIdentifyId);
        } else {
            lastEventId = dbHelper.addEvent(eventString);
            setLastEventId(lastEventId);
        }

        int numEventsToRemove = Math.min(
                Math.max(1, eventMaxCount/10),
                Constants.EVENT_REMOVE_BATCH_SIZE
        );
        if (dbHelper.getEventCount() > eventMaxCount) {
            dbHelper.removeEvents(dbHelper.getNthEventId(numEventsToRemove));
        }
        if (dbHelper.getIdentifyCount() > eventMaxCount) {
            dbHelper.removeIdentifys(dbHelper.getNthIdentifyId(numEventsToRemove));
        }

        long totalEventCount = dbHelper.getTotalEventCount(); // counts may have changed, refetch
        if ((totalEventCount % eventUploadThreshold) == 0 &&
                totalEventCount >= eventUploadThreshold) {
            updateServer();
        } else {
            updateServerLater(eventUploadPeriodMillis);
        }

        return (
            eventType.equals(Constants.IDENTIFY_EVENT) ||
            eventType.equals(Constants.GROUP_IDENTIFY_EVENT)
        ) ? lastIdentifyId : lastEventId;
    }

    // fetches key from dbHelper longValueStore
    // if key does not exist, return defaultValue instead
    private long getLongvalue(String key, long defaultValue) {
        Long value = dbHelper.getLongValue(key);
        return value == null ? defaultValue : value;
    }

    /**
     * Internal method to increment and fetch the next event sequence number.
     *
     * @return the next sequence number
     */
    long getNextSequenceNumber() {
        sequenceNumber++;
        dbHelper.insertOrReplaceKeyLongValue(SEQUENCE_NUMBER_KEY, sequenceNumber);
        return sequenceNumber;
    }

    /**
     * Internal method to set the last event time.
     *
     * @param timestamp the timestamp
     */
    void setLastEventTime(long timestamp) {
        lastEventTime = timestamp;
        dbHelper.insertOrReplaceKeyLongValue(LAST_EVENT_TIME_KEY, timestamp);
    }

    /**
     * Internal method to set the last event id.
     *
     * @param eventId the event id
     */
    void setLastEventId(long eventId) {
        lastEventId = eventId;
        dbHelper.insertOrReplaceKeyLongValue(LAST_EVENT_ID_KEY, eventId);
    }

    /**
     * Internal method to set the last identify id.
     *
     * @param identifyId the identify id
     */
    void setLastIdentifyId(long identifyId) {
        lastIdentifyId = identifyId;
        dbHelper.insertOrReplaceKeyLongValue(LAST_IDENTIFY_ID_KEY, identifyId);
    }

    /**
     * Gets the current session id.
     *
     * @return The current sessionId value.
     */
    public long getSessionId() {
        return sessionId;
    }

    /**
     * Internal method to set the previous session id.
     *
     * @param timestamp the timestamp
     */
    void setPreviousSessionId(long timestamp) {
        previousSessionId = timestamp;
        dbHelper.insertOrReplaceKeyLongValue(PREVIOUS_SESSION_ID_KEY, timestamp);
    }

    /**
     * Public method to start a new session if needed.
     *
     * @param timestamp the timestamp
     * @return whether or not a new session was started
     */
    public boolean startNewSessionIfNeeded(long timestamp) {
        if (inSession()) {
            if (isWithinMinTimeBetweenSessions(timestamp)) {
                refreshSessionTime(timestamp);
                return false;
            }

            startNewSession(timestamp);
            return true;
        }

        // no current session - check for previous session
        if (isWithinMinTimeBetweenSessions(timestamp)) {
            if (previousSessionId == -1) {
                startNewSession(timestamp);
                return true;
            }

            // extend previous session
            setSessionId(previousSessionId);
            refreshSessionTime(timestamp);
            return false;
        }

        startNewSession(timestamp);
        return true;
    }

    private void startNewSession(long timestamp) {
        // end previous session
        if (trackingSessionEvents) {
            sendSessionEvent(END_SESSION_EVENT);
        }

        // start new session
        setSessionId(timestamp);
        refreshSessionTime(timestamp);
        if (trackingSessionEvents) {
            sendSessionEvent(START_SESSION_EVENT);
        }
    }

    private boolean inSession() {
        return sessionId >= 0;
    }

    private boolean isWithinMinTimeBetweenSessions(long timestamp) {
        long sessionLimit = usingForegroundTracking ?
                minTimeBetweenSessionsMillis : sessionTimeoutMillis;
        return (timestamp - lastEventTime) < sessionLimit;
    }

    private void setSessionId(long timestamp) {
        sessionId = timestamp;
        setPreviousSessionId(timestamp);
    }

    /**
     * Internal method to refresh the current session time.
     *
     * @param timestamp the timestamp
     */
    void refreshSessionTime(long timestamp) {
        if (!inSession()) {
            return;
        }

        setLastEventTime(timestamp);
    }

    private void sendSessionEvent(final String sessionEvent) {
        if (!contextAndApiKeySet(String.format("sendSessionEvent('%s')", sessionEvent))) {
            return;
        }

        if (!inSession()) {
            return;
        }

        JSONObject apiProperties = new JSONObject();
        try {
            apiProperties.put("special", sessionEvent);
        } catch (JSONException e) {
            return;
        }

        logEvent(sessionEvent, null, apiProperties, null, null, null, lastEventTime, false, false);
    }

    /**
     * Internal method to handle on app exit foreground behavior.
     *
     * @param timestamp the timestamp
     */
    void onExitForeground(final long timestamp) {
        isEnteringForeground = false;
        inForeground = false;
        runOnLogThread(new Runnable() {
            @Override
            public void run() {
                if (Utils.isEmptyString(apiKey)) {
                    return;
                }
                refreshSessionTime(timestamp);
                if (flushEventsOnClose) {
                    identifyInterceptor.transferInterceptedIdentify();
                    updateServer();
                }

                // re-persist metadata into database for good measure
                dbHelper.insertOrReplaceKeyValue(DEVICE_ID_KEY, deviceId);
                dbHelper.insertOrReplaceKeyValue(USER_ID_KEY, userId);
                dbHelper.insertOrReplaceKeyLongValue(OPT_OUT_KEY, optOut ? 1L : 0L);
                dbHelper.insertOrReplaceKeyLongValue(PREVIOUS_SESSION_ID_KEY, sessionId);
                dbHelper.insertOrReplaceKeyLongValue(LAST_EVENT_TIME_KEY, lastEventTime);
            }
        });
    }

    /**
     * Internal method to handle on app enter foreground behavior.
     *
     * @param timestamp the timestamp
     */
    void onEnterForeground(final long timestamp) {
        isEnteringForeground = true;
        inForeground = true;
        runOnLogThread(new Runnable() {
            @Override
            public void run() {
                if (Utils.isEmptyString(apiKey)) {
                    return;
                }
                if (useDynamicConfig) {
                    ConfigManager.getInstance().refresh(new ConfigManager.RefreshListener() {
                        @Override
                        public void onFinished() {
                            url = ConfigManager.getInstance().getIngestionEndpoint();
                        }
                    }, serverZone);
                }
                // This should be true, unless somehow an event was tracked
                // between here and the beginning of this method
                // in that case the session is started in logEvent()
                if (isEnteringForeground) {
                    startNewSessionIfNeeded(timestamp);
                }
                isEnteringForeground = false;
            }
        });
    }

    /**
     * Log revenue amount via a revenue event.
     *
     * @param amount the amount
     * @deprecated - use {@code logRevenueV2} instead
     * @see <a href="https://github.com/amplitude/Amplitude-Android#tracking-revenue">
     *     Tracking Revenue</a>
     */
    public void logRevenue(double amount) {
        // Amount is in dollars
        // ex. $3.99 would be pass as logRevenue(3.99)
        logRevenue(null, 1, amount);
    }

    /**
     * Log revenue with a productId, quantity, and price.
     *
     * @param productId the product id
     * @param quantity  the quantity
     * @param price     the price
     * @deprecated - use {@code logRevenueV2} instead
     * @see <a href="https://github.com/amplitude/Amplitude-Android#tracking-revenue">
     *     Tracking Revenue</a>
     */
    public void logRevenue(String productId, int quantity, double price) {
        logRevenue(productId, quantity, price, null, null);
    }

    public void logRevenue(String productId, int quantity, double price, String receipt,
                           String receiptSignature) {
        logRevenue(productId, quantity, price, receipt, receiptSignature, null);
    }
    /**
     * Log revenue with a productId, quantity, price, and receipt data for revenue verification.
     *
     * @param productId        the product id
     * @param quantity         the quantity
     * @param price            the price
     * @param receipt          the receipt
     * @param receiptSignature the receipt signature
     * @param extra            the extra unstructured data for middleware
     * @deprecated - use {@code logRevenueV2} instead
     * @see <a href="https://github.com/amplitude/Amplitude-Android#tracking-revenue">
     *     Tracking Revenue</a>
     */
    public void logRevenue(String productId, int quantity, double price, String receipt,
            String receiptSignature, MiddlewareExtra extra) {
        if (!contextAndApiKeySet("logRevenue()")) {
            return;
        }

        // Log revenue in events
        JSONObject apiProperties = new JSONObject();
        try {
            apiProperties.put("special", Constants.AMP_REVENUE_EVENT);
            apiProperties.put("productId", productId);
            apiProperties.put("quantity", quantity);
            apiProperties.put("price", price);
            apiProperties.put("receipt", receipt);
            apiProperties.put("receiptSig", receiptSignature);
        } catch (JSONException e) {

        }

        logEventAsync(
            Constants.AMP_REVENUE_EVENT, null, apiProperties, null, null, null, getCurrentTimeMillis(), false, extra
        );
    }

    /**
     * Log revenue v2. Create a {@link Revenue} object to hold your revenue data and properties,
     * and log it as a revenue event using this method.
     *
     * @param revenue a {@link Revenue} object
     */
    public void logRevenueV2(Revenue revenue) {
        logRevenueV2(revenue, null);
    }

    public void logRevenueV2(Revenue revenue, MiddlewareExtra extra) {
        if (!contextAndApiKeySet("logRevenueV2()") || revenue == null || !revenue.isValidRevenue()) {
            return;
        }

        logEvent(Constants.AMP_REVENUE_EVENT, revenue.toJSONObject(), null, null, null, null, getCurrentTimeMillis(), false, extra, this.inForeground);
    }

    /**
     * Sets user properties. This is a convenience wrapper around the
     * {@link Identify} API to set multiple user properties with a single
     * command. <b>Note:</b> the replace parameter is deprecated and has no effect.
     *
     * @param userProperties the user properties
     * @param replace        the replace - has no effect
     * @deprecated
     */
    public void setUserProperties(final JSONObject userProperties, final boolean replace) {
        setUserProperties(userProperties);
    }

    /**
     * Sets user properties. This is a convenience wrapper around the
     * {@link Identify} API to set multiple user properties with a single
     * command.
     *
     * @param userProperties the user properties
     */
    public void setUserProperties(final JSONObject userProperties) {
        setUserProperties(userProperties, null);
    }

    /**
     * Sets user properties. This is a convenience wrapper around the
     * {@link Identify} API to set multiple user properties with a single
     * command.
     *
     * @param userProperties the user properties
     * @param extra          the extra unstructured data for middleware
     */
    public void setUserProperties(final JSONObject userProperties, MiddlewareExtra extra) {
        if (userProperties == null || userProperties.length() == 0 ||
                !contextAndApiKeySet("setUserProperties")) {
            return;
        }

        Identify identify = convertPropertiesToIdentify(userProperties);
        if (identify != null) {
            identify(identify, false, extra);
        }
    }

    private Identify convertPropertiesToIdentify(final JSONObject properties) {
        if (properties == null) {
            return null;
        }

        // sanitize and truncate properties before trying to convert to identify
        JSONObject sanitized = truncate(properties);
        if (sanitized.length() == 0) {
            return null;
        }

        Identify identify = new Identify();
        Iterator<?> keys = sanitized.keys();
        while (keys.hasNext()) {
            String key = (String) keys.next();
            try {
                identify.setUserProperty(key, sanitized.get(key));
            } catch (JSONException e) {
                logger.e(TAG, e.toString());
            }
        }
        return identify;
    }

    /**
     * Clear user properties. This will clear all user properties at once. <b>Note: the
     * result is irreversible!</b>
     */
    public void clearUserProperties() {
        Identify identify = new Identify().clearAll();
        identify(identify);
    }

    /**
     * Identify. Use this to send an {@link Identify} object containing
     * user property operations to Amplitude server.
     *
     * @param identify an {@link Identify} object
     */
    public void identify(Identify identify) {
        identify(identify, false);
    }

    public void identify(Identify identify, boolean outOfSession) {
        identify(identify, outOfSession, null);
    }

    /**
     * Identify. Use this to send an {@link com.amplitude.api.Identify} object containing
     * user property operations to Amplitude server. If outOfSession is true, then the identify
     * event is sent with a session id of -1, and does not trigger any session-handling logic.
     *
     * @param identify      an {@link Identify} object
     * @param outOfSession  whther to log the identify event out of session
     * @param extra         the extra unstructured data for middleware
     */
    public void identify(Identify identify, boolean outOfSession, MiddlewareExtra extra) {
        if (
            identify == null || identify.userPropertiesOperations.length() == 0 ||
            !contextAndApiKeySet("identify()")
        ) return;
        logEventAsync(
            Constants.IDENTIFY_EVENT, null, null, identify.userPropertiesOperations,
            null, null, getCurrentTimeMillis(), outOfSession, extra
        );
    }

    /**
     * Sets the user's group(s).
     *
     * @param groupType the group type (ex: orgId)
     * @param groupName the group name (ex: 15)
     */
    public void setGroup(String groupType, Object groupName) {
        setGroup(groupType, groupName, null);
    }

    /**
     * Sets the user's group(s).
     *
     * @param groupType the group type (ex: orgId)
     * @param groupName the group name (ex: 15)
     * @param extra     the extra unstructured data for middleware
     */
    public void setGroup(String groupType, Object groupName, MiddlewareExtra extra) {
        if (!contextAndApiKeySet("setGroup()") || Utils.isEmptyString(groupType)) {
            return;
        }

        JSONObject group = null;
        try {
            group = new JSONObject().put(groupType, groupName);
        } catch (JSONException e) {
            logger.e(TAG, e.toString());
        }

        Identify identify = new Identify().setUserProperty(groupType, groupName);
        logEventAsync(Constants.IDENTIFY_EVENT, null, null, identify.userPropertiesOperations,
                group, null, getCurrentTimeMillis(), false, extra);
    }

    public void groupIdentify(String groupType, Object groupName, Identify groupIdentify) {
        groupIdentify(groupType, groupName, groupIdentify, false);
    }

    public void groupIdentify(String groupType, Object groupName, Identify groupIdentify, boolean outOfSession) {
        groupIdentify(groupType, groupName, groupIdentify, outOfSession, null);
    }

    public void groupIdentify(String groupType, Object groupName, JSONObject groupProperties, boolean outOfSession, MiddlewareExtra extra) {
        Identify identify = convertPropertiesToIdentify(groupProperties);
        if (identify != null) {
            groupIdentify(groupType, groupName, identify, outOfSession, extra);
        }
    }

    public void groupIdentify(String groupType, Object groupName, Identify groupIdentify, boolean outOfSession, MiddlewareExtra extra) {
        if (groupIdentify == null || groupIdentify.userPropertiesOperations.length() == 0 ||
            !contextAndApiKeySet("groupIdentify()") || Utils.isEmptyString(groupType)) {

            return;
        }

        JSONObject group = null;
        try {
            group = new JSONObject().put(groupType, groupName);
        } catch (JSONException e) {
            logger.e(TAG, e.toString());
        }

        logEventAsync(
            Constants.GROUP_IDENTIFY_EVENT, null, null, null, group,
            groupIdentify.userPropertiesOperations, getCurrentTimeMillis(), outOfSession, extra
        );
    }

    /**
     * Truncate values in a JSON object. Any string values longer than 1024 characters will be
     * truncated to 1024 characters.
     * Any dictionary with more than 1000 items will be ignored.
     *
     * @param object the object
     * @return the truncated JSON object
     */
    public JSONObject truncate(JSONObject object) {
        if (object == null) {
            return new JSONObject();
        }

        if (object.length() > Constants.MAX_PROPERTY_KEYS) {
            logger.w(TAG, "Warning: too many properties (more than 1000), ignoring");
            return new JSONObject();
        }

        Iterator<?> keys = object.keys();
        while (keys.hasNext()) {
            String key = (String) keys.next();

            try {
                Object value = object.get(key);
                // do not truncate revenue receipt and receipt sig fields
                if (key.equals(Constants.AMP_REVENUE_RECEIPT) ||
                        key.equals(Constants.AMP_REVENUE_RECEIPT_SIG)) {
                    object.put(key, value);
                } else if (value.getClass().equals(String.class)) {
                    object.put(key, truncate((String) value));
                } else if (value.getClass().equals(JSONObject.class)) {
                    object.put(key, truncate((JSONObject) value));
                } else if (value.getClass().equals(JSONArray.class)) {
                    object.put(key, truncate((JSONArray) value));
                }
            } catch (JSONException e) {
                logger.e(TAG, e.toString());
            }
        }

        return object;
    }

    /**
     * Truncate values in a JSON array. Any string values longer than 1024 characters will be
     * truncated to 1024 characters.
     *
     * @param array the array
     * @return the truncated JSON array
     * @throws JSONException the json exception
     */
    public JSONArray truncate(JSONArray array) throws JSONException {
        if (array == null) {
            return new JSONArray();
        }

        for (int i = 0; i < array.length(); i++) {
            Object value = array.get(i);
            if (value.getClass().equals(String.class)) {
                array.put(i, truncate((String) value));
            } else if (value.getClass().equals(JSONObject.class)) {
                array.put(i, truncate((JSONObject) value));
            } else if (value.getClass().equals(JSONArray.class)) {
                array.put(i, truncate((JSONArray) value));
            }
        }
        return array;
    }

    /**
     * Truncate a string to 1024 characters.
     *
     * @param value the value
     * @return the truncated string
     */
    public static String truncate(String value) {
        return value.length() <= Constants.MAX_STRING_LENGTH ? value :
                value.substring(0, Constants.MAX_STRING_LENGTH);
    }


    /**
     * Gets the user's id. Can be null.
     *
     * @return The developer specified identifier for tracking within the analytics system.
     */
    public String getUserId() {
        return userId;
    }

    /**
     * Sets the user id (can be null).
     *
     * @param userId the user id
     * @return the AmplitudeClient
     */
    public AmplitudeClient setUserId(final String userId) {
        return setUserId(userId, false);
    }

    /**
     * Sets the user id (can be null).
     * If startNewSession is true, ends the session for the previous user and starts a new
     * session for the new user id.
     *
     * @param userId the user id
     * @return the AmplitudeClient
     */
    public AmplitudeClient setUserId(final String userId, final boolean startNewSession) {
        if (!contextAndApiKeySet("setUserId()")) {
            return this;
        }

        final AmplitudeClient client = this;
        runOnLogThread(new Runnable() {
            @Override
            public void run() {
                if (Utils.isEmptyString(client.apiKey)) {  // in case initialization failed
                    return;
                }

                // end previous session
                if (startNewSession && trackingSessionEvents) {
                    sendSessionEvent(END_SESSION_EVENT);
                }

                client.userId = userId;
                dbHelper.insertOrReplaceKeyValue(USER_ID_KEY, userId);

                // start new session
                if (startNewSession) {
                    long timestamp = getCurrentTimeMillis();
                    setSessionId(timestamp);
                    refreshSessionTime(timestamp);
                    if (trackingSessionEvents) {
                        sendSessionEvent(START_SESSION_EVENT);
                    }
                }

                // update the user in the core identity store to notify
                // experiment to re-fetch variants with the new identity
                client.connector.getIdentityStore().editIdentity().setUserId(userId).commit();
            }
        });
        return this;
    }

    /**
     * Sets a custom device id. <b>Note: only do this if you know what you are doing!</b>
     *
     * @param deviceId the device id
     * @return the AmplitudeClient
     */
    public AmplitudeClient setDeviceId(final String deviceId) {
        Set<String> invalidDeviceIds = getInvalidDeviceIds();
        if (!contextAndApiKeySet("setDeviceId()") || Utils.isEmptyString(deviceId) ||
                invalidDeviceIds.contains(deviceId)) {
            return this;
        }

        final AmplitudeClient client = this;
        runOnLogThread(new Runnable() {
            @Override
            public void run() {
                if (Utils.isEmptyString(client.apiKey)) {  // in case initialization failed
                    return;
                }
                client.deviceId = deviceId;
                saveDeviceId(deviceId);

                // update the user in the core identity store to notify
                // experiment to re-fetch variants with the new identity
                client.connector.getIdentityStore().editIdentity().setDeviceId(deviceId).commit();
            }
        });
        return this;
    }

    /**
     * Regenerates a new random deviceId for current user. Note: this is not recommended unless you
     * know what you are doing. This can be used in conjunction with setUserId(null) to anonymize
     * users after they log out. With a null userId and a completely new deviceId, the current user
     * would appear as a brand new user in dashboard.
     */
    public AmplitudeClient regenerateDeviceId() {
        if (!contextAndApiKeySet("regenerateDeviceId()")) {
            return this;
        }

        final AmplitudeClient client = this;
        runOnLogThread(new Runnable() {
            @Override
            public void run() {
                if (Utils.isEmptyString(client.apiKey)) { // in case initialization failed
                    return;
                }
                String randomId = DeviceInfo.generateUUID() + "R";
                setDeviceId(randomId);
            }
        });
        return this;
    }

    /**
     * Force SDK to upload any unsent events.
     */
    public void uploadEvents() {
        if (!contextAndApiKeySet("uploadEvents()")) {
            return;
        }

        logThread.post(new Runnable() {
            @Override
            public void run() {
                if (Utils.isEmptyString(apiKey)) {  // in case initialization failed
                    return;
                }
                identifyInterceptor.transferInterceptedIdentify();
                updateServer();
            }
        });
    }

    private void updateServerLater(long delayMillis) {
        if (updateScheduled.getAndSet(true)) {
            return;
        }

        logThread.postDelayed(new Runnable() {
            @Override
            public void run() {
                updateScheduled.set(false);
                updateServer();
            }
        }, delayMillis);
    }

    /**
     * Internal method to upload unsent events.
     */
    protected void updateServer() {
        updateServer(false);
    }

    /**
     * Internal method to upload unsent events. Limit controls whether to use event upload max
     * batch size or backoff upload batch size. <b>Note: </b> always call this on logThread
     *
     * @param limit the limit
     */
    protected void updateServer(boolean limit) {
        if (optOut || offline) {
            return;
        }

        // Flush middleware
        middlewareRunner.flush();

        // if returning out of this block, always be sure to set uploadingCurrently to false!!
        if (!uploadingCurrently.getAndSet(true)) {
            long totalEventCount = dbHelper.getTotalEventCount();
            long batchSize = Math.min(
                limit ? backoffUploadBatchSize : eventUploadMaxBatchSize,
                totalEventCount
            );

            if (batchSize <= 0) {
                uploadingCurrently.set(false);
                return;
            }

            try {
                List<JSONObject> events = dbHelper.getEvents(lastEventId, batchSize);
                List<JSONObject> identifys = dbHelper.getIdentifys(lastIdentifyId, batchSize);

                final Pair<Pair<Long, Long>, JSONArray> merged = mergeEventsAndIdentifys(
                        events, identifys, batchSize);
                final JSONArray mergedEvents = merged.second;
                if (mergedEvents.length() == 0) {
                    uploadingCurrently.set(false);
                    return;
                }
                final long maxEventId = merged.first.first;
                final long maxIdentifyId = merged.first.second;
                final String mergedEventsString = merged.second.toString();

                httpThread.post(new Runnable() {
                    @Override
                    public void run() {
                        makeEventUploadPostRequest(callFactory, mergedEventsString, maxEventId, maxIdentifyId);
                    }
                });
            } catch (JSONException e) {
                uploadingCurrently.set(false);
                logger.e(TAG, e.toString());
            } catch (CursorWindowAllocationException e) {
                // handle CursorWindowAllocationException when fetching events, defer upload
                uploadingCurrently.set(false);
                logger.e(TAG, String.format(
                    "Caught Cursor window exception during event upload, deferring upload: %s",
                    e.getMessage()
                ));
            }
        }
    }

    /**
     * Internal method to merge unsent events and identifies into a single array by sequence number.
     *
     * @param events    the events
     * @param identifys the identifys
     * @param numEvents the num events
     * @return the merged array, max event id, and max identify id
     * @throws JSONException the json exception
     */
    protected Pair<Pair<Long,Long>, JSONArray> mergeEventsAndIdentifys(List<JSONObject> events,
                            List<JSONObject> identifys, long numEvents) throws JSONException {
        JSONArray merged = new JSONArray();
        long maxEventId = -1;
        long maxIdentifyId = -1;

        while (merged.length() < numEvents) {
            boolean noEvents = events.isEmpty();
            boolean noIdentifys = identifys.isEmpty();

            // case 0: no events or identifys, nothing to grab
            // this case should never happen, as it means there are less identifys and events
            // than expected
            if (noEvents && noIdentifys) {
                logger.w(TAG, String.format(
                    "mergeEventsAndIdentifys: number of events and identifys " +
                    "less than expected by %d", numEvents - merged.length())
                );
                break;

            // case 1: no identifys, grab from events
            } else if (noIdentifys) {
                JSONObject event = events.remove(0);
                maxEventId = event.getLong("event_id");
                merged.put(event);

            // case 2: no events, grab from identifys
            } else if (noEvents) {
                JSONObject identify = identifys.remove(0);
                maxIdentifyId = identify.getLong("event_id");
                merged.put(identify);

            // case 3: need to compare sequence numbers
            } else {
                // events logged before v2.1.0 won't have a sequence number, put those first
                if (!events.get(0).has("sequence_number") ||
                        events.get(0).getLong("sequence_number") <
                        identifys.get(0).getLong("sequence_number")) {
                    JSONObject event = events.remove(0);
                    maxEventId = event.getLong("event_id");
                    merged.put(event);
                } else {
                    JSONObject identify = identifys.remove(0);
                    maxIdentifyId = identify.getLong("event_id");
                    merged.put(identify);
                }
            }
        }

        return new Pair<Pair<Long, Long>, JSONArray>(new Pair<Long,Long>(maxEventId, maxIdentifyId), merged);
    }

    /**
     * Internal method to generate the event upload post request.
     *
     * @param client        the client
     * @param events        the events
     * @param maxEventId    the max event id
     * @param maxIdentifyId the max identify id
     */
    protected void makeEventUploadPostRequest(Call.Factory client, String events, final long maxEventId, final long maxIdentifyId) {
        String apiVersionString = "" + Constants.API_VERSION;
        String timestampString = "" + getCurrentTimeMillis();

        FormBody body = new FormBody.Builder()
            .add("v", apiVersionString)
            .add("client", apiKey)
            .add("e", events)
            .add("upload_time", timestampString)
            .build();

        Request request;
        try {
             Request.Builder builder = new Request.Builder()
                     .url(url)
                     .post(body);

             if (!Utils.isEmptyString(bearerToken)) {
                builder.addHeader("Authorization", "Bearer " + bearerToken);
             }

             request = builder.build();
        } catch (IllegalArgumentException e) {
            logger.e(TAG, e.toString());
            uploadingCurrently.set(false);
            return;
        }

        boolean uploadSuccess = false;

        try {
            Response response = client.newCall(request).execute();
            String stringResponse = response.body().string();
            if (response.code() == 200) {
                uploadSuccess = true;
                logThread.post(new Runnable() {
                    @Override
                    public void run() {
                        if (maxEventId >= 0) dbHelper.removeEvents(maxEventId);
                        if (maxIdentifyId >= 0) dbHelper.removeIdentifys(maxIdentifyId);
                        uploadingCurrently.set(false);
                        if (dbHelper.getTotalEventCount() > eventUploadThreshold) {
                            logThread.post(new Runnable() {
                                @Override
                                public void run() {
                                    updateServer(backoffUpload);
                                }
                            });
                        }
                        else {
                            backoffUpload = false;
                            backoffUploadBatchSize = eventUploadMaxBatchSize;
                        }
                    }
                });
            } else if (response.code() == 400 && stringResponse.equals("invalid_api_key")) {
                logger.e(TAG, "Invalid API key, make sure your API key is correct in initialize()");
            } else if (response.code() == 400 && stringResponse.equals("bad_checksum")) {
                logger.w(TAG,
                        "Bad checksum, post request was mangled in transit, will attempt to reupload later");
            } else if (response.code() == 413) {

                // If blocked by one massive event, drop it
                if (backoffUpload && backoffUploadBatchSize == 1) {
                    if (maxEventId >= 0) dbHelper.removeEvent(maxEventId);
                    if (maxIdentifyId >= 0) dbHelper.removeIdentify(maxIdentifyId);
                    // maybe we want to reset backoffUploadBatchSize after dropping massive event
                }

                // Server complained about length of request, backoff and try again
                backoffUpload = true;
                int numEvents = Math.min((int)dbHelper.getEventCount(), backoffUploadBatchSize);
                backoffUploadBatchSize = (int)Math.ceil(numEvents / 2.0);
                logger.w(TAG, "Request too large, will decrease size and attempt to reupload");
                logThread.post(new Runnable() {
                   @Override
                    public void run() {
                       uploadingCurrently.set(false);
                       updateServer(true);
                   }
                });
            } else {
                logger.w(TAG, "Upload failed, " + stringResponse
                        + ", will attempt to reupload later");
            }
        } catch (java.net.ConnectException e) {
            // logger.w(TAG,
            // "No internet connection found, unable to upload events");
            lastError = e;
        } catch (java.net.UnknownHostException e) {
            // logger.w(TAG,
            // "No internet connection found, unable to upload events");
            lastError = e;
        } catch (IOException e) {
            logger.e(TAG, e.toString());
            lastError = e;
        } catch (AssertionError e) {
            // This can be caused by a NoSuchAlgorithmException thrown by DefaultHttpClient
            logger.e(TAG, "Exception:", e);
            lastError = e;
        } catch (Exception e) {
            // Just log any other exception so things don't crash on upload
            logger.e(TAG, "Exception:", e);
            lastError = e;
        }

        if (!uploadSuccess) {
            uploadingCurrently.set(false);
        }

    }

    protected DeviceInfo initializeDeviceInfo() {
        return new DeviceInfo(context, this.locationListening, appliedTrackingOptions.shouldTrackAdid());
    }

    /**
     * Get the current device id. Can be null if deviceId hasn't been initialized yet.
     *
     * @return A unique identifier for tracking within the analytics system.
     */
    public String getDeviceId() {
        return deviceId;
    }

    // don't need to keep this in memory, if only using it at most 1 or 2 times
    private Set<String> getInvalidDeviceIds() {
        Set<String> invalidDeviceIds = new HashSet<String>();
        invalidDeviceIds.add("");
        invalidDeviceIds.add("9774d56d682e549c");
        invalidDeviceIds.add("unknown");
        invalidDeviceIds.add("000000000000000"); // Common Serial Number
        invalidDeviceIds.add("Android");
        invalidDeviceIds.add("DEFACE");
        invalidDeviceIds.add("00000000-0000-0000-0000-000000000000");

        return invalidDeviceIds;
    }

    private String initializeDeviceId() {
        Set<String> invalidIds = getInvalidDeviceIds();

        // see if device id already stored in db
        String deviceId = dbHelper.getValue(DEVICE_ID_KEY);
        if (!(Utils.isEmptyString(deviceId) || invalidIds.contains(deviceId) || deviceId.endsWith("S"))) {
            return deviceId;
        }

        if (!newDeviceIdPerInstall && useAdvertisingIdForDeviceId && !deviceInfo.isLimitAdTrackingEnabled()) {
            // Android ID is deprecated by Google.
            // We are required to use Advertising ID, and respect the advertising ID preference

            String advertisingId = deviceInfo.getAdvertisingId();
            if (!(Utils.isEmptyString(advertisingId) || invalidIds.contains(advertisingId))) {
                saveDeviceId(advertisingId);
                return advertisingId;
            }
        }

        if (useAppSetIdForDeviceId) {
            String appSetId = deviceInfo.getAppSetId();
            if (!(Utils.isEmptyString(appSetId) || invalidIds.contains(appSetId))) {
                // Suffix with S for app set id so in future we can tell if device id is from app set id
                String appSetDeviceId = appSetId + "S";
                saveDeviceId(appSetDeviceId);
                return appSetDeviceId;
            }
        }

        // If this still fails, generate random identifier that does not persist
        // across installations. Append R to distinguish as randomly generated
        String randomId = deviceInfo.generateUUID() + "R";
        saveDeviceId(randomId);
        return randomId;
    }

    private void saveDeviceId(String deviceId) {
        dbHelper.insertOrReplaceKeyValue(DEVICE_ID_KEY, deviceId);
    }

    public AmplitudeClient setDeviceIdCallback(AmplitudeDeviceIdCallback callback) {
        this.deviceIdCallback = callback;
        return this;
    }

    protected void runOnLogThread(Runnable r) {
        if (Thread.currentThread() != logThread) {
            logThread.post(r);
        } else {
            r.run();
        }
    }

    /**
     * Internal method to replace null event fields with JSON null object.
     *
     * @param obj the obj
     * @return the object
     */
    protected Object replaceWithJSONNull(Object obj) {
        return obj == null ? JSONObject.NULL : obj;
    }

    /**
     * Internal method to check whether application context and api key are set
     *
     * @param methodName the parent method name to print in error message
     * @return whether application context and api key are set
     */
    protected synchronized boolean contextAndApiKeySet(String methodName) {
        if (context == null) {
            logger.e(TAG, "context cannot be null, set context with initialize() before calling "
                    + methodName);
            return false;
        }
        if (Utils.isEmptyString(apiKey)) {
            logger.e(TAG,
                    "apiKey cannot be null or empty, set apiKey with initialize() before calling "
                            + methodName);
            return false;
        }
        return true;
    }

    /**
     * Internal method to convert bytes to hex string
     *
     * @param bytes the bytes
     * @return the string
     */
    protected String bytesToHexString(byte[] bytes) {
        final char[] hexArray = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b',
                'c', 'd', 'e', 'f' };
        char[] hexChars = new char[bytes.length * 2];
        int v;
        for (int j = 0; j < bytes.length; j++) {
            v = bytes[j] & 0xFF;
            hexChars[j * 2] = hexArray[v >>> 4];
            hexChars[j * 2 + 1] = hexArray[v & 0x0F];
        }
        return new String(hexChars);
    }

    /**
     * Internal method to fetch the current time millis. Used for testing.
     *
     * @return the current time millis
     */
    protected long getCurrentTimeMillis() { return System.currentTimeMillis(); }

    /**
     * Set tracking plan information.
     * @param plan Plan object
     * @return the AmplitudeClient
     */
    public AmplitudeClient setPlan(Plan plan) {
        this.plan = plan;
        return this;
    }

    /**
     * Set ingestion metadata information.
     * @param ingestionMetadata IngestionMetadata object
     * @return the AmplitudeClient
     */
    public AmplitudeClient setIngestionMetadata(IngestionMetadata ingestionMetadata) {
        this.ingestionMetadata = ingestionMetadata;
        return this;
    }

    /**
     * Set Amplitude Server Zone, switch to zone related configuration,
     * including dynamic configuration and server url.
     *
     * To send data to Amplitude's EU servers, you need to configure the serverZone to EU like
     * client.setServerZone(AmplitudeServerZone.EU);
     *
     * @param serverZone AmplitudeServerZone, US or EU, default is US
     * @return the AmplitudeClient
     */
    public AmplitudeClient setServerZone(AmplitudeServerZone serverZone) {
        return setServerZone(serverZone, true);
    }

    /**
     * Set Amplitude Server Zone, switch to zone related configuration,
     * including dynamic configuration. If updateServerUrl is true, including server url as well.
     * Recommend to keep updateServerUrl to be true for alignment.
     *
     * @param serverZone AmplitudeServerZone, US or EU, default is US
     * @param updateServerUrl if update server url when update server zone, recommend setting true
     * @return
     */
    public AmplitudeClient setServerZone(AmplitudeServerZone serverZone, boolean updateServerUrl) {
        if (serverZone == null) {
            return null;
        }
        this.serverZone = serverZone;
        if (updateServerUrl) {
            setServerUrl(AmplitudeServerZone.getEventLogApiForZone(serverZone));
        }
        return this;
    }

    /**
     * Get Amplitude Server Zone
     *
     * @return the current Amplitude Server Zone
     */
    public AmplitudeServerZone getServerZone() {
        return this.serverZone;
    }
}


================================================
FILE: src/main/java/com/amplitude/api/AmplitudeDeviceIdCallback.java
================================================
package com.amplitude.api;

public interface AmplitudeDeviceIdCallback {
    void onDeviceIdReady(String deviceId);
}


================================================
FILE: src/main/java/com/amplitude/api/AmplitudeLog.java
================================================
package com.amplitude.api;

import android.util.Log;

public class AmplitudeLog {
    private volatile boolean enableLogging = true;
    private volatile int logLevel = Log.INFO; // default log level
    private AmplitudeLogCallback amplitudeLogCallback = null;

    protected static AmplitudeLog instance = new AmplitudeLog();

    public static AmplitudeLog getLogger() {
        return instance;
    }

    private AmplitudeLog() {} // prevent instantiation

    AmplitudeLog setEnableLogging(boolean enableLogging) {
        this.enableLogging = enableLogging;
        return instance;
    }

    AmplitudeLog setLogLevel(int logLevel) {
        this.logLevel = logLevel;
        return instance;
    }

    int d(String tag, String msg) {
        if (enableLogging && logLevel <= Log.DEBUG) return Log.d(tag, msg);
        return 0;
    }

    int d(String tag, String msg, Throwable tr) {
        if (enableLogging && logLevel <= Log.DEBUG) return Log.d(tag, msg, tr);
        return 0;
    }

    int e(String tag, String msg) {
        if (enableLogging && logLevel <= Log.ERROR) {
            if (this.amplitudeLogCallback != null) {
                this.amplitudeLogCallback.onError(tag, msg);
            }
            return Log.e(tag, msg);
        }
        return 0;
    }

    int e(String tag, String msg, Throwable tr) {
        if (enableLogging && logLevel <= Log.ERROR) {
            if (this.amplitudeLogCallback != null) {
                this.amplitudeLogCallback.onError(tag, msg);
            }
            return Log.e(tag, msg, tr);
        }
        return 0;
    }

    String getStackTraceString(Throwable tr) {
        return Log.getStackTraceString(tr);
    }

    int i(String tag, String msg) {
        if (enableLogging && logLevel <= Log.INFO) return Log.i(tag, msg);
        return 0;
    }

    int i(String tag, String msg, Throwable tr) {
        if (enableLogging && logLevel <= Log.INFO) return Log.i(tag, msg, tr);
        return 0;
    }

    boolean isLoggable(String tag, int level) {
        return Log.isLoggable(tag, level);
    }

    int println(int priority, String tag, String msg) {
        return Log.println(priority, tag, msg);
    }

    int v(String tag, String msg) {
        if (enableLogging && logLevel <= Log.VERBOSE) return Log.v(tag, msg);
        return 0;
    }

    int v(String tag, String msg, Throwable tr) {
        if (enableLogging && logLevel <= Log.VERBOSE) return Log.v(tag, msg, tr);
        return 0;
    }

    int w(String tag, String msg) {
        if (enableLogging && logLevel <= Log.WARN) return Log.w(tag, msg);
        return 0;
    }

    int w(String tag, Throwable tr) {
        if (enableLogging && logLevel <= Log.WARN) return Log.w(tag, tr);
        return 0;
    }

    int w(String tag, String msg, Throwable tr) {
        if (enableLogging && logLevel <= Log.WARN) return Log.w(tag, msg, tr);
        return 0;
    }

    // wtf = What a Terrible Failure, logged at level ASSERT
    int wtf(String tag, String msg) {
        if (enableLogging && logLevel <= Log.ASSERT) return Log.wtf(tag, msg);
        return 0;
    }

    int wtf(String tag, Throwable tr) {
        if (enableLogging && logLevel <= Log.ASSERT) return Log.wtf(tag, tr);
        return 0;
    }

    int wtf(String tag, String msg, Throwable tr) {
        if (enableLogging && logLevel <= Log.ASSERT) return Log.wtf(tag, msg, tr);
        return 0;
    }

    void setAmplitudeLogCallback(AmplitudeLogCallback callback) {
        this.amplitudeLogCallback = callback;
    }
}


================================================
FILE: src/main/java/com/amplitude/api/AmplitudeLogCallback.java
================================================
package com.amplitude.api;

public interface AmplitudeLogCallback {
    void onError(String tag, String message);
}


================================================
FILE: src/main/java/com/amplitude/api/AmplitudeServerZone.java
================================================
package com.amplitude.api;

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

/**
 * AmplitudeServerZone is for Data Residency and handling server zone related properties.
 * The server zones now are US and EU.
 *
 * For usage like sending data to Amplitude's EU servers, you need to configure the serverZone
 * property after initializing the client with setServerZone method.
 */
public enum AmplitudeServerZone {
    US, EU;

    private static Map<AmplitudeServerZone, String> amplitudeServerZoneEventLogApiMap =
        new HashMap<AmplitudeServerZone, String>() {{
            put(AmplitudeServerZone.US, Constants.EVENT_LOG_URL);
            put(AmplitudeServerZone.EU, Constants.EVENT_LOG_EU_URL);
        }};

    private static Map<AmplitudeServerZone, String> amplitudeServerZoneDynamicConfigMap =
        new HashMap<AmplitudeServerZone, String>() {{
            put(AmplitudeServerZone.US, Constants.DYNAMIC_CONFIG_URL);
            put(AmplitudeServerZone.EU, Constants.DYNAMIC_CONFIG_EU_URL);
        }};


    protected static String getEventLogApiForZone(AmplitudeServerZone serverZone) {
        if (amplitudeServerZoneEventLogApiMap.containsKey(serverZone)) {
            return amplitudeServerZoneEventLogApiMap.get(serverZone);
        }
        return Constants.EVENT_LOG_URL;
    }

    protected static String getDynamicConfigApi(AmplitudeServerZone serverZone) {
        if (amplitudeServerZoneDynamicConfigMap.containsKey(serverZone)) {
            return amplitudeServerZoneDynamicConfigMap.get(serverZone);
        }
        return Constants.DYNAMIC_CONFIG_URL;
    }

    public static AmplitudeServerZone getServerZone(String serverZone) {
        AmplitudeServerZone amplitudeServerZone = AmplitudeServerZone.US;
        switch (serverZone) {
            case "EU":
                amplitudeServerZone = AmplitudeServerZone.EU;
                break;
            case "US":
                amplitudeServerZone = AmplitudeServerZone.US;
                break;
            default:
                break;
        }
        return amplitudeServerZone;
    }
}


================================================
FILE: src/main/java/com/amplitude/api/ConfigManager.java
================================================
package com.amplitude.api;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

public class ConfigManager {
    private static String KEY_INGESTION_ENDPOINT = "ingestionEndpoint";

    private static ConfigManager instance = null;

    private String ingestionEndpoint = Constants.EVENT_LOG_URL;

    public String getIngestionEndpoint() {
        return ingestionEndpoint;
    }

    private ConfigManager() {
    }

    public void refresh(RefreshListener listener, AmplitudeServerZone serverZone) {
        try {
            String dynamicConfigUrl = AmplitudeServerZone.getDynamicConfigApi(serverZone);
            URL obj = new URL(dynamicConfigUrl);
            HttpURLConnection con = (HttpURLConnection) obj.openConnection();

            int responseCode = con.getResponseCode();

            if (responseCode == 200) {
                BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
                String inputLine;
                StringBuffer response = new StringBuffer();

                while ((inputLine = in.readLine()) != null) {
                    response.append(inputLine);
                }
                in.close();

                JSONObject json = new JSONObject(response.toString());
                if (json.has(KEY_INGESTION_ENDPOINT)) {
                    this.ingestionEndpoint = "https://" + json.getString(KEY_INGESTION_ENDPOINT);
                }
            }
        } catch (MalformedURLException e) {

        } catch (IOException e) {

        } catch (JSONException e) {

        } catch (Exception e) {
            
        }

        listener.onFinished();
    }

    public static ConfigManager getInstance() {
        if (instance == null) {
            instance = new ConfigManager();
        }

        return instance;
    }

    interface RefreshListener {
        void onFinished();
    }
}


================================================
FILE: src/main/java/com/amplitude/api/Constants.java
================================================
package com.amplitude.api;

import com.amplitude.BuildConfig;

public class Constants {

    public static final String LIBRARY = "amplitude-android";
    public static final String VERSION = BuildConfig.AMPLITUDE_VERSION;
    public static final String LIBRARY_UNKNOWN = "unknown-library";
    public static final String VERSION_UNKNOWN = "unknown-version";
    public static final String PLATFORM = "Android";

    public static final String EVENT_LOG_URL = "https://api2.amplitude.com/";
    public static final String EVENT_LOG_EU_URL = "https://api.eu.amplitude.com/";
    public static final String DYNAMIC_CONFIG_URL = "https://regionconfig.amplitude.com/";
    public static final String DYNAMIC_CONFIG_EU_URL = "https://regionconfig.eu.amplitude.com/";

    public static final String PACKAGE_NAME = "com.amplitude.api";

    public static final int API_VERSION = 2;

    public static final String DATABASE_NAME = PACKAGE_NAME;
    public static final int DATABASE_VERSION = 4;

    public static final String DEFAULT_INSTANCE = "$default_instance";

    public static final int EVENT_UPLOAD_THRESHOLD = 30;
    public static final int EVENT_UPLOAD_MAX_BATCH_SIZE = 50;
    public static final int EVENT_MAX_COUNT = 1000;
    public static final int EVENT_REMOVE_BATCH_SIZE = 20;
    public static final long EVENT_UPLOAD_PERIOD_MILLIS = 30 * 1000; // 30s
    public static final long MIN_TIME_BETWEEN_SESSIONS_MILLIS = 5 * 60 * 1000; // 5m
    public static final long SESSION_TIMEOUT_MILLIS = 30 * 60 * 1000; // 30m
    public static final long IDENTIFY_BATCH_INTERVAL_MILLIS = 30 * 1000; // 30s
    public static final int MAX_STRING_LENGTH = 1024;
    public static final int MAX_PROPERTY_KEYS = 1000;

    public static final String SHARED_PREFERENCES_NAME_PREFIX = PACKAGE_NAME;
    public static final String PREFKEY_LAST_EVENT_ID = PACKAGE_NAME + ".lastEventId";
    public static final String PREFKEY_LAST_EVENT_TIME = PACKAGE_NAME + ".lastEventTime";
    public static final String PREFKEY_LAST_IDENTIFY_ID = PACKAGE_NAME + ".lastIdentifyId";
    public static final String PREFKEY_PREVIOUS_SESSION_ID = PACKAGE_NAME + ".previousSessionId";
    public static final String PREFKEY_DEVICE_ID = PACKAGE_NAME + ".deviceId";
    public static final String PREFKEY_USER_ID = PACKAGE_NAME + ".userId";
    public static final String PREFKEY_OPT_OUT = PACKAGE_NAME + ".optOut";

    public static final String IDENTIFY_EVENT = "$identify";
    public static final String GROUP_IDENTIFY_EVENT = "$groupidentify";
    public static final String AMP_OP_ADD = "$add";
    public static final String AMP_OP_APPEND = "$append";
    public static final String AMP_OP_CLEAR_ALL = "$clearAll";
    public static final String AMP_OP_PREPEND = "$prepend";
    public static final String AMP_OP_SET = "$set";
    public static final String AMP_OP_SET_ONCE = "$setOnce";
    public static final String AMP_OP_UNSET = "$unset";
    public static final String AMP_OP_PREINSERT = "$preInsert";
    public static final String AMP_OP_POSTINSERT = "$postInsert";
    public static final String AMP_OP_REMOVE = "$remove";

    public static final String AMP_REVENUE_EVENT = "revenue_amount";
    public static final String AMP_REVENUE_PRODUCT_ID = "$productId";
    public static final String AMP_REVENUE_QUANTITY = "$quantity";
    public static final String AMP_REVENUE_PRICE = "$price";
    public static final String AMP_REVENUE_REVENUE_TYPE = "$revenueType";
    public static final String AMP_REVENUE_RECEIPT = "$receipt";
    public static final String AMP_REVENUE_RECEIPT_SIG = "$receiptSig";

    public static final String AMP_TRACKING_OPTION_ADID = "adid";
    public static final String AMP_TRACKING_OPTION_APP_SET_ID = "app_set_id";
    public static final String AMP_TRACKING_OPTION_CARRIER = "carrier";
    public static final String AMP_TRACKING_OPTION_CITY = "city";
    public static final String AMP_TRACKING_OPTION_COUNTRY = "country";
    public static final String AMP_TRACKING_OPTION_DEVICE_BRAND = "device_brand";
    public static final String AMP_TRACKING_OPTION_DEVICE_MANUFACTURER = "device_manufacturer";
    public static final String AMP_TRACKING_OPTION_DEVICE_MODEL = "device_model";
    public static final String AMP_TRACKING_OPTION_DMA = "dma";
    public static final String AMP_TRACKING_OPTION_IP_ADDRESS = "ip_address";
    public static final String AMP_TRACKING_OPTION_LANGUAGE = "language";
    public static final String AMP_TRACKING_OPTION_LAT_LNG = "lat_lng";
    public static final String AMP_TRACKING_OPTION_OS_NAME = "os_name";
    public static final String AMP_TRACKING_OPTION_OS_VERSION = "os_version";
    public static final String AMP_TRACKING_OPTION_API_LEVEL = "api_level";
    public static final String AMP_TRACKING_OPTION_PLATFORM = "platform";
    public static final String AMP_TRACKING_OPTION_REGION = "region";
    public static final String AMP_TRACKING_OPTION_VERSION_NAME = "version_name";

    public static final String AMP_PLAN_BRANCH = "branch";
    public static final String AMP_PLAN_SOURCE = "source";
    public static final String AMP_PLAN_VERSION = "version";
    public static final String AMP_PLAN_VERSION_ID = "versionId";

    public static final String AMP_INGESTION_METADATA_SOURCE_NAME = "source_name";
    public static final String AMP_INGESTION_METADATA_SOURCE_VERSION = "source_version";
}


================================================
FILE: src/main/java/com/amplitude/api/CursorWindowAllocationException.java
================================================
package com.amplitude.api;

/**
 * This is Amplitude's substitute for android.database.CursorWindowAllocationException.
 * Android's CursorWindow will throw that exception, but Android does not allow you to import
 * the exception class directly to catch it. This is Amplitude's stand-in for that class.
 *
 * @hide
 */
public class CursorWindowAllocationException extends RuntimeException {
    public CursorWindowAllocationException(String description) {
        super(description);
    }
}


================================================
FILE: src/main/java/com/amplitude/api/DatabaseHelper.java
================================================
package com.amplitude.api;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDoneException;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteStatement;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

class DatabaseHelper extends SQLiteOpenHelper {
    private static final String TAG = DatabaseHelper.class.getName();

    static final Map<String, DatabaseHelper> instances = new HashMap<String, DatabaseHelper>();

    protected static final String STORE_TABLE_NAME = "store";
    protected static final String LONG_STORE_TABLE_NAME = "long_store";
    private static final String KEY_FIELD = "key";
    private static final String VALUE_FIELD = "value";

    protected static final String EVENT_TABLE_NAME = "events";
    protected static final String IDENTIFY_TABLE_NAME = "identifys";
    protected static final String IDENTIFY_INTERCEPTOR_TABLE_NAME = "identify_interceptor";
    private static final String ID_FIELD = "id";
    private static final String EVENT_FIELD = "event";

    private static final String CREATE_STORE_TABLE = "CREATE TABLE IF NOT EXISTS "
            + STORE_TABLE_NAME + " (" + KEY_FIELD + " TEXT PRIMARY KEY NOT NULL, "
            + VALUE_FIELD + " TEXT);";
    private static final String CREATE_LONG_STORE_TABLE = "CREATE TABLE IF NOT EXISTS "
            + LONG_STORE_TABLE_NAME + " (" + KEY_FIELD + " TEXT PRIMARY KEY NOT NULL, "
            + VALUE_FIELD + " INTEGER);";
    private static final String CREATE_EVENTS_TABLE = "CREATE TABLE IF NOT EXISTS "
            + EVENT_TABLE_NAME + " (" + ID_FIELD + " INTEGER PRIMARY KEY AUTOINCREMENT, "
            + EVENT_FIELD + " TEXT);";
    private static final String CREATE_IDENTIFYS_TABLE = "CREATE TABLE IF NOT EXISTS "
            + IDENTIFY_TABLE_NAME + " (" + ID_FIELD + " INTEGER PRIMARY KEY AUTOINCREMENT, "
            + EVENT_FIELD + " TEXT);";
    private static final String CREATE_IDENTIFY_INTERCEPTOR_TABLE = "CREATE TABLE IF NOT EXISTS "
            + IDENTIFY_INTERCEPTOR_TABLE_NAME + " (" + ID_FIELD + " INTEGER PRIMARY KEY AUTOINCREMENT, "
            + EVENT_FIELD + " TEXT);";

    File file;
    private String instanceName;
    private boolean callResetListenerOnDatabaseReset = true;
    private DatabaseResetListener databaseResetListener;

    private static final AmplitudeLog logger = AmplitudeLog.getLogger();

    @Deprecated
    static DatabaseHelper getDatabaseHelper(Context context) {
        return getDatabaseHelper(context, null);
    }

    static synchronized DatabaseHelper getDatabaseHelper(Context context, String instance) {
        instance = Utils.normalizeInstanceName(instance);
        DatabaseHelper dbHelper = instances.get(instance);
        if (dbHelper == null) {
            dbHelper = new DatabaseHelper(context.getApplicationContext(), instance);
            instances.put(instance, dbHelper);
        }
        return dbHelper;
    }

    private static String getDatabaseName(String instance) {
        return (Utils.isEmptyString(instance) || instance.equals(Constants.DEFAULT_INSTANCE)) ? Constants.DATABASE_NAME : Constants.DATABASE_NAME + "_" + instance;
    }

    protected DatabaseHelper(Context context) {
        this(context, null);
    }

    protected DatabaseHelper(Context context, String instance) {
        super(context, getDatabaseName(instance), null, Constants.DATABASE_VERSION);
        file = context.getDatabasePath(getDatabaseName(instance));
        instanceName = Utils.normalizeInstanceName(instance);
    }

    void setDatabaseResetListener(DatabaseResetListener databaseResetListener) {
        this.databaseResetListener = databaseResetListener;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_STORE_TABLE);
        db.execSQL(CREATE_LONG_STORE_TABLE);
        // INTEGER PRIMARY KEY AUTOINCREMENT guarantees that all generated values
        // for the field will be monotonically increasing and unique over the
        // lifetime of the table, even if rows get removed
        db.execSQL(CREATE_EVENTS_TABLE);
        db.execSQL(CREATE_IDENTIFYS_TABLE);
        db.execSQL(CREATE_IDENTIFY_INTERCEPTOR_TABLE);

        // NOTE: the database file can become corrupted between interactions
        // getWriteableDatabase and getReadableDatabase will test for corruption
        // and actually delete the database file and call onCreate again if it's corrupted
        // Our normal catch exception and delete database does not get triggered in this scenario
        // Therefore we are also calling the reset callback inside onCreate
        if (databaseResetListener != null && callResetListenerOnDatabaseReset) {
            try {
                callResetListenerOnDatabaseReset = false;  // guards against stack overflow
                databaseResetListener.onDatabaseReset(db);
            } catch (SQLiteException e) {
                logger.e(TAG, String.format("databaseReset callback failed during onCreate"), e);
            } finally {
                callResetListenerOnDatabaseReset = true;
            }
        }
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        if (oldVersion > newVersion) {
            logger.e(TAG, "onUpgrade() with invalid oldVersion and newVersion");
            resetDatabase(db);
            return;
        }

        if (newVersion <= 1) {
            return;
        }

        switch (oldVersion) {
            case 1:
                db.execSQL(CREATE_STORE_TABLE);
                if (newVersion <= 2) break;

            case 2:
                db.execSQL(CREATE_IDENTIFYS_TABLE);
                db.execSQL(CREATE_LONG_STORE_TABLE);
                if (newVersion <= 3) break;

            case 3:
                db.execSQL(CREATE_IDENTIFY_INTERCEPTOR_TABLE);
                if (newVersion <= 4) break;

            case 4:
                break;

            default:
                logger.e(TAG, "onUpgrade() with unknown oldVersion " + oldVersion);
                resetDatabase(db);
        }
    }

    private void resetDatabase(SQLiteDatabase db) {
        db.execSQL("DROP TABLE IF EXISTS " + STORE_TABLE_NAME);
        db.execSQL("DROP TABLE IF EXISTS " + LONG_STORE_TABLE_NAME);
        db.execSQL("DROP TABLE IF EXISTS " + EVENT_TABLE_NAME);
        db.execSQL("DROP TABLE IF EXISTS " + IDENTIFY_TABLE_NAME);
        db.execSQL("DROP TABLE IF EXISTS " + IDENTIFY_INTERCEPTOR_TABLE_NAME);
        onCreate(db);
    }

    synchronized long insertOrReplaceKeyValue(String key, String value) {
        return value == null ? deleteKeyFromTable(STORE_TABLE_NAME, key) :
            insertOrReplaceKeyValueToTable(STORE_TABLE_NAME, key, value);
    }

    synchronized long insertOrReplaceKeyLongValue(String key, Long value) {
        return value == null ? deleteKeyFromTable(LONG_STORE_TABLE_NAME, key) :
            insertOrReplaceKeyValueToTable(LONG_STORE_TABLE_NAME, key, value);
    }

    synchronized long insertOrReplaceKeyValueToTable(String table, String key, Object value) {
        long result = -1;
        SQLiteDatabase db = null;
        try {
            db = getWritableDatabase();
            result = insertOrReplaceKeyValueToTable(db, table, key, value);
        } catch (SQLiteException e) {
            logger.e(TAG, String.format("insertOrReplaceKeyValue in %s failed", table), e);
            // Hard to recover from SQLiteExceptions, just start fresh
            delete();
        } catch (StackOverflowError e) {
            logger.e(TAG, String.format("insertOrReplaceKeyValue in %s failed", table), e);
            // potential stack overflow error when getting database on custom Android versions
            delete();
        } finally {
            if (db != null && db.isOpen()) {
                close();
            }
        }
        return result;
    }

    synchronized long insertOrReplaceKeyValueToTable(SQLiteDatabase db, String table, String key, Object value) throws SQLiteException, StackOverflowError {
        long result = -1;
        ContentValues contentValues = new ContentValues();
        contentValues.put(KEY_FIELD, key);
        if (value instanceof Long) {
            contentValues.put(VALUE_FIELD, (Long) value);
        } else {
            contentValues.put(VALUE_FIELD, (String) value);
        }
        result = insertKeyValueContentValuesIntoTable(db, table, contentValues);
        if (result == -1) {
            logger.w(TAG, "Insert failed");
        }
        return result;
    }

    synchronized long insertKeyValueContentValuesIntoTable(SQLiteDatabase db, String table, ContentValues contentValues) throws SQLiteException, StackOverflowError {
        return db.insertWithOnConflict(
            table,
            null,
            contentValues,
            SQLiteDatabase.CONFLICT_REPLACE
        );
    }

    synchronized long deleteKeyFromTable(String table, String key) {
        long result = -1;
        try {
            SQLiteDatabase db = getWritableDatabase();
            result = db.delete(table, KEY_FIELD + "=?", new String[]{key});
        } catch (SQLiteException e) {
            logger.e(TAG, String.format("deleteKey from %s failed", table), e);
            // Hard to recover from SQLiteExceptions, just start fresh
            delete();
        } catch (StackOverflowError e) {
            logger.e(TAG, String.format("deleteKey from %s failed", table), e);
            // potential stack overflow error when getting database on custom Android versions
            delete();
        } finally {
            close();
        }
        return result;
    }

    synchronized long addEvent(String event) {
        return addEventToTable(EVENT_TABLE_NAME, event);
    }

    synchronized long addIdentify(String identifyEvent) {
        return addEventToTable(IDENTIFY_TABLE_NAME, identifyEvent);
    }

    synchronized long addIdentifyInterceptor(String identifyEvent) {
        return addEventToTable(IDENTIFY_INTERCEPTOR_TABLE_NAME, identifyEvent);
    }

    private synchronized long addEventToTable(String table, String event) {
        long result = -1;
        try {
            SQLiteDatabase db = getWritableDatabase();
            ContentValues contentValues = new ContentValues();
            contentValues.put(EVENT_FIELD, event);
            result = insertEventContentValuesIntoTable(db, table, contentValues);
            if (result == -1) {
                logger.w(TAG, String.format("Insert into %s failed", table));
            }
        } catch (SQLiteException e) {
            logger.e(TAG, String.format("addEvent to %s failed", table), e);
            // Hard to recover from SQLiteExceptions, just start fresh
            delete();
        } catch (StackOverflowError e) {
            logger.e(TAG, String.format("addEvent to %s failed", table), e);
            // potential stack overflow error when getting database on custom Android versions
            delete();
        } finally {
            close();
        }
        return result;
    }

    synchronized long insertEventContentValuesIntoTable(SQLiteDatabase db, String table, ContentValues contentValues) throws SQLiteException, StackOverflowError {
        return db.insert(table, null, contentValues);
    }

    synchronized String getValue(String key) {
        return (String) getValueFromTable(STORE_TABLE_NAME, key);
    }

    synchronized Long getLongValue(String key) {
        return (Long) getValueFromTable(LONG_STORE_TABLE_NAME, key);
    }

    protected synchronized Object getValueFromTable(String table, String key) {
        Object value = null;
        Cursor cursor = null;
        try {
            SQLiteDatabase db = getReadableDatabase();
            cursor = queryDb(
                db, table, new String[]{KEY_FIELD, VALUE_FIELD}, KEY_FIELD + " = ?",
                new String[]{key}, null, null, null, null
            );
            if (cursor.moveToFirst()) {
                value = table.equals(STORE_TABLE_NAME) ? cursor.getString(1) : cursor.getLong(1);
            }
        } catch (SQLiteException e) {
            logger.e(TAG, String.format("getValue from %s failed", table), e);
            // Hard to recover from SQLiteExceptions, just start fresh
            delete();
        } catch (StackOverflowError e) {
            logger.e(TAG, String.format("getValue from %s failed", table), e);
            // potential stack overflow error when getting database on custom Android versions
            delete();
        } catch (IllegalStateException e) {  // put before Runtime since IllegalState extends
            // cursor window row too big exception
            handleIfCursorRowTooLargeException(e);
        } catch (RuntimeException e) {
            // cursor window allocation exception
            convertIfCursorWindowException(e);
        } finally {
            if (cursor != null) {
                cursor.close();
            }
            close();
        }
        return value;
    }

    synchronized List<JSONObject> getEvents(long upToId, long limit) throws JSONException {
        return getEventsFromTable(EVENT_TABLE_NAME, upToId, limit);
    }

    synchronized List<JSONObject> getIdentifys(
                                        long upToId, long limit) throws JSONException {
        return getEventsFromTable(IDENTIFY_TABLE_NAME, upToId, limit);
    }

    synchronized List<JSONObject> getIdentifyInterceptors(
            long upToId,
            long limit
    ) throws JSONException {
        return getEventsFromTable(IDENTIFY_INTERCEPTOR_TABLE_NAME, upToId, limit);
    }

    protected synchronized List<JSONObject> getEventsFromTable(
            String table, long upToId, long limit) throws JSONException {
        try {
            return getEventsBatchFromTable(table, upToId, limit);
        } catch (CursorWindowAllocationException e) {
            return getEventsRowByRowFromTable(table, upToId, limit);
        }
    }

    private List<JSONObject> getEventsBatchFromTable(
            String table, long upToId, long limit) throws JSONException {
        List<JSONObject> events = new LinkedList<JSONObject>();
        Cursor cursor = null;
        try {
            SQLiteDatabase db = getReadableDatabase();
            cursor = queryDb(
                db, table, new String[] { ID_FIELD, EVENT_FIELD },
                upToId >= 0 ? ID_FIELD + " <= " + upToId : null, null, null, null,
                ID_FIELD + " ASC", limit >= 0 ? "" + limit : null
            );

            while (cursor.moveToNext()) {
                long eventId = cursor.getLong(0);
                String event = cursor.getString(1);
                if (Utils.isEmptyString(event)) {
                    continue;
                }

                JSONObject obj = new JSONObject(event);
                obj.put("event_id", eventId);
                events.add(obj);
            }
        } catch (SQLiteException e) {
            logger.e(TAG, String.format("getEvents from %s failed", table), e);
            delete();
        } catch (StackOverflowError e) {
            logger.e(TAG, String.format("getEvents from %s failed", table), e);
            delete();
        } catch (IllegalStateException e) {  // put before Runtime since IllegalState extends
            handleIfCursorRowTooLargeException(e);
        } catch (RuntimeException e) {
            convertIfCursorWindowException(e);
        } finally {
            if (cursor != null) {
                cursor.close();
            }
            close();
        }
        return events;
    }

    private List<JSONObject> getEventsRowByRowFromTable(
                                    String table, long upToId, long limit) throws JSONException {
        List<Long> eventIds = new LinkedList<Long>();
        Cursor cursor = null;
        try {
            SQLiteDatabase db = getReadableDatabase();
            cursor = queryDb(
                db, table, new String[] { ID_FIELD },
                upToId >= 0 ? ID_FIELD + " <= " + upToId : null, null, null, null,
                ID_FIELD + " ASC", limit >= 0 ? "" + limit : null
            );

            while (cursor.moveToNext()) {
                long eventId = cursor.getLong(0);
                eventIds.add(eventId);
            }
        } catch (SQLiteException e) {
            logger.e(TAG, String.format("getEvents from %s failed", table), e);
            delete();
        } catch (StackOverflowError e) {
            logger.e(TAG, String.format("getEvents from %s failed", table), e);
            delete();
        } catch (IllegalStateException e) {  // put before Runtime since IllegalState extends
            handleIfCursorRowTooLargeException(e);
        } catch (RuntimeException e) {
            convertIfCursorWindowException(e);
        } finally {
            if (cursor != null) {
                cursor.close();
            }
            close();
        }

        try {
            List<JSONObject> events = new LinkedList<JSONObject>();
            for (Long eventId : eventIds) {
                JSONObject event = getEventFromTable(table, eventId);
                if (event != null) {
                    events.add(event);
                }
            }
            return events;
        } finally {
            close();
        }
    }

    protected synchronized JSONObject getEventFromTable(String table, long eventId) throws JSONException {
        JSONObject event = null;
        Cursor cursor = null;
        try {
            SQLiteDatabase db = getReadableDatabase();
            cursor = queryDb(
                db, table, new String[] { EVENT_FIELD },
               ID_FIELD + " = " + eventId,
                    null, null, null, null, null
            );

            if (cursor.moveToFirst()) {
                String eventData = cursor.getString(0);
                if (!Utils.isEmptyString(eventData)) {
                    event = new JSONObject(eventData);
                    event.put("event_id", eventId);
                }
            }
        } catch (SQLiteException e) {
            logger.e(TAG, String.format("getEvent from %s failed", table), e);
            delete();
        } catch (StackOverflowError e) {
            logger.e(TAG, String.format("getEvent from %s failed", table), e);
            delete();
        } catch (IllegalStateException e) {  // put before Runtime since IllegalState extends
            handleIfCursorRowTooLargeException(e);
        } catch (RuntimeException e) {
            convertIfCursorWindowException(e);
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
        return event;
    }

    synchronized long getEventCount() {
        return getEventCountFromTable(EVENT_TABLE_NAME);
    }

    synchronized long getIdentifyCount() {
        return getEventCountFromTable(IDENTIFY_TABLE_NAME);
    }

    synchronized long getTotalEventCount() {
        return getEventCount() + getIdentifyCount();
    }

    synchronized long getIdentifyInterceptorCount() {
        return getEventCountFromTable(IDENTIFY_INTERCEPTOR_TABLE_NAME);
    }

    private synchronized long getEventCountFromTable(String table) {
        long numberRows = 0;
        SQLiteStatement statement = null;
        try {
            SQLiteDatabase db = getReadableDatabase();
            String query = "SELECT COUNT(*) FROM " + table;
            statement = db.compileStatement(query);
            numberRows = statement.simpleQueryForLong();
        } catch (SQLiteException e) {
            logger.e(TAG, String.format("getNumberRows for %s failed", table), e);
            // Hard to recover from SQLiteExceptions, just start fresh
            delete();
        } catch (StackOverflowError e) {
            logger.e(TAG, String.format("getNumberRows for %s failed", table), e);
            // potential stack overflow error when getting database on custom Android versions
            delete();
        } finally {
            if (statement != null) {
                statement.close();
            }
            close();
        }
        return numberRows;
    }

    synchronized long getNthEventId(long n) {
        return getNthEventIdFromTable(EVENT_TABLE_NAME, n);
    }

    synchronized long getNthIdentifyId(long n) {
        return getNthEventIdFromTable(IDENTIFY_TABLE_NAME, n);
    }

    synchronized long getLastIdentifyInterceptorId() {
        return getNthEventIdFromTable(IDENTIFY_INTERCEPTOR_TABLE_NAME, 1, "DESC");
    }

    private synchronized long getNthEventIdFromTable(String table, long n) {
        return getNthEventIdFromTable(table, n, "ASC");
    }

    private synchronized long getNthEventIdFromTable(String table, long n, String orderBy) {
        long nthEventId = -1;
        SQLiteStatement statement = null;
        try {
            SQLiteDatabase db = getReadableDatabase();
            String query = "SELECT " + ID_FIELD + " FROM " + table + " ORDER BY " + ID_FIELD +
                    " " + orderBy + " LIMIT 1 OFFSET " + (n - 1);
            statement = db.compileStatement(query);
            nthEventId = -1;
            try {
                nthEventId = statement.simpleQueryForLong();
            } catch (SQLiteDoneException e) {
                logger.w(TAG, e);
            }
        } catch (SQLiteException e) {
            logger.e(TAG, String.format("getNthEventId from %s failed", table), e);
            // Hard to recover from SQLiteExceptions, just start fresh
            delete();
        } catch (StackOverflowError e) {
            logger.e(TAG, String.format("getNthEventId from %s failed", table), e);
            // potential stack overflow error when getting database on custom Android versions
            delete();
        } finally {
            if (statement != null) {
                statement.close();
            }
            close();
        }
        return nthEventId;
    }

    synchronized void removeEvents(long maxId) {
        removeEventsFromTable(EVENT_TABLE_NAME, maxId);
    }

    synchronized void removeIdentifys(long maxId) {
        removeEventsFromTable(IDENTIFY_TABLE_NAME, maxId);
    }

    synchronized void removeIdentifyInterceptors(long maxId) {
        removeEventsFromTable(IDENTIFY_INTERCEPTOR_TABLE_NAME, maxId);
    }

    private synchronized void removeEventsFromTable(String table, long maxId) {
        try {
            SQLiteDatabase db = getWritableDatabase();
            db.delete(table, ID_FIELD + " <= " + maxId, null);
        } catch (SQLiteException e) {
            logger.e(TAG, String.format("removeEvents from %s failed", table), e);
            delete();
        } catch (StackOverflowError e) {
            logger.e(TAG, String.format("removeEvents from %s failed", table), e);
            delete();
        } finally {
            close();
        }
    }

    synchronized void removeEvent(long id) {
        removeEventFromTable(EVENT_TABLE_NAME, id);
    }

    synchronized void removeIdentify(long id) {
        removeEventFromTable(IDENTIFY_TABLE_NAME, id);
    }

    synchronized void removeIdentifyIntercept(long id) {
        removeEventFromTable(IDENTIFY_INTERCEPTOR_TABLE_NAME, id);
    }

    private synchronized void removeEventFromTable(String table, long id) {
        try {
            SQLiteDatabase db = getWritableDatabase();
            db.delete(table, ID_FIELD + " = " + id, null);
        } catch (SQLiteException e) {
            logger.e(TAG, String.format("removeEvent from %s failed", table), e);
            delete();
        } catch (StackOverflowError e) {
            logger.e(TAG, String.format("removeEvent from %s failed", table), e);
            delete();
        } finally {
            close();
        }
    }

    private void delete() {
        // This only gets called if the database somehow gets corrupted AFTER being fetched
        // ie after the call to getWriteableDatabase / getReadableDatabase
        // or if a SQL exception occurs during the interaction
        try {
            close();
            file.delete();
        } catch (SecurityException e) {
            logger.e(TAG, "delete failed", e);
        } finally {
            if (databaseResetListener != null && callResetListenerOnDatabaseReset) {
                callResetListenerOnDatabaseReset = false;  // guards against stack overflow
                SQLiteDatabase db = null;
                try {
           
Download .txt
gitextract_d5k27ryi/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── Bug_report.md
│   │   ├── Feature_request.md
│   │   └── Question.md
│   └── workflows/
│       ├── codeql-analysis.yml
│       ├── jira-issue-create.yml
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── build.gradle
├── gradle.properties
├── gradlew
├── gradlew.bat
├── package.json
├── release.config.js
├── settings.gradle
└── src/
    ├── main/
    │   ├── AndroidManifest.xml
    │   ├── java/
    │   │   └── com/
    │   │       └── amplitude/
    │   │           ├── api/
    │   │           │   ├── Amplitude.java
    │   │           │   ├── AmplitudeCallbacks.java
    │   │           │   ├── AmplitudeClient.java
    │   │           │   ├── AmplitudeDeviceIdCallback.java
    │   │           │   ├── AmplitudeLog.java
    │   │           │   ├── AmplitudeLogCallback.java
    │   │           │   ├── AmplitudeServerZone.java
    │   │           │   ├── ConfigManager.java
    │   │           │   ├── Constants.java
    │   │           │   ├── CursorWindowAllocationException.java
    │   │           │   ├── DatabaseHelper.java
    │   │           │   ├── DatabaseResetListener.java
    │   │           │   ├── DeviceInfo.java
    │   │           │   ├── Identify.java
    │   │           │   ├── IdentifyInterceptor.java
    │   │           │   ├── IngestionMetadata.java
    │   │           │   ├── Middleware.java
    │   │           │   ├── MiddlewareExtended.java
    │   │           │   ├── MiddlewareExtra.java
    │   │           │   ├── MiddlewareNext.java
    │   │           │   ├── MiddlewarePayload.java
    │   │           │   ├── MiddlewareRunner.java
    │   │           │   ├── PinnedAmplitudeClient.java
    │   │           │   ├── Plan.java
    │   │           │   ├── Revenue.java
    │   │           │   ├── TrackingOptions.java
    │   │           │   ├── Utils.java
    │   │           │   └── WorkerThread.java
    │   │           ├── eventexplorer/
    │   │           │   ├── EventExplorer.java
    │   │           │   ├── EventExplorerInfoActivity.java
    │   │           │   └── EventExplorerTouchHandler.java
    │   │           ├── unity/
    │   │           │   └── plugins/
    │   │           │       └── AmplitudePlugin.java
    │   │           └── util/
    │   │               ├── DoubleCheck.java
    │   │               └── Provider.java
    │   └── res/
    │       ├── drawable/
    │       │   └── amp_button_bg.xml
    │       ├── layout/
    │       │   ├── amp_activity_eventexplorer_info.xml
    │       │   └── amp_bubble_view.xml
    │       └── values/
    │           ├── colors.xml
    │           └── strings.xml
    └── test/
        └── java/
            └── com/
                └── amplitude/
                    └── api/
                        ├── AmplitudeClientTest.java
                        ├── AmplitudeServerZoneTest.java
                        ├── AmplitudeTest.java
                        ├── BaseTest.java
                        ├── ConfigManagerTest.java
                        ├── DatabaseHelperTest.java
                        ├── DatabaseRecoveryTest.java
                        ├── DeviceInfoTest.java
                        ├── IdentifyTest.java
                        ├── IngestionMetadataTest.java
                        ├── InitializeTest.java
                        ├── MiddlewareRunnerTest.java
                        ├── MockGeocoder.java
                        ├── PinningTest.java
                        ├── PlanTest.java
                        ├── RevenueTest.java
                        ├── SessionTest.java
                        ├── TrackingOptionsTest.java
                        └── util/
                            ├── MockHttpURLConnectionHelper.java
                            └── MockURLStreamHandler.java
Download .txt
SYMBOL INDEX (1019 symbols across 54 files)

FILE: src/main/java/com/amplitude/api/Amplitude.java
  class Amplitude (line 20) | public class Amplitude {
    method getInstance (line 29) | public static AmplitudeClient getInstance() {
    method getInstance (line 40) | public static synchronized AmplitudeClient getInstance(String instance) {
    method initialize (line 57) | @Deprecated
    method initialize (line 70) | @Deprecated
    method enableNewDeviceIdPerInstall (line 80) | @Deprecated
    method useAdvertisingIdForDeviceId (line 88) | @Deprecated
    method enableLocationListening (line 96) | @Deprecated
    method disableLocationListening (line 104) | @Deprecated
    method setSessionTimeoutMillis (line 114) | @Deprecated
    method setOptOut (line 124) | @Deprecated
    method logEvent (line 134) | @Deprecated
    method logEvent (line 145) | @Deprecated
    method uploadEvents (line 153) | @Deprecated
    method startSession (line 161) | @Deprecated
    method endSession (line 167) | @Deprecated
    method logRevenue (line 175) | @Deprecated
    method logRevenue (line 187) | @Deprecated
    method logRevenue (line 201) | @Deprecated
    method setUserProperties (line 212) | @Deprecated
    method setUserProperties (line 223) | @Deprecated
    method setUserId (line 233) | @Deprecated
    method getDeviceId (line 243) | @Deprecated

FILE: src/main/java/com/amplitude/api/AmplitudeCallbacks.java
  class AmplitudeCallbacks (line 7) | class AmplitudeCallbacks implements Application.ActivityLifecycleCallbac...
    method AmplitudeCallbacks (line 15) | public AmplitudeCallbacks(AmplitudeClient clientInstance) {
    method onActivityCreated (line 25) | @Override
    method onActivityDestroyed (line 28) | @Override
    method onActivityPaused (line 31) | @Override
    method onActivityResumed (line 41) | @Override
    method onActivitySaveInstanceState (line 51) | @Override
    method onActivityStarted (line 54) | @Override
    method onActivityStopped (line 57) | @Override
    method getCurrentTimeMillis (line 60) | protected long getCurrentTimeMillis() {

FILE: src/main/java/com/amplitude/api/AmplitudeClient.java
  class AmplitudeClient (line 47) | public class AmplitudeClient {
    method AmplitudeClient (line 228) | public AmplitudeClient() {
    method AmplitudeClient (line 236) | public AmplitudeClient(String instance) {
    method initialize (line 252) | public AmplitudeClient initialize(Context context, String apiKey) {
    method initialize (line 266) | public AmplitudeClient initialize(Context context, String apiKey, Stri...
    method initialize (line 281) | public synchronized AmplitudeClient initialize(
    method initialize (line 308) | public synchronized AmplitudeClient initialize(
    method initializeInternal (line 336) | public synchronized AmplitudeClient initializeInternal(
    method enableForegroundTracking (line 463) | public AmplitudeClient enableForegroundTracking(Application app) {
    method enableDiagnosticLogging (line 479) | public AmplitudeClient enableDiagnosticLogging() {
    method disableDiagnosticLogging (line 487) | public AmplitudeClient disableDiagnosticLogging() {
    method setDiagnosticEventMaxCount (line 495) | public AmplitudeClient setDiagnosticEventMaxCount(int eventMaxCount) {
    method enableNewDeviceIdPerInstall (line 507) | public AmplitudeClient enableNewDeviceIdPerInstall(boolean newDeviceId...
    method useAdvertisingIdForDeviceId (line 517) | public AmplitudeClient useAdvertisingIdForDeviceId() {
    method useAppSetIdForDeviceId (line 527) | public AmplitudeClient useAppSetIdForDeviceId() {
    method enableLocationListening (line 540) | public AmplitudeClient enableLocationListening() {
    method disableLocationListening (line 556) | public AmplitudeClient disableLocationListening() {
    method setEventUploadThreshold (line 572) | public AmplitudeClient setEventUploadThreshold(int eventUploadThreshol...
    method setEventUploadMaxBatchSize (line 584) | public AmplitudeClient setEventUploadMaxBatchSize(int eventUploadMaxBa...
    method setEventMaxCount (line 599) | public AmplitudeClient setEventMaxCount(int eventMaxCount) {
    method setEventUploadPeriodMillis (line 612) | public AmplitudeClient setEventUploadPeriodMillis(int eventUploadPerio...
    method setMinTimeBetweenSessionsMillis (line 623) | public AmplitudeClient setMinTimeBetweenSessionsMillis(long minTimeBet...
    method setIdentifyBatchIntervalMillis (line 634) | public AmplitudeClient setIdentifyBatchIntervalMillis(long identifyBat...
    method setServerUrl (line 655) | public AmplitudeClient setServerUrl(String serverUrl) {
    method setBearerToken (line 667) | public AmplitudeClient setBearerToken(String token) {
    method setSessionTimeoutMillis (line 680) | public AmplitudeClient setSessionTimeoutMillis(long sessionTimeoutMill...
    method setTrackingOptions (line 685) | public AmplitudeClient setTrackingOptions(TrackingOptions trackingOpti...
    method enableCoppaControl (line 699) | public AmplitudeClient enableCoppaControl() {
    method disableCoppaControl (line 709) | public AmplitudeClient disableCoppaControl() {
    method setOptOut (line 722) | public AmplitudeClient setOptOut(final boolean optOut) {
    method getOptOut (line 741) | public Boolean getOptOut() {
    method setLibraryName (line 751) | public AmplitudeClient setLibraryName(final String libraryName) {
    method setLibraryVersion (line 762) | public AmplitudeClient setLibraryVersion(final String libraryVersion) {
    method isOptedOut (line 772) | public boolean isOptedOut() {
    method enableLogging (line 782) | public AmplitudeClient enableLogging(boolean enableLogging) {
    method setLogLevel (line 794) | public AmplitudeClient setLogLevel(int logLevel) {
    method setLogCallback (line 805) | public AmplitudeClient setLogCallback(AmplitudeLogCallback callback) {
    method setOffline (line 817) | public AmplitudeClient setOffline(boolean offline) {
    method setFlushEventsOnClose (line 834) | public AmplitudeClient setFlushEventsOnClose(boolean flushEventsOnClos...
    method trackSessionEvents (line 846) | public AmplitudeClient trackSessionEvents(boolean trackingSessionEvent...
    method setUseDynamicConfig (line 860) | public AmplitudeClient setUseDynamicConfig(boolean useDynamicConfig) {
    method showEventExplorer (line 870) | public void showEventExplorer(Activity activity) {
    method useForegroundTracking (line 880) | void useForegroundTracking() {
    method isUsingForegroundTracking (line 889) | boolean isUsingForegroundTracking() { return usingForegroundTracking; }
    method addEventMiddleware (line 894) | public void addEventMiddleware(Middleware middleware) {
    method isInForeground (line 903) | boolean isInForeground() { return inForeground; }
    method logEvent (line 911) | public void logEvent(String eventType) {
    method logEvent (line 922) | public void logEvent(String eventType, JSONObject eventProperties) {
    method logEvent (line 937) | public void logEvent(String eventType, JSONObject eventProperties, Mid...
    method logEvent (line 952) | public void logEvent(String eventType, JSONObject eventProperties, boo...
    method logEvent (line 966) | public void logEvent(String eventType, JSONObject eventProperties, JSO...
    method logEvent (line 982) | public void logEvent(String eventType, JSONObject eventProperties, JSO...
    method logEvent (line 1005) | public void logEvent(String eventType, JSONObject eventProperties, JSO...
    method logEvent (line 1010) | public void logEvent(String eventType, JSONObject eventProperties, JSO...
    method logEventSync (line 1024) | public void logEventSync(String eventType) {
    method logEventSync (line 1037) | public void logEventSync(String eventType, JSONObject eventProperties) {
    method logEventSync (line 1052) | public void logEventSync(String eventType, JSONObject eventProperties,...
    method logEventSync (line 1066) | public void logEventSync(String eventType, JSONObject eventProperties,...
    method logEventSync (line 1088) | public void logEventSync(String eventType, JSONObject eventProperties,...
    method logEventSync (line 1105) | public void logEventSync(String eventType, JSONObject eventProperties,...
    method validateLogEvent (line 1118) | protected boolean validateLogEvent(String eventType) {
    method logEventAsync (line 1138) | protected void logEventAsync(final String eventType, JSONObject eventP...
    method logEventAsync (line 1144) | protected void logEventAsync(final String eventType, JSONObject eventP...
    method logEvent (line 1207) | protected long logEvent(String eventType, JSONObject eventProperties, ...
    method logEvent (line 1213) | protected long logEvent(String eventType, JSONObject eventProperties, ...
    method saveEvent (line 1352) | protected long saveEvent(String eventType, JSONObject event, Middlewar...
    method saveEvent (line 1378) | protected long saveEvent(String eventType, JSONObject event) {
    method getLongvalue (line 1416) | private long getLongvalue(String key, long defaultValue) {
    method getNextSequenceNumber (line 1426) | long getNextSequenceNumber() {
    method setLastEventTime (line 1437) | void setLastEventTime(long timestamp) {
    method setLastEventId (line 1447) | void setLastEventId(long eventId) {
    method setLastIdentifyId (line 1457) | void setLastIdentifyId(long identifyId) {
    method getSessionId (line 1467) | public long getSessionId() {
    method setPreviousSessionId (line 1476) | void setPreviousSessionId(long timestamp) {
    method startNewSessionIfNeeded (line 1487) | public boolean startNewSessionIfNeeded(long timestamp) {
    method startNewSession (line 1515) | private void startNewSession(long timestamp) {
    method inSession (line 1529) | private boolean inSession() {
    method isWithinMinTimeBetweenSessions (line 1533) | private boolean isWithinMinTimeBetweenSessions(long timestamp) {
    method setSessionId (line 1539) | private void setSessionId(long timestamp) {
    method refreshSessionTime (line 1549) | void refreshSessionTime(long timestamp) {
    method sendSessionEvent (line 1557) | private void sendSessionEvent(final String sessionEvent) {
    method onExitForeground (line 1581) | void onExitForeground(final long timestamp) {
    method onEnterForeground (line 1611) | void onEnterForeground(final long timestamp) {
    method logRevenue (line 1647) | public void logRevenue(double amount) {
    method logRevenue (line 1663) | public void logRevenue(String productId, int quantity, double price) {
    method logRevenue (line 1667) | public void logRevenue(String productId, int quantity, double price, S...
    method logRevenue (line 1684) | public void logRevenue(String productId, int quantity, double price, S...
    method logRevenueV2 (line 1714) | public void logRevenueV2(Revenue revenue) {
    method logRevenueV2 (line 1718) | public void logRevenueV2(Revenue revenue, MiddlewareExtra extra) {
    method setUserProperties (line 1735) | public void setUserProperties(final JSONObject userProperties, final b...
    method setUserProperties (line 1746) | public void setUserProperties(final JSONObject userProperties) {
    method setUserProperties (line 1758) | public void setUserProperties(final JSONObject userProperties, Middlew...
    method convertPropertiesToIdentify (line 1770) | private Identify convertPropertiesToIdentify(final JSONObject properti...
    method clearUserProperties (line 1798) | public void clearUserProperties() {
    method identify (line 1809) | public void identify(Identify identify) {
    method identify (line 1813) | public void identify(Identify identify, boolean outOfSession) {
    method identify (line 1826) | public void identify(Identify identify, boolean outOfSession, Middlewa...
    method setGroup (line 1843) | public void setGroup(String groupType, Object groupName) {
    method setGroup (line 1854) | public void setGroup(String groupType, Object groupName, MiddlewareExt...
    method groupIdentify (line 1871) | public void groupIdentify(String groupType, Object groupName, Identify...
    method groupIdentify (line 1875) | public void groupIdentify(String groupType, Object groupName, Identify...
    method groupIdentify (line 1879) | public void groupIdentify(String groupType, Object groupName, JSONObje...
    method groupIdentify (line 1886) | public void groupIdentify(String groupType, Object groupName, Identify...
    method truncate (line 1914) | public JSONObject truncate(JSONObject object) {
    method truncate (line 1957) | public JSONArray truncate(JSONArray array) throws JSONException {
    method truncate (line 1981) | public static String truncate(String value) {
    method getUserId (line 1992) | public String getUserId() {
    method setUserId (line 2002) | public AmplitudeClient setUserId(final String userId) {
    method setUserId (line 2014) | public AmplitudeClient setUserId(final String userId, final boolean st...
    method setDeviceId (line 2059) | public AmplitudeClient setDeviceId(final String deviceId) {
    method regenerateDeviceId (line 2090) | public AmplitudeClient regenerateDeviceId() {
    method uploadEvents (line 2112) | public void uploadEvents() {
    method updateServerLater (line 2129) | private void updateServerLater(long delayMillis) {
    method updateServer (line 2146) | protected void updateServer() {
    method updateServer (line 2156) | protected void updateServer(boolean limit) {
    method mergeEventsAndIdentifys (line 2221) | protected Pair<Pair<Long,Long>, JSONArray> mergeEventsAndIdentifys(Lis...
    method makeEventUploadPostRequest (line 2281) | protected void makeEventUploadPostRequest(Call.Factory client, String ...
    method initializeDeviceInfo (line 2393) | protected DeviceInfo initializeDeviceInfo() {
    method getDeviceId (line 2402) | public String getDeviceId() {
    method getInvalidDeviceIds (line 2407) | private Set<String> getInvalidDeviceIds() {
    method initializeDeviceId (line 2420) | private String initializeDeviceId() {
    method saveDeviceId (line 2457) | private void saveDeviceId(String deviceId) {
    method setDeviceIdCallback (line 2461) | public AmplitudeClient setDeviceIdCallback(AmplitudeDeviceIdCallback c...
    method runOnLogThread (line 2466) | protected void runOnLogThread(Runnable r) {
    method replaceWithJSONNull (line 2480) | protected Object replaceWithJSONNull(Object obj) {
    method contextAndApiKeySet (line 2490) | protected synchronized boolean contextAndApiKeySet(String methodName) {
    method bytesToHexString (line 2511) | protected String bytesToHexString(byte[] bytes) {
    method getCurrentTimeMillis (line 2529) | protected long getCurrentTimeMillis() { return System.currentTimeMilli...
    method setPlan (line 2536) | public AmplitudeClient setPlan(Plan plan) {
    method setIngestionMetadata (line 2546) | public AmplitudeClient setIngestionMetadata(IngestionMetadata ingestio...
    method setServerZone (line 2561) | public AmplitudeClient setServerZone(AmplitudeServerZone serverZone) {
    method setServerZone (line 2574) | public AmplitudeClient setServerZone(AmplitudeServerZone serverZone, b...
    method getServerZone (line 2590) | public AmplitudeServerZone getServerZone() {

FILE: src/main/java/com/amplitude/api/AmplitudeDeviceIdCallback.java
  type AmplitudeDeviceIdCallback (line 3) | public interface AmplitudeDeviceIdCallback {
    method onDeviceIdReady (line 4) | void onDeviceIdReady(String deviceId);

FILE: src/main/java/com/amplitude/api/AmplitudeLog.java
  class AmplitudeLog (line 5) | public class AmplitudeLog {
    method getLogger (line 12) | public static AmplitudeLog getLogger() {
    method AmplitudeLog (line 16) | private AmplitudeLog() {}
    method setEnableLogging (line 18) | AmplitudeLog setEnableLogging(boolean enableLogging) {
    method setLogLevel (line 23) | AmplitudeLog setLogLevel(int logLevel) {
    method d (line 28) | int d(String tag, String msg) {
    method d (line 33) | int d(String tag, String msg, Throwable tr) {
    method e (line 38) | int e(String tag, String msg) {
    method e (line 48) | int e(String tag, String msg, Throwable tr) {
    method getStackTraceString (line 58) | String getStackTraceString(Throwable tr) {
    method i (line 62) | int i(String tag, String msg) {
    method i (line 67) | int i(String tag, String msg, Throwable tr) {
    method isLoggable (line 72) | boolean isLoggable(String tag, int level) {
    method println (line 76) | int println(int priority, String tag, String msg) {
    method v (line 80) | int v(String tag, String msg) {
    method v (line 85) | int v(String tag, String msg, Throwable tr) {
    method w (line 90) | int w(String tag, String msg) {
    method w (line 95) | int w(String tag, Throwable tr) {
    method w (line 100) | int w(String tag, String msg, Throwable tr) {
    method wtf (line 106) | int wtf(String tag, String msg) {
    method wtf (line 111) | int wtf(String tag, Throwable tr) {
    method wtf (line 116) | int wtf(String tag, String msg, Throwable tr) {
    method setAmplitudeLogCallback (line 121) | void setAmplitudeLogCallback(AmplitudeLogCallback callback) {

FILE: src/main/java/com/amplitude/api/AmplitudeLogCallback.java
  type AmplitudeLogCallback (line 3) | public interface AmplitudeLogCallback {
    method onError (line 4) | void onError(String tag, String message);

FILE: src/main/java/com/amplitude/api/AmplitudeServerZone.java
  type AmplitudeServerZone (line 13) | public enum AmplitudeServerZone {
    method getEventLogApiForZone (line 29) | protected static String getEventLogApiForZone(AmplitudeServerZone serv...
    method getDynamicConfigApi (line 36) | protected static String getDynamicConfigApi(AmplitudeServerZone server...
    method getServerZone (line 43) | public static AmplitudeServerZone getServerZone(String serverZone) {

FILE: src/main/java/com/amplitude/api/ConfigManager.java
  class ConfigManager (line 13) | public class ConfigManager {
    method getIngestionEndpoint (line 20) | public String getIngestionEndpoint() {
    method ConfigManager (line 24) | private ConfigManager() {
    method refresh (line 27) | public void refresh(RefreshListener listener, AmplitudeServerZone serv...
    method getInstance (line 63) | public static ConfigManager getInstance() {
    type RefreshListener (line 71) | interface RefreshListener {
      method onFinished (line 72) | void onFinished();

FILE: src/main/java/com/amplitude/api/Constants.java
  class Constants (line 5) | public class Constants {

FILE: src/main/java/com/amplitude/api/CursorWindowAllocationException.java
  class CursorWindowAllocationException (line 10) | public class CursorWindowAllocationException extends RuntimeException {
    method CursorWindowAllocationException (line 11) | public CursorWindowAllocationException(String description) {

FILE: src/main/java/com/amplitude/api/DatabaseHelper.java
  class DatabaseHelper (line 21) | class DatabaseHelper extends SQLiteOpenHelper {
    method getDatabaseHelper (line 60) | @Deprecated
    method getDatabaseHelper (line 65) | static synchronized DatabaseHelper getDatabaseHelper(Context context, ...
    method getDatabaseName (line 75) | private static String getDatabaseName(String instance) {
    method DatabaseHelper (line 79) | protected DatabaseHelper(Context context) {
    method DatabaseHelper (line 83) | protected DatabaseHelper(Context context, String instance) {
    method setDatabaseResetListener (line 89) | void setDatabaseResetListener(DatabaseResetListener databaseResetListe...
    method onCreate (line 93) | @Override
    method onUpgrade (line 121) | @Override
    method resetDatabase (line 156) | private void resetDatabase(SQLiteDatabase db) {
    method insertOrReplaceKeyValue (line 165) | synchronized long insertOrReplaceKeyValue(String key, String value) {
    method insertOrReplaceKeyLongValue (line 170) | synchronized long insertOrReplaceKeyLongValue(String key, Long value) {
    method insertOrReplaceKeyValueToTable (line 175) | synchronized long insertOrReplaceKeyValueToTable(String table, String ...
    method insertOrReplaceKeyValueToTable (line 197) | synchronized long insertOrReplaceKeyValueToTable(SQLiteDatabase db, St...
    method insertKeyValueContentValuesIntoTable (line 213) | synchronized long insertKeyValueContentValuesIntoTable(SQLiteDatabase ...
    method deleteKeyFromTable (line 222) | synchronized long deleteKeyFromTable(String table, String key) {
    method addEvent (line 241) | synchronized long addEvent(String event) {
    method addIdentify (line 245) | synchronized long addIdentify(String identifyEvent) {
    method addIdentifyInterceptor (line 249) | synchronized long addIdentifyInterceptor(String identifyEvent) {
    method addEventToTable (line 253) | private synchronized long addEventToTable(String table, String event) {
    method insertEventContentValuesIntoTable (line 277) | synchronized long insertEventContentValuesIntoTable(SQLiteDatabase db,...
    method getValue (line 281) | synchronized String getValue(String key) {
    method getLongValue (line 285) | synchronized Long getLongValue(String key) {
    method getValueFromTable (line 289) | protected synchronized Object getValueFromTable(String table, String k...
    method getEvents (line 324) | synchronized List<JSONObject> getEvents(long upToId, long limit) throw...
    method getIdentifys (line 328) | synchronized List<JSONObject> getIdentifys(
    method getIdentifyInterceptors (line 333) | synchronized List<JSONObject> getIdentifyInterceptors(
    method getEventsFromTable (line 340) | protected synchronized List<JSONObject> getEventsFromTable(
    method getEventsBatchFromTable (line 349) | private List<JSONObject> getEventsBatchFromTable(
    method getEventsRowByRowFromTable (line 391) | private List<JSONObject> getEventsRowByRowFromTable(
    method getEventFromTable (line 438) | protected synchronized JSONObject getEventFromTable(String table, long...
    method getEventCount (line 474) | synchronized long getEventCount() {
    method getIdentifyCount (line 478) | synchronized long getIdentifyCount() {
    method getTotalEventCount (line 482) | synchronized long getTotalEventCount() {
    method getIdentifyInterceptorCount (line 486) | synchronized long getIdentifyInterceptorCount() {
    method getEventCountFromTable (line 490) | private synchronized long getEventCountFromTable(String table) {
    method getNthEventId (line 515) | synchronized long getNthEventId(long n) {
    method getNthIdentifyId (line 519) | synchronized long getNthIdentifyId(long n) {
    method getLastIdentifyInterceptorId (line 523) | synchronized long getLastIdentifyInterceptorId() {
    method getNthEventIdFromTable (line 527) | private synchronized long getNthEventIdFromTable(String table, long n) {
    method getNthEventIdFromTable (line 531) | private synchronized long getNthEventIdFromTable(String table, long n,...
    method removeEvents (line 562) | synchronized void removeEvents(long maxId) {
    method removeIdentifys (line 566) | synchronized void removeIdentifys(long maxId) {
    method removeIdentifyInterceptors (line 570) | synchronized void removeIdentifyInterceptors(long maxId) {
    method removeEventsFromTable (line 574) | private synchronized void removeEventsFromTable(String table, long max...
    method removeEvent (line 589) | synchronized void removeEvent(long id) {
    method removeIdentify (line 593) | synchronized void removeIdentify(long id) {
    method removeIdentifyIntercept (line 597) | synchronized void removeIdentifyIntercept(long id) {
    method removeEventFromTable (line 601) | private synchronized void removeEventFromTable(String table, long id) {
    method delete (line 616) | private void delete() {
    method dbFileExists (line 645) | boolean dbFileExists() {
    method queryDb (line 650) | Cursor queryDb(
    method handleIfCursorRowTooLargeException (line 661) | private void handleIfCursorRowTooLargeException(IllegalStateException ...
    method convertIfCursorWindowException (line 675) | private static void convertIfCursorWindowException(RuntimeException e) {

FILE: src/main/java/com/amplitude/api/DatabaseResetListener.java
  type DatabaseResetListener (line 5) | public interface DatabaseResetListener {
    method onDatabaseReset (line 6) | public void onDatabaseReset(SQLiteDatabase db);

FILE: src/main/java/com/amplitude/api/DeviceInfo.java
  class DeviceInfo (line 26) | @SuppressWarnings("MissingPermission")
    class CachedInfo (line 47) | private class CachedInfo {
      method CachedInfo (line 62) | private CachedInfo() {
      method getVersionName (line 81) | private String getVersionName() {
      method getOsName (line 94) | private String getOsName() {
      method getOsVersion (line 98) | private String getOsVersion() {
      method getBrand (line 102) | private String getBrand() {
      method getManufacturer (line 106) | private String getManufacturer() {
      method getModel (line 110) | private String getModel() {
      method getCarrier (line 114) | private String getCarrier() {
      method getCountry (line 125) | private String getCountry() {
      method getCountryFromLocation (line 142) | private String getCountryFromLocation() {
      method getCountryFromNetwork (line 179) | private String getCountryFromNetwork() {
      method getLocale (line 195) | private Locale getLocale() {
      method getCountryFromLocale (line 209) | private String getCountryFromLocale() {
      method getLanguage (line 213) | private String getLanguage() {
      method getAdvertisingId (line 217) | private String getAdvertisingId() {
      method getAppSetId (line 230) | private String getAppSetId() {
      method getAndCacheAmazonAdvertisingId (line 254) | private String getAndCacheAmazonAdvertisingId() {
      method getAndCacheGoogleAdvertisingId (line 263) | private String getAndCacheGoogleAdvertisingId() {
      method checkGPSEnabled (line 289) | private boolean checkGPSEnabled() {
    method DeviceInfo (line 317) | public DeviceInfo(Context context, boolean locationListening, boolean ...
    method getCachedInfo (line 323) | private CachedInfo getCachedInfo() {
    method prefetch (line 330) | public void prefetch() {
    method generateUUID (line 334) | public static String generateUUID() {
    method getVersionName (line 338) | public String getVersionName() {
    method getOsName (line 342) | public String getOsName() {
    method getOsVersion (line 346) | public String getOsVersion() {
    method getBrand (line 350) | public String getBrand() {
    method getManufacturer (line 354) | public String getManufacturer() {
    method getModel (line 358) | public String getModel() {
    method getCarrier (line 362) | public String getCarrier() {
    method getCountry (line 366) | public String getCountry() {
    method getLanguage (line 370) | public String getLanguage() {
    method getAdvertisingId (line 374) | public String getAdvertisingId() {
    method isLimitAdTrackingEnabled (line 378) | public boolean isLimitAdTrackingEnabled() {
    method getAppSetId (line 382) | public String getAppSetId() {
    method isGooglePlayServicesEnabled (line 386) | public boolean isGooglePlayServicesEnabled() { return getCachedInfo()....
    method getMostRecentLocation (line 388) | public Location getMostRecentLocation() {
    method isLocationListening (line 446) | public boolean isLocationListening() {
    method setLocationListening (line 450) | public void setLocationListening(boolean locationListening) {
    method getGeocoder (line 455) | protected Geocoder getGeocoder() {

FILE: src/main/java/com/amplitude/api/Identify.java
  class Identify (line 27) | public class Identify {
    method setOnce (line 51) | public Identify setOnce(String property, boolean value) {
    method setOnce (line 64) | public Identify setOnce(String property, double value) {
    method setOnce (line 77) | public Identify setOnce(String property, float value) {
    method setOnce (line 90) | public Identify setOnce(String property, int value) {
    method setOnce (line 103) | public Identify setOnce(String property, long value) {
    method setOnce (line 116) | public Identify setOnce(String property, String value) {
    method setOnce (line 129) | public Identify setOnce(String property, JSONArray values) {
    method setOnce (line 142) | public Identify setOnce(String property, JSONObject values) {
    method setOnce (line 155) | public Identify setOnce(String property, boolean[] values) {
    method setOnce (line 168) | public Identify setOnce(String property, double[] values) {
    method setOnce (line 181) | public Identify setOnce(String property, float[] values) {
    method setOnce (line 194) | public Identify setOnce(String property, int[] values) {
    method setOnce (line 207) | public Identify setOnce(String property, long[] values) {
    method setOnce (line 220) | public Identify setOnce(String property, String[] values) {
    method set (line 233) | public Identify set(String property, boolean value) {
    method set (line 245) | public Identify set(String property, double value) {
    method set (line 257) | public Identify set(String property, float value) {
    method set (line 269) | public Identify set(String property, int value) {
    method set (line 281) | public Identify set(String property, long value) {
    method set (line 293) | public Identify set(String property, String value) {
    method set (line 305) | public Identify set(String property, JSONObject values) {
    method set (line 317) | public Identify set(String property, JSONArray values) {
    method set (line 329) | public Identify set(String property, boolean[] values) {
    method set (line 341) | public Identify set(String property, double[] values) {
    method set (line 353) | public Identify set(String property, float[] values) {
    method set (line 365) | public Identify set(String property, int[] values) {
    method set (line 377) | public Identify set(String property, long[] values) {
    method set (line 389) | public Identify set(String property, String[] values) {
    method add (line 404) | public Identify add(String property, double value) {
    method add (line 418) | public Identify add(String property, float value) {
    method add (line 432) | public Identify add(String property, int value) {
    method add (line 446) | public Identify add(String property, long value) {
    method add (line 461) | public Identify add(String property, String value) {
    method add (line 476) | public Identify add(String property, JSONObject values) {
    method append (line 492) | public Identify append(String property, boolean value) {
    method append (line 507) | public Identify append(String property, double value) {
    method append (line 522) | public Identify append(String property, float value) {
    method append (line 537) | public Identify append(String property, int value) {
    method append (line 552) | public Identify append(String property, long value) {
    method append (line 567) | public Identify append(String property, String value) {
    method append (line 582) | public Identify append(String property, JSONArray values) {
    method append (line 598) | public Identify append(String property, JSONObject values) {
    method append (line 613) | public Identify append(String property, boolean[] values) {
    method append (line 628) | public Identify append(String property, double[] values) {
    method append (line 643) | public Identify append(String property, float[] values) {
    method append (line 658) | public Identify append(String property, int[] values) {
    method append (line 673) | public Identify append(String property, long[] values) {
    method append (line 688) | public Identify append(String property, String[] values) {
    method prepend (line 705) | public Identify prepend(String property, boolean value) {
    method prepend (line 721) | public Identify prepend(String property, double value) {
    method prepend (line 737) | public Identify prepend(String property, float value) {
    method prepend (line 753) | public Identify prepend(String property, int value) {
    method prepend (line 769) | public Identify prepend(String property, long value) {
    method prepend (line 785) | public Identify prepend(String property, String value) {
    method prepend (line 801) | public Identify prepend(String property, JSONArray values) {
    method prepend (line 818) | public Identify prepend(String property, JSONObject values) {
    method prepend (line 834) | public Identify prepend(String property, boolean[] values) {
    method prepend (line 850) | public Identify prepend(String property, double[] values) {
    method prepend (line 866) | public Identify prepend(String property, float[] values) {
    method prepend (line 882) | public Identify prepend(String property, int[] values) {
    method prepend (line 898) | public Identify prepend(String property, long[] values) {
    method prepend (line 914) | public Identify prepend(String property, String[] values) {
    method unset (line 926) | public Identify unset(String property) {
    method clearAll (line 938) | public Identify clearAll() {
    method preInsert (line 968) | public Identify preInsert(String property, boolean value) {
    method preInsert (line 984) | public Identify preInsert(String property, double value) {
    method preInsert (line 1000) | public Identify preInsert(String property, float value) {
    method preInsert (line 1016) | public Identify preInsert(String property, int value) {
    method preInsert (line 1032) | public Identify preInsert(String property, long value) {
    method preInsert (line 1048) | public Identify preInsert(String property, String value) {
    method preInsert (line 1064) | public Identify preInsert(String property, JSONArray values) {
    method preInsert (line 1080) | public Identify preInsert(String property, JSONObject values) {
    method preInsert (line 1097) | public Identify preInsert(String property, boolean[] values) {
    method preInsert (line 1113) | public Identify preInsert(String property, double[] values) {
    method preInsert (line 1129) | public Identify preInsert(String property, float[] values){
    method preInsert (line 1145) | public Identify preInsert(String property, int[] values){
    method preInsert (line 1161) | public Identify preInsert(String property, long[] values) {
    method preInsert (line 1177) | public Identify preInsert(String property, String[] values) {
    method postInsert (line 1193) | public Identify postInsert(String property, boolean value) {
    method postInsert (line 1209) | public Identify postInsert(String property, double value) {
    method postInsert (line 1225) | public Identify postInsert(String property, float value) {
    method postInsert (line 1241) | public Identify postInsert(String property, int value) {
    method postInsert (line 1257) | public Identify postInsert(String property, long value) {
    method postInsert (line 1273) | public Identify postInsert(String property, String value) {
    method postInsert (line 1289) | public Identify postInsert(String property, JSONArray values) {
    method postInsert (line 1305) | public Identify postInsert(String property, JSONObject values) {
    method postInsert (line 1321) | public Identify postInsert(String property, boolean[] values) {
    method postInsert (line 1337) | public Identify postInsert(String property, double[] values) {
    method postInsert (line 1353) | public Identify postInsert(String property, float[] values){
    method postInsert (line 1369) | public Identify postInsert(String property, int[] values){
    method postInsert (line 1385) | public Identify postInsert(String property, long[] values) {
    method postInsert (line 1401) | public Identify postInsert(String property, String[] values) {
    method remove (line 1415) | public Identify remove(String property, boolean value) {
    method remove (line 1429) | public Identify remove(String property, double value) {
    method remove (line 1443) | public Identify remove(String property, float value) {
    method remove (line 1457) | public Identify remove(String property, int value) {
    method remove (line 1471) | public Identify remove(String property, long value) {
    method remove (line 1485) | public Identify remove(String property, String value) {
    method remove (line 1499) | public Identify remove(String property, JSONArray values) {
    method remove (line 1513) | public Identify remove(String property, JSONObject values) {
    method remove (line 1527) | public Identify remove(String property, boolean[] values) {
    method remove (line 1541) | public Identify remove(String property, double[] values) {
    method remove (line 1555) | public Identify remove(String property, float[] values){
    method remove (line 1569) | public Identify remove(String property, int[] values){
    method remove (line 1583) | public Identify remove(String property, long[] values) {
    method remove (line 1597) | public Identify remove(String property, String[] values) {
    method addToUserProperties (line 1602) | private void addToUserProperties(String operation, String property, Ob...
    method booleanArrayToJSONArray (line 1648) | private JSONArray booleanArrayToJSONArray(boolean[] values) {
    method floatArrayToJSONArray (line 1654) | private JSONArray floatArrayToJSONArray(float[] values) {
    method doubleArrayToJSONArray (line 1668) | private JSONArray doubleArrayToJSONArray(double[] values) {
    method intArrayToJSONArray (line 1682) | private JSONArray intArrayToJSONArray(int[] values) {
    method longArrayToJSONArray (line 1688) | private JSONArray longArrayToJSONArray(long[] values) {
    method stringArrayToJSONArray (line 1694) | private JSONArray stringArrayToJSONArray(String[] values) {
    method setUserProperty (line 1707) | Identify setUserProperty(String property, Object value) {
    method setOnce (line 1722) | public Identify setOnce(String property, Object value) {
    method set (line 1739) | public Identify set(String property, Object value) {
    method getUserPropertiesOperations (line 1752) | public JSONObject getUserPropertiesOperations() {

FILE: src/main/java/com/amplitude/api/IdentifyInterceptor.java
  class IdentifyInterceptor (line 14) | class IdentifyInterceptor {
    method IdentifyInterceptor (line 34) | public  IdentifyInterceptor (
    method intercept (line 56) | public JSONObject intercept(String eventType, JSONObject event) {
    method setIdentifyBatchIntervalMillis (line 91) | public void setIdentifyBatchIntervalMillis(long identifyBatchIntervalM...
    method getTransferIdentifyEvent (line 95) | private JSONObject getTransferIdentifyEvent() {
    method scheduleTransfer (line 114) | private void scheduleTransfer() {
    method transferInterceptedIdentify (line 128) | public void transferInterceptedIdentify() {
    method mergeIdentifyInterceptList (line 136) | private JSONObject mergeIdentifyInterceptList(List<JSONObject> identif...
    method mergeUserProperties (line 146) | private void mergeUserProperties(JSONObject userProperties, JSONObject...
    method isSetOnly (line 156) | private boolean isSetOnly(JSONObject event) {
    method isClearAll (line 160) | private boolean isClearAll(JSONObject event) {
    method isSetGroups (line 164) | private boolean isSetGroups(JSONObject event) {
    method isActionOnly (line 172) | private boolean isActionOnly(JSONObject event, String action) {
    method saveIdentifyProperties (line 181) | private long saveIdentifyProperties(JSONObject event) {
    method isIdentityUpdated (line 185) | private boolean isIdentityUpdated(JSONObject event) {
    method isIdUpdated (line 207) | private boolean isIdUpdated(String id, String updateId) {

FILE: src/main/java/com/amplitude/api/IngestionMetadata.java
  class IngestionMetadata (line 6) | public class IngestionMetadata {
    method setSourceName (line 22) | public IngestionMetadata setSourceName(String sourceName) {
    method setSourceVersion (line 32) | public IngestionMetadata setSourceVersion(String sourceVersion) {
    method toJSONObject (line 41) | protected JSONObject toJSONObject() {

FILE: src/main/java/com/amplitude/api/Middleware.java
  type Middleware (line 3) | public interface Middleware {
    method run (line 4) | void run(MiddlewarePayload payload, MiddlewareNext next);

FILE: src/main/java/com/amplitude/api/MiddlewareExtended.java
  type MiddlewareExtended (line 3) | interface MiddlewareExtended extends Middleware {
    method flush (line 4) | void flush();

FILE: src/main/java/com/amplitude/api/MiddlewareExtra.java
  class MiddlewareExtra (line 6) | public class MiddlewareExtra extends HashMap<String, Object> {
    method MiddlewareExtra (line 7) | public MiddlewareExtra() {
    method MiddlewareExtra (line 10) | public MiddlewareExtra(Map<String, Object> map) {

FILE: src/main/java/com/amplitude/api/MiddlewareNext.java
  type MiddlewareNext (line 3) | public interface MiddlewareNext {
    method run (line 4) | public void run(MiddlewarePayload curPayload);

FILE: src/main/java/com/amplitude/api/MiddlewarePayload.java
  class MiddlewarePayload (line 5) | public class MiddlewarePayload {
    method MiddlewarePayload (line 9) | public MiddlewarePayload(JSONObject event, MiddlewareExtra extra) {
    method MiddlewarePayload (line 14) | public MiddlewarePayload(JSONObject event) {

FILE: src/main/java/com/amplitude/api/MiddlewareRunner.java
  class MiddlewareRunner (line 8) | public class MiddlewareRunner {
    method MiddlewareRunner (line 11) | public MiddlewareRunner() {
    method add (line 15) | public void add(Middleware middleware) {
    method runMiddlewares (line 19) | private void runMiddlewares(List<Middleware> middlewares, MiddlewarePa...
    method run (line 32) | public boolean run(MiddlewarePayload payload) {
    method run (line 43) | public void run(MiddlewarePayload payload, MiddlewareNext next) {
    method flush (line 48) | void flush() {

FILE: src/main/java/com/amplitude/api/PinnedAmplitudeClient.java
  class PinnedAmplitudeClient (line 36) | public class PinnedAmplitudeClient extends AmplitudeClient {
    method getCertificate (line 101) | protected static String getCertificate(AmplitudeServerZone serverZone) {
    method getPinnedCertificateChain (line 108) | protected static SSLContextBuilder getPinnedCertificateChain(Amplitude...
    class SSLContextBuilder (line 116) | protected static class SSLContextBuilder {
      method SSLContextBuilder (line 120) | public SSLContextBuilder() {
      method SSLContextBuilder (line 123) | public SSLContextBuilder(AmplitudeServerZone serverZone) {
      method addCertificate (line 133) | public SSLContextBuilder addCertificate(String certificateBase64) {
      method build (line 143) | public SSLContext build() {
    method getInstance (line 181) | public static PinnedAmplitudeClient getInstance() {
    method getInstance (line 192) | public static synchronized PinnedAmplitudeClient getInstance(String in...
    method PinnedAmplitudeClient (line 210) | public PinnedAmplitudeClient(String instance) {
    method initializeInternal (line 219) | public synchronized AmplitudeClient initializeInternal(
    method initialize (line 289) | @Override
    method initialize (line 294) | public synchronized AmplitudeClient initialize(
    method getPinnedCertSslSocketFactory (line 308) | protected SSLSocketFactory getPinnedCertSslSocketFactory(AmplitudeServ...
    method getPinnedCertSslSocketFactory (line 318) | protected SSLSocketFactory getPinnedCertSslSocketFactory(SSLContextBui...
    method setServerZone (line 337) | @Override

FILE: src/main/java/com/amplitude/api/Plan.java
  class Plan (line 6) | public class Plan {
    method setBranch (line 30) | public Plan setBranch(String branch) {
    method setSource (line 40) | public Plan setSource(String source) {
    method setVersion (line 50) | public Plan setVersion(String version) {
    method setVersionId (line 60) | public Plan setVersionId(String versionId) {
    method toJSONObject (line 69) | protected JSONObject toJSONObject() {

FILE: src/main/java/com/amplitude/api/Revenue.java
  class Revenue (line 27) | public class Revenue {
    method isValidRevenue (line 69) | protected boolean isValidRevenue() {
    method setProductId (line 83) | public Revenue setProductId(String productId) {
    method setQuantity (line 98) | public Revenue setQuantity(int quantity) {
    method setPrice (line 109) | public Revenue setPrice(double price) {
    method setRevenueType (line 120) | public Revenue setRevenueType(String revenueType) {
    method setReceipt (line 132) | public Revenue setReceipt(String receipt, String receiptSignature) {
    method setRevenueProperties (line 145) | public Revenue setRevenueProperties(JSONObject revenueProperties) {
    method setEventProperties (line 158) | public Revenue setEventProperties(JSONObject eventProperties) {
    method toJSONObject (line 168) | protected JSONObject toJSONObject() {
    method equals (line 193) | @Override
    method hashCode (line 219) | @Override

FILE: src/main/java/com/amplitude/api/TrackingOptions.java
  class TrackingOptions (line 9) | public class TrackingOptions {
    method disableAdid (line 30) | public TrackingOptions disableAdid() {
    method shouldTrackAdid (line 35) | boolean shouldTrackAdid() {
    method disableAppSetId (line 39) | public TrackingOptions disableAppSetId() {
    method shouldTrackAppSetId (line 44) | boolean shouldTrackAppSetId() {
    method disableCarrier (line 48) | public TrackingOptions disableCarrier() {
    method shouldTrackCarrier (line 53) | boolean shouldTrackCarrier() {
    method disableCity (line 57) | public TrackingOptions disableCity() {
    method shouldTrackCity (line 62) | boolean shouldTrackCity() {
    method disableCountry (line 66) | public TrackingOptions disableCountry() {
    method shouldTrackCountry (line 71) | boolean shouldTrackCountry() {
    method disableDeviceBrand (line 75) | public TrackingOptions disableDeviceBrand() {
    method shouldTrackDeviceBrand (line 80) | boolean shouldTrackDeviceBrand() {
    method disableDeviceManufacturer (line 84) | public TrackingOptions disableDeviceManufacturer() {
    method shouldTrackDeviceManufacturer (line 89) | boolean shouldTrackDeviceManufacturer() {
    method disableDeviceModel (line 93) | public TrackingOptions disableDeviceModel() {
    method shouldTrackDeviceModel (line 98) | boolean shouldTrackDeviceModel() {
    method disableDma (line 102) | public TrackingOptions disableDma() {
    method shouldTrackDma (line 107) | boolean shouldTrackDma() {
    method disableIpAddress (line 111) | public TrackingOptions disableIpAddress() {
    method shouldTrackIpAddress (line 116) | boolean shouldTrackIpAddress() {
    method disableLanguage (line 120) | public TrackingOptions disableLanguage() {
    method shouldTrackLanguage (line 125) | boolean shouldTrackLanguage() {
    method disableLatLng (line 129) | public TrackingOptions disableLatLng() {
    method shouldTrackLatLng (line 134) | boolean shouldTrackLatLng() {
    method disableOsName (line 138) | public TrackingOptions disableOsName() {
    method shouldTrackOsName (line 143) | boolean shouldTrackOsName() {
    method disableOsVersion (line 147) | public TrackingOptions disableOsVersion() {
    method shouldTrackOsVersion (line 152) | boolean shouldTrackOsVersion() {
    method disableApiLevel (line 156) | public TrackingOptions disableApiLevel() {
    method shouldTrackApiLevel (line 161) | boolean shouldTrackApiLevel() {
    method disablePlatform (line 165) | public TrackingOptions disablePlatform() {
    method shouldTrackPlatform (line 170) | boolean shouldTrackPlatform() {
    method disableRegion (line 174) | public TrackingOptions disableRegion() {
    method shouldTrackRegion (line 179) | boolean shouldTrackRegion() {
    method disableVersionName (line 183) | public TrackingOptions disableVersionName() {
    method shouldTrackVersionName (line 188) | boolean shouldTrackVersionName() {
    method disableTrackingField (line 192) | private void disableTrackingField(String field) {
    method getApiPropertiesTrackingOptions (line 196) | protected JSONObject getApiPropertiesTrackingOptions() {
    method shouldTrackField (line 214) | private boolean shouldTrackField(String field) {
    method mergeIn (line 218) | TrackingOptions mergeIn(TrackingOptions other) {
    method copyOf (line 226) | static TrackingOptions copyOf(TrackingOptions other) {
    method forCoppaControl (line 235) | static TrackingOptions forCoppaControl() {
    method equals (line 244) | public boolean equals(Object other) {

FILE: src/main/java/com/amplitude/api/Utils.java
  class Utils (line 15) | public class Utils {
    method cloneJSONObject (line 24) | static JSONObject cloneJSONObject(final JSONObject obj) {
    method compareJSONObjects (line 55) | static boolean compareJSONObjects(JSONObject o1, JSONObject o2) {
    method isEmptyString (line 98) | public static boolean isEmptyString(String s) {
    method normalizeInstanceName (line 102) | static String normalizeInstanceName(String instance) {
    method checkLocationPermissionAllowed (line 109) | static boolean checkLocationPermissionAllowed(Context context) {
    method checkPermissionAllowed (line 114) | static boolean checkPermissionAllowed(Context context, String permissi...

FILE: src/main/java/com/amplitude/api/WorkerThread.java
  class WorkerThread (line 7) | public class WorkerThread extends HandlerThread {
    method WorkerThread (line 9) | public WorkerThread(String name) {
    method getHandler (line 15) | Handler getHandler() {
    method post (line 19) | void post(Runnable r) {
    method postDelayed (line 24) | void postDelayed(Runnable r, long delayMillis) {
    method removeCallbacks (line 29) | void removeCallbacks(Runnable r) {
    method waitForInitialization (line 34) | private synchronized void waitForInitialization() {

FILE: src/main/java/com/amplitude/eventexplorer/EventExplorer.java
  class EventExplorer (line 16) | public class EventExplorer {
    method EventExplorer (line 20) | public EventExplorer(String instanceName) {
    method show (line 24) | public void show(final Activity rootActivity) {
    method prepareWindowManagerLayoutParams (line 46) | private WindowManager.LayoutParams prepareWindowManagerLayoutParams(Co...

FILE: src/main/java/com/amplitude/eventexplorer/EventExplorerInfoActivity.java
  class EventExplorerInfoActivity (line 18) | public class EventExplorerInfoActivity extends Activity {
    method onCreate (line 26) | @Override
    method copyText (line 68) | private void copyText(Context context, String text) {

FILE: src/main/java/com/amplitude/eventexplorer/EventExplorerTouchHandler.java
  class EventExplorerTouchHandler (line 8) | public class EventExplorerTouchHandler implements View.OnTouchListener {
    method EventExplorerTouchHandler (line 19) | EventExplorerTouchHandler(WindowManager windowManager,
    method onTouch (line 27) | @Override
    method isAClick (line 56) | private boolean isAClick(float startX, float endX, float startY, float...

FILE: src/main/java/com/amplitude/unity/plugins/AmplitudePlugin.java
  class AmplitudePlugin (line 17) | public class AmplitudePlugin {
    method ToJSONObject (line 19) | public static JSONObject ToJSONObject(String jsonString) {
    method ToJSONArray (line 29) | public static JSONArray ToJSONArray(String[] values) {
    method init (line 37) | public static void init(String instanceName, Context context, String a...
    method init (line 41) | public static void init(String instanceName, Context context, String a...
    method setTrackingOptions (line 45) | public static void setTrackingOptions(String instanceName, String trac...
    method enableForegroundTracking (line 106) | public static void enableForegroundTracking(String instanceName, Appli...
    method enableCoppaControl (line 110) | public static void enableCoppaControl(String instanceName) {
    method disableCoppaControl (line 114) | public static void disableCoppaControl(String instanceName) {
    method setLibraryName (line 118) | public static void setLibraryName(String instanceName, String libraryN...
    method setLibraryVersion (line 122) | public static void setLibraryVersion(String instanceName, String libra...
    method setServerUrl (line 126) | public static void setServerUrl(String instanceName, String serverUrl) {
    method setServerZone (line 130) | public static void setServerZone(String instanceName, String serverZon...
    method setUseDynamicConfig (line 135) | public static void setUseDynamicConfig(String instanceName, boolean us...
    method startSession (line 139) | @Deprecated
    method endSession (line 142) | @Deprecated
    method logEvent (line 145) | public static void logEvent(String instanceName, String event) {
    method logEvent (line 149) | public static void logEvent(String instanceName, String event, String ...
    method logEvent (line 153) | public static void logEvent(String instanceName, String event, String ...
    method uploadEvents (line 157) | public static void uploadEvents(String instanceName) {
    method useAdvertisingIdForDeviceId (line 161) | public static void useAdvertisingIdForDeviceId(String instanceName) {
    method useAppSetIdForDeviceId (line 165) | public static void useAppSetIdForDeviceId(String instanceName) {
    method setOffline (line 169) | public static void setOffline(String instanceName, boolean offline) {
    method setUserId (line 173) | public static void setUserId(String instanceName, String userId) {
    method setOptOut (line 177) | public static void setOptOut(String instanceName, boolean enabled) {
    method setMinTimeBetweenSessionsMillis (line 181) | public static void setMinTimeBetweenSessionsMillis(String instanceName...
    method setEventUploadPeriodMillis (line 185) | public static void setEventUploadPeriodMillis(String instanceName, int...
    method setUserProperties (line 189) | public static void setUserProperties(String instanceName, String jsonP...
    method setGroup (line 193) | public static void setGroup(String instanceName, String groupType, Str...
    method setGroup (line 197) | public static void setGroup(String instanceName, String groupType, Str...
    method logRevenue (line 201) | public static void logRevenue(String instanceName, double amount) {
    method logRevenue (line 205) | public static void logRevenue(String instanceName, String productId, i...
    method logRevenue (line 209) | public static void logRevenue(String instanceName, String productId, i...
    method logRevenue (line 213) | public static void logRevenue(String instanceName, String productId, i...
    method getDeviceId (line 230) | public static String getDeviceId(String instanceName) {
    method setDeviceId (line 234) | public static void setDeviceId(String instanceName, String deviceId) {
    method regenerateDeviceId (line 238) | public static void regenerateDeviceId(String instanceName) { Amplitude...
    method trackSessionEvents (line 240) | public static void trackSessionEvents(String instanceName, boolean ena...
    method getSessionId (line 244) | public static long getSessionId(String instanceName) { return Amplitud...
    method clearUserProperties (line 249) | public static void clearUserProperties(String instanceName) {
    method unsetUserProperty (line 254) | public static void unsetUserProperty(String instanceName, String prope...
    method setOnceUserProperty (line 259) | public static void setOnceUserProperty(String instanceName, String pro...
    method setOnceUserProperty (line 263) | public static void setOnceUserProperty(String instanceName, String pro...
    method setOnceUserProperty (line 267) | public static void setOnceUserProperty(String instanceName, String pro...
    method setOnceUserProperty (line 271) | public static void setOnceUserProperty(String instanceName, String pro...
    method setOnceUserProperty (line 275) | public static void setOnceUserProperty(String instanceName, String pro...
    method setOnceUserProperty (line 279) | public static void setOnceUserProperty(String instanceName, String pro...
    method setOnceUserPropertyDict (line 283) | public static void setOnceUserPropertyDict(String instanceName, String...
    method setOnceUserPropertyList (line 287) | public static void setOnceUserPropertyList(String instanceName, String...
    method setOnceUserProperty (line 297) | public static void setOnceUserProperty(String instanceName, String pro...
    method setOnceUserProperty (line 301) | public static void setOnceUserProperty(String instanceName, String pro...
    method setOnceUserProperty (line 305) | public static void setOnceUserProperty(String instanceName, String pro...
    method setOnceUserProperty (line 309) | public static void setOnceUserProperty(String instanceName, String pro...
    method setOnceUserProperty (line 313) | public static void setOnceUserProperty(String instanceName, String pro...
    method setOnceUserProperty (line 317) | public static void setOnceUserProperty(String instanceName, String pro...
    method setUserProperty (line 322) | public static void setUserProperty(String instanceName, String propert...
    method setUserProperty (line 326) | public static void setUserProperty(String instanceName, String propert...
    method setUserProperty (line 330) | public static void setUserProperty(String instanceName, String propert...
    method setUserProperty (line 334) | public static void setUserProperty(String instanceName, String propert...
    method setUserProperty (line 338) | public static void setUserProperty(String instanceName, String propert...
    method setUserProperty (line 342) | public static void setUserProperty(String instanceName, String propert...
    method setUserPropertyDict (line 346) | public static void setUserPropertyDict(String instanceName, String pro...
    method setUserPropertyList (line 350) | public static void setUserPropertyList(String instanceName, String pro...
    method setUserProperty (line 360) | public static void setUserProperty(String instanceName, String propert...
    method setUserProperty (line 364) | public static void setUserProperty(String instanceName, String propert...
    method setUserProperty (line 368) | public static void setUserProperty(String instanceName, String propert...
    method setUserProperty (line 372) | public static void setUserProperty(String instanceName, String propert...
    method setUserProperty (line 376) | public static void setUserProperty(String instanceName, String propert...
    method setUserProperty (line 380) | public static void setUserProperty(String instanceName, String propert...
    method addUserProperty (line 385) | public static void addUserProperty(String instanceName, String propert...
    method addUserProperty (line 389) | public static void addUserProperty(String instanceName, String propert...
    method addUserProperty (line 393) | public static void addUserProperty(String instanceName, String propert...
    method addUserProperty (line 397) | public static void addUserProperty(String instanceName, String propert...
    method addUserProperty (line 401) | public static void addUserProperty(String instanceName, String propert...
    method addUserPropertyDict (line 405) | public static void addUserPropertyDict(String instanceName, String pro...
    method appendUserProperty (line 410) | public static void appendUserProperty(String instanceName, String prop...
    method appendUserProperty (line 414) | public static void appendUserProperty(String instanceName, String prop...
    method appendUserProperty (line 418) | public static void appendUserProperty(String instanceName, String prop...
    method appendUserProperty (line 422) | public static void appendUserProperty(String instanceName, String prop...
    method appendUserProperty (line 426) | public static void appendUserProperty(String instanceName, String prop...
    method appendUserProperty (line 430) | public static void appendUserProperty(String instanceName, String prop...
    method appendUserPropertyDict (line 434) | public static void appendUserPropertyDict(String instanceName, String ...
    method appendUserPropertyList (line 438) | public static void appendUserPropertyList(String instanceName, String ...
    method appendUserProperty (line 448) | public static void appendUserProperty(String instanceName, String prop...
    method appendUserProperty (line 452) | public static void appendUserProperty(String instanceName, String prop...
    method appendUserProperty (line 456) | public static void appendUserProperty(String instanceName, String prop...
    method appendUserProperty (line 460) | public static void appendUserProperty(String instanceName, String prop...
    method appendUserProperty (line 464) | public static void appendUserProperty(String instanceName, String prop...
    method appendUserProperty (line 468) | public static void appendUserProperty(String instanceName, String prop...
    method prependUserProperty (line 473) | public static void prependUserProperty(String instanceName, String pro...
    method prependUserProperty (line 477) | public static void prependUserProperty(String instanceName, String pro...
    method prependUserProperty (line 481) | public static void prependUserProperty(String instanceName, String pro...
    method prependUserProperty (line 485) | public static void prependUserProperty(String instanceName, String pro...
    method prependUserProperty (line 489) | public static void prependUserProperty(String instanceName, String pro...
    method prependUserProperty (line 493) | public static void prependUserProperty(String instanceName, String pro...
    method prependUserPropertyDict (line 497) | public static void prependUserPropertyDict(String instanceName, String...
    method prependUserPropertyList (line 501) | public static void prependUserPropertyList(String instanceName, String...
    method prependUserProperty (line 511) | public static void prependUserProperty(String instanceName, String pro...
    method prependUserProperty (line 515) | public static void prependUserProperty(String instanceName, String pro...
    method prependUserProperty (line 519) | public static void prependUserProperty(String instanceName, String pro...
    method prependUserProperty (line 523) | public static void prependUserProperty(String instanceName, String pro...
    method prependUserProperty (line 527) | public static void prependUserProperty(String instanceName, String pro...
    method prependUserProperty (line 531) | public static void prependUserProperty(String instanceName, String pro...
    method preInsertUserProperty (line 536) | public static void preInsertUserProperty(String instanceName, String p...
    method preInsertUserProperty (line 540) | public static void preInsertUserProperty(String instanceName, String p...
    method preInsertUserProperty (line 544) | public static void preInsertUserProperty(String instanceName, String p...
    method preInsertUserProperty (line 548) | public static void preInsertUserProperty(String instanceName, String p...
    method preInsertUserProperty (line 552) | public static void preInsertUserProperty(String instanceName, String p...
    method preInsertUserProperty (line 556) | public static void preInsertUserProperty(String instanceName, String p...
    method preInsertUserPropertyDict (line 560) | public static void preInsertUserPropertyDict(String instanceName, Stri...
    method preInsertUserPropertyList (line 564) | public static void preInsertUserPropertyList(String instanceName, Stri...
    method preInsertUserProperty (line 574) | public static void preInsertUserProperty(String instanceName, String p...
    method preInsertUserProperty (line 578) | public static void preInsertUserProperty(String instanceName, String p...
    method preInsertUserProperty (line 582) | public static void preInsertUserProperty(String instanceName, String p...
    method preInsertUserProperty (line 586) | public static void preInsertUserProperty(String instanceName, String p...
    method preInsertUserProperty (line 590) | public static void preInsertUserProperty(String instanceName, String p...
    method preInsertUserProperty (line 594) | public static void preInsertUserProperty(String instanceName, String p...
    method postInsertUserProperty (line 599) | public static void postInsertUserProperty(String instanceName, String ...
    method postInsertUserProperty (line 603) | public static void postInsertUserProperty(String instanceName, String ...
    method postInsertUserProperty (line 607) | public static void postInsertUserProperty(String instanceName, String ...
    method postInsertUserProperty (line 611) | public static void postInsertUserProperty(String instanceName, String ...
    method postInsertUserProperty (line 615) | public static void postInsertUserProperty(String instanceName, String ...
    method postInsertUserProperty (line 619) | public static void postInsertUserProperty(String instanceName, String ...
    method postInsertUserPropertyDict (line 623) | public static void postInsertUserPropertyDict(String instanceName, Str...
    method postInsertUserPropertyList (line 627) | public static void postInsertUserPropertyList(String instanceName, Str...
    method postInsertUserProperty (line 637) | public static void postInsertUserProperty(String instanceName, String ...
    method postInsertUserProperty (line 641) | public static void postInsertUserProperty(String instanceName, String ...
    method postInsertUserProperty (line 645) | public static void postInsertUserProperty(String instanceName, String ...
    method postInsertUserProperty (line 649) | public static void postInsertUserProperty(String instanceName, String ...
    method postInsertUserProperty (line 653) | public static void postInsertUserProperty(String instanceName, String ...
    method postInsertUserProperty (line 657) | public static void postInsertUserProperty(String instanceName, String ...
    method removeUserProperty (line 662) | public static void removeUserProperty(String instanceName, String prop...
    method removeUserProperty (line 666) | public static void removeUserProperty(String instanceName, String prop...
    method removeUserProperty (line 670) | public static void removeUserProperty(String instanceName, String prop...
    method removeUserProperty (line 674) | public static void removeUserProperty(String instanceName, String prop...
    method removeUserProperty (line 678) | public static void removeUserProperty(String instanceName, String prop...
    method removeUserProperty (line 682) | public static void removeUserProperty(String instanceName, String prop...
    method removeUserPropertyDict (line 686) | public static void removeUserPropertyDict(String instanceName, String ...
    method removeUserPropertyList (line 690) | public static void removeUserPropertyList(String instanceName, String ...
    method removeUserProperty (line 700) | public static void removeUserProperty(String instanceName, String prop...
    method removeUserProperty (line 704) | public static void removeUserProperty(String instanceName, String prop...
    method removeUserProperty (line 708) | public static void removeUserProperty(String instanceName, String prop...
    method removeUserProperty (line 712) | public static void removeUserProperty(String instanceName, String prop...
    method removeUserProperty (line 716) | public static void removeUserProperty(String instanceName, String prop...
    method removeUserProperty (line 720) | public static void removeUserProperty(String instanceName, String prop...

FILE: src/main/java/com/amplitude/util/DoubleCheck.java
  class DoubleCheck (line 9) | public class DoubleCheck<T> implements Provider<T> {
    method DoubleCheck (line 15) | private DoubleCheck(Provider<T> provider) {
    method get (line 20) | @SuppressWarnings("unchecked") // cast only happens when result comes ...
    method reentrantCheck (line 44) | public static Object reentrantCheck(Object currentInstance, Object new...
    method provider (line 56) | public static <P extends Provider<T>, T> Provider<T> provider(P delega...

FILE: src/main/java/com/amplitude/util/Provider.java
  type Provider (line 3) | public interface Provider<T> {
    method get (line 4) | T get();

FILE: src/test/java/com/amplitude/api/AmplitudeClientTest.java
  class AmplitudeClientTest (line 44) | @RunWith(AndroidJUnit4.class)
    method generateStringWithLength (line 48) | private String generateStringWithLength(int length, char c) {
    method setUp (line 55) | @Before
    method tearDown (line 62) | @After
    method testConstructor (line 67) | @Test
    method testSetUserId (line 77) | @Test
    method testSetUserIdTwice (line 94) | @Test
    method testSetDeviceId (line 121) | @Test
    method testSetUserProperties (line 188) | @Test
    method testSetCustomLibrary (line 222) | @Test
    method testSetCustomLibraryWithNullValues (line 243) | @Test
    method testIdentifyMultipleOperations (line 264) | @Test
    method testOptOut (line 299) | @Test
    method testOffline (line 329) | @Test
    method testLogEvent (line 351) | @Test
    method testIdentify (line 357) | @Test
    method testNullIdentify (line 404) | @Test
    method testLog3Events (line 418) | @Test
    method testLog3Identifys (line 451) | @Test
    method testLogEventAndIdentify (line 505) | @Test
    method testMergeEventsAndIdentifys (line 553) | @Test
    method testMergeEventBackwardsCompatible (line 649) | @Test
    method testRemoveAfterSuccessfulUpload (line 718) | @Test
    method testLogEventHasUUID (line 751) | @Test
    method testLogRevenue (line 767) | @Test
    method testLogRevenueV2 (line 818) | @Test
    method testLogEventSync (line 875) | @Test
    method testEmptyEventProps (line 903) | @Test
    method testSaveEventLogic (line 912) | @Test
    method testRequestTooLargeBackoffLogic (line 946) | @Test
    method testUploadRemainingEvents (line 1000) | @Test
    method testBackoffRemoveIdentify (line 1062) | @Test
    method testLimitTrackingEnabled (line 1103) | @Test
    method testTruncateString (line 1113) | @Test
    method testTruncateJSONObject (line 1122) | @Test
    method testTruncateNullJSONObject (line 1150) | @Test
    method testTruncateEventAndIdentify (line 1158) | @Test
    method testAutoIncrementSequenceNumber (line 1188) | @Test
    method testSetOffline (line 1198) | @Test
    method testSetOfflineTruncate (line 1221) | @Test
    method testTruncateEventsQueues (line 1261) | @Test
    method testTruncateEventsQueuesWithOneEvent (line 1280) | @Test
    method testClearUserProperties (line 1299) | @Test
    method testSetGroup (line 1322) | @Test
    method testLogEventWithGroups (line 1351) | @Test
    method testMergeEventsArrayIndexOutOfBounds (line 1377) | @Test
    method testCursorWindowAllocationException (line 1410) | @Test
    method testBlockTooManyEventUserProperties (line 1449) | @Test
    method testLogEventWithTimestamp (line 1491) | @Test
    method testRegenerateDeviceId (line 1506) | @Test
    method testSendNullEvents (line 1520) | @Test
    method testHandleUploadExceptions (line 1540) | @Test
    method testDefaultPlatform (line 1569) | @Test
    method testOverridePlatform (line 1595) | @Test
    method testSetTrackingConfig (line 1628) | @Test
    method testEnableCoppaControl (line 1679) | @Test
    method testEnableCoppaControlWithOptions (line 1794) | @Test
    method testGroupIdentifyMultipleOperations (line 1927) | @Test
    method testGroupIdentifyPropertiesObject (line 1974) | @Test
    method testSetLogCallback (line 2020) | @Test
    method testSetPlan (line 2041) | @Test
    method testSetIngestionMetadata (line 2064) | @Test
    method testSetServerZoneWithoutUpdateServerUrl (line 2087) | @Test
    method testSetServerZoneAndUpdateServerUrl (line 2096) | @Test
    method testMiddlewareSupport (line 2104) | @Test
    method testWithSwallowMiddleware (line 2135) | @Test
    method setIdentifyBatchIntervalMillis (line 2152) | @Test
    method testMultipleIdentifyWithSetActionOnlySendOneIdentifyEvent (line 2160) | @Test
    method testMultipleIdentifyWithSetActionOnlyAndOneEvent (line 2202) | @Test
    method testMultipleIdentifyWithSetActionAndOneEventAndIdentify (line 2273) | @Test
    method testIdentifyInterceptWithSetAndClearAll (line 2355) | @Test
    method testMultipleIdentifyWithSetActionAndAnotherIdentify (line 2392) | @Test
    method testUploadEventsSendInterceptedIdentify (line 2444) | @Test
    method testMultipleIdentifyWithSetActionAndSetGroup (line 2486) | @Test
    method testMultipleIdentifyWithSetActionAndUserIdUpdated (line 2555) | @Test
    method testNullUserPropertyFilteredOut (line 2612) | @Test

FILE: src/test/java/com/amplitude/api/AmplitudeServerZoneTest.java
  class AmplitudeServerZoneTest (line 13) | @RunWith(Parameterized.class)
    method data (line 17) | @Parameterized.Parameters
    method AmplitudeServerZoneTest (line 31) | public AmplitudeServerZoneTest(
    method testGetCorrectUrlForAmplitudeServerZone (line 43) | @Test

FILE: src/test/java/com/amplitude/api/AmplitudeTest.java
  class AmplitudeTest (line 21) | @RunWith(AndroidJUnit4.class)
    method setUp (line 25) | @Before
    method tearDown (line 30) | @After
    method testGetInstance (line 35) | @Test
    method testSeparateInstancesLogEventsSeparately (line 65) | @Test

FILE: src/test/java/com/amplitude/api/BaseTest.java
  class BaseTest (line 34) | public class BaseTest {
    class MockClock (line 36) | protected class MockClock {
      method setTimestamps (line 40) | public void setTimestamps(long [] timestamps) {
      method currentTimeMillis (line 44) | public long currentTimeMillis() {
    class AmplitudeClientWithTime (line 53) | protected class AmplitudeClientWithTime extends AmplitudeClient {
      method AmplitudeClientWithTime (line 56) | public AmplitudeClientWithTime(MockClock mockClock) { this.mockClock...
      method getCurrentTimeMillis (line 58) | @Override
    class MockDatabaseHelper (line 63) | protected class MockDatabaseHelper extends DatabaseHelper {
      method MockDatabaseHelper (line 65) | protected MockDatabaseHelper(Context context) {
      method queryDb (line 69) | @Override
    method setUp (line 92) | public void setUp() throws Exception {
    method setUp (line 101) | public void setUp(boolean withServer) throws Exception {
    method tearDown (line 142) | public void tearDown() throws Exception {
    method runRequest (line 157) | public RecordedRequest runRequest(AmplitudeClient amplitude) {
    method sendEvent (line 169) | public RecordedRequest sendEvent(AmplitudeClient amplitude, String nam...
    method sendIdentify (line 178) | public RecordedRequest sendIdentify(AmplitudeClient amplitude, Identif...
    method getUnsentEventCount (line 187) | public long getUnsentEventCount() {
    method getUnsentIdentifyCount (line 191) | public long getUnsentIdentifyCount() {
    method getIdentifyInterceptorCount (line 195) | public long getIdentifyInterceptorCount() {
    method getLastUnsentEvent (line 199) | public JSONObject getLastUnsentEvent() {
    method getLastUnsentIdentify (line 204) | public JSONObject getLastUnsentIdentify() {
    method getUnsentEvents (line 209) | public JSONArray getUnsentEvents(int limit) {
    method getUnsentIdentifys (line 213) | public JSONArray getUnsentIdentifys(int limit) {
    method getUnsentEventsFromTable (line 217) | public JSONArray getUnsentEventsFromTable(String table, int limit) {
    method getLastEvent (line 236) | public JSONObject getLastEvent() {
    method getLastIdentify (line 240) | public JSONObject getLastIdentify() {
    method getLastEventFromTable (line 244) | public JSONObject getLastEventFromTable(String table) {
    method getLastIdentifyInterceptor (line 256) | public JSONObject getLastIdentifyInterceptor() {
    method getEventsFromRequest (line 267) | public JSONArray getEventsFromRequest(RecordedRequest request) throws ...
    method parseRequest (line 276) | public static Map<String, String> parseRequest(String request) {
    method getPrivateFieldValueFromClient (line 291) | protected Object getPrivateFieldValueFromClient(AmplitudeClient client...

FILE: src/test/java/com/amplitude/api/ConfigManagerTest.java
  class ConfigManagerTest (line 19) | @RunWith(AndroidJUnit4.class)
    method testRefreshForEU (line 24) | @Test

FILE: src/test/java/com/amplitude/api/DatabaseHelperTest.java
  class DatabaseHelperTest (line 22) | @RunWith(AndroidJUnit4.class)
    method setUp (line 28) | @Before
    method tearDown (line 34) | @After
    method addEvent (line 40) | protected long addEvent(String type) {
    method addEventToTable (line 44) | protected long addEventToTable(String table, String type, JSONObject p...
    method addIdentify (line 56) | protected long addIdentify(String identifyEvent) {
    method addIdentifyInterceptor (line 60) | protected long addIdentifyInterceptor(String property) {
    method insertOrReplaceKeyValue (line 73) | protected long insertOrReplaceKeyValue(String key, String value) {
    method insertOrReplaceKeyLongValue (line 77) | protected long insertOrReplaceKeyLongValue(String key, Long value) {
    method getValue (line 81) | protected String getValue(String key) {
    method getLongValue (line 85) | protected Long getLongValue(String key) { return dbInstance.getLongVal...
    method testCreate (line 87) | @Test
    method testUpgradeVersion1ToVersion2 (line 99) | @Test
    method testUpgradeVersion2ToVersion3 (line 145) | @Test
    method testUpgradeVersion1ToVersion3 (line 183) | @Test
    method testUpgradeVersion1ToVersion4 (line 229) | @Test
    method testUpgradeVersion2ToVersion4 (line 274) | @Test
    method testUpgradeVersion3ToVersion4 (line 311) | @Test
    method testInsertOrReplaceKeyValue (line 336) | @Test
    method testInsertOrReplaceKeyLongValue (line 350) | @Test
    method testInsertNullValues (line 364) | @Test
    method testAddEvent (line 381) | @Test
    method testAddIdentify (line 391) | @Test
    method testGetEvents (line 401) | @Test
    method testGetIdentifys (line 456) | @Test
    method testGetEventCount (line 513) | @Test
    method testGetIdentifyCount (line 533) | @Test
    method testGetNthEventId (line 553) | @Test
    method testGetNthIdentifyId (line 578) | @Test
    method testNoConflictBetweenEventsAndIdentifys (line 603) | @Test
    method testNullEventString (line 630) | @Test
    method testGetDatabaseHelper (line 637) | @Test
    method testSeparateInstances (line 677) | @Test
    method testGetLastIdentifyInterceptorId (line 699) | @Test

FILE: src/test/java/com/amplitude/api/DatabaseRecoveryTest.java
  class DatabaseRecoveryTest (line 34) | @RunWith(AndroidJUnit4.class)
    method setUp (line 42) | @Before
    method tearDown (line 57) | @After
    method testRecoverFromDatabaseReset (line 65) | @Test
    method testDatabaseResetAvoidStackOverflow (line 121) | @Test
    method testCorruptingDatabaseFile (line 175) | @Test
    method testDeletedDatabaseFile (line 233) | @Test

FILE: src/test/java/com/amplitude/api/DeviceInfoTest.java
  class DeviceInfoTest (line 55) | @RunWith(AndroidJUnit4.class)
    method makeLocation (line 74) | private static Location makeLocation(String provider, double lat, doub...
    method setUp (line 85) | @Before
    method tearDown (line 102) | @After
    method testGetVersionName (line 107) | @Test
    method testGetBrand (line 112) | @Test
    method testGetManufacturer (line 117) | @Test
    method testGetModel (line 122) | @Test
    method testGetCarrier (line 127) | @Test
    method testGetCountry (line 132) | @Test
    method testGetCountryFromNetwork (line 137) | @Test
    method testGetLanguage (line 174) | @Test
    method testGetAdvertisingIdFromGoogleDevice (line 179) | @Test
    method testGetAdvertisingIdFromGoogleDeviceDisabledTrackAdid (line 200) | @Test
    method testGetAdvertisingIdFromAmazonDevice (line 212) | @Test
    method testGetAdvertisingIdFromAmazonDeviceDisabledTrackAdid (line 229) | @Test
    method testGPSDisabled (line 245) | @Test
    method testGPSEnabled (line 262) | @Test
    method testNoLocation (line 286) | @Test
    method testUseAdvertisingIdAsDeviceId (line 293) | @Test
    method testDontUseAdvertisingIdAsDeviceId (line 319) | @Test
    method testDeviceIdEqualsToAppSetId (line 346) | @Test
    method testToggleAppSetIdInEvents (line 385) | @Test
    class DeviceInfoAmplitudeClient (line 436) | private class DeviceInfoAmplitudeClient extends AmplitudeClient {
      method initializeDeviceInfo (line 437) | protected DeviceInfo initializeDeviceInfo() {
      method publicInitializeDeviceInfo (line 440) | public DeviceInfo publicInitializeDeviceInfo() {
      method DeviceInfoAmplitudeClient (line 443) | public DeviceInfoAmplitudeClient(String instance) {

FILE: src/test/java/com/amplitude/api/IdentifyTest.java
  class IdentifyTest (line 16) | @RunWith(AndroidJUnit4.class)
    method setUp (line 20) | @Before
    method tearDown (line 23) | @After
    method testUnsetProperty (line 26) | @Test
    method testSetProperty (line 38) | @Test
    method testSetOnceProperty (line 71) | @Test
    method testAddProperty (line 104) | @Test
    method testAppendProperty (line 135) | @Test
    method testPrependProperty (line 193) | @Test
    method testPreInsertProperty (line 251) | @Test
    method testPostInsertProperty (line 309) | @Test
    method testRemoveProperty (line 367) | @Test
    method testMultipleOperations (line 425) | @Test
    method testDisallowDuplicateProperties (line 476) | @Test
    method testDisallowOtherOperationsOnClearAllIdentify (line 491) | @Test
    method testDisallowClearAllOnIdentifysWithOtherOperations (line 506) | @Test
    method testGetUserPropertyOperations (line 521) | @Test

FILE: src/test/java/com/amplitude/api/IngestionMetadataTest.java
  class IngestionMetadataTest (line 13) | @RunWith(AndroidJUnit4.class)
    method testToJSONObject (line 17) | @Test

FILE: src/test/java/com/amplitude/api/InitializeTest.java
  class InitializeTest (line 25) | @RunWith(AndroidJUnit4.class)
    method setUp (line 29) | @Before
    method tearDown (line 34) | @After
    method testInitializeUserId (line 39) | @Test
    method testInitializeUserIdFromDb (line 59) | @Test
    method testInitializeOptOut (line 75) | @Test
    method testInitializeOptOutFromDB (line 94) | @Test
    method testInitializeLastEventId (line 107) | @Test
    method testInitializePreviousSessionId (line 132) | @Test
    method testInitializeLastEventTime (line 143) | @Test
    method testSkipSharedPrefsToDb (line 155) | @Test
    method testInitializePreviousSessionIdLastEventTime (line 187) | @Test
    method testReloadDeviceIdFromDatabase (line 221) | @Test
    method testInitializeDeviceIdWithRandomUUID (line 234) | @Test

FILE: src/test/java/com/amplitude/api/MiddlewareRunnerTest.java
  class MiddlewareRunnerTest (line 17) | @RunWith(AndroidJUnit4.class)
    method testMiddlewareRun (line 22) | @Test
    method testRunWithNotPassMiddleware (line 45) | @Test
    method testMiddlewareFlush (line 84) | @Test

FILE: src/test/java/com/amplitude/api/MockGeocoder.java
  class MockGeocoder (line 10) | @Implements(Geocoder.class)
    method isPresent (line 12) | @Implementation

FILE: src/test/java/com/amplitude/api/PinningTest.java
  class PinningTest (line 19) | @RunWith(AndroidJUnit4.class)
    method setUp (line 23) | @Before
    method tearDown (line 31) | @After
    method testSslPinningUS (line 37) | @Test
    method testSslPinningEU (line 57) | @Test
    method testSslPinningSwitch (line 76) | @Test
    method testSslPinningInvalid (line 97) | @Test
    class InvalidPinnedAmplitudeClient (line 115) | private static class InvalidPinnedAmplitudeClient extends PinnedAmplit...
      method InvalidPinnedAmplitudeClient (line 147) | public InvalidPinnedAmplitudeClient() {

FILE: src/test/java/com/amplitude/api/PlanTest.java
  class PlanTest (line 13) | @RunWith(AndroidJUnit4.class)
    method testToJSONObject (line 17) | @Test

FILE: src/test/java/com/amplitude/api/RevenueTest.java
  class RevenueTest (line 19) | @RunWith(AndroidJUnit4.class)
    method setUp (line 23) | @Before
    method tearDown (line 26) | @After
    method testProductId (line 29) | @Test
    method testQuantity (line 48) | @Test
    method testPrice (line 61) | @Test
    method testRevenueType (line 74) | @Test
    method testReceipt (line 96) | @Test
    method testRevenueProperties (line 113) | @Test
    method testEventProperties (line 130) | @Test
    method testValidRevenue (line 147) | @Test
    method testToJSONObject (line 165) | @Test
    method testEquals (line 189) | @Test
    method testHashCode (line 205) | @Test

FILE: src/test/java/com/amplitude/api/SessionTest.java
  class SessionTest (line 24) | @RunWith(AndroidJUnit4.class)
    class AmplitudeCallbacksWithTime (line 29) | private class AmplitudeCallbacksWithTime extends AmplitudeCallbacks {
      method AmplitudeCallbacksWithTime (line 34) | public AmplitudeCallbacksWithTime(AmplitudeClient client, long [] ti...
      method getCurrentTimeMillis (line 40) | @Override
    method setUp (line 46) | @Before
    method tearDown (line 53) | @After
    method testDefaultStartSession (line 58) | @Test
    method testDefaultTriggerNewSession (line 71) | @Test
    method testDefaultExtendSession (line 102) | @Test
    method testDefaultStartSessionWithTracking (line 142) | @Test
    method testDefaultStartSessionWithTrackingSynchronous (line 167) | @Test
    method testDefaultTriggerNewSessionWithTracking (line 193) | @Test
    method testDefaultTriggerNewSessionWithTrackingSynchronous (line 252) | @Test
    method testDefaultExtendSessionWithTracking (line 312) | @Test
    method testDefaultExtendSessionWithTrackingSynchronous (line 363) | @Test
    method testEnableAccurateTracking (line 415) | @Test
    method testAccurateOnResumeStartSession (line 422) | @Test
    method testAccurateOnResumeStartSessionWithTracking (line 441) | @Test
    method testAccurateOnPauseRefreshTimestamp (line 479) | @Test
    method testAccurateOnPauseRefreshTimestampWithTracking (line 504) | @Test
    method testAccurateOnResumeTriggerNewSession (line 535) | @Test
    method testAccurateOnResumeTriggerNewSessionWithTracking (line 579) | @Test
    method testAccurateOnResumeExtendSession (line 654) | @Test
    method testAccurateOnResumeExtendSessionWithTracking (line 691) | @Test
    method testAccurateLogAsyncEvent (line 743) | @Test
    method testAccurateLogAsyncEventWithTracking (line 780) | @Test
    method testLogOutOfSessionEvent (line 849) | @Test
    method testOnPauseFlushEvents (line 885) | @Test
    method testOnPauseFlushEventsDisabled (line 915) | @Test
    method testIdentifyTriggerNewSession (line 942) | @Test
    method testOutOfSessionIdentifyDoesNotTriggerNewSession (line 973) | @Test
    method testSetUserIdAndStartNewSessionWithTracking (line 1000) | @Test
    method testSetUserIdAndDoNotStartNewSessionWithTracking (line 1056) | @Test
    method testSetUserIdAndStartNewSessionWithoutTracking (line 1094) | @Test

FILE: src/test/java/com/amplitude/api/TrackingOptionsTest.java
  class TrackingOptionsTest (line 20) | @RunWith(AndroidJUnit4.class)
    method setUp (line 24) | @Before
    method tearDown (line 27) | @After
    method testDisableFields (line 30) | @Test
    method testGetApiPropertiesTrackingOptions (line 59) | @Test
    method testGetCoppaControlTrackingOptions (line 72) | @Test
    method testMerging (line 81) | @Test
    method testCopyOf (line 94) | @Test
    method testEquals (line 103) | @Test

FILE: src/test/java/com/amplitude/api/util/MockHttpURLConnectionHelper.java
  class MockHttpURLConnectionHelper (line 13) | public class MockHttpURLConnectionHelper {
    method getMockHttpURLConnection (line 15) | public static HttpURLConnection getMockHttpURLConnection(int code, Str...

FILE: src/test/java/com/amplitude/api/util/MockURLStreamHandler.java
  class MockURLStreamHandler (line 11) | public class MockURLStreamHandler extends URLStreamHandler implements UR...
    method getInstance (line 17) | public static MockURLStreamHandler getInstance() {
    method openConnection (line 21) | @Override
    method resetConnections (line 26) | public void resetConnections() {
    method setConnection (line 30) | public MockURLStreamHandler setConnection(URL url, URLConnection urlCo...
    method createURLStreamHandler (line 35) | @Override
Condensed preview — 78 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (737K chars).
[
  {
    "path": ".github/ISSUE_TEMPLATE/Bug_report.md",
    "chars": 617,
    "preview": "---\nname: Bug report 🐛\nabout: You're having technical issues\nlabels: 'bug'\n---\n\n<!--- Please fill out the template to th"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/Feature_request.md",
    "chars": 351,
    "preview": "---\nname: Feature Request 🚀\nabout: You'd like something added to the SDK\nlabels: 'feature request'\n---\n\n<!--- Please fil"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/Question.md",
    "chars": 116,
    "preview": "---\nname: Question ❓\nabout: Ask a question\nlabels: 'question'\n---\n\n## Summary\n\n<!-- What do you need help with? -->\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "chars": 2565,
    "preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
  },
  {
    "path": ".github/workflows/jira-issue-create.yml",
    "chars": 711,
    "preview": "# Creates jira tickets for new github issues to help triage\nname: Jira Issue Creator For Android\n\non:\n  issues:\n    type"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 2916,
    "preview": "name: Release\n\non:\n  workflow_dispatch:\n    inputs:\n      dryRun:\n        description: 'Do a dry run to preview instead "
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 567,
    "preview": "name: Build and Test\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\njobs:\n  test:\n "
  },
  {
    "path": ".gitignore",
    "chars": 360,
    "preview": "# Gradle files\n.gradle/\nbuild/\ndistribution/\n\n# Local configuration file (sdk path, etc)\nlocal.properties\n\n# Android Stu"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 19325,
    "preview": "## [2.40.3](https://github.com/amplitude/Amplitude-Android/compare/v2.40.2...v2.40.3) (2025-04-29)\n\n\n### Bug Fixes\n\n* de"
  },
  {
    "path": "LICENSE",
    "chars": 1076,
    "preview": "MIT License\n\nCopyright (c) 2014 Amplitude Analytics\n\nPermission is hereby granted, free of charge, to any person obtaini"
  },
  {
    "path": "README.md",
    "chars": 3172,
    "preview": "<p align=\"center\">\n  <a href=\"https://amplitude.com\" target=\"_blank\" align=\"center\">\n    <img src=\"https://static.amplit"
  },
  {
    "path": "build.gradle",
    "chars": 7472,
    "preview": "group = ARTIFACT_GROUP\nversion = ARTIFACT_VERSION\n\nbuildscript {\n    repositories {\n        google()\n        jcenter()\n "
  },
  {
    "path": "gradle.properties",
    "chars": 859,
    "preview": "ARTIFACT_VERSION=2.40.3\nARTIFACT_GROUP=com.amplitude\n\nPOM_PACKAGING=aar\nPOM_DESCRIPTION=Amplitude Android SDK\n\nPOM_URL=h"
  },
  {
    "path": "gradlew",
    "chars": 5764,
    "preview": "#!/usr/bin/env sh\n\n#\n# Copyright 2015 the original author or authors.\n#\n# Licensed under the Apache License, Version 2.0"
  },
  {
    "path": "gradlew.bat",
    "chars": 2942,
    "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": "package.json",
    "chars": 282,
    "preview": "{\n  \"private\": true,\n  \"dependencies\": {\n    \"lodash\": \"4.17.21\",\n    \"semantic-release\": \"17.4.7\",\n    \"@semantic-relea"
  },
  {
    "path": "release.config.js",
    "chars": 1350,
    "preview": "module.exports = {\n  \"branches\": [\n    {name: 'beta', prerelease: true},\n    \"main\"\n  ],\n  \"tagFormat\": [\"v${version}\"],"
  },
  {
    "path": "settings.gradle",
    "chars": 33,
    "preview": "rootProject.name = 'android-sdk'\n"
  },
  {
    "path": "src/main/AndroidManifest.xml",
    "chars": 175,
    "preview": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\" package=\"com.amplitude\">\n\n    <uses-permission andr"
  },
  {
    "path": "src/main/java/com/amplitude/api/Amplitude.java",
    "chars": 6430,
    "preview": "package com.amplitude.api;\n\nimport android.content.Context;\n\nimport org.json.JSONObject;\n\nimport java.util.HashMap;\nimpo"
  },
  {
    "path": "src/main/java/com/amplitude/api/AmplitudeCallbacks.java",
    "chars": 1769,
    "preview": "package com.amplitude.api;\n\nimport android.app.Activity;\nimport android.app.Application;\nimport android.os.Bundle;\n\nclas"
  },
  {
    "path": "src/main/java/com/amplitude/api/AmplitudeClient.java",
    "chars": 98368,
    "preview": "package com.amplitude.api;\n\nimport android.app.Activity;\nimport android.app.Application;\nimport android.content.Context;"
  },
  {
    "path": "src/main/java/com/amplitude/api/AmplitudeDeviceIdCallback.java",
    "chars": 118,
    "preview": "package com.amplitude.api;\n\npublic interface AmplitudeDeviceIdCallback {\n    void onDeviceIdReady(String deviceId);\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/api/AmplitudeLog.java",
    "chars": 3542,
    "preview": "package com.amplitude.api;\n\nimport android.util.Log;\n\npublic class AmplitudeLog {\n    private volatile boolean enableLog"
  },
  {
    "path": "src/main/java/com/amplitude/api/AmplitudeLogCallback.java",
    "chars": 116,
    "preview": "package com.amplitude.api;\n\npublic interface AmplitudeLogCallback {\n    void onError(String tag, String message);\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/api/AmplitudeServerZone.java",
    "chars": 2087,
    "preview": "package com.amplitude.api;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * AmplitudeServerZone is for Data Resi"
  },
  {
    "path": "src/main/java/com/amplitude/api/ConfigManager.java",
    "chars": 2082,
    "preview": "package com.amplitude.api;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport java.io.BufferedReader;\ni"
  },
  {
    "path": "src/main/java/com/amplitude/api/Constants.java",
    "chars": 5369,
    "preview": "package com.amplitude.api;\n\nimport com.amplitude.BuildConfig;\n\npublic class Constants {\n\n    public static final String "
  },
  {
    "path": "src/main/java/com/amplitude/api/CursorWindowAllocationException.java",
    "chars": 493,
    "preview": "package com.amplitude.api;\n\n/**\n * This is Amplitude's substitute for android.database.CursorWindowAllocationException.\n"
  },
  {
    "path": "src/main/java/com/amplitude/api/DatabaseHelper.java",
    "chars": 26880,
    "preview": "package com.amplitude.api;\n\nimport android.content.ContentValues;\nimport android.content.Context;\nimport android.databas"
  },
  {
    "path": "src/main/java/com/amplitude/api/DatabaseResetListener.java",
    "chars": 171,
    "preview": "package com.amplitude.api;\n\nimport android.database.sqlite.SQLiteDatabase;\n\npublic interface DatabaseResetListener {\n   "
  },
  {
    "path": "src/main/java/com/amplitude/api/DeviceInfo.java",
    "chars": 16428,
    "preview": "package com.amplitude.api;\n\nimport android.content.ContentResolver;\nimport android.content.Context;\nimport android.conte"
  },
  {
    "path": "src/main/java/com/amplitude/api/Identify.java",
    "chars": 76932,
    "preview": "package com.amplitude.api;\n\nimport org.json.JSONArray;\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimpor"
  },
  {
    "path": "src/main/java/com/amplitude/api/IdentifyInterceptor.java",
    "chars": 7712,
    "preview": "package com.amplitude.api;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport java.util.Iterator;\nimpor"
  },
  {
    "path": "src/main/java/com/amplitude/api/IngestionMetadata.java",
    "chars": 1764,
    "preview": "package com.amplitude.api;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\npublic class IngestionMetadata {"
  },
  {
    "path": "src/main/java/com/amplitude/api/Middleware.java",
    "chars": 118,
    "preview": "package com.amplitude.api;\n\npublic interface Middleware {\n\tvoid run(MiddlewarePayload payload, MiddlewareNext next);\n}"
  },
  {
    "path": "src/main/java/com/amplitude/api/MiddlewareExtended.java",
    "chars": 94,
    "preview": "package com.amplitude.api;\n\ninterface MiddlewareExtended extends Middleware {\n\tvoid flush();\n}"
  },
  {
    "path": "src/main/java/com/amplitude/api/MiddlewareExtra.java",
    "chars": 276,
    "preview": "package com.amplitude.api;\n\nimport java.util.Map;\nimport java.util.HashMap;\n\npublic class MiddlewareExtra extends HashMa"
  },
  {
    "path": "src/main/java/com/amplitude/api/MiddlewareNext.java",
    "chars": 115,
    "preview": "package com.amplitude.api;\n\npublic interface MiddlewareNext {\n    public void run(MiddlewarePayload curPayload);\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/api/MiddlewarePayload.java",
    "chars": 372,
    "preview": "package com.amplitude.api;\n\nimport org.json.JSONObject;\n\npublic class MiddlewarePayload {\n    public JSONObject event;\n "
  },
  {
    "path": "src/main/java/com/amplitude/api/MiddlewareRunner.java",
    "chars": 1756,
    "preview": "package com.amplitude.api;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.ConcurrentLin"
  },
  {
    "path": "src/main/java/com/amplitude/api/PinnedAmplitudeClient.java",
    "chars": 16050,
    "preview": "package com.amplitude.api;\n\nimport android.content.Context;\n\nimport com.amplitude.util.DoubleCheck;\nimport com.amplitude"
  },
  {
    "path": "src/main/java/com/amplitude/api/Plan.java",
    "chars": 2569,
    "preview": "package com.amplitude.api;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\npublic class Plan {\n    private "
  },
  {
    "path": "src/main/java/com/amplitude/api/Revenue.java",
    "chars": 8338,
    "preview": "package com.amplitude.api;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\n/**\n * <h1>Revenue</h1>\n * Reven"
  },
  {
    "path": "src/main/java/com/amplitude/api/TrackingOptions.java",
    "chars": 7646,
    "preview": "package com.amplitude.api;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport java.util.HashSet;\nimport"
  },
  {
    "path": "src/main/java/com/amplitude/api/Utils.java",
    "chars": 4060,
    "preview": "package com.amplitude.api;\n\nimport android.Manifest;\nimport android.app.Activity;\nimport android.content.Context;\nimport"
  },
  {
    "path": "src/main/java/com/amplitude/api/WorkerThread.java",
    "chars": 751,
    "preview": "package com.amplitude.api;\n\nimport android.os.Handler;\nimport android.os.HandlerThread;\nimport android.os.Process;\n\npubl"
  },
  {
    "path": "src/main/java/com/amplitude/eventexplorer/EventExplorer.java",
    "chars": 2689,
    "preview": "package com.amplitude.eventexplorer;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.conten"
  },
  {
    "path": "src/main/java/com/amplitude/eventexplorer/EventExplorerInfoActivity.java",
    "chars": 2852,
    "preview": "package com.amplitude.eventexplorer;\n\nimport android.app.Activity;\nimport android.content.ClipData;\nimport android.conte"
  },
  {
    "path": "src/main/java/com/amplitude/eventexplorer/EventExplorerTouchHandler.java",
    "chars": 2280,
    "preview": "package com.amplitude.eventexplorer;\n\nimport android.content.Intent;\nimport android.view.MotionEvent;\nimport android.vie"
  },
  {
    "path": "src/main/java/com/amplitude/unity/plugins/AmplitudePlugin.java",
    "chars": 32718,
    "preview": "package com.amplitude.unity.plugins;\n\nimport android.app.Application;\nimport android.content.Context;\n\nimport com.amplit"
  },
  {
    "path": "src/main/java/com/amplitude/util/DoubleCheck.java",
    "chars": 2613,
    "preview": "package com.amplitude.util;\n\n\n/**\n * Copy from https://github.com/google/dagger/blob/master/java/dagger/internal/DoubleC"
  },
  {
    "path": "src/main/java/com/amplitude/util/Provider.java",
    "chars": 74,
    "preview": "package com.amplitude.util;\n\npublic interface Provider<T> {\n    T get();\n}"
  },
  {
    "path": "src/main/res/drawable/amp_button_bg.xml",
    "chars": 219,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:sha"
  },
  {
    "path": "src/main/res/layout/amp_activity_eventexplorer_info.xml",
    "chars": 4896,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    andr"
  },
  {
    "path": "src/main/res/layout/amp_bubble_view.xml",
    "chars": 444,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    andro"
  },
  {
    "path": "src/main/res/values/colors.xml",
    "chars": 350,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"amp_transparent\">#00000000</color>\n    <color name=\""
  },
  {
    "path": "src/main/res/values/strings.xml",
    "chars": 367,
    "preview": "<resources>\n    <string name=\"amp_label_copy\">Copy</string>\n    <string name=\"amp_label_copied\">Copied To Clipboard</str"
  },
  {
    "path": "src/test/java/com/amplitude/api/AmplitudeClientTest.java",
    "chars": 122338,
    "preview": "package com.amplitude.api;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.json.JSONArray;\nimport org"
  },
  {
    "path": "src/test/java/com/amplitude/api/AmplitudeServerZoneTest.java",
    "chars": 1852,
    "preview": "package com.amplitude.api;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.junit.runners.Parameteriz"
  },
  {
    "path": "src/test/java/com/amplitude/api/AmplitudeTest.java",
    "chars": 6637,
    "preview": "package com.amplitude.api;\n\nimport android.content.Context;\nimport android.content.SharedPreferences;\n\nimport androidx.t"
  },
  {
    "path": "src/test/java/com/amplitude/api/BaseTest.java",
    "chars": 10812,
    "preview": "package com.amplitude.api;\n\nimport android.content.Context;\nimport android.content.pm.ApplicationInfo;\nimport android.co"
  },
  {
    "path": "src/test/java/com/amplitude/api/ConfigManagerTest.java",
    "chars": 1574,
    "preview": "package com.amplitude.api;\n\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport com.amplitude.api.util.MockHt"
  },
  {
    "path": "src/test/java/com/amplitude/api/DatabaseHelperTest.java",
    "chars": 30433,
    "preview": "package com.amplitude.api;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.json.JSONException;\nimport"
  },
  {
    "path": "src/test/java/com/amplitude/api/DatabaseRecoveryTest.java",
    "chars": 13374,
    "preview": "package com.amplitude.api;\n\nimport android.content.ContentValues;\nimport android.database.sqlite.SQLiteDatabase;\nimport "
  },
  {
    "path": "src/test/java/com/amplitude/api/DeviceInfoTest.java",
    "chars": 17076,
    "preview": "package com.amplitude.api;\n\nimport android.content.ContentResolver;\nimport android.content.Context;\nimport android.conte"
  },
  {
    "path": "src/test/java/com/amplitude/api/IdentifyTest.java",
    "chars": 23799,
    "preview": "package com.amplitude.api;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.json.JSONArray;\nimport org"
  },
  {
    "path": "src/test/java/com/amplitude/api/IngestionMetadataTest.java",
    "chars": 975,
    "preview": "package com.amplitude.api;\n\nimport static org.junit.Assert.assertEquals;\n\nimport androidx.test.ext.junit.runners.Android"
  },
  {
    "path": "src/test/java/com/amplitude/api/InitializeTest.java",
    "chars": 9505,
    "preview": "package com.amplitude.api;\n\nimport android.content.Context;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimpo"
  },
  {
    "path": "src/test/java/com/amplitude/api/MiddlewareRunnerTest.java",
    "chars": 3766,
    "preview": "package com.amplitude.api;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.json.JSONException;\nimport"
  },
  {
    "path": "src/test/java/com/amplitude/api/MockGeocoder.java",
    "chars": 415,
    "preview": "package com.amplitude.api;\n\nimport android.location.Geocoder;\n\nimport org.robolectric.annotation.Implementation;\nimport "
  },
  {
    "path": "src/test/java/com/amplitude/api/PinningTest.java",
    "chars": 6543,
    "preview": "package com.amplitude.api;\n\nimport android.os.SystemClock;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimpor"
  },
  {
    "path": "src/test/java/com/amplitude/api/PlanTest.java",
    "chars": 1162,
    "preview": "package com.amplitude.api;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.json.JSONException;\nimport"
  },
  {
    "path": "src/test/java/com/amplitude/api/RevenueTest.java",
    "chars": 7380,
    "preview": "package com.amplitude.api;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.json.JSONException;\nimport"
  },
  {
    "path": "src/test/java/com/amplitude/api/SessionTest.java",
    "chars": 51764,
    "preview": "package com.amplitude.api;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.json.JSONArray;\nimport org"
  },
  {
    "path": "src/test/java/com/amplitude/api/TrackingOptionsTest.java",
    "chars": 4246,
    "preview": "package com.amplitude.api;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.json.JSONException;\nimport"
  },
  {
    "path": "src/test/java/com/amplitude/api/util/MockHttpURLConnectionHelper.java",
    "chars": 1075,
    "preview": "package com.amplitude.api.util;\n\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport"
  },
  {
    "path": "src/test/java/com/amplitude/api/util/MockURLStreamHandler.java",
    "chars": 1066,
    "preview": "package com.amplitude.api.util;\n\nimport java.io.IOException;\nimport java.net.URL;\nimport java.net.URLConnection;\nimport "
  }
]

About this extraction

This page contains the full source code of the amplitude/Amplitude-Android GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 78 files (691.8 KB), approximately 157.1k tokens, and a symbol index with 1019 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!