[
  {
    "path": ".github/ISSUE_TEMPLATE/Bug_report.md",
    "content": "---\nname: Bug report 🐛\nabout: You're having technical issues\nlabels: 'bug'\n---\n\n<!--- Please fill out the template to the best of your ability -->\n\n## Expected Behavior\n<!--- What should have happened? -->\n\n## Current Behavior\n<!--- What went wrong? -->\n\n## Possible Solution\n<!--- (Not obligatory) Suggest a fix/reason -->\n\n## Steps to Reproduce\n<!--- Please provide a clear sequence of steps to reproduce this bug --> \n<!--- Include code and images, if relevant -->\n1.\n2.\n3.\n4.\n\n## Environment\n- SDK Version: <!-- E.g. v2.28.0 -->\n- Android API Level: <!-- E.g. 8.1.0 -->\n- Device: <!--- E.g. Samsung Galaxy S20-->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/Feature_request.md",
    "content": "---\nname: Feature Request 🚀\nabout: You'd like something added to the SDK\nlabels: 'feature request'\n---\n\n<!--- Please fill out the template to the best of your ability -->\n\n## Summary\n\n<!-- Please describe what feature you would like added -->\n\n## Motivations\n\n<!-- Please explain what value this feature would add. E.g. what problem does it solve -->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/Question.md",
    "content": "---\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",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\n#\n# ******** NOTE ********\n# We have attempted to detect the languages in your repository. Please check\n# the `language` matrix defined below to confirm you have the correct set of\n# supported CodeQL languages.\n#\nname: \"CodeQL\"\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [ main ]\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [ 'java' ]\n        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]\n        # Learn more:\n        # 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\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@b8d3b6e8af63cde30bdc382c0bc28114f4346c88 # v2\n      with:\n        languages: ${{ matrix.language }}\n        # If you wish to specify custom queries, you can do so here or in a config file.\n        # By default, queries listed here will override any specified in a config file.\n        # Prefix the list here with \"+\" to use these queries and those in the config file.\n        # queries: ./path/to/local/query, your-org/your-repo/queries@main\n\n    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).\n    # If this step fails, then you should remove it and run the build manually (see below)\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@b8d3b6e8af63cde30bdc382c0bc28114f4346c88 # v2\n\n    # ℹ️ Command-line programs to run using the OS shell.\n    # 📚 https://git.io/JvXDl\n\n    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines\n    #    and modify them (or add more) to build your code if your project\n    #    uses a compiled language\n\n    #- run: |\n    #   make bootstrap\n    #   make release\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@b8d3b6e8af63cde30bdc382c0bc28114f4346c88 # v2\n"
  },
  {
    "path": ".github/workflows/jira-issue-create.yml",
    "content": "# Creates jira tickets for new github issues to help triage\nname: Jira Issue Creator For Android\n\non:\n  issues:\n    types: [opened]\n  workflow_call:\n    inputs:\n      label:\n        type: string\n\njobs:\n  call-workflow-passing-data:\n    uses: amplitude/Amplitude-TypeScript/.github/workflows/jira-issue-create-template.yml@8dadabbe62161729e3aa83c0d664e106b748c8cc # @amplitude/plugin-session-replay-react-native@0.4.9\n    with:\n      label: \"Android\"\n      subcomponent: \"dx_legacy_android_sdk\"\n    secrets:\n      JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}\n      JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}\n      JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}\n      JIRA_PROJECT: ${{ secrets.JIRA_PROJECT }}\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  workflow_dispatch:\n    inputs:\n      dryRun:\n        description: 'Do a dry run to preview instead of a real release'\n        required: true\n        default: 'true'\n\njobs:\n  authorize:\n    name: Authorize\n    runs-on: ubuntu-latest\n    steps:\n      - name: ${{ github.actor }} permission check to do a release\n        uses: \"lannonbr/repo-permission-check-action@2bb8c89ba8bf115c4bfab344d6a6f442b24c9a1f\" # 2.0.2\n        with:\n          permission: \"write\"\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n  release:\n    name: Release\n    runs-on: ubuntu-latest\n    needs: [authorize]\n    steps:\n      - name: Checkout\n        uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4\n\n      - name: Set up JDK 8\n        uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4\n        with:\n          java-version: '8'\n          distribution: 'zulu'\n\n      - name: Build\n        run: ./gradlew build\n\n      - name: Test\n        run: ./gradlew test --info\n\n      - name: Configure GPG\n        env:\n          GPG_KEY_CONTENTS: ${{ secrets.GPG_KEY_CONTENTS }}\n          SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }}\n        run: |\n          sudo bash -c \"echo '$GPG_KEY_CONTENTS' | base64 -d > '$SIGNING_SECRET_KEY_RING_FILE'\"\n\n      - name: Configure Sonatype\n        env:\n          GRADLE_PROP_FILE: local.properties\n        run: |\n          echo \"sonatypeUsername=${{ secrets.OSSRH_USERNAME }}\" >> $GRADLE_PROP_FILE\n          echo \"sonatypePassword=${{ secrets.OSSRH_PASSWORD }}\" >> $GRADLE_PROP_FILE\n          echo \"sonatypeStagingProfileId=${{ secrets.SONATYPE_STAGING_PROFILE_ID }}\" >> $GRADLE_PROP_FILE\n          echo \"signing.keyId=${{ secrets.SIGNING_KEY_ID }}\" >> $GRADLE_PROP_FILE\n          echo \"signing.password=${{ secrets.SIGNING_PASSWORD }}\" >> $GRADLE_PROP_FILE\n          echo \"signing.secretKeyRingFile=${{ secrets.SIGNING_SECRET_KEY_RING_FILE }}\" >> $GRADLE_PROP_FILE\n\n      - name: Semantic Release --dry-run\n        if: ${{ github.event.inputs.dryRun == 'true'}}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          GIT_AUTHOR_NAME: amplitude-sdk-bot\n          GIT_AUTHOR_EMAIL: amplitude-sdk-bot@users.noreply.github.com\n          GIT_COMMITTER_NAME: amplitude-sdk-bot\n          GIT_COMMITTER_EMAIL: amplitude-sdk-bot@users.noreply.github.com\n        run: |\n          npm ci\n          npm exec semantic-release -- --dry-run\n\n      - name: Semantic Release\n        if: ${{ github.event.inputs.dryRun == 'false'}}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          GIT_AUTHOR_NAME: amplitude-sdk-bot\n          GIT_AUTHOR_EMAIL: amplitude-sdk-bot@users.noreply.github.com\n          GIT_COMMITTER_NAME: amplitude-sdk-bot\n          GIT_COMMITTER_EMAIL: amplitude-sdk-bot@users.noreply.github.com\n        run: |\n          npm ci\n          npm exec semantic-release\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Build and Test\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\njobs:\n  test:\n    name: Build and Test\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4\n\n      - name: Set up JDK 8\n        uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4\n        with:\n          java-version: '8'\n          distribution: 'zulu'\n\n      - name: Build\n        run: ./gradlew build\n\n      - name: Test\n        run: ./gradlew test --info\n"
  },
  {
    "path": ".gitignore",
    "content": "# Gradle files\n.gradle/\nbuild/\ndistribution/\n\n# Local configuration file (sdk path, etc)\nlocal.properties\n\n# Android Studio generated folders\n.navigation/\ncaptures/\n.externalNativeBuild\ncaches/\ndaemon/\nnative/\nwrapper/\n\n# IntelliJ project files\n*.iml\n.idea/\n\n# Java binary files\n*.class\n\n# Misc\n.DS_Store\n*.log\n*.asc\n*.bak\nchanges.txt\nrelease.sh\nnode_modules/\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## [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* default location listening to false ([#407](https://github.com/amplitude/Amplitude-Android/issues/407)) ([27d3758](https://github.com/amplitude/Amplitude-Android/commit/27d3758fd8427d44e15813356ada2e3234ad62b8))\n\n## [2.40.2](https://github.com/amplitude/Amplitude-Android/compare/v2.40.1...v2.40.2) (2024-06-12)\n\n\n### Bug Fixes\n\n* 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))\n\n## [2.40.1](https://github.com/amplitude/Amplitude-Android/compare/v2.40.0...v2.40.1) (2024-04-17)\n\n\n### Bug Fixes\n\n* always run middleware flush on updateServer ([#400](https://github.com/amplitude/Amplitude-Android/issues/400)) ([fbee357](https://github.com/amplitude/Amplitude-Android/commit/fbee357d6ba9eb540101cf09393f7eebafdfd49d))\n\n# [2.40.0](https://github.com/amplitude/Amplitude-Android/compare/v2.39.9...v2.40.0) (2024-04-17)\n\n\n### Features\n\n* middleware session replay integration ([#399](https://github.com/amplitude/Amplitude-Android/issues/399)) ([28bbbe6](https://github.com/amplitude/Amplitude-Android/commit/28bbbe6ad2d0a0fe6424645e6105ecb8c2be7a4f))\n\n## [2.39.9](https://github.com/amplitude/Amplitude-Android/compare/v2.39.8...v2.39.9) (2024-02-27)\n\n\n### Bug Fixes\n\n* 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))\n\n## [2.39.8](https://github.com/amplitude/Amplitude-Android/compare/v2.39.7...v2.39.8) (2023-07-21)\n\n\n### Bug Fixes\n\n* 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))\n\n## [2.39.7](https://github.com/amplitude/Amplitude-Android/compare/v2.39.6...v2.39.7) (2023-07-07)\n\n\n### Bug Fixes\n\n* 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))\n\n## [2.39.6](https://github.com/amplitude/Amplitude-Android/compare/v2.39.5...v2.39.6) (2023-07-06)\n\n\n### Bug Fixes\n\n* remove md5 usage ([#372](https://github.com/amplitude/Amplitude-Android/issues/372)) ([c849590](https://github.com/amplitude/Amplitude-Android/commit/c84959086f9a645f87a3175caaec3022154ee6bb))\n\n## [2.39.5](https://github.com/amplitude/Amplitude-Android/compare/v2.39.4...v2.39.5) (2023-06-13)\n\n\n### Bug Fixes\n\n* 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))\n\n## [2.39.4](https://github.com/amplitude/Amplitude-Android/compare/v2.39.3...v2.39.4) (2023-06-01)\n\n\n### Bug Fixes\n\n* 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))\n\n## [2.39.3](https://github.com/amplitude/Amplitude-Android/compare/v2.39.2...v2.39.3) (2023-04-29)\n\n\n### Bug Fixes\n\n* 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))\n* update identify interceptor to identify only ([#357](https://github.com/amplitude/Amplitude-Android/issues/357)) ([afd3251](https://github.com/amplitude/Amplitude-Android/commit/afd3251f75c30b130a42805e49b31673916cedf1))\n\n### As of September 21, 2020 CHANGELOG.md is no longer manually updated. \nPlease check the [releases page](https://github.com/amplitude/Amplitude-Android/releases) for up to date changes.\n\n## 2.28.2 (Sep 13, 2020)\n* Add `setMinTimeBetweenSessionsMillis` in plugin for Unity Plugin to use.\n\n## 2.28.1 (Aug 26, 2020)\n* Add `setOffline` in plugin for Unity Plugin to use.\n\n## 2.28.0 (Aug 10, 2020)\n\n* Introducing useDynamicConfig flag!! Turning this flag on will find the best server url automatically based on users' geo location.\n* Note 1. If you have your own proxy server and use setServerUrl API, please leave this OFF.\n* Note 2. If you have users in China Mainland, we suggest you turn this on.\n* Note 3. By default, this feature is OFF. So you need to explicitly set it to ON to use it.\n\n## 2.27.0 (Jul 14, 2020)\n\n* Added setServerUrl to `AmplitudePlugin` to enable it for Unity SDK too.\n* Fix an issue during location fetching.\n\n## 2.26.1 (Jun 15, 2020)\n\n* Fix the incorrect behavior of `disableLocationListening`. If you want to disable location listening over LocationManager. Please call called before initialization.\n\n## 2.26.0 (Jun 2, 2020)\n\n* Remove ComodoRSA certificate for SSL pinning.\n\n## 2.25.2 (May 13, 2020)\n\n* Add 3 APIs to `AmplitudePlugin` (`uploadEvents`, `useAdvertisingIdForDeviceId`, `setDeviceId`)\n\n## 2.25.1 (Apr 3, 2020)\n\n* Remove the declaration of location related permissions in manifest file.\n\n## 2.25.0 (Mar 17, 2020)\n\n* 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.\n\n## 2.24.2 (Feb 5, 2020)\n\n* Now you can set auth token! Use `AmplitudeClient#setBearerToken(String token)` please!\n\n## 2.24.1 (Jan 28, 2020)\n\n* Fix the issue that `version` property shows old version.\n\n## 2.24.0 (Jan 28, 2020)\n\n* 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()`\n\n## 2.23.2 (Aug 05, 2019)\n\n* Catch exceptions when fetching most recent location.\n\n## 2.23.1 (Jul 19, 2019)\n\n* Handle SQLite database crashes caused by fetching events that exceed 2MB (max size of cursor window).\n\n## 2.23.0 (Apr 22, 2019)\n\n* Make `startNewSessionIfNeeded` a public method. Only call this if you know what you are doing. This may trigger a new session to start.\n\n## 2.22.1 (Mar 21, 2019)\n\n* Store deviceId in SharedPreferences as backup in case SQLite database fails or becomes corrupted.\n\n## 2.22.0 (Jan 18, 2019)\n\n* Add ability to set a custom server URL for uploading events using `setServerUrl`.\n\n## 2.21.0 (Dec 05, 2018)\n\n* Update SDK to better handle when the SQLite database file gets corrupted between interactions.\n* Add optional diagnostic logging that tracks exceptions thrown in the SDK and sends to Amplitude.\n\n## 2.20.0 (Oct 15, 2018)\n\n* 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.\n\n## 2.19.1 (Aug 14, 2018)\n\n* Update SDK to better handle SQLite Exceptions.\n\n## 2.19.0 (Jul 24, 2018)\n\n* 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.\n\n## 2.18.2 (Jul 24, 2018)\n\n* Use randomly generated device id if user has limitAdTracking enabled.\n\n## 2.18.1 (May 07, 2018)\n\n* Updating to [OkHttp 3.10.0](https://github.com/square/okhttp/blob/master/CHANGELOG.md#version-3100)\n* 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.\n\n## 2.18.0 (Apr 19, 2018)\n\n* Added a `setUserId` method with optional boolean argument `startNewSession`, which when `true` starts a new session after changing the userId.\n\n## 2.17.0 (Feb 05, 2018)\n\n* 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`.\n\n## 2.16.0 (Nov 27, 2017)\n\n* Expose a public `getUserPropertiesOperations` method on the `Identify` class.\n* Handle exceptions when the LocationManager is not available for fetching location.\n\n## 2.15.0 (Oct 04, 2017)\n\n* Updating to latest version of OkHttp3 ([3.9.0](https://github.com/square/okhttp/blob/master/CHANGELOG.md#version-390))\n\n## 2.14.1 (Jul 27, 2017)\n\n* Switch to an internal implementation of `isEmptyString` instead of Android TextUtils.\n\n## 2.14.0 (Jul 05, 2017)\n\n* 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.\n\n## 2.13.4 (May 09, 2017)\n\n* Handle exceptions when fetching device carrier information. Thanks to @fkam-tt for the pull request.\n* Copy userProperties on main thread in `setUserProperties` to prevent ConcurrentModificationExceptions.\n* Migrating setup instructions and SDK documentation in the README file to Zendesk articles.\n\n## 2.13.3 (Mar 13, 2017)\n\n* Handle exceptions when reading from database. Only affects certain Fairphone and LG devices.\n* Handle exceptions when building request to upload event data. Only affects certain Lenovo devices.\n\n## 2.13.2 (Dec 22, 2016)\n\n* Fix crash when pulling null unsent event strings during upload.\n* Fix bug where unserializable events were being saved to unsent events table.\n* Added more logging around JSON serialization errors when logging events.\n\n## 2.13.1 (Dec 15, 2016)\n\n* Fix bug where `regenerateDeviceId` was not being run on background thread. DeviceInfo.generateUUID() should be a static method.\n\n## 2.13.0 (Dec 05, 2016)\n\n* 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.\n\n## 2.12.0 (Nov 07, 2016)\n\n* 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.\n\n## 2.11.0 (Oct 26, 2016)\n\n* 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.\n\n## 2.10.0 (Oct 12, 2016)\n\n* 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.\n* 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.\n* Handle IllegalArgumentException thrown by Android Geocoder for bad lat / lon values.\n\n## 2.9.2 (Jul 14, 2016)\n\n* Fix bug where `enableLocationListening` and `disableLocationListening` were not being run on background thread. Thanks to @elevenfive for PR.\n* Update `Revenue` class to expose public `equals` and `hashCode` methods.\n\n## 2.9.1 (Jul 11, 2016)\n\n* Fix bug where `setOptOut` was not being run on background thread.\n* `productId` is no longer a required field for `Revenue` logged via `logRevenueV2`.\n* Fix bug where receipt and receiptSignature were being truncated if they were too long (exceeded 1024 characters).\n\n## 2.9.0 (Jul 07, 2016)\n\n* 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);`\n\n## 2.8.0 (Jun 29, 2016)\n\n* Run the `initialize` logic on the background thread so that the SQLite database operations do not delay the main thread.\n* Add support for Amazon Advertising ID (use in place of Google Advertising ID on Amazon devices). Thanks to @jcomo for the pull request.\n\n## 2.7.2 (May 24, 2016)\n\n* 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.\n* Fix bug where fetching the user's location on select devices throws a SecurityException, causing a crash.\n\n## 2.7.1 (Apr 19, 2016)\n\n* RevenueProperties is a confusing name and should actually be eventProperties. Deprecating Revenue.setRevenueProperties and replacing it with Revenue.setEventProperties, and clarified in Readme.\n\n## 2.7.0 (Apr 19, 2016)\n\n* Add support setting groups for users and events. See [Readme](https://github.com/amplitude/Amplitude-Android#setting-groups) for more information.\n* Add helper method `getSessionId` to expose the current sessionId value.\n* 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.\n* Fix crash when trying to enableForegroundTracking with the PinnedAmplitudeClient. AmplitudeClient methods should be using `this` instead of static `instance` variable.\n\n## 2.6.0 (Mar 29, 2016)\n\n* Update to OKHttp v3.0.1.\n* Add support for prepend user property operation.\n* Fix bug where merging events for upload causes array index out of bounds exception.\n* Migrate shared preferences (userId and event meta data) to Sqlite db to support apps with multiple processes.\n\n## 2.5.1 (Mar 14, 2016)\n\n* Fix bug where updateServer sets the wrong batchLimit when limit is false.\n\n## 2.5.0 (Jan 15, 2016)\n\n* Add ability to clear all user properties.\n* Check that SDK is initialized when user calls enableForegroundTracking, identify, setUserProperties.\n\n## 2.4.0 (Dec 15, 2015)\n\n* Add support for append user property operation.\n\n## 2.3.0 (Nov 30, 2015)\n\n* Log if Google Play Services is enabled for the application.\n\n## 2.2.0 (Oct 20, 2015)\n\n* Removed all references to Apache HTTPClient to support Android M.\n* Handle exceptions when fetching last known location from LocationManager.\n* Add ability to set custom deviceId.\n* Handle exception when cloning JSON object.\n* Maintain only one instance of OKHttpClient.\n* Add AmplitudeLog helper class that supports enabling and disabling of logging as well as setting of the log level.\n* Fix bug where event and identify queues are not truncated if eventMaxCount is less than eventRemoveBatchSize.\n\n## 2.1.0 (Oct 04, 2015)\n\n* Add support for user properties operations (set, setOnce, add, unset).\n* Fix bug where end session event was not being sent upon app reopen.\n\n## 2.0.4 (Sep 23, 2015)\n\n* Fix bug where deviceInfo was trying to use Geocoder if none present.\n\n## 2.0.3 (Sep 22, 2015)\n\n* Fix bug where deviceId was being fetched on main thread.\n\n## 2.0.2 (Aug 24, 2015)\n\n* Fix Maven jar, fixed build file.\n\n## 2.0.1 (Aug 21, 2015)\n\n* Catch all exceptions thrown by Android TelephonyManager and NullPointerExceptions thrown by geocoder during country lookup.\n\n## 2.0.0 (Aug 20, 2015)\n\n* Expose user ID with getUserId.\n* 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.\n* The minimum supported API level is 9. API level 14 is required for foreground tracking.\n* Always track Android advertising ID (ADID) regardless of limit ad tracking enabled.\n* Track if limit ad tracking enabled as an API property for each logged event.\n* Database upgraded to version 2: added a new store table for key value pairs.\n* Device ID is now saved to and reloaded from the SQLite database (instead of SharedPrefs because SharedPrefs currently does not support multiple processes).\n* MessageDigest.getInstance(String) is not threadsafe (known Android issue). Replaced with alternate MD5 implementation from http://org.rodage.com/pub/java/security/MD5.java.\n* Create a copy of input userProperties JSONObject in setUserProperties to try and prevent ConcurrentModificationException.\n\n## 1.7.0 (May 29, 2015)\n\n* Enable configuration of eventUploadThreshold, eventMaxCount,\n  eventUploadMaxBatchSize, eventUploadPeriodSeconds, minTimeBetweenSessionsMillis,\n  and sessionTimeoutMillis.\n\n## 1.6.3 (May 06, 2015)\n\n* Add offline mode to turn off server uploading for a time.\n* Add synchronous logging. Logs events to the DB synchronously to guarantee event persistence.\n\n## 1.6.2 (Apr 17, 2015)\n\n* Change protection on AmplitudeClient to public.\n\n## 1.6.1 (Apr 13, 2015)\n\n* Fix double class inclusion in jar distribution\n\n## 1.6.0 (Apr 08, 2015)\n\n* Fix crash under aggressive proguard optimizations.\n* Fix device id being lost occasionally on app update.\n* Fix exception when calling logEvent with empty JSONObject.\n* Log a DEBUG message on each event.\n\n## 1.5.0 (Mar 24, 2015)\n\n* Add PinnedAmplitudeClient to support SSL pinning.\n* Deprecate static methods on Amplitude. Switch to using Amplitude.getInstance().\n* Upgrade HTTP client to okhttp.\n\n## 1.4.6 (Mar 16, 2015)\n\n* Fix bug when initializing with user id. Api key was not set properly.\n\n## 1.4.4 (Mar 11, 2015)\n\n* Expose setUserProperties(JSONObject, boolean) as a static\n* Handle null edge cases in location request\n* Add user opt out support\n* Merge user properties in setUserProperties by default\n* Refactor Amplitude to be a singleton to support tests\n* Add option to disable fine-grained location tracking\n* Fix crash: ConcurrentModificationException in HashMap\n* Fix crash: CursorWindowAllocationException in SQLite\n\n## 1.4.3 (Nov 13, 2014)\n\n* Update field names, split platform and os, and send library information\n\n## 1.4.2 (Nov 7, 2014)\n\n* Don't log end session event if session isn't open\n* Fix creating a new session id when the previous session id is invalid or non existant\n\n## 1.4.1 (Jul 16, 2014)\n\n* Hotfix extra class file in jar.\n\n## 1.4.0 (Jul 1, 2014)\n\n* Send androidADID with events\n* Use Google Play Advertising ID instead of Android ID, if set. Default / fall back on using a random UUID\n* Pull country from reverse geocode, then telephony network country, then locale\n\n## 1.3.0 (Jun 4, 2014)\n\n* Add getDeviceId to unity plugin\n* Add additional logRevenue methods for receipt validation\n* Make device ID public\n* Fix bug where first event was getting skipped from upload\n* Catch SQLiteExceptions\n* Catch exceptions through by Apache HTTPClient\n\n## 1.0.0 (May 1, 2014)\n\n* Initial packaged release\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2014 Amplitude Analytics\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <a href=\"https://amplitude.com\" target=\"_blank\" align=\"center\">\n    <img src=\"https://static.amplitude.com/lightning/46c85bfd91905de8047f1ee65c7c93d6fa9ee6ea/static/media/amplitude-logo-with-text.4fb9e463.svg\" width=\"280\">\n  </a>\n  <br />\n</p>\n\n<div align=\"center\">\n\n[![Legacy SDK](https://img.shields.io/badge/state-legacy-yellow)](https://github.com/amplitude/Amplitude-Kotlin)\n[![Maven Central](https://img.shields.io/maven-central/v/com.amplitude/android-sdk?versionPrefix=2)](https://mvnrepository.com/artifact/com.amplitude/android-sdk/latest)\n\n</div>\n\n# Announcement 📣\n\nAmplitude 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.\n\nTo learn more about the new SDK, here are some useful links:\n\n* Maven Central: https://search.maven.org/artifact/com.amplitude/analytics-android\n* GitHub: https://github.com/amplitude/Amplitude-Kotlin\n* Documentation: https://www.docs.developers.amplitude.com/data/sdks/android-kotlin\n\n# Official Amplitude Android SDK\n\n##### _February 17, 2023_ - [v2.39.2](https://github.com/amplitude/Amplitude-Android/releases/tag/v2.39.2)\n\n## Amplitude and Ampli SDK\n[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).\n\n## Installation and Quick Start\nPlease visit our :100:[Developer Center](https://www.docs.developers.amplitude.com/data/sdks/android/) for instructions on installing and using our the SDK.\n\n## Javadoc\nSee our [Android SDK Reference](http://amplitude.github.io/Amplitude-Android/) for a list and description of all available SDK methods.\n\n## Demo Applications\n* A [demo application](https://github.com/amplitude/Android-Demo) showing the integration of our SDK using Gradle.\n* A [demo application](https://github.com/amplitude/Segment-Android-Demo) showing the integration of our SDK using [Segment's](https://segment.com) Android SDK.\n* A [demo application](https://github.com/amplitude/GTM-Android-Demo) demonstrating a potential integration with Google Tag Manager.\n\n## Changelog\nClick [here](https://github.com/amplitude/Amplitude-Android/wiki/Changelog) to view the Android SDK Changelog.\n\n## Need Help?\nIf 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).\n"
  },
  {
    "path": "build.gradle",
    "content": "group = ARTIFACT_GROUP\nversion = ARTIFACT_VERSION\n\nbuildscript {\n    repositories {\n        google()\n        jcenter()\n    }\n\n    dependencies {\n        classpath 'com.android.tools.build:gradle:3.5.2'\n    }\n}\n\napply plugin: 'com.android.library'\napply plugin: 'maven'\napply plugin: 'signing'\n\next {\n    artifactId = 'amplitude-android-sdk'\n}\n\nrepositories {\n    // The order in which you list these repositories matter.\n    google()\n    jcenter()\n}\n\nandroid {\n    compileSdkVersion 28\n    buildToolsVersion '28.0.3'\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    defaultConfig {\n        minSdkVersion 14\n        // Note: Can't target to the latest 29 now, since running Robolectric tests on 29 will\n        // require Java 9 above. However, Android Studio will error out when setting up Java to 9\n        // above.\n        targetSdkVersion 28\n\n        buildConfigField \"String\", \"AMPLITUDE_VERSION\", \"\\\"${version}\\\"\"\n\n        testInstrumentationRunner 'androidx.test.ext.junit.runners.AndroidJUnit4'\n\n        // The following argument makes the Android Test Orchestrator run its\n        // \"pm clear\" command after each test invocation. This command ensures\n        // that the app's state is completely cleared between tests.\n        testInstrumentationRunnerArguments clearPackageData: 'true'\n    }\n\n    lintOptions {\n        abortOnError true\n        textReport true\n        warningsAsErrors false\n    }\n\n    testOptions {\n        unitTests.includeAndroidResources = true\n    }\n}\n\ndependencies {\n    implementation 'com.amplitude:analytics-connector:1.0.0'\n    implementation 'com.squareup.okhttp3:okhttp:4.2.2'\n    testImplementation 'com.squareup.okhttp3:mockwebserver:4.2.2'\n    testImplementation 'org.robolectric:robolectric:4.3.1'\n    testImplementation 'org.robolectric:shadows-maps:3.4-rc2'\n    testImplementation 'org.powermock:powermock-module-junit4:1.6.6'\n    testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.6'\n    testImplementation 'org.powermock:powermock-api-mockito:1.6.6'\n    testImplementation 'org.powermock:powermock-classloading-xstream:1.6.6'\n    testImplementation 'com.google.android:support-v4:r6'\n    testImplementation 'com.google.android.gms:play-services-ads:18.3.0'\n    testImplementation 'com.google.android.gms:play-services-base:17.1.0'\n    testImplementation 'org.json:json:20140107'\n\n    testImplementation \"junit:junit:4.12\"\n\n    // Core library\n    testImplementation 'androidx.test:core:1.2.0'\n\n    // AndroidJUnitRunner and JUnit Rules\n    testImplementation 'androidx.test:runner:1.2.0'\n    testImplementation 'androidx.test:rules:1.2.0'\n\n    // Assertions\n    testImplementation 'androidx.test.ext:junit:1.1.1'\n}\n\nFile secretPropsFile = project.rootProject.file('local.properties')\nif (secretPropsFile.exists()) {\n    // Read local.properties file first if it exists\n    Properties p = new Properties()\n    new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) }\n    p.each { name, value -> ext[name] = value }\n\n}\n\n// ======== For SDK Releases ========\nuploadArchives {\n    repositories.mavenDeployer {\n        beforeDeployment {\n            MavenDeployment deployment -> signing.signPom(deployment)\n        }\n\n        pom.groupId = ARTIFACT_GROUP\n        pom.version = ARTIFACT_VERSION\n\n        pom.project {\n            name project.name\n            version ARTIFACT_VERSION\n            packaging POM_PACKAGING\n            description POM_DESCRIPTION\n            url POM_URL\n\n            scm {\n                url POM_SCM_URL\n                connection POM_SCM_CONNECTION\n                developerConnection POM_SCM_DEV_CONNECTION\n            }\n\n            licenses {\n                license {\n                    name POM_LICENCE_NAME\n                    url POM_LICENCE_URL\n                    distribution POM_LICENCE_DIST\n                }\n            }\n            developers {\n                developer {\n                    id POM_DEVELOPER_ID\n                    name POM_DEVELOPER_NAME\n                    email POM_DEVELOPER_EMAIL\n                    organization POM_DEVELOPER_ORG\n                    organizationUrl POM_DEVELOPER_ORG_URL\n                }\n            }\n        }\n\n        pom.whenConfigured { pom ->\n            pom.dependencies*.optional = true\n            pom.dependencies.find { dep ->\n                dep.groupId == 'com.amplitude' && dep.artifactId == 'analytics-connector'\n            }.optional = false\n        }\n\n        repository(url: RELEASE_REPOSITORY_URL) {\n            authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())\n        }\n        snapshotRepository(url: SNAPSHOT_REPOSITORY_URL) {\n            authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())\n        }\n    }\n}\n\ntask install(type: Upload, dependsOn: assemble) {\n    repositories.mavenInstaller {\n        configuration = configurations.archives\n\n        pom.groupId = ARTIFACT_GROUP\n        pom.version = ARTIFACT_VERSION\n\n        pom.project {\n            name project.name\n            version ARTIFACT_VERSION\n            packaging POM_PACKAGING\n            description POM_DESCRIPTION\n            url POM_URL\n\n            scm {\n                url POM_SCM_URL\n                connection POM_SCM_CONNECTION\n                developerConnection POM_SCM_DEV_CONNECTION\n            }\n\n            licenses {\n                license {\n                    name POM_LICENCE_NAME\n                    url POM_LICENCE_URL\n                    distribution POM_LICENCE_DIST\n                }\n            }\n            developers {\n                developer {\n                    id POM_DEVELOPER_ID\n                    name POM_DEVELOPER_NAME\n                    email POM_DEVELOPER_EMAIL\n                    organization POM_DEVELOPER_ORG\n                    organizationUrl POM_DEVELOPER_ORG_URL\n                }\n            }\n        }\n    }\n}\n\ntask androidJavadocs(type: Javadoc, dependsOn: ':generateReleaseBuildConfig') {\n    source = android.sourceSets.main.java.srcDirs\n    classpath += project.files(android.getBootClasspath().join(File.pathSeparator))\n    classpath += project.files('build/generated/source/buildConfig/release')\n\n    exclude(\n            '**/R.*',\n            '**/security/**',\n            '**/unity/**',\n            '**/api/AmplitudeLog.java',\n            '**/api/Constants.java',\n            '**/api/DeviceInfo.java',\n            '**/api/Utils.java',\n            '**/api/WorkerThread.java',\n            '**/api/CursorWindowAllocationException.java'\n    )\n    options {\n        encoding = 'UTF-8'\n        docEncoding = 'UTF-8'\n        charSet = 'UTF-8'\n    }\n    failOnError false\n}\n\ntask androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) {\n    classifier = 'javadoc'\n    from androidJavadocs.destinationDir\n}\n\ntask androidSourcesJar(type: Jar) {\n    classifier = 'sources'\n    from android.sourceSets.main.java.srcDirs\n}\n\nsigning {\n    required { isReleaseBuild() && gradle.taskGraph.hasTask(\":uploadArchives\") }\n    sign configurations.archives\n}\n\nartifacts {\n    archives androidSourcesJar\n    archives androidJavadocsJar\n}\n\ndef isReleaseBuild() {\n    return version.contains(\"SNAPSHOT\") == false\n}\n\ndef getRepositoryUsername() {\n    return hasProperty('sonatypeUsername') ? sonatypeUsername : \"\"\n}\n\ndef getRepositoryPassword() {\n    return hasProperty('sonatypePassword') ? sonatypePassword : \"\"\n}\n"
  },
  {
    "path": "gradle.properties",
    "content": "ARTIFACT_VERSION=2.40.3\nARTIFACT_GROUP=com.amplitude\n\nPOM_PACKAGING=aar\nPOM_DESCRIPTION=Amplitude Android SDK\n\nPOM_URL=https://github.com/amplitude/Amplitude-Android\nPOM_SCM_URL=https://github.com/amplitude/Amplitude-Android\nPOM_SCM_CONNECTION=scm:git:http://github.com/amplitude/Amplitude-Android\nPOM_SCM_DEV_CONNECTION=scm:git:git@github.com:amplitude/Amplitude-Android.git\nPOM_LICENCE_NAME=The MIT License\nPOM_LICENCE_URL=http://www.opensource.org/licenses/mit-license.php\nPOM_LICENCE_DIST=repo\nPOM_DEVELOPER_ID=amplitude_sdk_dev\nPOM_DEVELOPER_NAME=Amplitude SDK Developers\nPOM_DEVELOPER_EMAIL=sdk.dev@amplitude.com\nPOM_DEVELOPER_ORG=Amplitude\nPOM_DEVELOPER_ORG_URL=https://amplitude.com/\n\nRELEASE_REPOSITORY_URL=https://oss.sonatype.org/service/local/staging/deploy/maven2/\nSNAPSHOT_REPOSITORY_URL=https://oss.sonatype.org/content/repositories/snapshots/\n"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env sh\n\n#\n# Copyright 2015 the original author or authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn () {\n    echo \"$*\"\n}\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif [ \"$cygwin\" = \"true\" -o \"$msys\" = \"true\" ] ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=`expr $i + 1`\n    done\n    case $i in\n        0) set -- ;;\n        1) set -- \"$args0\" ;;\n        2) set -- \"$args0\" \"$args1\" ;;\n        3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Escape application args\nsave () {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=`save \"$@\"`\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@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 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n\r\n@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif \"%ERRORLEVEL%\" == \"0\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:init\r\n@rem Get command-line arguments, handling Windows variants\r\n\r\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\r\n\r\n:win9xME_args\r\n@rem Slurp the command line arguments.\r\nset CMD_LINE_ARGS=\r\nset _SKIP=2\r\n\r\n:win9xME_args_slurp\r\nif \"x%~1\" == \"x\" goto execute\r\n\r\nset CMD_LINE_ARGS=%*\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\r\nexit /b 1\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"private\": true,\n  \"dependencies\": {\n    \"lodash\": \"4.17.21\",\n    \"semantic-release\": \"17.4.7\",\n    \"@semantic-release/changelog\": \"5.0.1\",\n    \"@semantic-release/git\": \"9.0.1\",\n    \"@google/semantic-release-replace-plugin\": \"1.2.0\",\n    \"@semantic-release/exec\": \"5.0.0\"\n  }\n}\n"
  },
  {
    "path": "release.config.js",
    "content": "module.exports = {\n  \"branches\": [\n    {name: 'beta', prerelease: true},\n    \"main\"\n  ],\n  \"tagFormat\": [\"v${version}\"],\n  \"plugins\": [\n    [\"@semantic-release/commit-analyzer\", {\n      \"preset\": \"angular\",\n      \"parserOpts\": {\n        \"noteKeywords\": [\"BREAKING CHANGE\", \"BREAKING CHANGES\", \"BREAKING\"]\n      }\n    }],\n    [\"@semantic-release/release-notes-generator\", {\n      \"preset\": \"angular\",\n    }],\n    [\"@semantic-release/changelog\", {\n      \"changelogFile\": \"CHANGELOG.md\"\n    }],\n    \"@semantic-release/github\",\n    [\n      \"@google/semantic-release-replace-plugin\",\n      {\n        \"replacements\": [\n          {\n            \"files\": [\"gradle.properties\"],\n            \"from\": \"ARTIFACT_VERSION=.*\",\n            \"to\": \"ARTIFACT_VERSION=${nextRelease.version}\",\n            \"results\": [\n              {\n                \"file\": \"gradle.properties\",\n                \"hasChanged\": true,\n                \"numMatches\": 1,\n                \"numReplacements\": 1\n              }\n            ],\n            \"countMatches\": true\n          },\n        ]\n      }\n    ],\n    [\"@semantic-release/git\", {\n      \"assets\": [\"gradle.properties\", \"CHANGELOG.md\"],\n      \"message\": \"chore(release): ${nextRelease.version} [skip ci]\\n\\n${nextRelease.notes}\"\n    }],\n    [\"@semantic-release/exec\", {\n      \"publishCmd\": \"./gradlew uploadArchives\",\n    }],\n  ],\n}\n"
  },
  {
    "path": "settings.gradle",
    "content": "rootProject.name = 'android-sdk'\n"
  },
  {
    "path": "src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\" package=\"com.amplitude\">\n\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n\n</manifest>\n"
  },
  {
    "path": "src/main/java/com/amplitude/api/Amplitude.java",
    "content": "package com.amplitude.api;\n\nimport android.content.Context;\n\nimport org.json.JSONObject;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n\n/**\n * <h1>Amplitude</h1>\n * This is the main Amplitude class that manages SDK instances. <br><br>\n * <b>NOTE:</b> All of the methods except {@code getInstance()} have been deprecated.\n * Please call those methods on the AmplitudeClient instance instead, for example:\n * {@code Amplitude.getInstance().logEvent();}\n *\n * @see com.amplitude.api.AmplitudeClient AmplitudeClient\n */\npublic class Amplitude {\n\n    static final Map<String, AmplitudeClient> instances = new HashMap<String, AmplitudeClient>();\n\n    /**\n     * Gets the default instance.\n     *\n     * @return the default instance\n     */\n    public static AmplitudeClient getInstance() {\n        return getInstance(null);\n    }\n\n    /**\n     * Gets the specified instance. If instance is null or empty string, fetches the default\n     * instance instead.\n     *\n     * @param instance name to get \"ex app 1\"\n     * @return the specified instance\n     */\n    public static synchronized AmplitudeClient getInstance(String instance) {\n        instance = Utils.normalizeInstanceName(instance);\n        AmplitudeClient client = instances.get(instance);\n        if (client == null) {\n            client = new AmplitudeClient(instance);\n            instances.put(instance, client);\n        }\n        return client;\n    }\n\n    /**\n     * Initialize the SDK with the Android app context and Amplitude API key.\n     * Initializing is required before calling other methods such as {@code logEvent();}.\n     *\n     * @param context the context\n     * @param apiKey  the api key\n     */\n    @Deprecated\n    public static void initialize(Context context, String apiKey) {\n        getInstance().initialize(context, apiKey);\n    }\n\n    /**\n     * Initialize the SDK with the Android app context, Amplitude API key, and a user Id.\n     * Initializing is required before calling other methods such as {@code logEvent();}.\n     *\n     * @param context the context\n     * @param apiKey  the api key\n     * @param userId  the user id\n     */\n    @Deprecated\n    public static void initialize(Context context, String apiKey, String userId) {\n        getInstance().initialize(context, apiKey, userId);\n    }\n\n    /**\n     * Enable new device id per install.\n     *\n     * @param newDeviceIdPerInstall the new device id per install\n     */\n    @Deprecated\n    public static void enableNewDeviceIdPerInstall(boolean newDeviceIdPerInstall) {\n        getInstance().enableNewDeviceIdPerInstall(newDeviceIdPerInstall);\n    }\n\n    /**\n     * Use advertising id for device id.\n     */\n    @Deprecated\n    public static void useAdvertisingIdForDeviceId() {\n        getInstance().useAdvertisingIdForDeviceId();\n    }\n\n    /**\n     * Enable location listening.\n     */\n    @Deprecated\n    public static void enableLocationListening() {\n        getInstance().enableLocationListening();\n    }\n\n    /**\n     * Disable location listening.\n     */\n    @Deprecated\n    public static void disableLocationListening() {\n        getInstance().disableLocationListening();\n    }\n\n    /**\n     * Sets session timeout millis.\n     *\n     * @param sessionTimeoutMillis the session timeout millis\n     */\n    @Deprecated\n    public static void setSessionTimeoutMillis(long sessionTimeoutMillis) {\n        getInstance().setSessionTimeoutMillis(sessionTimeoutMillis);\n    }\n\n    /**\n     * Sets opt out.\n     *\n     * @param optOut the opt out\n     */\n    @Deprecated\n    public static void setOptOut(boolean optOut) {\n        getInstance().setOptOut(optOut);\n    }\n\n    /**\n     * Log event.\n     *\n     * @param eventType the event type\n     */\n    @Deprecated\n    public static void logEvent(String eventType) {\n        getInstance().logEvent(eventType);\n    }\n\n    /**\n     * Log event.\n     *\n     * @param eventType       the event type\n     * @param eventProperties the event properties\n     */\n    @Deprecated\n    public static void logEvent(String eventType, JSONObject eventProperties) {\n        getInstance().logEvent(eventType, eventProperties);\n    }\n\n    /**\n     * Upload events.\n     */\n    @Deprecated\n    public static void uploadEvents() {\n        getInstance().uploadEvents();\n    }\n\n    /**\n     * Start session.\n     */\n    @Deprecated\n    public static void startSession() { return; }\n\n    /**\n     * End session.\n     */\n    @Deprecated\n    public static void endSession() { return; }\n\n    /**\n     * Log revenue.\n     *\n     * @param amount the amount\n     */\n    @Deprecated\n    public static void logRevenue(double amount) {\n        getInstance().logRevenue(amount);\n    }\n\n    /**\n     * Log revenue.\n     *\n     * @param productId the product id\n     * @param quantity  the quantity\n     * @param price     the price\n     */\n    @Deprecated\n    public static void logRevenue(String productId, int quantity, double price) {\n        getInstance().logRevenue(productId, quantity, price);\n    }\n\n    /**\n     * Log revenue.\n     *\n     * @param productId        the product id\n     * @param quantity         the quantity\n     * @param price            the price\n     * @param receipt          the receipt\n     * @param receiptSignature the receipt signature\n     */\n    @Deprecated\n    public static void logRevenue(String productId, int quantity, double price, String receipt,\n            String receiptSignature) {\n        getInstance().logRevenue(productId, quantity, price, receipt, receiptSignature);\n    }\n\n    /**\n     * Sets user properties.\n     *\n     * @param userProperties the user properties\n     */\n    @Deprecated\n    public static void setUserProperties(JSONObject userProperties) {\n        getInstance().setUserProperties(userProperties);\n    }\n\n    /**\n     * Sets user properties.\n     *\n     * @param userProperties the user properties\n     * @param replace        the replace\n     */\n    @Deprecated\n    public static void setUserProperties(JSONObject userProperties, boolean replace) {\n        getInstance().setUserProperties(userProperties, replace);\n    }\n\n    /**\n     * Sets user id.\n     *\n     * @param userId the user id\n     */\n    @Deprecated\n    public static void setUserId(String userId) {\n        getInstance().setUserId(userId);\n    }\n\n    /**\n     * Gets device id.\n     *\n     * @return the device id\n     */\n    @Deprecated\n    public static String getDeviceId() {\n        return getInstance().getDeviceId();\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/api/AmplitudeCallbacks.java",
    "content": "package com.amplitude.api;\n\nimport android.app.Activity;\nimport android.app.Application;\nimport android.os.Bundle;\n\nclass AmplitudeCallbacks implements Application.ActivityLifecycleCallbacks {\n\n    private static final String TAG = AmplitudeCallbacks.class.getName();\n    private static final String NULLMSG = \"Need to initialize AmplitudeCallbacks with AmplitudeClient instance\";\n\n    private AmplitudeClient clientInstance = null;\n    private static AmplitudeLog logger = AmplitudeLog.getLogger();\n\n    public AmplitudeCallbacks(AmplitudeClient clientInstance) {\n        if (clientInstance == null) {\n            logger.e(TAG, NULLMSG);\n            return;\n        }\n\n        this.clientInstance = clientInstance;\n        clientInstance.useForegroundTracking();\n    }\n\n    @Override\n    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {}\n\n    @Override\n    public void onActivityDestroyed(Activity activity) {}\n\n    @Override\n    public void onActivityPaused(Activity activity) {\n        if (clientInstance == null) {\n            logger.e(TAG, NULLMSG);\n            return;\n        }\n\n        clientInstance.onExitForeground(getCurrentTimeMillis());\n    }\n\n    @Override\n    public void onActivityResumed(Activity activity) {\n        if (clientInstance == null) {\n            logger.e(TAG, NULLMSG);\n            return;\n        }\n\n        clientInstance.onEnterForeground(getCurrentTimeMillis());\n    }\n\n    @Override\n    public void onActivitySaveInstanceState(Activity activity, Bundle outstate) {}\n\n    @Override\n    public void onActivityStarted(Activity activity) {}\n\n    @Override\n    public void onActivityStopped(Activity activity) {}\n\n    protected long getCurrentTimeMillis() {\n        return System.currentTimeMillis();\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/api/AmplitudeClient.java",
    "content": "package com.amplitude.api;\n\nimport android.app.Activity;\nimport android.app.Application;\nimport android.content.Context;\nimport android.database.sqlite.SQLiteDatabase;\nimport android.location.Location;\nimport android.os.Build;\nimport android.util.Pair;\n\n\nimport com.amplitude.analytics.connector.AnalyticsConnector;\nimport com.amplitude.analytics.connector.Identity;\nimport com.amplitude.analytics.connector.util.JSONUtil;\nimport com.amplitude.eventexplorer.EventExplorer;\nimport com.amplitude.util.DoubleCheck;\nimport com.amplitude.util.Provider;\n\nimport org.json.JSONArray;\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport java.io.IOException;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.UUID;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\nimport kotlin.Unit;\nimport okhttp3.Call;\nimport okhttp3.FormBody;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request;\nimport okhttp3.Response;\n\n/**\n * <h1>AmplitudeClient</h1>\n * This is the SDK instance class that contains all of the SDK functionality.<br><br>\n * <b>Note:</b> call the methods on the default shared instance in the Amplitude class,\n * for example: {@code Amplitude.getInstance().logEvent();}<br><br>\n * Many of the SDK functions return the SDK instance back, allowing you to chain multiple method\n * calls together, for example: {@code Amplitude.getInstance().initialize(this, \"APIKEY\").enableForegroundTracking(getApplication())}\n */\npublic class AmplitudeClient {\n\n    /**\n     * The class identifier tag used in logging. TAG = {@code \"com.amplitude.api.AmplitudeClient\";}\n     */\n    private static final String TAG = AmplitudeClient.class.getName();\n\n    /**\n     * The event type for start session events.\n     */\n    public static final String START_SESSION_EVENT = \"session_start\";\n    /**\n     * The event type for end session events.\n     */\n    public static final String END_SESSION_EVENT = \"session_end\";\n\n    /**\n     * The pref/database key for the device ID value.\n     */\n    public static final String DEVICE_ID_KEY = \"device_id\";\n    /**\n     * The pref/database key for the user ID value.\n     */\n    public static final String USER_ID_KEY = \"user_id\";\n    /**\n     * The pref/database key for the opt out flag.\n     */\n    public static final String OPT_OUT_KEY = \"opt_out\";\n    /**\n     * The pref/database key for the sequence number.\n     */\n    public static final String SEQUENCE_NUMBER_KEY = \"sequence_number\";\n    /**\n     * The pref/database key for the last event time.\n     */\n    public static final String LAST_EVENT_TIME_KEY = \"last_event_time\";\n    /**\n     * The pref/database key for the last event ID value.\n     */\n    public static final String LAST_EVENT_ID_KEY = \"last_event_id\";\n    /**\n     * The pref/database key for the last identify ID value.\n     */\n    public static final String LAST_IDENTIFY_ID_KEY = \"last_identify_id\";\n    /**\n     * The pref/database key for the previous session ID value.\n     */\n    public static final String PREVIOUS_SESSION_ID_KEY = \"previous_session_id\";\n\n    private static final AmplitudeLog logger = AmplitudeLog.getLogger();\n\n    /**\n     * The Android App Context.\n     */\n    protected Context context;\n    /**\n     * The shared OkHTTPClient instance.\n     */\n    protected Call.Factory callFactory;\n    /**\n     * The shared Amplitude database helper instance.\n     */\n    protected DatabaseHelper dbHelper;\n    /**\n     * The Amplitude App API key.\n     */\n    protected String apiKey;\n    /**\n     * The name for this instance of AmplitudeClient.\n     */\n    protected String instanceName;\n    /**\n     * The user's ID value.\n     */\n    protected String userId;\n    /**\n     * The user's Device ID value.\n     */\n    protected String deviceId;\n    private boolean newDeviceIdPerInstall = false;\n    private boolean useAdvertisingIdForDeviceId = false;\n    private boolean useAppSetIdForDeviceId = false;\n    protected boolean initialized = false;\n    private AmplitudeDeviceIdCallback deviceIdCallback;\n    private boolean optOut = false;\n    private boolean offline = false;\n    TrackingOptions inputTrackingOptions = new TrackingOptions();\n    TrackingOptions appliedTrackingOptions = TrackingOptions.copyOf(inputTrackingOptions);\n    JSONObject apiPropertiesTrackingOptions = appliedTrackingOptions.getApiPropertiesTrackingOptions();\n    private boolean coppaControlEnabled = false;\n    private boolean locationListening = false;\n    private EventExplorer eventExplorer;\n    private Plan plan;\n    private IdentifyInterceptor identifyInterceptor;\n\n    /**\n     * The ingestion metadata.\n     */\n    private IngestionMetadata ingestionMetadata;\n\n    /**\n     * Amplitude Server Zone\n     */\n    private AmplitudeServerZone serverZone = AmplitudeServerZone.US;\n\n    /**\n     * The device's Platform value.\n     */\n    protected String platform;\n\n    /**\n     * Event metadata\n     */\n    long sessionId = -1;\n    long sequenceNumber = 0;\n    long lastEventId = -1;\n    long lastIdentifyId = -1;\n    long lastEventTime = -1;\n    long previousSessionId = -1;\n\n    protected DeviceInfo deviceInfo;\n\n    /**\n     * The current session ID value.\n     */\n    private int eventUploadThreshold = Constants.EVENT_UPLOAD_THRESHOLD;\n    private int eventUploadMaxBatchSize = Constants.EVENT_UPLOAD_MAX_BATCH_SIZE;\n    private int eventMaxCount = Constants.EVENT_MAX_COUNT;\n    private long eventUploadPeriodMillis = Constants.EVENT_UPLOAD_PERIOD_MILLIS;\n    private long minTimeBetweenSessionsMillis = Constants.MIN_TIME_BETWEEN_SESSIONS_MILLIS;\n    private long identifyBatchIntervalMillis= Constants.IDENTIFY_BATCH_INTERVAL_MILLIS;\n    private long sessionTimeoutMillis = Constants.SESSION_TIMEOUT_MILLIS;\n    private boolean backoffUpload = false;\n    private int backoffUploadBatchSize = eventUploadMaxBatchSize;\n    private boolean usingForegroundTracking = false;\n    private boolean trackingSessionEvents = false;\n    private boolean inForeground = false;\n    private boolean isEnteringForeground = false;\n    private boolean flushEventsOnClose = true;\n    private String libraryName = Constants.LIBRARY;\n    private String libraryVersion = Constants.VERSION;\n    private boolean useDynamicConfig = false;\n\n    private AtomicBoolean updateScheduled = new AtomicBoolean(false);\n    /**\n     * Whether or not the SDK is in the process of uploading events.\n     */\n    AtomicBoolean uploadingCurrently = new AtomicBoolean(false);\n\n    /**\n     * The last SDK error - used for testing.\n     */\n    Throwable lastError;\n    /**\n     * The url for Amplitude API endpoint\n     */\n    String url = Constants.EVENT_LOG_URL;\n    /**\n     * The Bearer Token for authentication\n     */\n    String bearerToken = null;\n    /**\n     * The background event logging worker thread instance.\n     */\n    WorkerThread logThread = new WorkerThread(\"logThread\");\n    /**\n     * The background event uploading worker thread instance.\n     */\n    WorkerThread httpThread = new WorkerThread(\"httpThread\");\n    /**\n     * The core package for integrating with the Experiment SDK.\n     */\n    final AnalyticsConnector connector;\n    /**\n     * The runner for middleware\n     */\n    MiddlewareRunner middlewareRunner = new MiddlewareRunner();\n\n    /**\n     * Instantiates a new default instance AmplitudeClient and starts worker threads.\n     */\n    public AmplitudeClient() {\n        this(null);\n    }\n\n    /**\n     * Instantiates a new AmplitudeClient with instance name and starts worker threads.\n     * @param instance\n     */\n    public AmplitudeClient(String instance) {\n        this.instanceName = Utils.normalizeInstanceName(instance);\n        logThread.start();\n        httpThread.start();\n        this.connector = AnalyticsConnector.getInstance(this.instanceName);\n    }\n\n    /**\n     * Initialize the Amplitude SDK with the Android application context and your Amplitude\n     * App API key. <b>Note:</b> initialization is required before you log events and modify\n     * user properties.\n     *\n     * @param context the Android application context\n     * @param apiKey  your Amplitude App API key\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient initialize(Context context, String apiKey) {\n        return initialize(context, apiKey, null);\n    }\n\n    /**\n     * Initialize the Amplitude SDK with the Android application context, your Amplitude App API\n     * key, and a user ID for the current user. <b>Note:</b> initialization is required before\n     * you log events and modify user properties.\n     *\n     * @param context the Android application context\n     * @param apiKey  your Amplitude App API key\n     * @param userId  the user id to set\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient initialize(Context context, String apiKey, String userId) {\n        return initialize(context, apiKey, userId, null, false);\n    }\n\n    /**\n     * Initialize the Amplitude SDK with the Android application context, your Amplitude App API\n     * key, a user ID for the current user, and a custom platform value.\n     * <b>Note:</b> initialization is required before you log events and modify user properties.\n     *\n     * @param context the Android application context\n     * @param apiKey  your Amplitude App API key\n     * @param userId  the user id to set\n     * @param\n     * @return the AmplitudeClient\n     */\n    public synchronized AmplitudeClient initialize(\n            final Context context,\n            final String apiKey,\n            final String userId,\n            final String platform,\n            final boolean enableDiagnosticLogging\n    ) {\n        return this.initializeInternal(\n                context,\n                apiKey,\n                userId,\n                platform,\n                enableDiagnosticLogging,\n                null);\n    }\n\n    /**\n     * Initialize the Amplitude SDK with the Android application context, your Amplitude App API\n     * key, a user ID for the current user, and a custom platform value.\n     * <b>Note:</b> initialization is required before you log events and modify user properties.\n     *\n     * @param context the Android application context\n     * @param apiKey  your Amplitude App API key\n     * @param userId  the user id to set\n     * @param callFactory the call factory that used by Amplitude to make http request\n     * @return the AmplitudeClient\n     */\n    public synchronized AmplitudeClient initialize(\n            final Context context,\n            final String apiKey,\n            final String userId,\n            final String platform,\n            final boolean enableDiagnosticLogging,\n            final Call.Factory callFactory\n    ) {\n        return this.initializeInternal(\n                context,\n                apiKey,\n                userId,\n                platform,\n                enableDiagnosticLogging,\n                callFactory);\n    }\n\n    /**\n     * Initialize the Amplitude SDK with the Android application context, your Amplitude App API\n     * key, a user ID for the current user, and a custom platform value.\n     * <b>Note:</b> initialization is required before you log events and modify user properties.\n     *\n     * @param context the Android application context\n     * @param apiKey  your Amplitude App API key\n     * @param userId  the user id to set\n     * @param\n     * @return the AmplitudeClient\n     */\n    public synchronized AmplitudeClient initializeInternal(\n            final Context context,\n            final String apiKey,\n            final String userId,\n            final String platform,\n            final boolean enableDiagnosticLogging,\n            final Call.Factory callFactory\n    ) {\n        if (context == null) {\n            logger.e(TAG, \"Argument context cannot be null in initialize()\");\n            return this;\n        }\n\n        if (Utils.isEmptyString(apiKey)) {\n            logger.e(TAG, \"Argument apiKey cannot be null or blank in initialize()\");\n            return this;\n        }\n\n        this.context = context.getApplicationContext();\n        this.apiKey = apiKey;\n        this.dbHelper = DatabaseHelper.getDatabaseHelper(this.context, this.instanceName);\n        this.platform = Utils.isEmptyString(platform) ? Constants.PLATFORM : platform;\n\n        final AmplitudeClient client = this;\n        runOnLogThread(() -> {\n            if (!initialized) {\n                // this try block is idempotent, so it's safe to retry initialize if failed\n                try {\n                    if (callFactory == null) {\n                        // defer OkHttp client to first call\n                        final Provider<Call.Factory> callProvider\n                                = DoubleCheck.provider(OkHttpClient::new);\n                        this.callFactory = request -> callProvider.get().newCall(request);\n                    } else {\n                        this.callFactory = callFactory;\n                    }\n\n                    if (useDynamicConfig) {\n                        ConfigManager.getInstance().refresh(new ConfigManager.RefreshListener() {\n                            @Override\n                            public void onFinished() {\n                                url = ConfigManager.getInstance().getIngestionEndpoint();\n                            }\n                        }, serverZone);\n                    }\n\n                    deviceInfo = initializeDeviceInfo();\n                    deviceId = initializeDeviceId();\n                    if (this.deviceIdCallback != null) {\n                        this.deviceIdCallback.onDeviceIdReady(deviceId);\n                    }\n\n                    if (userId != null) {\n                        client.userId = userId;\n                        dbHelper.insertOrReplaceKeyValue(USER_ID_KEY, userId);\n                    } else {\n                        client.userId = dbHelper.getValue(USER_ID_KEY);\n                    }\n\n                    identifyInterceptor = new IdentifyInterceptor(dbHelper, logThread, identifyBatchIntervalMillis, this);\n\n                    // set up listener to core package to receive exposure events from Experiment\n                    connector.getEventBridge().setEventReceiver(analyticsEvent -> {\n                        String eventType = analyticsEvent.getEventType();\n                        JSONObject eventProperties = JSONUtil.toJSONObject(analyticsEvent.getEventProperties());\n                        JSONObject userProperties = JSONUtil.toJSONObject(analyticsEvent.getUserProperties());\n                        logEventAsync(eventType, eventProperties, null, userProperties,\n                            null, null, getCurrentTimeMillis(), false);\n                        return Unit.INSTANCE;\n                    });\n\n                    // Set user ID and device ID in core identity store for use in Experiment SDK\n                    connector.getIdentityStore().setIdentity(new Identity(userId, deviceId, new HashMap<>()));\n\n                    // May take some time...\n                    deviceInfo.prefetch();\n\n                    final Long optOutLong = dbHelper.getLongValue(OPT_OUT_KEY);\n                    optOut = optOutLong != null && optOutLong == 1;\n\n                    // try to restore previous session id\n                    previousSessionId = getLongvalue(PREVIOUS_SESSION_ID_KEY, -1);\n                    if (previousSessionId >= 0) {\n                        sessionId = previousSessionId;\n                    }\n\n                    // reload event meta data\n                    sequenceNumber = getLongvalue(SEQUENCE_NUMBER_KEY, 0);\n                    lastEventId = getLongvalue(LAST_EVENT_ID_KEY, -1);\n                    lastIdentifyId = getLongvalue(LAST_IDENTIFY_ID_KEY, -1);\n                    lastEventTime = getLongvalue(LAST_EVENT_TIME_KEY, -1);\n\n                    // install database reset listener to re-insert metadata in memory\n                    dbHelper.setDatabaseResetListener(new DatabaseResetListener() {\n                        @Override\n                        public void onDatabaseReset(SQLiteDatabase db) {\n                            dbHelper.insertOrReplaceKeyValueToTable(db, DatabaseHelper.STORE_TABLE_NAME, DEVICE_ID_KEY, client.deviceId);\n                            dbHelper.insertOrReplaceKeyValueToTable(db, DatabaseHelper.STORE_TABLE_NAME, USER_ID_KEY, client.userId);\n                            dbHelper.insertOrReplaceKeyValueToTable(db, DatabaseHelper.LONG_STORE_TABLE_NAME, OPT_OUT_KEY, client.optOut ? 1L : 0L);\n                            dbHelper.insertOrReplaceKeyValueToTable(db, DatabaseHelper.LONG_STORE_TABLE_NAME, PREVIOUS_SESSION_ID_KEY, client.sessionId);\n                            dbHelper.insertOrReplaceKeyValueToTable(db, DatabaseHelper.LONG_STORE_TABLE_NAME, LAST_EVENT_TIME_KEY, client.lastEventTime);\n                        }\n                    });\n\n                    initialized = true;\n\n                } catch (CursorWindowAllocationException e) {  // treat as uninitialized SDK\n                    logger.e(TAG, String.format(\n                            \"Failed to initialize Amplitude SDK due to: %s\", e.getMessage()\n                    ));\n                    client.apiKey = null;\n                }\n            }\n        });\n\n        return this;\n    }\n\n    /**\n     * Enable foreground tracking for the SDK. This is <b>HIGHLY RECOMMENDED</b>, and will allow\n     * for accurate session tracking.\n     *\n     * @param app the Android application\n     * @return the AmplitudeClient\n     * @see <a href=\"https://github.com/amplitude/Amplitude-Android#tracking-sessions\">\n     *     Tracking Sessions</a>\n     */\n    public AmplitudeClient enableForegroundTracking(Application app) {\n        if (usingForegroundTracking || !contextAndApiKeySet(\"enableForegroundTracking()\")) {\n            return this;\n        }\n\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {\n            app.registerActivityLifecycleCallbacks(new AmplitudeCallbacks(this));\n        }\n\n        return this;\n    }\n\n    /**\n     * @deprecated - We removed Diagnostics class and this function has no-op.\n     * Will completely remove it in the near future.\n     */\n    public AmplitudeClient enableDiagnosticLogging() {\n        return this;\n    }\n\n    /**\n     * @deprecated - We removed Diagnostics class and this function has no-op.\n     * Will completely remove it in the near future.\n     */\n    public AmplitudeClient disableDiagnosticLogging() {\n        return this;\n    }\n\n    /**\n     * @deprecated - We removed Diagnostics class and this function has no-op.\n     * Will completely remove it in the near future.\n     */\n    public AmplitudeClient setDiagnosticEventMaxCount(int eventMaxCount) {\n        return this;\n    }\n\n    /**\n     * Whether to set a new device ID per install. If true, then the SDK will always generate a new\n     * device ID on app install (as opposed to re-using an existing value like ADID).\n     *\n     * @param newDeviceIdPerInstall whether to set a new device ID on app install.\n     * @return the AmplitudeClient\n     * @deprecated\n     */\n    public AmplitudeClient enableNewDeviceIdPerInstall(boolean newDeviceIdPerInstall) {\n        this.newDeviceIdPerInstall = newDeviceIdPerInstall;\n        return this;\n    }\n\n    /**\n     * Whether to use the Android advertising ID (ADID) as the user's device ID.\n     *\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient useAdvertisingIdForDeviceId() {\n        useAdvertisingIdForDeviceId = true;\n        return this;\n    }\n\n    /**\n     * Use Android app set id as the user's device ID.\n     *\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient useAppSetIdForDeviceId() {\n        useAppSetIdForDeviceId = true;\n        return this;\n    }\n\n    /**\n     * Enable location listening in the SDK. This will add the user's current lat/lon coordinates\n     * to every event logged.\n     *\n     * This function should be called before SDK initialization, e.g. {@link #initialize(Context, String)}.\n     *\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient enableLocationListening() {\n        this.locationListening = true;\n        if (this.deviceInfo != null) {\n            this.deviceInfo.setLocationListening(true);\n        }\n        return this;\n    }\n\n    /**\n     * Disable location listening in the SDK. This will stop the sending of the user's current\n     * lat/lon coordinates.\n     *\n     * This function should be called before SDK initialization, e.g. {@link #initialize(Context, String)}.\n     *\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient disableLocationListening() {\n        this.locationListening = false;\n        if (this.deviceInfo != null) {\n            this.deviceInfo.setLocationListening(false);\n        }\n        return this;\n    }\n\n    /**\n     * Sets event upload threshold. The SDK will attempt to batch upload unsent events\n     * every eventUploadPeriodMillis milliseconds, or if the unsent event count exceeds the\n     * event upload threshold.\n     *\n     * @param eventUploadThreshold the event upload threshold\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient setEventUploadThreshold(int eventUploadThreshold) {\n        this.eventUploadThreshold = eventUploadThreshold;\n        return this;\n    }\n\n    /**\n     * Sets event upload max batch size. This controls the maximum number of events sent with\n     * each upload request.\n     *\n     * @param eventUploadMaxBatchSize the event upload max batch size\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient setEventUploadMaxBatchSize(int eventUploadMaxBatchSize) {\n        this.eventUploadMaxBatchSize = eventUploadMaxBatchSize;\n        this.backoffUploadBatchSize = eventUploadMaxBatchSize;\n        return this;\n    }\n\n    /**\n     * Sets event max count. This is the maximum number of unsent events to keep on the device\n     * (for example if the device does not have internet connectivity and cannot upload events).\n     * If the number of unsent events exceeds the max count, then the SDK begins dropping events,\n     * starting from the earliest logged.\n     *\n     * @param eventMaxCount the event max count\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient setEventMaxCount(int eventMaxCount) {\n        this.eventMaxCount = eventMaxCount;\n        return this;\n    }\n\n    /**\n     * Sets event upload period millis. The SDK will attempt to batch upload unsent events\n     * every eventUploadPeriodMillis milliseconds, or if the unsent event count exceeds the\n     * event upload threshold.\n     *\n     * @param eventUploadPeriodMillis the event upload period millis\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient setEventUploadPeriodMillis(int eventUploadPeriodMillis) {\n        this.eventUploadPeriodMillis = eventUploadPeriodMillis;\n        return this;\n    }\n\n    /**\n     * Sets min time between sessions millis.\n     *\n     * @param minTimeBetweenSessionsMillis the min time between sessions millis\n     * @return the min time between sessions millis\n     */\n    public AmplitudeClient setMinTimeBetweenSessionsMillis(long minTimeBetweenSessionsMillis) {\n        this.minTimeBetweenSessionsMillis = minTimeBetweenSessionsMillis;\n        return this;\n    }\n\n    /**\n     * Sets min time for identify batch millis.\n     *\n     * @param identifyBatchIntervalMillis the time interval for identify batch interval\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient setIdentifyBatchIntervalMillis(long identifyBatchIntervalMillis) {\n        if (identifyBatchIntervalMillis < eventUploadPeriodMillis) {\n            logger.w(TAG, \"Warning: minimum batch interval is event upload period.\");\n            return this;\n        }\n        this.identifyBatchIntervalMillis = identifyBatchIntervalMillis;\n        if (this.identifyInterceptor != null) {\n            identifyInterceptor.setIdentifyBatchIntervalMillis(identifyBatchIntervalMillis);\n        }\n        return this;\n    }\n\n    /**\n     * Sets a custom server url for event upload.\n     *\n     * We now have a new method setServerZone. To send data to Amplitude's EU servers, recommend to\n     * use setServerZone method like client.setServerZone(AmplitudeServerZone.EU);\n     *\n     * @param serverUrl - a string url for event upload.\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient setServerUrl(String serverUrl) {\n        if (!Utils.isEmptyString(serverUrl)) {\n            url = serverUrl;\n        }\n        return this;\n    }\n\n    /**\n     * Set Bearer Token to be included in request header.\n     * @param token\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient setBearerToken(String token) {\n        this.bearerToken = token;\n        return this;\n    }\n\n    /**\n     * Sets session timeout millis. If foreground tracking has not been enabled with\n     * @{code enableForegroundTracking()}, then new sessions will be started after\n     * sessionTimeoutMillis milliseconds have passed since the last event logged.\n     *\n     * @param sessionTimeoutMillis the session timeout millis\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient setSessionTimeoutMillis(long sessionTimeoutMillis) {\n        this.sessionTimeoutMillis = sessionTimeoutMillis;\n        return this;\n    }\n\n    public AmplitudeClient setTrackingOptions(TrackingOptions trackingOptions) {\n        inputTrackingOptions = trackingOptions;\n        appliedTrackingOptions = TrackingOptions.copyOf(inputTrackingOptions);\n        if (coppaControlEnabled) {\n            appliedTrackingOptions.mergeIn(TrackingOptions.forCoppaControl());\n        }\n        apiPropertiesTrackingOptions = appliedTrackingOptions.getApiPropertiesTrackingOptions();\n        return this;\n    }\n\n    /**\n     * Enable COPPA (Children's Online Privacy Protection Act) restrictions on ADID, city, IP address and location tracking.\n     * This can be used by any customer that does not want to collect ADID, city, IP address and location tracking.\n     */\n    public AmplitudeClient enableCoppaControl() {\n        coppaControlEnabled = true;\n        appliedTrackingOptions.mergeIn(TrackingOptions.forCoppaControl());\n        apiPropertiesTrackingOptions = appliedTrackingOptions.getApiPropertiesTrackingOptions();\n        return this;\n    }\n\n    /**\n     * Disable COPPA (Children's Online Privacy Protection Act) restrictions on ADID, city, IP address and location tracking.\n     */\n    public AmplitudeClient disableCoppaControl() {\n        coppaControlEnabled = false;\n        appliedTrackingOptions = TrackingOptions.copyOf(inputTrackingOptions);\n        apiPropertiesTrackingOptions = appliedTrackingOptions.getApiPropertiesTrackingOptions();\n        return this;\n    }\n\n    /**\n     * Sets opt out. If true then the SDK does not track any events for the user.\n     *\n     * @param optOut whether or not to opt the user out of tracking\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient setOptOut(final boolean optOut) {\n        if (!contextAndApiKeySet(\"setOptOut()\")) {\n            return this;\n        }\n\n        final AmplitudeClient client = this;\n        runOnLogThread(new Runnable() {\n            @Override\n            public void run() {\n                if (Utils.isEmptyString(apiKey)) { // in case initialization failed\n                    return;\n                }\n                client.optOut = optOut;\n                dbHelper.insertOrReplaceKeyLongValue(OPT_OUT_KEY, optOut ? 1L : 0L);\n            }\n        });\n        return this;\n    }\n\n    public Boolean getOptOut() {\n        return optOut;\n    }\n\n    /**\n     * Library name is default as `amplitude-android`.\n     * Notice: You will only want to set it when following conditions are met.\n     * 1. You develop your own library which bridges Amplitude Android native library.\n     * 2. You want to track your library as one of the data sources.\n     */\n    public AmplitudeClient setLibraryName(final String libraryName) {\n        this.libraryName = libraryName;\n        return this;\n    }\n\n    /**\n     * Library version is default as the latest Amplitude Android SDK version.\n     * Notice: You will only want to set it when following conditions are met.\n     * 1. You develop your own library which bridges Amplitude Android native library.\n     * 2. You want to track your library as one of the data sources.\n     */\n    public AmplitudeClient setLibraryVersion(final String libraryVersion) {\n        this.libraryVersion = libraryVersion;\n        return this;\n    }\n\n    /**\n     * Returns whether or not the user is opted out of tracking.\n     *\n     * @return the optOut flag value\n     */\n    public boolean isOptedOut() {\n        return optOut;\n    }\n\n    /**\n     * Enable/disable message logging by the SDK.\n     *\n     * @param enableLogging whether to enable message logging by the SDK.\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient enableLogging(boolean enableLogging) {\n        logger.setEnableLogging(enableLogging);\n        return this;\n    }\n\n    /**\n     * Sets the logging level. Logging messages will only appear if they are the same severity\n     * level or higher than the set log level.\n     *\n     * @param logLevel the log level\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient setLogLevel(int logLevel) {\n        logger.setLogLevel(logLevel);\n        return this;\n    }\n\n    /**\n     * Set log callback, it can help read and collect error message from sdk\n     *\n     * @param callback\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient setLogCallback(AmplitudeLogCallback callback) {\n        logger.setAmplitudeLogCallback(callback);\n        return this;\n    }\n\n    /**\n     * Sets offline. If offline is true, then the SDK will not upload events to Amplitude servers;\n     * however, it will still log events.\n     *\n     * @param offline whether or not the SDK should be offline\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient setOffline(boolean offline) {\n        this.offline = offline;\n\n        // Try to update to the server once offline mode is disabled.\n        if (!offline) {\n            uploadEvents();\n        }\n\n        return this;\n    }\n\n    /**\n     * Enable/disable flushing of unsent events on app close (enabled by default).\n     *\n     * @param flushEventsOnClose whether to flush unsent events on app close\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient setFlushEventsOnClose(boolean flushEventsOnClose) {\n        this.flushEventsOnClose = flushEventsOnClose;\n        return this;\n    }\n\n    /**\n     * Track session events amplitude client. If enabled then the SDK will automatically send\n     * start and end session events to mark the start and end of the user's sessions.\n     *\n     * @param trackingSessionEvents whether to enable tracking of session events\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient trackSessionEvents(boolean trackingSessionEvents) {\n        this.trackingSessionEvents = trackingSessionEvents;\n        return this;\n    }\n\n    /**\n     * Turning this flag on will find the best server url automatically based on users' geo location.\n     * Note:\n     * 1. If you have your own proxy server and use `setServerUrl` API, please leave this off.\n     * 2. If you have users in China Mainland, we suggest you turn this on.\n     *\n     * @param useDynamicConfig whether to enable dynamic config\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient setUseDynamicConfig(boolean useDynamicConfig) {\n        this.useDynamicConfig = useDynamicConfig;\n        return this;\n    }\n\n    /**\n     * Show Amplitude Event Explorer for the given activity.\n     *\n     * @param activity root activity\n     */\n    public void showEventExplorer(Activity activity) {\n        if (this.eventExplorer == null) {\n            this.eventExplorer = new EventExplorer(this.instanceName);\n        }\n        this.eventExplorer.show(activity);\n    }\n\n    /**\n     * Set foreground tracking to true.\n     */\n    void useForegroundTracking() {\n        usingForegroundTracking = true;\n    }\n\n    /**\n     * Whether foreground tracking is enabled.\n     *\n     * @return whether foreground tracking is enabled\n     */\n    boolean isUsingForegroundTracking() { return usingForegroundTracking; }\n\n    /**\n     * Add middleware to the middleware runner\n     */\n    public void addEventMiddleware(Middleware middleware) {\n        middlewareRunner.add(middleware);\n    }\n\n    /**\n     * Whether app is in the foreground.\n     *\n     * @return whether app is in the foreground\n     */\n    boolean isInForeground() { return inForeground; }\n\n    /**\n     * Log an event with the specified event type.\n     * <b>Note:</b> this is asynchronous and happens on a background thread.\n     *\n     * @param eventType the event type\n     */\n    public void logEvent(String eventType) {\n        logEvent(eventType, null);\n    }\n\n    /**\n     * Log an event with the specified event type and event properties.\n     * <b>Note:</b> this is asynchronous and happens on a background thread.\n     *\n     * @param eventType       the event type\n     * @param eventProperties the event properties\n     */\n    public void logEvent(String eventType, JSONObject eventProperties) {\n        logEvent(eventType, eventProperties, false);\n    }\n\n    /**\n     * Log an event with the specified event type, event properties, with optional out of session\n     * flag. If out of session is true, then the sessionId will be -1 for the event, indicating\n     * that it is not part of the current session. Note: this might be useful when logging events\n     * for notifications received.\n     * <b>Note:</b> this is asynchronous and happens on a background thread.\n     *\n     * @param eventType       the event type\n     * @param eventProperties the event properties\n     * @param extra           the extra unstructured data for middleware\n     */\n    public void logEvent(String eventType, JSONObject eventProperties, MiddlewareExtra extra) {\n        logEvent(eventType, eventProperties, null, getCurrentTimeMillis(), false, extra);\n    }\n\n    /**\n     * Log an event with the specified event type, event properties, with optional out of session\n     * flag. If out of session is true, then the sessionId will be -1 for the event, indicating\n     * that it is not part of the current session. Note: this might be useful when logging events\n     * for notifications received.\n     * <b>Note:</b> this is asynchronous and happens on a background thread.\n     *\n     * @param eventType       the event type\n     * @param eventProperties the event properties\n     * @param outOfSession    the out of session\n     */\n    public void logEvent(String eventType, JSONObject eventProperties, boolean outOfSession) {\n        logEvent(eventType, eventProperties, null, outOfSession);\n    }\n\n    /**\n     * Log an event with the specified event type, event properties, and groups. Use this to set\n     * event-level groups, meaning the group(s) set only apply for this specific event and does\n     * not persist on the user.\n     * <b>Note:</b> this is asynchronous and happens on a background thread.\n     *\n     * @param eventType       the event type\n     * @param eventProperties the event properties\n     * @param groups          the groups\n     */\n    public void logEvent(String eventType, JSONObject eventProperties, JSONObject groups) {\n        logEvent(eventType, eventProperties, groups, false);\n    }\n\n    /**\n     * Log event with the specified event type, event properties, groups, with optional out of\n     * session flag. If out of session is true, then the sessionId will be -1 for the event,\n     * indicating that it is not part of the current session. Note: this might be useful when\n     * logging events for notifications received.\n     * <b>Note:</b> this is asynchronous and happens on a background thread.\n     *\n     * @param eventType       the event type\n     * @param eventProperties the event properties\n     * @param groups          the groups\n     * @param outOfSession    the out of session\n     */\n    public void logEvent(String eventType, JSONObject eventProperties, JSONObject groups, boolean outOfSession) {\n        logEvent(eventType, eventProperties, groups, getCurrentTimeMillis(), outOfSession);\n    }\n\n    /**\n     * Log event with the specified event type, event properties, groups, timestamp, with optional\n     * out of session flag. If out of session is true, then the sessionId will be -1 for the event,\n     * indicating that it is not part of the current session. Note: this might be useful when\n     * logging events for notifications received.\n     * <b>Note:</b> this is asynchronous and happens on a background thread.\n     *\n     * @param eventType       the event type\n     * @param eventProperties the event properties\n     * @param groups          the groups\n     * @param timestamp       the timestamp in millisecond since epoch\n     * @param outOfSession    the out of session\n     * @see <a href=\"https://github.com/amplitude/Amplitude-Android#setting-event-properties\">\n     *     Setting Event Properties</a>\n     * @see <a href=\"https://github.com/amplitude/Amplitude-Android#setting-groups\">\n     *     Setting Groups</a>\n     * @see <a href=\"https://github.com/amplitude/Amplitude-Android#tracking-sessions\">\n     *     Tracking Sessions</a>\n     */\n    public void logEvent(String eventType, JSONObject eventProperties, JSONObject groups, long timestamp, boolean outOfSession) {\n        logEvent(eventType, eventProperties, groups,\n                timestamp, outOfSession, null);\n    }\n\n    public void logEvent(String eventType, JSONObject eventProperties, JSONObject groups, long timestamp, boolean outOfSession, MiddlewareExtra extra) {\n        if (validateLogEvent(eventType)) {\n            logEventAsync(\n                eventType, eventProperties, null, null, groups, null,\n                timestamp, outOfSession, extra);\n        }\n    }\n\n    /**\n     * Log an event with the specified event type.\n     * <b>Note:</b> this is version is synchronous and blocks the main thread until done.\n     *\n     * @param eventType the event type\n     */\n    public void logEventSync(String eventType) {\n        logEventSync(eventType, null);\n    }\n\n    /**\n     * Log an event with the specified event type and event properties.\n     * <b>Note:</b> this is version is synchronous and blocks the main thread until done.\n     *\n     * @param eventType       the event type\n     * @param eventProperties the event properties\n     * @see <a href=\"https://github.com/amplitude/Amplitude-Android#setting-event-properties\">\n     *     Setting Event Properties</a>\n     */\n    public void logEventSync(String eventType, JSONObject eventProperties) {\n        logEventSync(eventType, eventProperties, false);\n    }\n\n    /**\n     * Log an event with the specified event type, event properties, with optional out of session\n     * flag. If out of session is true, then the sessionId will be -1 for the event, indicating\n     * that it is not part of the current session. Note: this might be useful when logging events\n     * for notifications received.\n     * <b>Note:</b> this is version is synchronous and blocks the main thread until done.\n     *\n     * @param eventType       the event type\n     * @param eventProperties the event properties\n     * @param outOfSession    the out of session\n     */\n    public void logEventSync(String eventType, JSONObject eventProperties, boolean outOfSession) {\n        logEventSync(eventType, eventProperties, null, outOfSession);\n    }\n\n    /**\n     * Log an event with the specified event type, event properties, and groups. Use this to set\n     * event-level groups, meaning the group(s) set only apply for this specific event and does\n     * not persist on the user.\n     * <b>Note:</b> this is version is synchronous and blocks the main thread until done.\n     *\n     * @param eventType       the event type\n     * @param eventProperties the event properties\n     * @param groups          the groups\n     */\n    public void logEventSync(String eventType, JSONObject eventProperties, JSONObject groups) {\n        logEventSync(eventType, eventProperties, groups, false);\n    }\n\n    /**\n     * Log event with the specified event type, event properties, groups, with optional out of\n     * session flag. If out of session is true, then the sessionId will be -1 for the event,\n     * indicating that it is not part of the current session. Note: this might be useful when\n     * logging events for notifications received.\n     * <b>Note:</b> this is version is synchronous and blocks the main thread until done.\n     *\n     * @param eventType       the event type\n     * @param eventProperties the event properties\n     * @param groups          the groups\n     * @param outOfSession    the out of session\n     * @see <a href=\"https://github.com/amplitude/Amplitude-Android#setting-event-properties\">\n     *     Setting Event Properties</a>\n     * @see <a href=\"https://github.com/amplitude/Amplitude-Android#setting-groups\">\n     *     Setting Groups</a>\n     * @see <a href=\"https://github.com/amplitude/Amplitude-Android#tracking-sessions\">\n     *     Tracking Sessions</a>\n     */\n    public void logEventSync(String eventType, JSONObject eventProperties, JSONObject groups, boolean outOfSession) {\n        logEventSync(eventType, eventProperties, groups, getCurrentTimeMillis(), outOfSession);\n    }\n\n    /**\n     * Log event with the specified event type, event properties, groups, timestamp,  with optional\n     * sout of ession flag. If out of session is true, then the sessionId will be -1 for the event,\n     * indicating that it is not part of the current session. Note: this might be useful when\n     * logging events for notifications received.\n     * <b>Note:</b> this is version is synchronous and blocks the main thread until done.\n     *\n     * @param eventType       the event type\n     * @param eventProperties the event properties\n     * @param groups          the groups\n     * @param timestamp       the timestamp in milliseconds since epoch\n     * @param outOfSession    the out of session\n     */\n    public void logEventSync(String eventType, JSONObject eventProperties, JSONObject groups, long timestamp, boolean outOfSession) {\n        if (validateLogEvent(eventType)) {\n            logEvent(eventType, eventProperties, null, null, groups, null, timestamp, outOfSession, this.inForeground);\n        }\n    }\n\n    /**\n     * Validate the event type being logged. Also verifies that the context and API key\n     * have been set already with an initialize call.\n     *\n     * @param eventType the event type\n     * @return true if the event type is valid\n     */\n    protected boolean validateLogEvent(String eventType) {\n        if (Utils.isEmptyString(eventType)) {\n            logger.e(TAG, \"Argument eventType cannot be null or blank in logEvent()\");\n            return false;\n        }\n\n        return contextAndApiKeySet(\"logEvent()\");\n    }\n\n    /**\n     * Log event async. Internal method to handle the synchronous logging of events.\n     *\n     * @param eventType       the event type\n     * @param eventProperties the event properties\n     * @param apiProperties   the api properties\n     * @param userProperties  the user properties\n     * @param groups          the groups\n     * @param timestamp       the timestamp\n     * @param outOfSession    the out of session\n     */\n    protected void logEventAsync(final String eventType, JSONObject eventProperties,\n           JSONObject apiProperties, JSONObject userProperties, JSONObject groups,\n           JSONObject groupProperties, final long timestamp, final boolean outOfSession) {\n        logEventAsync(eventType,eventProperties, apiProperties, userProperties, groups,groupProperties, timestamp, outOfSession, null);\n    }\n\n    protected void logEventAsync(final String eventType, JSONObject eventProperties,\n            JSONObject apiProperties, JSONObject userProperties, JSONObject groups,\n            JSONObject groupProperties, final long timestamp, final boolean outOfSession, MiddlewareExtra extra) {\n        // Clone the incoming eventProperties object before sending over\n        // to the log thread. Helps avoid ConcurrentModificationException\n        // if the caller starts mutating the object they passed in.\n        // Only does a shallow copy, so it's still possible, though unlikely,\n        // to hit concurrent access if the caller mutates deep in the object.\n        if (eventProperties != null) {\n            eventProperties = Utils.cloneJSONObject(eventProperties);\n        }\n\n        if (apiProperties != null) {\n            apiProperties = Utils.cloneJSONObject(apiProperties);\n        }\n\n        if (userProperties != null) {\n            userProperties = Utils.cloneJSONObject(userProperties);\n        }\n\n        if (groups != null) {\n            groups = Utils.cloneJSONObject(groups);\n        }\n\n        if (groupProperties != null) {\n            groupProperties = Utils.cloneJSONObject(groupProperties);\n        }\n\n        final JSONObject copyEventProperties = eventProperties;\n        final JSONObject copyApiProperties = apiProperties;\n        final JSONObject copyUserProperties = userProperties;\n        final JSONObject copyGroups = groups;\n        final JSONObject copyGroupProperties = groupProperties;\n        final boolean isForeground = this.inForeground;\n        runOnLogThread(new Runnable() {\n            @Override\n            public void run() {\n                if (Utils.isEmptyString(apiKey)) {  // in case initialization failed\n                    return;\n                }\n                logEvent(\n                    eventType, copyEventProperties, copyApiProperties,\n                    copyUserProperties, copyGroups, copyGroupProperties, timestamp, outOfSession, extra,\n                    isForeground\n                );\n            }\n        });\n    }\n\n    /**\n     * Log event. Internal method to handle the asynchronous logging of events on background\n     * thread.\n     *\n     * @param eventType       the event type\n     * @param eventProperties the event properties\n     * @param apiProperties   the api properties\n     * @param userProperties  the user properties\n     * @param groups          the groups\n     * @param timestamp       the timestamp\n     * @param outOfSession    the out of session\n     * @param inForeground    in foreground\n     * @return the event ID if succeeded, else -1.\n     */\n    protected long logEvent(String eventType, JSONObject eventProperties, JSONObject apiProperties,\n                            JSONObject userProperties, JSONObject groups, JSONObject groupProperties,\n                            long timestamp, boolean outOfSession, boolean inForeground) {\n        return logEvent(eventType, eventProperties, apiProperties, userProperties, groups, groupProperties, timestamp, outOfSession, null, inForeground);\n    }\n\n    protected long logEvent(String eventType, JSONObject eventProperties, JSONObject apiProperties,\n            JSONObject userProperties, JSONObject groups, JSONObject groupProperties,\n            long timestamp, boolean outOfSession, MiddlewareExtra extra, boolean inForeground) {\n\n        logger.d(TAG, \"Logged event to Amplitude: \" + eventType);\n\n        if (optOut) {\n            return -1;\n        }\n\n        // skip session check if logging start_session or end_session events\n        boolean loggingSessionEvent = trackingSessionEvents &&\n                (eventType.equals(START_SESSION_EVENT) || eventType.equals(END_SESSION_EVENT));\n\n        if (!loggingSessionEvent && !outOfSession) {\n            // default case + corner case when async logEvent between onPause and onResume\n            if (!inForeground || isEnteringForeground){\n                isEnteringForeground = false;\n                startNewSessionIfNeeded(timestamp);\n            } else {\n                refreshSessionTime(timestamp);\n            }\n        }\n\n        long result = -1;\n        JSONObject event = new JSONObject();\n        try {\n            event.put(\"event_type\", replaceWithJSONNull(eventType));\n            event.put(\"timestamp\", timestamp);\n            event.put(\"user_id\", replaceWithJSONNull(userId));\n            event.put(\"device_id\", replaceWithJSONNull(deviceId));\n            event.put(\"session_id\", outOfSession ? -1 : sessionId);\n            event.put(\"uuid\", UUID.randomUUID().toString());\n            event.put(\"sequence_number\", getNextSequenceNumber());\n\n            if (appliedTrackingOptions.shouldTrackVersionName()) {\n                event.put(\"version_name\", replaceWithJSONNull(deviceInfo.getVersionName()));\n            }\n            if (appliedTrackingOptions.shouldTrackOsName()) {\n                event.put(\"os_name\", replaceWithJSONNull(deviceInfo.getOsName()));\n            }\n            if (appliedTrackingOptions.shouldTrackOsVersion()) {\n                event.put(\"os_version\", replaceWithJSONNull(deviceInfo.getOsVersion()));\n            }\n            if (appliedTrackingOptions.shouldTrackApiLevel()) {\n                event.put(\"api_level\", replaceWithJSONNull(Build.VERSION.SDK_INT));\n            }\n            if (appliedTrackingOptions.shouldTrackDeviceBrand()) {\n                event.put(\"device_brand\", replaceWithJSONNull(deviceInfo.getBrand()));\n            }\n            if (appliedTrackingOptions.shouldTrackDeviceManufacturer()) {\n                event.put(\"device_manufacturer\", replaceWithJSONNull(deviceInfo.getManufacturer()));\n            }\n            if (appliedTrackingOptions.shouldTrackDeviceModel()) {\n                event.put(\"device_model\", replaceWithJSONNull(deviceInfo.getModel()));\n            }\n            if (appliedTrackingOptions.shouldTrackCarrier()) {\n                event.put(\"carrier\", replaceWithJSONNull(deviceInfo.getCarrier()));\n            }\n            if (appliedTrackingOptions.shouldTrackCountry()) {\n                event.put(\"country\", replaceWithJSONNull(deviceInfo.getCountry()));\n            }\n            if (appliedTrackingOptions.shouldTrackLanguage()) {\n                event.put(\"language\", replaceWithJSONNull(deviceInfo.getLanguage()));\n            }\n            if (appliedTrackingOptions.shouldTrackPlatform()) {\n                event.put(\"platform\", platform);\n            }\n\n            JSONObject library = new JSONObject();\n            library.put(\"name\", this.libraryName == null ? Constants.LIBRARY_UNKNOWN : this.libraryName);\n            library.put(\"version\", this.libraryVersion == null ? Constants.VERSION_UNKNOWN : this.libraryVersion);\n            event.put(\"library\", library);\n\n            if (plan != null) {\n                event.put(\"plan\", plan.toJSONObject());\n            }\n\n            if (ingestionMetadata != null) {\n                event.put(\"ingestion_metadata\", ingestionMetadata.toJSONObject());\n            }\n\n            apiProperties = (apiProperties == null) ? new JSONObject() : apiProperties;\n            if (apiPropertiesTrackingOptions != null && apiPropertiesTrackingOptions.length() > 0) {\n                apiProperties.put(\"tracking_options\", apiPropertiesTrackingOptions);\n            }\n\n            if (appliedTrackingOptions.shouldTrackLatLng()) {\n                Location location = deviceInfo.getMostRecentLocation();\n                if (location != null) {\n                    JSONObject locationJSON = new JSONObject();\n                    locationJSON.put(\"lat\", location.getLatitude());\n                    locationJSON.put(\"lng\", location.getLongitude());\n                    apiProperties.put(\"location\", locationJSON);\n                }\n            }\n            if (appliedTrackingOptions.shouldTrackAdid() && deviceInfo.getAdvertisingId() != null) {\n                apiProperties.put(\"androidADID\", deviceInfo.getAdvertisingId());\n            }\n            if (appliedTrackingOptions.shouldTrackAppSetId() && deviceInfo.getAppSetId() != null) {\n                apiProperties.put(\"android_app_set_id\", deviceInfo.getAppSetId());\n            }\n            apiProperties.put(\"limit_ad_tracking\", deviceInfo.isLimitAdTrackingEnabled());\n            apiProperties.put(\"gps_enabled\", deviceInfo.isGooglePlayServicesEnabled());\n\n            event.put(\"api_properties\", apiProperties);\n            event.put(\"event_properties\", (eventProperties == null) ? new JSONObject()\n                : truncate(eventProperties));\n            event.put(\"user_properties\", (userProperties == null) ? new JSONObject()\n                : truncate(userProperties));\n            event.put(\"groups\", (groups == null) ? new JSONObject() : truncate(groups));\n            event.put(\"group_properties\", (groupProperties == null) ? new JSONObject()\n                : truncate(groupProperties));\n            result = saveEvent(eventType, event, extra);\n\n            // If the the event is an identify, update the user properties to the core identity\n            // for experiment SDK to consume.\n            if (eventType.equals(Constants.IDENTIFY_EVENT) && userProperties != null) {\n                connector.getIdentityStore().editIdentity()\n                    .updateUserProperties(JSONUtil.toUpdateUserPropertiesMap(userProperties))\n                    .commit();\n            }\n        } catch (JSONException e) {\n            logger.e(TAG, String.format(\n                \"JSON Serialization of event type %s failed, skipping: %s\", eventType, e.toString()\n            ));\n        }\n\n        return result;\n    }\n\n    /**\n     * Save event long. Internal method to save an event to the database.\n     *\n     * @param eventType the event type\n     * @param event     the event\n     * @param extra     the extra unstructured data for middleware\n     * @return the event ID if succeeded, else -1\n     */\n    protected long saveEvent(String eventType, JSONObject event, MiddlewareExtra extra) {\n        if (!middlewareRunner.run(new MiddlewarePayload(event, extra))) return -1;\n\n        if (Utils.isEmptyString(event.toString())) {\n            logger.e(TAG, String.format(\n                \"Detected empty event string for event type %s, skipping\", eventType\n            ));\n            return -1;\n        }\n\n        // Intercept event\n        event = identifyInterceptor.intercept(eventType, event);\n        if (event == null) {\n            return -1;\n        }\n\n        return saveEvent(eventType, event);\n    }\n\n    /**\n     * Save event. Internal method to save an event.\n     *\n     * @param eventType the event type\n     * @param event     the event\n     * @return the event ID if succeeded, else -1\n     */\n    protected long saveEvent(String eventType, JSONObject event) {\n        String eventString = event.toString();\n\n        if (eventType.equals(Constants.IDENTIFY_EVENT) || eventType.equals(Constants.GROUP_IDENTIFY_EVENT)) {\n            lastIdentifyId = dbHelper.addIdentify(eventString);\n            setLastIdentifyId(lastIdentifyId);\n        } else {\n            lastEventId = dbHelper.addEvent(eventString);\n            setLastEventId(lastEventId);\n        }\n\n        int numEventsToRemove = Math.min(\n                Math.max(1, eventMaxCount/10),\n                Constants.EVENT_REMOVE_BATCH_SIZE\n        );\n        if (dbHelper.getEventCount() > eventMaxCount) {\n            dbHelper.removeEvents(dbHelper.getNthEventId(numEventsToRemove));\n        }\n        if (dbHelper.getIdentifyCount() > eventMaxCount) {\n            dbHelper.removeIdentifys(dbHelper.getNthIdentifyId(numEventsToRemove));\n        }\n\n        long totalEventCount = dbHelper.getTotalEventCount(); // counts may have changed, refetch\n        if ((totalEventCount % eventUploadThreshold) == 0 &&\n                totalEventCount >= eventUploadThreshold) {\n            updateServer();\n        } else {\n            updateServerLater(eventUploadPeriodMillis);\n        }\n\n        return (\n            eventType.equals(Constants.IDENTIFY_EVENT) ||\n            eventType.equals(Constants.GROUP_IDENTIFY_EVENT)\n        ) ? lastIdentifyId : lastEventId;\n    }\n\n    // fetches key from dbHelper longValueStore\n    // if key does not exist, return defaultValue instead\n    private long getLongvalue(String key, long defaultValue) {\n        Long value = dbHelper.getLongValue(key);\n        return value == null ? defaultValue : value;\n    }\n\n    /**\n     * Internal method to increment and fetch the next event sequence number.\n     *\n     * @return the next sequence number\n     */\n    long getNextSequenceNumber() {\n        sequenceNumber++;\n        dbHelper.insertOrReplaceKeyLongValue(SEQUENCE_NUMBER_KEY, sequenceNumber);\n        return sequenceNumber;\n    }\n\n    /**\n     * Internal method to set the last event time.\n     *\n     * @param timestamp the timestamp\n     */\n    void setLastEventTime(long timestamp) {\n        lastEventTime = timestamp;\n        dbHelper.insertOrReplaceKeyLongValue(LAST_EVENT_TIME_KEY, timestamp);\n    }\n\n    /**\n     * Internal method to set the last event id.\n     *\n     * @param eventId the event id\n     */\n    void setLastEventId(long eventId) {\n        lastEventId = eventId;\n        dbHelper.insertOrReplaceKeyLongValue(LAST_EVENT_ID_KEY, eventId);\n    }\n\n    /**\n     * Internal method to set the last identify id.\n     *\n     * @param identifyId the identify id\n     */\n    void setLastIdentifyId(long identifyId) {\n        lastIdentifyId = identifyId;\n        dbHelper.insertOrReplaceKeyLongValue(LAST_IDENTIFY_ID_KEY, identifyId);\n    }\n\n    /**\n     * Gets the current session id.\n     *\n     * @return The current sessionId value.\n     */\n    public long getSessionId() {\n        return sessionId;\n    }\n\n    /**\n     * Internal method to set the previous session id.\n     *\n     * @param timestamp the timestamp\n     */\n    void setPreviousSessionId(long timestamp) {\n        previousSessionId = timestamp;\n        dbHelper.insertOrReplaceKeyLongValue(PREVIOUS_SESSION_ID_KEY, timestamp);\n    }\n\n    /**\n     * Public method to start a new session if needed.\n     *\n     * @param timestamp the timestamp\n     * @return whether or not a new session was started\n     */\n    public boolean startNewSessionIfNeeded(long timestamp) {\n        if (inSession()) {\n            if (isWithinMinTimeBetweenSessions(timestamp)) {\n                refreshSessionTime(timestamp);\n                return false;\n            }\n\n            startNewSession(timestamp);\n            return true;\n        }\n\n        // no current session - check for previous session\n        if (isWithinMinTimeBetweenSessions(timestamp)) {\n            if (previousSessionId == -1) {\n                startNewSession(timestamp);\n                return true;\n            }\n\n            // extend previous session\n            setSessionId(previousSessionId);\n            refreshSessionTime(timestamp);\n            return false;\n        }\n\n        startNewSession(timestamp);\n        return true;\n    }\n\n    private void startNewSession(long timestamp) {\n        // end previous session\n        if (trackingSessionEvents) {\n            sendSessionEvent(END_SESSION_EVENT);\n        }\n\n        // start new session\n        setSessionId(timestamp);\n        refreshSessionTime(timestamp);\n        if (trackingSessionEvents) {\n            sendSessionEvent(START_SESSION_EVENT);\n        }\n    }\n\n    private boolean inSession() {\n        return sessionId >= 0;\n    }\n\n    private boolean isWithinMinTimeBetweenSessions(long timestamp) {\n        long sessionLimit = usingForegroundTracking ?\n                minTimeBetweenSessionsMillis : sessionTimeoutMillis;\n        return (timestamp - lastEventTime) < sessionLimit;\n    }\n\n    private void setSessionId(long timestamp) {\n        sessionId = timestamp;\n        setPreviousSessionId(timestamp);\n    }\n\n    /**\n     * Internal method to refresh the current session time.\n     *\n     * @param timestamp the timestamp\n     */\n    void refreshSessionTime(long timestamp) {\n        if (!inSession()) {\n            return;\n        }\n\n        setLastEventTime(timestamp);\n    }\n\n    private void sendSessionEvent(final String sessionEvent) {\n        if (!contextAndApiKeySet(String.format(\"sendSessionEvent('%s')\", sessionEvent))) {\n            return;\n        }\n\n        if (!inSession()) {\n            return;\n        }\n\n        JSONObject apiProperties = new JSONObject();\n        try {\n            apiProperties.put(\"special\", sessionEvent);\n        } catch (JSONException e) {\n            return;\n        }\n\n        logEvent(sessionEvent, null, apiProperties, null, null, null, lastEventTime, false, false);\n    }\n\n    /**\n     * Internal method to handle on app exit foreground behavior.\n     *\n     * @param timestamp the timestamp\n     */\n    void onExitForeground(final long timestamp) {\n        isEnteringForeground = false;\n        inForeground = false;\n        runOnLogThread(new Runnable() {\n            @Override\n            public void run() {\n                if (Utils.isEmptyString(apiKey)) {\n                    return;\n                }\n                refreshSessionTime(timestamp);\n                if (flushEventsOnClose) {\n                    identifyInterceptor.transferInterceptedIdentify();\n                    updateServer();\n                }\n\n                // re-persist metadata into database for good measure\n                dbHelper.insertOrReplaceKeyValue(DEVICE_ID_KEY, deviceId);\n                dbHelper.insertOrReplaceKeyValue(USER_ID_KEY, userId);\n                dbHelper.insertOrReplaceKeyLongValue(OPT_OUT_KEY, optOut ? 1L : 0L);\n                dbHelper.insertOrReplaceKeyLongValue(PREVIOUS_SESSION_ID_KEY, sessionId);\n                dbHelper.insertOrReplaceKeyLongValue(LAST_EVENT_TIME_KEY, lastEventTime);\n            }\n        });\n    }\n\n    /**\n     * Internal method to handle on app enter foreground behavior.\n     *\n     * @param timestamp the timestamp\n     */\n    void onEnterForeground(final long timestamp) {\n        isEnteringForeground = true;\n        inForeground = true;\n        runOnLogThread(new Runnable() {\n            @Override\n            public void run() {\n                if (Utils.isEmptyString(apiKey)) {\n                    return;\n                }\n                if (useDynamicConfig) {\n                    ConfigManager.getInstance().refresh(new ConfigManager.RefreshListener() {\n                        @Override\n                        public void onFinished() {\n                            url = ConfigManager.getInstance().getIngestionEndpoint();\n                        }\n                    }, serverZone);\n                }\n                // This should be true, unless somehow an event was tracked\n                // between here and the beginning of this method\n                // in that case the session is started in logEvent()\n                if (isEnteringForeground) {\n                    startNewSessionIfNeeded(timestamp);\n                }\n                isEnteringForeground = false;\n            }\n        });\n    }\n\n    /**\n     * Log revenue amount via a revenue event.\n     *\n     * @param amount the amount\n     * @deprecated - use {@code logRevenueV2} instead\n     * @see <a href=\"https://github.com/amplitude/Amplitude-Android#tracking-revenue\">\n     *     Tracking Revenue</a>\n     */\n    public void logRevenue(double amount) {\n        // Amount is in dollars\n        // ex. $3.99 would be pass as logRevenue(3.99)\n        logRevenue(null, 1, amount);\n    }\n\n    /**\n     * Log revenue with a productId, quantity, and price.\n     *\n     * @param productId the product id\n     * @param quantity  the quantity\n     * @param price     the price\n     * @deprecated - use {@code logRevenueV2} instead\n     * @see <a href=\"https://github.com/amplitude/Amplitude-Android#tracking-revenue\">\n     *     Tracking Revenue</a>\n     */\n    public void logRevenue(String productId, int quantity, double price) {\n        logRevenue(productId, quantity, price, null, null);\n    }\n\n    public void logRevenue(String productId, int quantity, double price, String receipt,\n                           String receiptSignature) {\n        logRevenue(productId, quantity, price, receipt, receiptSignature, null);\n    }\n    /**\n     * Log revenue with a productId, quantity, price, and receipt data for revenue verification.\n     *\n     * @param productId        the product id\n     * @param quantity         the quantity\n     * @param price            the price\n     * @param receipt          the receipt\n     * @param receiptSignature the receipt signature\n     * @param extra            the extra unstructured data for middleware\n     * @deprecated - use {@code logRevenueV2} instead\n     * @see <a href=\"https://github.com/amplitude/Amplitude-Android#tracking-revenue\">\n     *     Tracking Revenue</a>\n     */\n    public void logRevenue(String productId, int quantity, double price, String receipt,\n            String receiptSignature, MiddlewareExtra extra) {\n        if (!contextAndApiKeySet(\"logRevenue()\")) {\n            return;\n        }\n\n        // Log revenue in events\n        JSONObject apiProperties = new JSONObject();\n        try {\n            apiProperties.put(\"special\", Constants.AMP_REVENUE_EVENT);\n            apiProperties.put(\"productId\", productId);\n            apiProperties.put(\"quantity\", quantity);\n            apiProperties.put(\"price\", price);\n            apiProperties.put(\"receipt\", receipt);\n            apiProperties.put(\"receiptSig\", receiptSignature);\n        } catch (JSONException e) {\n\n        }\n\n        logEventAsync(\n            Constants.AMP_REVENUE_EVENT, null, apiProperties, null, null, null, getCurrentTimeMillis(), false, extra\n        );\n    }\n\n    /**\n     * Log revenue v2. Create a {@link Revenue} object to hold your revenue data and properties,\n     * and log it as a revenue event using this method.\n     *\n     * @param revenue a {@link Revenue} object\n     */\n    public void logRevenueV2(Revenue revenue) {\n        logRevenueV2(revenue, null);\n    }\n\n    public void logRevenueV2(Revenue revenue, MiddlewareExtra extra) {\n        if (!contextAndApiKeySet(\"logRevenueV2()\") || revenue == null || !revenue.isValidRevenue()) {\n            return;\n        }\n\n        logEvent(Constants.AMP_REVENUE_EVENT, revenue.toJSONObject(), null, null, null, null, getCurrentTimeMillis(), false, extra, this.inForeground);\n    }\n\n    /**\n     * Sets user properties. This is a convenience wrapper around the\n     * {@link Identify} API to set multiple user properties with a single\n     * command. <b>Note:</b> the replace parameter is deprecated and has no effect.\n     *\n     * @param userProperties the user properties\n     * @param replace        the replace - has no effect\n     * @deprecated\n     */\n    public void setUserProperties(final JSONObject userProperties, final boolean replace) {\n        setUserProperties(userProperties);\n    }\n\n    /**\n     * Sets user properties. This is a convenience wrapper around the\n     * {@link Identify} API to set multiple user properties with a single\n     * command.\n     *\n     * @param userProperties the user properties\n     */\n    public void setUserProperties(final JSONObject userProperties) {\n        setUserProperties(userProperties, null);\n    }\n\n    /**\n     * Sets user properties. This is a convenience wrapper around the\n     * {@link Identify} API to set multiple user properties with a single\n     * command.\n     *\n     * @param userProperties the user properties\n     * @param extra          the extra unstructured data for middleware\n     */\n    public void setUserProperties(final JSONObject userProperties, MiddlewareExtra extra) {\n        if (userProperties == null || userProperties.length() == 0 ||\n                !contextAndApiKeySet(\"setUserProperties\")) {\n            return;\n        }\n\n        Identify identify = convertPropertiesToIdentify(userProperties);\n        if (identify != null) {\n            identify(identify, false, extra);\n        }\n    }\n\n    private Identify convertPropertiesToIdentify(final JSONObject properties) {\n        if (properties == null) {\n            return null;\n        }\n\n        // sanitize and truncate properties before trying to convert to identify\n        JSONObject sanitized = truncate(properties);\n        if (sanitized.length() == 0) {\n            return null;\n        }\n\n        Identify identify = new Identify();\n        Iterator<?> keys = sanitized.keys();\n        while (keys.hasNext()) {\n            String key = (String) keys.next();\n            try {\n                identify.setUserProperty(key, sanitized.get(key));\n            } catch (JSONException e) {\n                logger.e(TAG, e.toString());\n            }\n        }\n        return identify;\n    }\n\n    /**\n     * Clear user properties. This will clear all user properties at once. <b>Note: the\n     * result is irreversible!</b>\n     */\n    public void clearUserProperties() {\n        Identify identify = new Identify().clearAll();\n        identify(identify);\n    }\n\n    /**\n     * Identify. Use this to send an {@link Identify} object containing\n     * user property operations to Amplitude server.\n     *\n     * @param identify an {@link Identify} object\n     */\n    public void identify(Identify identify) {\n        identify(identify, false);\n    }\n\n    public void identify(Identify identify, boolean outOfSession) {\n        identify(identify, outOfSession, null);\n    }\n\n    /**\n     * Identify. Use this to send an {@link com.amplitude.api.Identify} object containing\n     * user property operations to Amplitude server. If outOfSession is true, then the identify\n     * event is sent with a session id of -1, and does not trigger any session-handling logic.\n     *\n     * @param identify      an {@link Identify} object\n     * @param outOfSession  whther to log the identify event out of session\n     * @param extra         the extra unstructured data for middleware\n     */\n    public void identify(Identify identify, boolean outOfSession, MiddlewareExtra extra) {\n        if (\n            identify == null || identify.userPropertiesOperations.length() == 0 ||\n            !contextAndApiKeySet(\"identify()\")\n        ) return;\n        logEventAsync(\n            Constants.IDENTIFY_EVENT, null, null, identify.userPropertiesOperations,\n            null, null, getCurrentTimeMillis(), outOfSession, extra\n        );\n    }\n\n    /**\n     * Sets the user's group(s).\n     *\n     * @param groupType the group type (ex: orgId)\n     * @param groupName the group name (ex: 15)\n     */\n    public void setGroup(String groupType, Object groupName) {\n        setGroup(groupType, groupName, null);\n    }\n\n    /**\n     * Sets the user's group(s).\n     *\n     * @param groupType the group type (ex: orgId)\n     * @param groupName the group name (ex: 15)\n     * @param extra     the extra unstructured data for middleware\n     */\n    public void setGroup(String groupType, Object groupName, MiddlewareExtra extra) {\n        if (!contextAndApiKeySet(\"setGroup()\") || Utils.isEmptyString(groupType)) {\n            return;\n        }\n\n        JSONObject group = null;\n        try {\n            group = new JSONObject().put(groupType, groupName);\n        } catch (JSONException e) {\n            logger.e(TAG, e.toString());\n        }\n\n        Identify identify = new Identify().setUserProperty(groupType, groupName);\n        logEventAsync(Constants.IDENTIFY_EVENT, null, null, identify.userPropertiesOperations,\n                group, null, getCurrentTimeMillis(), false, extra);\n    }\n\n    public void groupIdentify(String groupType, Object groupName, Identify groupIdentify) {\n        groupIdentify(groupType, groupName, groupIdentify, false);\n    }\n\n    public void groupIdentify(String groupType, Object groupName, Identify groupIdentify, boolean outOfSession) {\n        groupIdentify(groupType, groupName, groupIdentify, outOfSession, null);\n    }\n\n    public void groupIdentify(String groupType, Object groupName, JSONObject groupProperties, boolean outOfSession, MiddlewareExtra extra) {\n        Identify identify = convertPropertiesToIdentify(groupProperties);\n        if (identify != null) {\n            groupIdentify(groupType, groupName, identify, outOfSession, extra);\n        }\n    }\n\n    public void groupIdentify(String groupType, Object groupName, Identify groupIdentify, boolean outOfSession, MiddlewareExtra extra) {\n        if (groupIdentify == null || groupIdentify.userPropertiesOperations.length() == 0 ||\n            !contextAndApiKeySet(\"groupIdentify()\") || Utils.isEmptyString(groupType)) {\n\n            return;\n        }\n\n        JSONObject group = null;\n        try {\n            group = new JSONObject().put(groupType, groupName);\n        } catch (JSONException e) {\n            logger.e(TAG, e.toString());\n        }\n\n        logEventAsync(\n            Constants.GROUP_IDENTIFY_EVENT, null, null, null, group,\n            groupIdentify.userPropertiesOperations, getCurrentTimeMillis(), outOfSession, extra\n        );\n    }\n\n    /**\n     * Truncate values in a JSON object. Any string values longer than 1024 characters will be\n     * truncated to 1024 characters.\n     * Any dictionary with more than 1000 items will be ignored.\n     *\n     * @param object the object\n     * @return the truncated JSON object\n     */\n    public JSONObject truncate(JSONObject object) {\n        if (object == null) {\n            return new JSONObject();\n        }\n\n        if (object.length() > Constants.MAX_PROPERTY_KEYS) {\n            logger.w(TAG, \"Warning: too many properties (more than 1000), ignoring\");\n            return new JSONObject();\n        }\n\n        Iterator<?> keys = object.keys();\n        while (keys.hasNext()) {\n            String key = (String) keys.next();\n\n            try {\n                Object value = object.get(key);\n                // do not truncate revenue receipt and receipt sig fields\n                if (key.equals(Constants.AMP_REVENUE_RECEIPT) ||\n                        key.equals(Constants.AMP_REVENUE_RECEIPT_SIG)) {\n                    object.put(key, value);\n                } else if (value.getClass().equals(String.class)) {\n                    object.put(key, truncate((String) value));\n                } else if (value.getClass().equals(JSONObject.class)) {\n                    object.put(key, truncate((JSONObject) value));\n                } else if (value.getClass().equals(JSONArray.class)) {\n                    object.put(key, truncate((JSONArray) value));\n                }\n            } catch (JSONException e) {\n                logger.e(TAG, e.toString());\n            }\n        }\n\n        return object;\n    }\n\n    /**\n     * Truncate values in a JSON array. Any string values longer than 1024 characters will be\n     * truncated to 1024 characters.\n     *\n     * @param array the array\n     * @return the truncated JSON array\n     * @throws JSONException the json exception\n     */\n    public JSONArray truncate(JSONArray array) throws JSONException {\n        if (array == null) {\n            return new JSONArray();\n        }\n\n        for (int i = 0; i < array.length(); i++) {\n            Object value = array.get(i);\n            if (value.getClass().equals(String.class)) {\n                array.put(i, truncate((String) value));\n            } else if (value.getClass().equals(JSONObject.class)) {\n                array.put(i, truncate((JSONObject) value));\n            } else if (value.getClass().equals(JSONArray.class)) {\n                array.put(i, truncate((JSONArray) value));\n            }\n        }\n        return array;\n    }\n\n    /**\n     * Truncate a string to 1024 characters.\n     *\n     * @param value the value\n     * @return the truncated string\n     */\n    public static String truncate(String value) {\n        return value.length() <= Constants.MAX_STRING_LENGTH ? value :\n                value.substring(0, Constants.MAX_STRING_LENGTH);\n    }\n\n\n    /**\n     * Gets the user's id. Can be null.\n     *\n     * @return The developer specified identifier for tracking within the analytics system.\n     */\n    public String getUserId() {\n        return userId;\n    }\n\n    /**\n     * Sets the user id (can be null).\n     *\n     * @param userId the user id\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient setUserId(final String userId) {\n        return setUserId(userId, false);\n    }\n\n    /**\n     * Sets the user id (can be null).\n     * If startNewSession is true, ends the session for the previous user and starts a new\n     * session for the new user id.\n     *\n     * @param userId the user id\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient setUserId(final String userId, final boolean startNewSession) {\n        if (!contextAndApiKeySet(\"setUserId()\")) {\n            return this;\n        }\n\n        final AmplitudeClient client = this;\n        runOnLogThread(new Runnable() {\n            @Override\n            public void run() {\n                if (Utils.isEmptyString(client.apiKey)) {  // in case initialization failed\n                    return;\n                }\n\n                // end previous session\n                if (startNewSession && trackingSessionEvents) {\n                    sendSessionEvent(END_SESSION_EVENT);\n                }\n\n                client.userId = userId;\n                dbHelper.insertOrReplaceKeyValue(USER_ID_KEY, userId);\n\n                // start new session\n                if (startNewSession) {\n                    long timestamp = getCurrentTimeMillis();\n                    setSessionId(timestamp);\n                    refreshSessionTime(timestamp);\n                    if (trackingSessionEvents) {\n                        sendSessionEvent(START_SESSION_EVENT);\n                    }\n                }\n\n                // update the user in the core identity store to notify\n                // experiment to re-fetch variants with the new identity\n                client.connector.getIdentityStore().editIdentity().setUserId(userId).commit();\n            }\n        });\n        return this;\n    }\n\n    /**\n     * Sets a custom device id. <b>Note: only do this if you know what you are doing!</b>\n     *\n     * @param deviceId the device id\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient setDeviceId(final String deviceId) {\n        Set<String> invalidDeviceIds = getInvalidDeviceIds();\n        if (!contextAndApiKeySet(\"setDeviceId()\") || Utils.isEmptyString(deviceId) ||\n                invalidDeviceIds.contains(deviceId)) {\n            return this;\n        }\n\n        final AmplitudeClient client = this;\n        runOnLogThread(new Runnable() {\n            @Override\n            public void run() {\n                if (Utils.isEmptyString(client.apiKey)) {  // in case initialization failed\n                    return;\n                }\n                client.deviceId = deviceId;\n                saveDeviceId(deviceId);\n\n                // update the user in the core identity store to notify\n                // experiment to re-fetch variants with the new identity\n                client.connector.getIdentityStore().editIdentity().setDeviceId(deviceId).commit();\n            }\n        });\n        return this;\n    }\n\n    /**\n     * Regenerates a new random deviceId for current user. Note: this is not recommended unless you\n     * know what you are doing. This can be used in conjunction with setUserId(null) to anonymize\n     * users after they log out. With a null userId and a completely new deviceId, the current user\n     * would appear as a brand new user in dashboard.\n     */\n    public AmplitudeClient regenerateDeviceId() {\n        if (!contextAndApiKeySet(\"regenerateDeviceId()\")) {\n            return this;\n        }\n\n        final AmplitudeClient client = this;\n        runOnLogThread(new Runnable() {\n            @Override\n            public void run() {\n                if (Utils.isEmptyString(client.apiKey)) { // in case initialization failed\n                    return;\n                }\n                String randomId = DeviceInfo.generateUUID() + \"R\";\n                setDeviceId(randomId);\n            }\n        });\n        return this;\n    }\n\n    /**\n     * Force SDK to upload any unsent events.\n     */\n    public void uploadEvents() {\n        if (!contextAndApiKeySet(\"uploadEvents()\")) {\n            return;\n        }\n\n        logThread.post(new Runnable() {\n            @Override\n            public void run() {\n                if (Utils.isEmptyString(apiKey)) {  // in case initialization failed\n                    return;\n                }\n                identifyInterceptor.transferInterceptedIdentify();\n                updateServer();\n            }\n        });\n    }\n\n    private void updateServerLater(long delayMillis) {\n        if (updateScheduled.getAndSet(true)) {\n            return;\n        }\n\n        logThread.postDelayed(new Runnable() {\n            @Override\n            public void run() {\n                updateScheduled.set(false);\n                updateServer();\n            }\n        }, delayMillis);\n    }\n\n    /**\n     * Internal method to upload unsent events.\n     */\n    protected void updateServer() {\n        updateServer(false);\n    }\n\n    /**\n     * Internal method to upload unsent events. Limit controls whether to use event upload max\n     * batch size or backoff upload batch size. <b>Note: </b> always call this on logThread\n     *\n     * @param limit the limit\n     */\n    protected void updateServer(boolean limit) {\n        if (optOut || offline) {\n            return;\n        }\n\n        // Flush middleware\n        middlewareRunner.flush();\n\n        // if returning out of this block, always be sure to set uploadingCurrently to false!!\n        if (!uploadingCurrently.getAndSet(true)) {\n            long totalEventCount = dbHelper.getTotalEventCount();\n            long batchSize = Math.min(\n                limit ? backoffUploadBatchSize : eventUploadMaxBatchSize,\n                totalEventCount\n            );\n\n            if (batchSize <= 0) {\n                uploadingCurrently.set(false);\n                return;\n            }\n\n            try {\n                List<JSONObject> events = dbHelper.getEvents(lastEventId, batchSize);\n                List<JSONObject> identifys = dbHelper.getIdentifys(lastIdentifyId, batchSize);\n\n                final Pair<Pair<Long, Long>, JSONArray> merged = mergeEventsAndIdentifys(\n                        events, identifys, batchSize);\n                final JSONArray mergedEvents = merged.second;\n                if (mergedEvents.length() == 0) {\n                    uploadingCurrently.set(false);\n                    return;\n                }\n                final long maxEventId = merged.first.first;\n                final long maxIdentifyId = merged.first.second;\n                final String mergedEventsString = merged.second.toString();\n\n                httpThread.post(new Runnable() {\n                    @Override\n                    public void run() {\n                        makeEventUploadPostRequest(callFactory, mergedEventsString, maxEventId, maxIdentifyId);\n                    }\n                });\n            } catch (JSONException e) {\n                uploadingCurrently.set(false);\n                logger.e(TAG, e.toString());\n            } catch (CursorWindowAllocationException e) {\n                // handle CursorWindowAllocationException when fetching events, defer upload\n                uploadingCurrently.set(false);\n                logger.e(TAG, String.format(\n                    \"Caught Cursor window exception during event upload, deferring upload: %s\",\n                    e.getMessage()\n                ));\n            }\n        }\n    }\n\n    /**\n     * Internal method to merge unsent events and identifies into a single array by sequence number.\n     *\n     * @param events    the events\n     * @param identifys the identifys\n     * @param numEvents the num events\n     * @return the merged array, max event id, and max identify id\n     * @throws JSONException the json exception\n     */\n    protected Pair<Pair<Long,Long>, JSONArray> mergeEventsAndIdentifys(List<JSONObject> events,\n                            List<JSONObject> identifys, long numEvents) throws JSONException {\n        JSONArray merged = new JSONArray();\n        long maxEventId = -1;\n        long maxIdentifyId = -1;\n\n        while (merged.length() < numEvents) {\n            boolean noEvents = events.isEmpty();\n            boolean noIdentifys = identifys.isEmpty();\n\n            // case 0: no events or identifys, nothing to grab\n            // this case should never happen, as it means there are less identifys and events\n            // than expected\n            if (noEvents && noIdentifys) {\n                logger.w(TAG, String.format(\n                    \"mergeEventsAndIdentifys: number of events and identifys \" +\n                    \"less than expected by %d\", numEvents - merged.length())\n                );\n                break;\n\n            // case 1: no identifys, grab from events\n            } else if (noIdentifys) {\n                JSONObject event = events.remove(0);\n                maxEventId = event.getLong(\"event_id\");\n                merged.put(event);\n\n            // case 2: no events, grab from identifys\n            } else if (noEvents) {\n                JSONObject identify = identifys.remove(0);\n                maxIdentifyId = identify.getLong(\"event_id\");\n                merged.put(identify);\n\n            // case 3: need to compare sequence numbers\n            } else {\n                // events logged before v2.1.0 won't have a sequence number, put those first\n                if (!events.get(0).has(\"sequence_number\") ||\n                        events.get(0).getLong(\"sequence_number\") <\n                        identifys.get(0).getLong(\"sequence_number\")) {\n                    JSONObject event = events.remove(0);\n                    maxEventId = event.getLong(\"event_id\");\n                    merged.put(event);\n                } else {\n                    JSONObject identify = identifys.remove(0);\n                    maxIdentifyId = identify.getLong(\"event_id\");\n                    merged.put(identify);\n                }\n            }\n        }\n\n        return new Pair<Pair<Long, Long>, JSONArray>(new Pair<Long,Long>(maxEventId, maxIdentifyId), merged);\n    }\n\n    /**\n     * Internal method to generate the event upload post request.\n     *\n     * @param client        the client\n     * @param events        the events\n     * @param maxEventId    the max event id\n     * @param maxIdentifyId the max identify id\n     */\n    protected void makeEventUploadPostRequest(Call.Factory client, String events, final long maxEventId, final long maxIdentifyId) {\n        String apiVersionString = \"\" + Constants.API_VERSION;\n        String timestampString = \"\" + getCurrentTimeMillis();\n\n        FormBody body = new FormBody.Builder()\n            .add(\"v\", apiVersionString)\n            .add(\"client\", apiKey)\n            .add(\"e\", events)\n            .add(\"upload_time\", timestampString)\n            .build();\n\n        Request request;\n        try {\n             Request.Builder builder = new Request.Builder()\n                     .url(url)\n                     .post(body);\n\n             if (!Utils.isEmptyString(bearerToken)) {\n                builder.addHeader(\"Authorization\", \"Bearer \" + bearerToken);\n             }\n\n             request = builder.build();\n        } catch (IllegalArgumentException e) {\n            logger.e(TAG, e.toString());\n            uploadingCurrently.set(false);\n            return;\n        }\n\n        boolean uploadSuccess = false;\n\n        try {\n            Response response = client.newCall(request).execute();\n            String stringResponse = response.body().string();\n            if (response.code() == 200) {\n                uploadSuccess = true;\n                logThread.post(new Runnable() {\n                    @Override\n                    public void run() {\n                        if (maxEventId >= 0) dbHelper.removeEvents(maxEventId);\n                        if (maxIdentifyId >= 0) dbHelper.removeIdentifys(maxIdentifyId);\n                        uploadingCurrently.set(false);\n                        if (dbHelper.getTotalEventCount() > eventUploadThreshold) {\n                            logThread.post(new Runnable() {\n                                @Override\n                                public void run() {\n                                    updateServer(backoffUpload);\n                                }\n                            });\n                        }\n                        else {\n                            backoffUpload = false;\n                            backoffUploadBatchSize = eventUploadMaxBatchSize;\n                        }\n                    }\n                });\n            } else if (response.code() == 400 && stringResponse.equals(\"invalid_api_key\")) {\n                logger.e(TAG, \"Invalid API key, make sure your API key is correct in initialize()\");\n            } else if (response.code() == 400 && stringResponse.equals(\"bad_checksum\")) {\n                logger.w(TAG,\n                        \"Bad checksum, post request was mangled in transit, will attempt to reupload later\");\n            } else if (response.code() == 413) {\n\n                // If blocked by one massive event, drop it\n                if (backoffUpload && backoffUploadBatchSize == 1) {\n                    if (maxEventId >= 0) dbHelper.removeEvent(maxEventId);\n                    if (maxIdentifyId >= 0) dbHelper.removeIdentify(maxIdentifyId);\n                    // maybe we want to reset backoffUploadBatchSize after dropping massive event\n                }\n\n                // Server complained about length of request, backoff and try again\n                backoffUpload = true;\n                int numEvents = Math.min((int)dbHelper.getEventCount(), backoffUploadBatchSize);\n                backoffUploadBatchSize = (int)Math.ceil(numEvents / 2.0);\n                logger.w(TAG, \"Request too large, will decrease size and attempt to reupload\");\n                logThread.post(new Runnable() {\n                   @Override\n                    public void run() {\n                       uploadingCurrently.set(false);\n                       updateServer(true);\n                   }\n                });\n            } else {\n                logger.w(TAG, \"Upload failed, \" + stringResponse\n                        + \", will attempt to reupload later\");\n            }\n        } catch (java.net.ConnectException e) {\n            // logger.w(TAG,\n            // \"No internet connection found, unable to upload events\");\n            lastError = e;\n        } catch (java.net.UnknownHostException e) {\n            // logger.w(TAG,\n            // \"No internet connection found, unable to upload events\");\n            lastError = e;\n        } catch (IOException e) {\n            logger.e(TAG, e.toString());\n            lastError = e;\n        } catch (AssertionError e) {\n            // This can be caused by a NoSuchAlgorithmException thrown by DefaultHttpClient\n            logger.e(TAG, \"Exception:\", e);\n            lastError = e;\n        } catch (Exception e) {\n            // Just log any other exception so things don't crash on upload\n            logger.e(TAG, \"Exception:\", e);\n            lastError = e;\n        }\n\n        if (!uploadSuccess) {\n            uploadingCurrently.set(false);\n        }\n\n    }\n\n    protected DeviceInfo initializeDeviceInfo() {\n        return new DeviceInfo(context, this.locationListening, appliedTrackingOptions.shouldTrackAdid());\n    }\n\n    /**\n     * Get the current device id. Can be null if deviceId hasn't been initialized yet.\n     *\n     * @return A unique identifier for tracking within the analytics system.\n     */\n    public String getDeviceId() {\n        return deviceId;\n    }\n\n    // don't need to keep this in memory, if only using it at most 1 or 2 times\n    private Set<String> getInvalidDeviceIds() {\n        Set<String> invalidDeviceIds = new HashSet<String>();\n        invalidDeviceIds.add(\"\");\n        invalidDeviceIds.add(\"9774d56d682e549c\");\n        invalidDeviceIds.add(\"unknown\");\n        invalidDeviceIds.add(\"000000000000000\"); // Common Serial Number\n        invalidDeviceIds.add(\"Android\");\n        invalidDeviceIds.add(\"DEFACE\");\n        invalidDeviceIds.add(\"00000000-0000-0000-0000-000000000000\");\n\n        return invalidDeviceIds;\n    }\n\n    private String initializeDeviceId() {\n        Set<String> invalidIds = getInvalidDeviceIds();\n\n        // see if device id already stored in db\n        String deviceId = dbHelper.getValue(DEVICE_ID_KEY);\n        if (!(Utils.isEmptyString(deviceId) || invalidIds.contains(deviceId) || deviceId.endsWith(\"S\"))) {\n            return deviceId;\n        }\n\n        if (!newDeviceIdPerInstall && useAdvertisingIdForDeviceId && !deviceInfo.isLimitAdTrackingEnabled()) {\n            // Android ID is deprecated by Google.\n            // We are required to use Advertising ID, and respect the advertising ID preference\n\n            String advertisingId = deviceInfo.getAdvertisingId();\n            if (!(Utils.isEmptyString(advertisingId) || invalidIds.contains(advertisingId))) {\n                saveDeviceId(advertisingId);\n                return advertisingId;\n            }\n        }\n\n        if (useAppSetIdForDeviceId) {\n            String appSetId = deviceInfo.getAppSetId();\n            if (!(Utils.isEmptyString(appSetId) || invalidIds.contains(appSetId))) {\n                // Suffix with S for app set id so in future we can tell if device id is from app set id\n                String appSetDeviceId = appSetId + \"S\";\n                saveDeviceId(appSetDeviceId);\n                return appSetDeviceId;\n            }\n        }\n\n        // If this still fails, generate random identifier that does not persist\n        // across installations. Append R to distinguish as randomly generated\n        String randomId = deviceInfo.generateUUID() + \"R\";\n        saveDeviceId(randomId);\n        return randomId;\n    }\n\n    private void saveDeviceId(String deviceId) {\n        dbHelper.insertOrReplaceKeyValue(DEVICE_ID_KEY, deviceId);\n    }\n\n    public AmplitudeClient setDeviceIdCallback(AmplitudeDeviceIdCallback callback) {\n        this.deviceIdCallback = callback;\n        return this;\n    }\n\n    protected void runOnLogThread(Runnable r) {\n        if (Thread.currentThread() != logThread) {\n            logThread.post(r);\n        } else {\n            r.run();\n        }\n    }\n\n    /**\n     * Internal method to replace null event fields with JSON null object.\n     *\n     * @param obj the obj\n     * @return the object\n     */\n    protected Object replaceWithJSONNull(Object obj) {\n        return obj == null ? JSONObject.NULL : obj;\n    }\n\n    /**\n     * Internal method to check whether application context and api key are set\n     *\n     * @param methodName the parent method name to print in error message\n     * @return whether application context and api key are set\n     */\n    protected synchronized boolean contextAndApiKeySet(String methodName) {\n        if (context == null) {\n            logger.e(TAG, \"context cannot be null, set context with initialize() before calling \"\n                    + methodName);\n            return false;\n        }\n        if (Utils.isEmptyString(apiKey)) {\n            logger.e(TAG,\n                    \"apiKey cannot be null or empty, set apiKey with initialize() before calling \"\n                            + methodName);\n            return false;\n        }\n        return true;\n    }\n\n    /**\n     * Internal method to convert bytes to hex string\n     *\n     * @param bytes the bytes\n     * @return the string\n     */\n    protected String bytesToHexString(byte[] bytes) {\n        final char[] hexArray = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b',\n                'c', 'd', 'e', 'f' };\n        char[] hexChars = new char[bytes.length * 2];\n        int v;\n        for (int j = 0; j < bytes.length; j++) {\n            v = bytes[j] & 0xFF;\n            hexChars[j * 2] = hexArray[v >>> 4];\n            hexChars[j * 2 + 1] = hexArray[v & 0x0F];\n        }\n        return new String(hexChars);\n    }\n\n    /**\n     * Internal method to fetch the current time millis. Used for testing.\n     *\n     * @return the current time millis\n     */\n    protected long getCurrentTimeMillis() { return System.currentTimeMillis(); }\n\n    /**\n     * Set tracking plan information.\n     * @param plan Plan object\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient setPlan(Plan plan) {\n        this.plan = plan;\n        return this;\n    }\n\n    /**\n     * Set ingestion metadata information.\n     * @param ingestionMetadata IngestionMetadata object\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient setIngestionMetadata(IngestionMetadata ingestionMetadata) {\n        this.ingestionMetadata = ingestionMetadata;\n        return this;\n    }\n\n    /**\n     * Set Amplitude Server Zone, switch to zone related configuration,\n     * including dynamic configuration and server url.\n     *\n     * To send data to Amplitude's EU servers, you need to configure the serverZone to EU like\n     * client.setServerZone(AmplitudeServerZone.EU);\n     *\n     * @param serverZone AmplitudeServerZone, US or EU, default is US\n     * @return the AmplitudeClient\n     */\n    public AmplitudeClient setServerZone(AmplitudeServerZone serverZone) {\n        return setServerZone(serverZone, true);\n    }\n\n    /**\n     * Set Amplitude Server Zone, switch to zone related configuration,\n     * including dynamic configuration. If updateServerUrl is true, including server url as well.\n     * Recommend to keep updateServerUrl to be true for alignment.\n     *\n     * @param serverZone AmplitudeServerZone, US or EU, default is US\n     * @param updateServerUrl if update server url when update server zone, recommend setting true\n     * @return\n     */\n    public AmplitudeClient setServerZone(AmplitudeServerZone serverZone, boolean updateServerUrl) {\n        if (serverZone == null) {\n            return null;\n        }\n        this.serverZone = serverZone;\n        if (updateServerUrl) {\n            setServerUrl(AmplitudeServerZone.getEventLogApiForZone(serverZone));\n        }\n        return this;\n    }\n\n    /**\n     * Get Amplitude Server Zone\n     *\n     * @return the current Amplitude Server Zone\n     */\n    public AmplitudeServerZone getServerZone() {\n        return this.serverZone;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/api/AmplitudeDeviceIdCallback.java",
    "content": "package com.amplitude.api;\n\npublic interface AmplitudeDeviceIdCallback {\n    void onDeviceIdReady(String deviceId);\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/api/AmplitudeLog.java",
    "content": "package com.amplitude.api;\n\nimport android.util.Log;\n\npublic class AmplitudeLog {\n    private volatile boolean enableLogging = true;\n    private volatile int logLevel = Log.INFO; // default log level\n    private AmplitudeLogCallback amplitudeLogCallback = null;\n\n    protected static AmplitudeLog instance = new AmplitudeLog();\n\n    public static AmplitudeLog getLogger() {\n        return instance;\n    }\n\n    private AmplitudeLog() {} // prevent instantiation\n\n    AmplitudeLog setEnableLogging(boolean enableLogging) {\n        this.enableLogging = enableLogging;\n        return instance;\n    }\n\n    AmplitudeLog setLogLevel(int logLevel) {\n        this.logLevel = logLevel;\n        return instance;\n    }\n\n    int d(String tag, String msg) {\n        if (enableLogging && logLevel <= Log.DEBUG) return Log.d(tag, msg);\n        return 0;\n    }\n\n    int d(String tag, String msg, Throwable tr) {\n        if (enableLogging && logLevel <= Log.DEBUG) return Log.d(tag, msg, tr);\n        return 0;\n    }\n\n    int e(String tag, String msg) {\n        if (enableLogging && logLevel <= Log.ERROR) {\n            if (this.amplitudeLogCallback != null) {\n                this.amplitudeLogCallback.onError(tag, msg);\n            }\n            return Log.e(tag, msg);\n        }\n        return 0;\n    }\n\n    int e(String tag, String msg, Throwable tr) {\n        if (enableLogging && logLevel <= Log.ERROR) {\n            if (this.amplitudeLogCallback != null) {\n                this.amplitudeLogCallback.onError(tag, msg);\n            }\n            return Log.e(tag, msg, tr);\n        }\n        return 0;\n    }\n\n    String getStackTraceString(Throwable tr) {\n        return Log.getStackTraceString(tr);\n    }\n\n    int i(String tag, String msg) {\n        if (enableLogging && logLevel <= Log.INFO) return Log.i(tag, msg);\n        return 0;\n    }\n\n    int i(String tag, String msg, Throwable tr) {\n        if (enableLogging && logLevel <= Log.INFO) return Log.i(tag, msg, tr);\n        return 0;\n    }\n\n    boolean isLoggable(String tag, int level) {\n        return Log.isLoggable(tag, level);\n    }\n\n    int println(int priority, String tag, String msg) {\n        return Log.println(priority, tag, msg);\n    }\n\n    int v(String tag, String msg) {\n        if (enableLogging && logLevel <= Log.VERBOSE) return Log.v(tag, msg);\n        return 0;\n    }\n\n    int v(String tag, String msg, Throwable tr) {\n        if (enableLogging && logLevel <= Log.VERBOSE) return Log.v(tag, msg, tr);\n        return 0;\n    }\n\n    int w(String tag, String msg) {\n        if (enableLogging && logLevel <= Log.WARN) return Log.w(tag, msg);\n        return 0;\n    }\n\n    int w(String tag, Throwable tr) {\n        if (enableLogging && logLevel <= Log.WARN) return Log.w(tag, tr);\n        return 0;\n    }\n\n    int w(String tag, String msg, Throwable tr) {\n        if (enableLogging && logLevel <= Log.WARN) return Log.w(tag, msg, tr);\n        return 0;\n    }\n\n    // wtf = What a Terrible Failure, logged at level ASSERT\n    int wtf(String tag, String msg) {\n        if (enableLogging && logLevel <= Log.ASSERT) return Log.wtf(tag, msg);\n        return 0;\n    }\n\n    int wtf(String tag, Throwable tr) {\n        if (enableLogging && logLevel <= Log.ASSERT) return Log.wtf(tag, tr);\n        return 0;\n    }\n\n    int wtf(String tag, String msg, Throwable tr) {\n        if (enableLogging && logLevel <= Log.ASSERT) return Log.wtf(tag, msg, tr);\n        return 0;\n    }\n\n    void setAmplitudeLogCallback(AmplitudeLogCallback callback) {\n        this.amplitudeLogCallback = callback;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/api/AmplitudeLogCallback.java",
    "content": "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",
    "content": "package com.amplitude.api;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * AmplitudeServerZone is for Data Residency and handling server zone related properties.\n * The server zones now are US and EU.\n *\n * For usage like sending data to Amplitude's EU servers, you need to configure the serverZone\n * property after initializing the client with setServerZone method.\n */\npublic enum AmplitudeServerZone {\n    US, EU;\n\n    private static Map<AmplitudeServerZone, String> amplitudeServerZoneEventLogApiMap =\n        new HashMap<AmplitudeServerZone, String>() {{\n            put(AmplitudeServerZone.US, Constants.EVENT_LOG_URL);\n            put(AmplitudeServerZone.EU, Constants.EVENT_LOG_EU_URL);\n        }};\n\n    private static Map<AmplitudeServerZone, String> amplitudeServerZoneDynamicConfigMap =\n        new HashMap<AmplitudeServerZone, String>() {{\n            put(AmplitudeServerZone.US, Constants.DYNAMIC_CONFIG_URL);\n            put(AmplitudeServerZone.EU, Constants.DYNAMIC_CONFIG_EU_URL);\n        }};\n\n\n    protected static String getEventLogApiForZone(AmplitudeServerZone serverZone) {\n        if (amplitudeServerZoneEventLogApiMap.containsKey(serverZone)) {\n            return amplitudeServerZoneEventLogApiMap.get(serverZone);\n        }\n        return Constants.EVENT_LOG_URL;\n    }\n\n    protected static String getDynamicConfigApi(AmplitudeServerZone serverZone) {\n        if (amplitudeServerZoneDynamicConfigMap.containsKey(serverZone)) {\n            return amplitudeServerZoneDynamicConfigMap.get(serverZone);\n        }\n        return Constants.DYNAMIC_CONFIG_URL;\n    }\n\n    public static AmplitudeServerZone getServerZone(String serverZone) {\n        AmplitudeServerZone amplitudeServerZone = AmplitudeServerZone.US;\n        switch (serverZone) {\n            case \"EU\":\n                amplitudeServerZone = AmplitudeServerZone.EU;\n                break;\n            case \"US\":\n                amplitudeServerZone = AmplitudeServerZone.US;\n                break;\n            default:\n                break;\n        }\n        return amplitudeServerZone;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/api/ConfigManager.java",
    "content": "package com.amplitude.api;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStreamReader;\nimport java.net.HttpURLConnection;\nimport java.net.MalformedURLException;\nimport java.net.URL;\n\npublic class ConfigManager {\n    private static String KEY_INGESTION_ENDPOINT = \"ingestionEndpoint\";\n\n    private static ConfigManager instance = null;\n\n    private String ingestionEndpoint = Constants.EVENT_LOG_URL;\n\n    public String getIngestionEndpoint() {\n        return ingestionEndpoint;\n    }\n\n    private ConfigManager() {\n    }\n\n    public void refresh(RefreshListener listener, AmplitudeServerZone serverZone) {\n        try {\n            String dynamicConfigUrl = AmplitudeServerZone.getDynamicConfigApi(serverZone);\n            URL obj = new URL(dynamicConfigUrl);\n            HttpURLConnection con = (HttpURLConnection) obj.openConnection();\n\n            int responseCode = con.getResponseCode();\n\n            if (responseCode == 200) {\n                BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));\n                String inputLine;\n                StringBuffer response = new StringBuffer();\n\n                while ((inputLine = in.readLine()) != null) {\n                    response.append(inputLine);\n                }\n                in.close();\n\n                JSONObject json = new JSONObject(response.toString());\n                if (json.has(KEY_INGESTION_ENDPOINT)) {\n                    this.ingestionEndpoint = \"https://\" + json.getString(KEY_INGESTION_ENDPOINT);\n                }\n            }\n        } catch (MalformedURLException e) {\n\n        } catch (IOException e) {\n\n        } catch (JSONException e) {\n\n        } catch (Exception e) {\n            \n        }\n\n        listener.onFinished();\n    }\n\n    public static ConfigManager getInstance() {\n        if (instance == null) {\n            instance = new ConfigManager();\n        }\n\n        return instance;\n    }\n\n    interface RefreshListener {\n        void onFinished();\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/api/Constants.java",
    "content": "package com.amplitude.api;\n\nimport com.amplitude.BuildConfig;\n\npublic class Constants {\n\n    public static final String LIBRARY = \"amplitude-android\";\n    public static final String VERSION = BuildConfig.AMPLITUDE_VERSION;\n    public static final String LIBRARY_UNKNOWN = \"unknown-library\";\n    public static final String VERSION_UNKNOWN = \"unknown-version\";\n    public static final String PLATFORM = \"Android\";\n\n    public static final String EVENT_LOG_URL = \"https://api2.amplitude.com/\";\n    public static final String EVENT_LOG_EU_URL = \"https://api.eu.amplitude.com/\";\n    public static final String DYNAMIC_CONFIG_URL = \"https://regionconfig.amplitude.com/\";\n    public static final String DYNAMIC_CONFIG_EU_URL = \"https://regionconfig.eu.amplitude.com/\";\n\n    public static final String PACKAGE_NAME = \"com.amplitude.api\";\n\n    public static final int API_VERSION = 2;\n\n    public static final String DATABASE_NAME = PACKAGE_NAME;\n    public static final int DATABASE_VERSION = 4;\n\n    public static final String DEFAULT_INSTANCE = \"$default_instance\";\n\n    public static final int EVENT_UPLOAD_THRESHOLD = 30;\n    public static final int EVENT_UPLOAD_MAX_BATCH_SIZE = 50;\n    public static final int EVENT_MAX_COUNT = 1000;\n    public static final int EVENT_REMOVE_BATCH_SIZE = 20;\n    public static final long EVENT_UPLOAD_PERIOD_MILLIS = 30 * 1000; // 30s\n    public static final long MIN_TIME_BETWEEN_SESSIONS_MILLIS = 5 * 60 * 1000; // 5m\n    public static final long SESSION_TIMEOUT_MILLIS = 30 * 60 * 1000; // 30m\n    public static final long IDENTIFY_BATCH_INTERVAL_MILLIS = 30 * 1000; // 30s\n    public static final int MAX_STRING_LENGTH = 1024;\n    public static final int MAX_PROPERTY_KEYS = 1000;\n\n    public static final String SHARED_PREFERENCES_NAME_PREFIX = PACKAGE_NAME;\n    public static final String PREFKEY_LAST_EVENT_ID = PACKAGE_NAME + \".lastEventId\";\n    public static final String PREFKEY_LAST_EVENT_TIME = PACKAGE_NAME + \".lastEventTime\";\n    public static final String PREFKEY_LAST_IDENTIFY_ID = PACKAGE_NAME + \".lastIdentifyId\";\n    public static final String PREFKEY_PREVIOUS_SESSION_ID = PACKAGE_NAME + \".previousSessionId\";\n    public static final String PREFKEY_DEVICE_ID = PACKAGE_NAME + \".deviceId\";\n    public static final String PREFKEY_USER_ID = PACKAGE_NAME + \".userId\";\n    public static final String PREFKEY_OPT_OUT = PACKAGE_NAME + \".optOut\";\n\n    public static final String IDENTIFY_EVENT = \"$identify\";\n    public static final String GROUP_IDENTIFY_EVENT = \"$groupidentify\";\n    public static final String AMP_OP_ADD = \"$add\";\n    public static final String AMP_OP_APPEND = \"$append\";\n    public static final String AMP_OP_CLEAR_ALL = \"$clearAll\";\n    public static final String AMP_OP_PREPEND = \"$prepend\";\n    public static final String AMP_OP_SET = \"$set\";\n    public static final String AMP_OP_SET_ONCE = \"$setOnce\";\n    public static final String AMP_OP_UNSET = \"$unset\";\n    public static final String AMP_OP_PREINSERT = \"$preInsert\";\n    public static final String AMP_OP_POSTINSERT = \"$postInsert\";\n    public static final String AMP_OP_REMOVE = \"$remove\";\n\n    public static final String AMP_REVENUE_EVENT = \"revenue_amount\";\n    public static final String AMP_REVENUE_PRODUCT_ID = \"$productId\";\n    public static final String AMP_REVENUE_QUANTITY = \"$quantity\";\n    public static final String AMP_REVENUE_PRICE = \"$price\";\n    public static final String AMP_REVENUE_REVENUE_TYPE = \"$revenueType\";\n    public static final String AMP_REVENUE_RECEIPT = \"$receipt\";\n    public static final String AMP_REVENUE_RECEIPT_SIG = \"$receiptSig\";\n\n    public static final String AMP_TRACKING_OPTION_ADID = \"adid\";\n    public static final String AMP_TRACKING_OPTION_APP_SET_ID = \"app_set_id\";\n    public static final String AMP_TRACKING_OPTION_CARRIER = \"carrier\";\n    public static final String AMP_TRACKING_OPTION_CITY = \"city\";\n    public static final String AMP_TRACKING_OPTION_COUNTRY = \"country\";\n    public static final String AMP_TRACKING_OPTION_DEVICE_BRAND = \"device_brand\";\n    public static final String AMP_TRACKING_OPTION_DEVICE_MANUFACTURER = \"device_manufacturer\";\n    public static final String AMP_TRACKING_OPTION_DEVICE_MODEL = \"device_model\";\n    public static final String AMP_TRACKING_OPTION_DMA = \"dma\";\n    public static final String AMP_TRACKING_OPTION_IP_ADDRESS = \"ip_address\";\n    public static final String AMP_TRACKING_OPTION_LANGUAGE = \"language\";\n    public static final String AMP_TRACKING_OPTION_LAT_LNG = \"lat_lng\";\n    public static final String AMP_TRACKING_OPTION_OS_NAME = \"os_name\";\n    public static final String AMP_TRACKING_OPTION_OS_VERSION = \"os_version\";\n    public static final String AMP_TRACKING_OPTION_API_LEVEL = \"api_level\";\n    public static final String AMP_TRACKING_OPTION_PLATFORM = \"platform\";\n    public static final String AMP_TRACKING_OPTION_REGION = \"region\";\n    public static final String AMP_TRACKING_OPTION_VERSION_NAME = \"version_name\";\n\n    public static final String AMP_PLAN_BRANCH = \"branch\";\n    public static final String AMP_PLAN_SOURCE = \"source\";\n    public static final String AMP_PLAN_VERSION = \"version\";\n    public static final String AMP_PLAN_VERSION_ID = \"versionId\";\n\n    public static final String AMP_INGESTION_METADATA_SOURCE_NAME = \"source_name\";\n    public static final String AMP_INGESTION_METADATA_SOURCE_VERSION = \"source_version\";\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/api/CursorWindowAllocationException.java",
    "content": "package com.amplitude.api;\n\n/**\n * This is Amplitude's substitute for android.database.CursorWindowAllocationException.\n * Android's CursorWindow will throw that exception, but Android does not allow you to import\n * the exception class directly to catch it. This is Amplitude's stand-in for that class.\n *\n * @hide\n */\npublic class CursorWindowAllocationException extends RuntimeException {\n    public CursorWindowAllocationException(String description) {\n        super(description);\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/api/DatabaseHelper.java",
    "content": "package com.amplitude.api;\n\nimport android.content.ContentValues;\nimport android.content.Context;\nimport android.database.Cursor;\nimport android.database.sqlite.SQLiteDatabase;\nimport android.database.sqlite.SQLiteDoneException;\nimport android.database.sqlite.SQLiteException;\nimport android.database.sqlite.SQLiteOpenHelper;\nimport android.database.sqlite.SQLiteStatement;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport java.io.File;\nimport java.util.HashMap;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Map;\n\nclass DatabaseHelper extends SQLiteOpenHelper {\n    private static final String TAG = DatabaseHelper.class.getName();\n\n    static final Map<String, DatabaseHelper> instances = new HashMap<String, DatabaseHelper>();\n\n    protected static final String STORE_TABLE_NAME = \"store\";\n    protected static final String LONG_STORE_TABLE_NAME = \"long_store\";\n    private static final String KEY_FIELD = \"key\";\n    private static final String VALUE_FIELD = \"value\";\n\n    protected static final String EVENT_TABLE_NAME = \"events\";\n    protected static final String IDENTIFY_TABLE_NAME = \"identifys\";\n    protected static final String IDENTIFY_INTERCEPTOR_TABLE_NAME = \"identify_interceptor\";\n    private static final String ID_FIELD = \"id\";\n    private static final String EVENT_FIELD = \"event\";\n\n    private static final String CREATE_STORE_TABLE = \"CREATE TABLE IF NOT EXISTS \"\n            + STORE_TABLE_NAME + \" (\" + KEY_FIELD + \" TEXT PRIMARY KEY NOT NULL, \"\n            + VALUE_FIELD + \" TEXT);\";\n    private static final String CREATE_LONG_STORE_TABLE = \"CREATE TABLE IF NOT EXISTS \"\n            + LONG_STORE_TABLE_NAME + \" (\" + KEY_FIELD + \" TEXT PRIMARY KEY NOT NULL, \"\n            + VALUE_FIELD + \" INTEGER);\";\n    private static final String CREATE_EVENTS_TABLE = \"CREATE TABLE IF NOT EXISTS \"\n            + EVENT_TABLE_NAME + \" (\" + ID_FIELD + \" INTEGER PRIMARY KEY AUTOINCREMENT, \"\n            + EVENT_FIELD + \" TEXT);\";\n    private static final String CREATE_IDENTIFYS_TABLE = \"CREATE TABLE IF NOT EXISTS \"\n            + IDENTIFY_TABLE_NAME + \" (\" + ID_FIELD + \" INTEGER PRIMARY KEY AUTOINCREMENT, \"\n            + EVENT_FIELD + \" TEXT);\";\n    private static final String CREATE_IDENTIFY_INTERCEPTOR_TABLE = \"CREATE TABLE IF NOT EXISTS \"\n            + IDENTIFY_INTERCEPTOR_TABLE_NAME + \" (\" + ID_FIELD + \" INTEGER PRIMARY KEY AUTOINCREMENT, \"\n            + EVENT_FIELD + \" TEXT);\";\n\n    File file;\n    private String instanceName;\n    private boolean callResetListenerOnDatabaseReset = true;\n    private DatabaseResetListener databaseResetListener;\n\n    private static final AmplitudeLog logger = AmplitudeLog.getLogger();\n\n    @Deprecated\n    static DatabaseHelper getDatabaseHelper(Context context) {\n        return getDatabaseHelper(context, null);\n    }\n\n    static synchronized DatabaseHelper getDatabaseHelper(Context context, String instance) {\n        instance = Utils.normalizeInstanceName(instance);\n        DatabaseHelper dbHelper = instances.get(instance);\n        if (dbHelper == null) {\n            dbHelper = new DatabaseHelper(context.getApplicationContext(), instance);\n            instances.put(instance, dbHelper);\n        }\n        return dbHelper;\n    }\n\n    private static String getDatabaseName(String instance) {\n        return (Utils.isEmptyString(instance) || instance.equals(Constants.DEFAULT_INSTANCE)) ? Constants.DATABASE_NAME : Constants.DATABASE_NAME + \"_\" + instance;\n    }\n\n    protected DatabaseHelper(Context context) {\n        this(context, null);\n    }\n\n    protected DatabaseHelper(Context context, String instance) {\n        super(context, getDatabaseName(instance), null, Constants.DATABASE_VERSION);\n        file = context.getDatabasePath(getDatabaseName(instance));\n        instanceName = Utils.normalizeInstanceName(instance);\n    }\n\n    void setDatabaseResetListener(DatabaseResetListener databaseResetListener) {\n        this.databaseResetListener = databaseResetListener;\n    }\n\n    @Override\n    public void onCreate(SQLiteDatabase db) {\n        db.execSQL(CREATE_STORE_TABLE);\n        db.execSQL(CREATE_LONG_STORE_TABLE);\n        // INTEGER PRIMARY KEY AUTOINCREMENT guarantees that all generated values\n        // for the field will be monotonically increasing and unique over the\n        // lifetime of the table, even if rows get removed\n        db.execSQL(CREATE_EVENTS_TABLE);\n        db.execSQL(CREATE_IDENTIFYS_TABLE);\n        db.execSQL(CREATE_IDENTIFY_INTERCEPTOR_TABLE);\n\n        // NOTE: the database file can become corrupted between interactions\n        // getWriteableDatabase and getReadableDatabase will test for corruption\n        // and actually delete the database file and call onCreate again if it's corrupted\n        // Our normal catch exception and delete database does not get triggered in this scenario\n        // Therefore we are also calling the reset callback inside onCreate\n        if (databaseResetListener != null && callResetListenerOnDatabaseReset) {\n            try {\n                callResetListenerOnDatabaseReset = false;  // guards against stack overflow\n                databaseResetListener.onDatabaseReset(db);\n            } catch (SQLiteException e) {\n                logger.e(TAG, String.format(\"databaseReset callback failed during onCreate\"), e);\n            } finally {\n                callResetListenerOnDatabaseReset = true;\n            }\n        }\n    }\n\n    @Override\n    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {\n        if (oldVersion > newVersion) {\n            logger.e(TAG, \"onUpgrade() with invalid oldVersion and newVersion\");\n            resetDatabase(db);\n            return;\n        }\n\n        if (newVersion <= 1) {\n            return;\n        }\n\n        switch (oldVersion) {\n            case 1:\n                db.execSQL(CREATE_STORE_TABLE);\n                if (newVersion <= 2) break;\n\n            case 2:\n                db.execSQL(CREATE_IDENTIFYS_TABLE);\n                db.execSQL(CREATE_LONG_STORE_TABLE);\n                if (newVersion <= 3) break;\n\n            case 3:\n                db.execSQL(CREATE_IDENTIFY_INTERCEPTOR_TABLE);\n                if (newVersion <= 4) break;\n\n            case 4:\n                break;\n\n            default:\n                logger.e(TAG, \"onUpgrade() with unknown oldVersion \" + oldVersion);\n                resetDatabase(db);\n        }\n    }\n\n    private void resetDatabase(SQLiteDatabase db) {\n        db.execSQL(\"DROP TABLE IF EXISTS \" + STORE_TABLE_NAME);\n        db.execSQL(\"DROP TABLE IF EXISTS \" + LONG_STORE_TABLE_NAME);\n        db.execSQL(\"DROP TABLE IF EXISTS \" + EVENT_TABLE_NAME);\n        db.execSQL(\"DROP TABLE IF EXISTS \" + IDENTIFY_TABLE_NAME);\n        db.execSQL(\"DROP TABLE IF EXISTS \" + IDENTIFY_INTERCEPTOR_TABLE_NAME);\n        onCreate(db);\n    }\n\n    synchronized long insertOrReplaceKeyValue(String key, String value) {\n        return value == null ? deleteKeyFromTable(STORE_TABLE_NAME, key) :\n            insertOrReplaceKeyValueToTable(STORE_TABLE_NAME, key, value);\n    }\n\n    synchronized long insertOrReplaceKeyLongValue(String key, Long value) {\n        return value == null ? deleteKeyFromTable(LONG_STORE_TABLE_NAME, key) :\n            insertOrReplaceKeyValueToTable(LONG_STORE_TABLE_NAME, key, value);\n    }\n\n    synchronized long insertOrReplaceKeyValueToTable(String table, String key, Object value) {\n        long result = -1;\n        SQLiteDatabase db = null;\n        try {\n            db = getWritableDatabase();\n            result = insertOrReplaceKeyValueToTable(db, table, key, value);\n        } catch (SQLiteException e) {\n            logger.e(TAG, String.format(\"insertOrReplaceKeyValue in %s failed\", table), e);\n            // Hard to recover from SQLiteExceptions, just start fresh\n            delete();\n        } catch (StackOverflowError e) {\n            logger.e(TAG, String.format(\"insertOrReplaceKeyValue in %s failed\", table), e);\n            // potential stack overflow error when getting database on custom Android versions\n            delete();\n        } finally {\n            if (db != null && db.isOpen()) {\n                close();\n            }\n        }\n        return result;\n    }\n\n    synchronized long insertOrReplaceKeyValueToTable(SQLiteDatabase db, String table, String key, Object value) throws SQLiteException, StackOverflowError {\n        long result = -1;\n        ContentValues contentValues = new ContentValues();\n        contentValues.put(KEY_FIELD, key);\n        if (value instanceof Long) {\n            contentValues.put(VALUE_FIELD, (Long) value);\n        } else {\n            contentValues.put(VALUE_FIELD, (String) value);\n        }\n        result = insertKeyValueContentValuesIntoTable(db, table, contentValues);\n        if (result == -1) {\n            logger.w(TAG, \"Insert failed\");\n        }\n        return result;\n    }\n\n    synchronized long insertKeyValueContentValuesIntoTable(SQLiteDatabase db, String table, ContentValues contentValues) throws SQLiteException, StackOverflowError {\n        return db.insertWithOnConflict(\n            table,\n            null,\n            contentValues,\n            SQLiteDatabase.CONFLICT_REPLACE\n        );\n    }\n\n    synchronized long deleteKeyFromTable(String table, String key) {\n        long result = -1;\n        try {\n            SQLiteDatabase db = getWritableDatabase();\n            result = db.delete(table, KEY_FIELD + \"=?\", new String[]{key});\n        } catch (SQLiteException e) {\n            logger.e(TAG, String.format(\"deleteKey from %s failed\", table), e);\n            // Hard to recover from SQLiteExceptions, just start fresh\n            delete();\n        } catch (StackOverflowError e) {\n            logger.e(TAG, String.format(\"deleteKey from %s failed\", table), e);\n            // potential stack overflow error when getting database on custom Android versions\n            delete();\n        } finally {\n            close();\n        }\n        return result;\n    }\n\n    synchronized long addEvent(String event) {\n        return addEventToTable(EVENT_TABLE_NAME, event);\n    }\n\n    synchronized long addIdentify(String identifyEvent) {\n        return addEventToTable(IDENTIFY_TABLE_NAME, identifyEvent);\n    }\n\n    synchronized long addIdentifyInterceptor(String identifyEvent) {\n        return addEventToTable(IDENTIFY_INTERCEPTOR_TABLE_NAME, identifyEvent);\n    }\n\n    private synchronized long addEventToTable(String table, String event) {\n        long result = -1;\n        try {\n            SQLiteDatabase db = getWritableDatabase();\n            ContentValues contentValues = new ContentValues();\n            contentValues.put(EVENT_FIELD, event);\n            result = insertEventContentValuesIntoTable(db, table, contentValues);\n            if (result == -1) {\n                logger.w(TAG, String.format(\"Insert into %s failed\", table));\n            }\n        } catch (SQLiteException e) {\n            logger.e(TAG, String.format(\"addEvent to %s failed\", table), e);\n            // Hard to recover from SQLiteExceptions, just start fresh\n            delete();\n        } catch (StackOverflowError e) {\n            logger.e(TAG, String.format(\"addEvent to %s failed\", table), e);\n            // potential stack overflow error when getting database on custom Android versions\n            delete();\n        } finally {\n            close();\n        }\n        return result;\n    }\n\n    synchronized long insertEventContentValuesIntoTable(SQLiteDatabase db, String table, ContentValues contentValues) throws SQLiteException, StackOverflowError {\n        return db.insert(table, null, contentValues);\n    }\n\n    synchronized String getValue(String key) {\n        return (String) getValueFromTable(STORE_TABLE_NAME, key);\n    }\n\n    synchronized Long getLongValue(String key) {\n        return (Long) getValueFromTable(LONG_STORE_TABLE_NAME, key);\n    }\n\n    protected synchronized Object getValueFromTable(String table, String key) {\n        Object value = null;\n        Cursor cursor = null;\n        try {\n            SQLiteDatabase db = getReadableDatabase();\n            cursor = queryDb(\n                db, table, new String[]{KEY_FIELD, VALUE_FIELD}, KEY_FIELD + \" = ?\",\n                new String[]{key}, null, null, null, null\n            );\n            if (cursor.moveToFirst()) {\n                value = table.equals(STORE_TABLE_NAME) ? cursor.getString(1) : cursor.getLong(1);\n            }\n        } catch (SQLiteException e) {\n            logger.e(TAG, String.format(\"getValue from %s failed\", table), e);\n            // Hard to recover from SQLiteExceptions, just start fresh\n            delete();\n        } catch (StackOverflowError e) {\n            logger.e(TAG, String.format(\"getValue from %s failed\", table), e);\n            // potential stack overflow error when getting database on custom Android versions\n            delete();\n        } catch (IllegalStateException e) {  // put before Runtime since IllegalState extends\n            // cursor window row too big exception\n            handleIfCursorRowTooLargeException(e);\n        } catch (RuntimeException e) {\n            // cursor window allocation exception\n            convertIfCursorWindowException(e);\n        } finally {\n            if (cursor != null) {\n                cursor.close();\n            }\n            close();\n        }\n        return value;\n    }\n\n    synchronized List<JSONObject> getEvents(long upToId, long limit) throws JSONException {\n        return getEventsFromTable(EVENT_TABLE_NAME, upToId, limit);\n    }\n\n    synchronized List<JSONObject> getIdentifys(\n                                        long upToId, long limit) throws JSONException {\n        return getEventsFromTable(IDENTIFY_TABLE_NAME, upToId, limit);\n    }\n\n    synchronized List<JSONObject> getIdentifyInterceptors(\n            long upToId,\n            long limit\n    ) throws JSONException {\n        return getEventsFromTable(IDENTIFY_INTERCEPTOR_TABLE_NAME, upToId, limit);\n    }\n\n    protected synchronized List<JSONObject> getEventsFromTable(\n            String table, long upToId, long limit) throws JSONException {\n        try {\n            return getEventsBatchFromTable(table, upToId, limit);\n        } catch (CursorWindowAllocationException e) {\n            return getEventsRowByRowFromTable(table, upToId, limit);\n        }\n    }\n\n    private List<JSONObject> getEventsBatchFromTable(\n            String table, long upToId, long limit) throws JSONException {\n        List<JSONObject> events = new LinkedList<JSONObject>();\n        Cursor cursor = null;\n        try {\n            SQLiteDatabase db = getReadableDatabase();\n            cursor = queryDb(\n                db, table, new String[] { ID_FIELD, EVENT_FIELD },\n                upToId >= 0 ? ID_FIELD + \" <= \" + upToId : null, null, null, null,\n                ID_FIELD + \" ASC\", limit >= 0 ? \"\" + limit : null\n            );\n\n            while (cursor.moveToNext()) {\n                long eventId = cursor.getLong(0);\n                String event = cursor.getString(1);\n                if (Utils.isEmptyString(event)) {\n                    continue;\n                }\n\n                JSONObject obj = new JSONObject(event);\n                obj.put(\"event_id\", eventId);\n                events.add(obj);\n            }\n        } catch (SQLiteException e) {\n            logger.e(TAG, String.format(\"getEvents from %s failed\", table), e);\n            delete();\n        } catch (StackOverflowError e) {\n            logger.e(TAG, String.format(\"getEvents from %s failed\", table), e);\n            delete();\n        } catch (IllegalStateException e) {  // put before Runtime since IllegalState extends\n            handleIfCursorRowTooLargeException(e);\n        } catch (RuntimeException e) {\n            convertIfCursorWindowException(e);\n        } finally {\n            if (cursor != null) {\n                cursor.close();\n            }\n            close();\n        }\n        return events;\n    }\n\n    private List<JSONObject> getEventsRowByRowFromTable(\n                                    String table, long upToId, long limit) throws JSONException {\n        List<Long> eventIds = new LinkedList<Long>();\n        Cursor cursor = null;\n        try {\n            SQLiteDatabase db = getReadableDatabase();\n            cursor = queryDb(\n                db, table, new String[] { ID_FIELD },\n                upToId >= 0 ? ID_FIELD + \" <= \" + upToId : null, null, null, null,\n                ID_FIELD + \" ASC\", limit >= 0 ? \"\" + limit : null\n            );\n\n            while (cursor.moveToNext()) {\n                long eventId = cursor.getLong(0);\n                eventIds.add(eventId);\n            }\n        } catch (SQLiteException e) {\n            logger.e(TAG, String.format(\"getEvents from %s failed\", table), e);\n            delete();\n        } catch (StackOverflowError e) {\n            logger.e(TAG, String.format(\"getEvents from %s failed\", table), e);\n            delete();\n        } catch (IllegalStateException e) {  // put before Runtime since IllegalState extends\n            handleIfCursorRowTooLargeException(e);\n        } catch (RuntimeException e) {\n            convertIfCursorWindowException(e);\n        } finally {\n            if (cursor != null) {\n                cursor.close();\n            }\n            close();\n        }\n\n        try {\n            List<JSONObject> events = new LinkedList<JSONObject>();\n            for (Long eventId : eventIds) {\n                JSONObject event = getEventFromTable(table, eventId);\n                if (event != null) {\n                    events.add(event);\n                }\n            }\n            return events;\n        } finally {\n            close();\n        }\n    }\n\n    protected synchronized JSONObject getEventFromTable(String table, long eventId) throws JSONException {\n        JSONObject event = null;\n        Cursor cursor = null;\n        try {\n            SQLiteDatabase db = getReadableDatabase();\n            cursor = queryDb(\n                db, table, new String[] { EVENT_FIELD },\n               ID_FIELD + \" = \" + eventId,\n                    null, null, null, null, null\n            );\n\n            if (cursor.moveToFirst()) {\n                String eventData = cursor.getString(0);\n                if (!Utils.isEmptyString(eventData)) {\n                    event = new JSONObject(eventData);\n                    event.put(\"event_id\", eventId);\n                }\n            }\n        } catch (SQLiteException e) {\n            logger.e(TAG, String.format(\"getEvent from %s failed\", table), e);\n            delete();\n        } catch (StackOverflowError e) {\n            logger.e(TAG, String.format(\"getEvent from %s failed\", table), e);\n            delete();\n        } catch (IllegalStateException e) {  // put before Runtime since IllegalState extends\n            handleIfCursorRowTooLargeException(e);\n        } catch (RuntimeException e) {\n            convertIfCursorWindowException(e);\n        } finally {\n            if (cursor != null) {\n                cursor.close();\n            }\n        }\n        return event;\n    }\n\n    synchronized long getEventCount() {\n        return getEventCountFromTable(EVENT_TABLE_NAME);\n    }\n\n    synchronized long getIdentifyCount() {\n        return getEventCountFromTable(IDENTIFY_TABLE_NAME);\n    }\n\n    synchronized long getTotalEventCount() {\n        return getEventCount() + getIdentifyCount();\n    }\n\n    synchronized long getIdentifyInterceptorCount() {\n        return getEventCountFromTable(IDENTIFY_INTERCEPTOR_TABLE_NAME);\n    }\n\n    private synchronized long getEventCountFromTable(String table) {\n        long numberRows = 0;\n        SQLiteStatement statement = null;\n        try {\n            SQLiteDatabase db = getReadableDatabase();\n            String query = \"SELECT COUNT(*) FROM \" + table;\n            statement = db.compileStatement(query);\n            numberRows = statement.simpleQueryForLong();\n        } catch (SQLiteException e) {\n            logger.e(TAG, String.format(\"getNumberRows for %s failed\", table), e);\n            // Hard to recover from SQLiteExceptions, just start fresh\n            delete();\n        } catch (StackOverflowError e) {\n            logger.e(TAG, String.format(\"getNumberRows for %s failed\", table), e);\n            // potential stack overflow error when getting database on custom Android versions\n            delete();\n        } finally {\n            if (statement != null) {\n                statement.close();\n            }\n            close();\n        }\n        return numberRows;\n    }\n\n    synchronized long getNthEventId(long n) {\n        return getNthEventIdFromTable(EVENT_TABLE_NAME, n);\n    }\n\n    synchronized long getNthIdentifyId(long n) {\n        return getNthEventIdFromTable(IDENTIFY_TABLE_NAME, n);\n    }\n\n    synchronized long getLastIdentifyInterceptorId() {\n        return getNthEventIdFromTable(IDENTIFY_INTERCEPTOR_TABLE_NAME, 1, \"DESC\");\n    }\n\n    private synchronized long getNthEventIdFromTable(String table, long n) {\n        return getNthEventIdFromTable(table, n, \"ASC\");\n    }\n\n    private synchronized long getNthEventIdFromTable(String table, long n, String orderBy) {\n        long nthEventId = -1;\n        SQLiteStatement statement = null;\n        try {\n            SQLiteDatabase db = getReadableDatabase();\n            String query = \"SELECT \" + ID_FIELD + \" FROM \" + table + \" ORDER BY \" + ID_FIELD +\n                    \" \" + orderBy + \" LIMIT 1 OFFSET \" + (n - 1);\n            statement = db.compileStatement(query);\n            nthEventId = -1;\n            try {\n                nthEventId = statement.simpleQueryForLong();\n            } catch (SQLiteDoneException e) {\n                logger.w(TAG, e);\n            }\n        } catch (SQLiteException e) {\n            logger.e(TAG, String.format(\"getNthEventId from %s failed\", table), e);\n            // Hard to recover from SQLiteExceptions, just start fresh\n            delete();\n        } catch (StackOverflowError e) {\n            logger.e(TAG, String.format(\"getNthEventId from %s failed\", table), e);\n            // potential stack overflow error when getting database on custom Android versions\n            delete();\n        } finally {\n            if (statement != null) {\n                statement.close();\n            }\n            close();\n        }\n        return nthEventId;\n    }\n\n    synchronized void removeEvents(long maxId) {\n        removeEventsFromTable(EVENT_TABLE_NAME, maxId);\n    }\n\n    synchronized void removeIdentifys(long maxId) {\n        removeEventsFromTable(IDENTIFY_TABLE_NAME, maxId);\n    }\n\n    synchronized void removeIdentifyInterceptors(long maxId) {\n        removeEventsFromTable(IDENTIFY_INTERCEPTOR_TABLE_NAME, maxId);\n    }\n\n    private synchronized void removeEventsFromTable(String table, long maxId) {\n        try {\n            SQLiteDatabase db = getWritableDatabase();\n            db.delete(table, ID_FIELD + \" <= \" + maxId, null);\n        } catch (SQLiteException e) {\n            logger.e(TAG, String.format(\"removeEvents from %s failed\", table), e);\n            delete();\n        } catch (StackOverflowError e) {\n            logger.e(TAG, String.format(\"removeEvents from %s failed\", table), e);\n            delete();\n        } finally {\n            close();\n        }\n    }\n\n    synchronized void removeEvent(long id) {\n        removeEventFromTable(EVENT_TABLE_NAME, id);\n    }\n\n    synchronized void removeIdentify(long id) {\n        removeEventFromTable(IDENTIFY_TABLE_NAME, id);\n    }\n\n    synchronized void removeIdentifyIntercept(long id) {\n        removeEventFromTable(IDENTIFY_INTERCEPTOR_TABLE_NAME, id);\n    }\n\n    private synchronized void removeEventFromTable(String table, long id) {\n        try {\n            SQLiteDatabase db = getWritableDatabase();\n            db.delete(table, ID_FIELD + \" = \" + id, null);\n        } catch (SQLiteException e) {\n            logger.e(TAG, String.format(\"removeEvent from %s failed\", table), e);\n            delete();\n        } catch (StackOverflowError e) {\n            logger.e(TAG, String.format(\"removeEvent from %s failed\", table), e);\n            delete();\n        } finally {\n            close();\n        }\n    }\n\n    private void delete() {\n        // This only gets called if the database somehow gets corrupted AFTER being fetched\n        // ie after the call to getWriteableDatabase / getReadableDatabase\n        // or if a SQL exception occurs during the interaction\n        try {\n            close();\n            file.delete();\n        } catch (SecurityException e) {\n            logger.e(TAG, \"delete failed\", e);\n        } finally {\n            if (databaseResetListener != null && callResetListenerOnDatabaseReset) {\n                callResetListenerOnDatabaseReset = false;  // guards against stack overflow\n                SQLiteDatabase db = null;\n                try {\n                    db = getWritableDatabase();\n                    databaseResetListener.onDatabaseReset(db);\n                } catch (SQLiteException e) {\n                    logger.e(TAG, String.format(\"databaseReset callback failed during delete\"), e);\n                }\n                finally {\n                    callResetListenerOnDatabaseReset = true;\n                    if (db != null && db.isOpen()) {\n                        close();\n                    }\n                }\n            }\n        }\n    }\n\n    boolean dbFileExists() {\n        return file.exists();\n    }\n\n    // add level of indirection to facilitate mocking during unit tests\n    Cursor queryDb(\n        SQLiteDatabase db, String table, String[] columns, String selection,\n        String[] selectionArgs, String groupBy, String having, String orderBy, String limit\n    ) {\n        return db.query(table, columns, selection, selectionArgs, groupBy, having, orderBy, limit);\n    }\n\n    /*\n        Checks if the IllegalStateException is caused by CursorWindow row too big exception\n        If it is, then we want to reset the database to clear the bad data\n     */\n    private void handleIfCursorRowTooLargeException(IllegalStateException e) {\n        String message = e.getMessage();\n        if (!Utils.isEmptyString(message) && message.contains(\"Couldn't read\") && message.contains(\"CursorWindow\")) {\n            delete();\n        } else {\n            throw e;\n        }\n    }\n\n    /*\n        Checks if the RuntimeException is an android.database.CursorWindowAllocationException.\n        If it is, then wrap the message in Amplitude's CursorWindowAllocationException so the\n        AmplitudeClient can handle it. If not then rethrow.\n     */\n    private static void convertIfCursorWindowException(RuntimeException e) {\n        String message = e.getMessage();\n        if (!Utils.isEmptyString(message) && (message.startsWith(\"Cursor window allocation of\") || message.startsWith(\"Could not allocate CursorWindow\"))) {\n            throw new CursorWindowAllocationException(message);\n        } else {\n            throw e;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/api/DatabaseResetListener.java",
    "content": "package com.amplitude.api;\n\nimport android.database.sqlite.SQLiteDatabase;\n\npublic interface DatabaseResetListener {\n    public void onDatabaseReset(SQLiteDatabase db);\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/api/DeviceInfo.java",
    "content": "package com.amplitude.api;\n\nimport android.content.ContentResolver;\nimport android.content.Context;\nimport android.content.pm.PackageInfo;\nimport android.content.pm.PackageManager.NameNotFoundException;\nimport android.content.res.Configuration;\nimport android.content.res.Resources;\nimport android.location.Address;\nimport android.location.Geocoder;\nimport android.location.Location;\nimport android.location.LocationManager;\nimport android.os.Build;\nimport android.os.LocaleList;\nimport android.provider.Settings.Secure;\nimport android.telephony.TelephonyManager;\n\nimport java.io.IOException;\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.UUID;\n\n@SuppressWarnings(\"MissingPermission\")\npublic class DeviceInfo {\n\n    private static final String TAG = DeviceInfo.class.getName();\n\n    public static final String OS_NAME = \"android\";\n\n    private static final String SETTING_LIMIT_AD_TRACKING = \"limit_ad_tracking\";\n    private static final String SETTING_ADVERTISING_ID = \"advertising_id\";\n\n    private boolean locationListening;\n\n    private boolean shouldTrackAdid;\n\n    private Context context;\n\n    private CachedInfo cachedInfo;\n\n    /**\n     * Internal class serves as a cache\n     */\n    private class CachedInfo {\n        private String advertisingId;\n        private String country;\n        private String versionName;\n        private String osName;\n        private String osVersion;\n        private String brand;\n        private String manufacturer;\n        private String model;\n        private String carrier;\n        private String language;\n        private boolean limitAdTrackingEnabled;\n        private boolean gpsEnabled; // google play services\n        private String appSetId;\n\n        private CachedInfo() {\n            advertisingId = getAdvertisingId();\n            versionName = getVersionName();\n            osName = getOsName();\n            osVersion = getOsVersion();\n            brand = getBrand();\n            manufacturer = getManufacturer();\n            model = getModel();\n            carrier = getCarrier();\n            country = getCountry();\n            language = getLanguage();\n            gpsEnabled = checkGPSEnabled();\n            appSetId = getAppSetId();\n        }\n\n        /**\n         * Internal methods for getting raw information\n         */\n\n        private String getVersionName() {\n            PackageInfo packageInfo;\n            try {\n                packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);\n                return packageInfo.versionName;\n            } catch (NameNotFoundException e) {\n\n            } catch (Exception e) {\n                \n            }\n            return null;\n        }\n\n        private String getOsName() {\n            return OS_NAME;\n        }\n\n        private String getOsVersion() {\n            return Build.VERSION.RELEASE;\n        }\n\n        private String getBrand() {\n            return Build.BRAND;\n        }\n\n        private String getManufacturer() {\n            return Build.MANUFACTURER;\n        }\n\n        private String getModel() {\n            return Build.MODEL;\n        }\n\n        private String getCarrier() {\n            try {\n                TelephonyManager manager = (TelephonyManager) context\n                        .getSystemService(Context.TELEPHONY_SERVICE);\n                return manager.getNetworkOperatorName();\n            } catch (Exception e) {\n                // Failed to get network operator name from network\n            }\n            return null;\n        }\n\n        private String getCountry() {\n            // This should not be called on the main thread.\n\n            // Prioritize reverse geocode, but until we have a result from that,\n            // we try to grab the country from the network, and finally the locale\n            String country = getCountryFromLocation();\n            if (!Utils.isEmptyString(country)) {\n                return country;\n            }\n\n            country = getCountryFromNetwork();\n            if (!Utils.isEmptyString(country)) {\n                return country;\n            }\n            return getCountryFromLocale();\n        }\n\n        private String getCountryFromLocation() {\n            if (!isLocationListening()) {\n                return null;\n            }\n\n            Location recent = getMostRecentLocation();\n            if (recent != null) {\n                try {\n                    if (Geocoder.isPresent()) {\n                        Geocoder geocoder = getGeocoder();\n                        List<Address> addresses = geocoder.getFromLocation(recent.getLatitude(),\n                                recent.getLongitude(), 1);\n                        if (addresses != null) {\n                            for (Address address : addresses) {\n                                if (address != null) {\n                                    return address.getCountryCode();\n                                }\n                            }\n                        }\n                    }\n                } catch (IOException e) {\n                    // Failed to reverse geocode location\n                } catch (NullPointerException e) {\n                    // Failed to reverse geocode location\n                } catch (NoSuchMethodError e) {\n                    // failed to fetch geocoder\n                } catch (IllegalArgumentException e) {\n                    // Bad lat / lon values can cause Geocoder to throw IllegalArgumentExceptions\n                } catch (IllegalStateException e) {\n                    // sometimes the location manager is unavailable\n                } catch (SecurityException e) {\n                    // Customized Android System without Google Play Service Installed\n                }\n            }\n            return null;\n        }\n\n        private String getCountryFromNetwork() {\n            try {\n                TelephonyManager manager = (TelephonyManager) context\n                        .getSystemService(Context.TELEPHONY_SERVICE);\n                if (manager.getPhoneType() != TelephonyManager.PHONE_TYPE_CDMA) {\n                    String country = manager.getNetworkCountryIso();\n                    if (country != null) {\n                        return country.toUpperCase(Locale.US);\n                    }\n                }\n            } catch (Exception e) {\n                // Failed to get country from network\n            }\n            return null;\n        }\n\n        private Locale getLocale() {\n            final Configuration configuration = Resources.getSystem().getConfiguration();\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {\n                final LocaleList localeList = configuration.getLocales();\n                if (localeList.isEmpty()) {\n                    return Locale.getDefault();\n                } else {\n                    return localeList.get(0);\n                }\n            } else {\n                return configuration.locale;\n            }\n        }\n\n        private String getCountryFromLocale() {\n            return getLocale().getCountry();\n        }\n\n        private String getLanguage() {\n            return getLocale().getLanguage();\n        }\n\n        private String getAdvertisingId() {\n            if (!shouldTrackAdid) {\n                return null;\n            }\n\n            // This should not be called on the main thread.\n            if (\"Amazon\".equals(getManufacturer())) {\n                return getAndCacheAmazonAdvertisingId();\n            } else {\n                return getAndCacheGoogleAdvertisingId();\n            }\n        }\n\n        private String getAppSetId() {\n            try {\n                Class AppSet = Class\n                        .forName(\"com.google.android.gms.appset.AppSet\");\n                Method getClient = AppSet.getMethod(\"getClient\", Context.class);\n                Object appSetIdClient = getClient.invoke(null, context);\n                Method getAppSetIdInfo = appSetIdClient.getClass().getMethod(\"getAppSetIdInfo\");\n                Object taskWithAppSetInfo = getAppSetIdInfo.invoke(appSetIdClient);\n                Class Tasks = Class.forName(\"com.google.android.gms.tasks.Tasks\");\n                Method await = Tasks.getMethod(\"await\", Class.forName(\"com.google.android.gms.tasks.Task\"));\n                Object appSetInfo = await.invoke(null, taskWithAppSetInfo);\n                Method getId = appSetInfo.getClass().getMethod(\"getId\");\n                appSetId = (String) getId.invoke(appSetInfo);\n            } catch (ClassNotFoundException e) {\n                AmplitudeLog.getLogger().w(TAG, \"Google Play Services SDK not found for app set id!\");\n            } catch (InvocationTargetException e) {\n                AmplitudeLog.getLogger().w(TAG, \"Google Play Services not available for app set id\");\n            } catch (Exception e) {\n                AmplitudeLog.getLogger().e(TAG, \"Encountered an error connecting to Google Play Services for app set id\", e);\n            }\n\n            return appSetId;\n        }\n\n        private String getAndCacheAmazonAdvertisingId() {\n            ContentResolver cr = context.getContentResolver();\n\n            limitAdTrackingEnabled = Secure.getInt(cr, SETTING_LIMIT_AD_TRACKING, 0) == 1;\n            advertisingId = Secure.getString(cr, SETTING_ADVERTISING_ID);\n\n            return advertisingId;\n        }\n\n        private String getAndCacheGoogleAdvertisingId() {\n            try {\n                Class AdvertisingIdClient = Class\n                        .forName(\"com.google.android.gms.ads.identifier.AdvertisingIdClient\");\n                Method getAdvertisingInfo = AdvertisingIdClient.getMethod(\"getAdvertisingIdInfo\",\n                        Context.class);\n                Object advertisingInfo = getAdvertisingInfo.invoke(null, context);\n                Method isLimitAdTrackingEnabled = advertisingInfo.getClass().getMethod(\n                        \"isLimitAdTrackingEnabled\");\n                Boolean limitAdTrackingEnabled = (Boolean) isLimitAdTrackingEnabled\n                        .invoke(advertisingInfo);\n                this.limitAdTrackingEnabled =\n                        limitAdTrackingEnabled != null && limitAdTrackingEnabled;\n                Method getId = advertisingInfo.getClass().getMethod(\"getId\");\n                advertisingId = (String) getId.invoke(advertisingInfo);\n            } catch (ClassNotFoundException e) {\n                AmplitudeLog.getLogger().w(TAG, \"Google Play Services SDK not found for advertising id!\");\n            } catch (InvocationTargetException e) {\n                AmplitudeLog.getLogger().w(TAG, \"Google Play Services not available for advertising id\");\n            } catch (Exception e) {\n                AmplitudeLog.getLogger().e(TAG, \"Encountered an error connecting to Google Play Services for advertising id\", e);\n            }\n\n            return advertisingId;\n        }\n\n        private boolean checkGPSEnabled() {\n            // This should not be called on the main thread.\n            try {\n                Class GPSUtil = Class\n                        .forName(\"com.google.android.gms.common.GooglePlayServicesUtil\");\n                Method getGPSAvailable = GPSUtil.getMethod(\"isGooglePlayServicesAvailable\",\n                        Context.class);\n                Integer status = (Integer) getGPSAvailable.invoke(null, context);\n                // status 0 corresponds to com.google.android.gms.common.ConnectionResult.SUCCESS;\n                return status != null && status.intValue() == 0;\n            } catch (NoClassDefFoundError e) {\n                AmplitudeLog.getLogger().w(TAG, \"Google Play Services Util not found!\");\n            } catch (ClassNotFoundException e) {\n                AmplitudeLog.getLogger().w(TAG, \"Google Play Services Util not found!\");\n            } catch (NoSuchMethodException e) {\n                AmplitudeLog.getLogger().w(TAG, \"Google Play Services not available\");\n            } catch (InvocationTargetException e) {\n                AmplitudeLog.getLogger().w(TAG, \"Google Play Services not available\");\n            } catch (IllegalAccessException e) {\n                AmplitudeLog.getLogger().w(TAG, \"Google Play Services not available\");\n            } catch (Exception e) {\n                AmplitudeLog.getLogger().w(TAG,\n                        \"Error when checking for Google Play Services: \" + e);\n            }\n            return false;\n        }\n    }\n\n    public DeviceInfo(Context context, boolean locationListening, boolean shouldTrackAdid) {\n        this.context = context;\n        this.locationListening = locationListening;\n        this.shouldTrackAdid = shouldTrackAdid;\n    }\n\n    private CachedInfo getCachedInfo() {\n        if (cachedInfo == null) {\n            cachedInfo = new CachedInfo();\n        }\n        return cachedInfo;\n    }\n\n    public void prefetch() {\n        getCachedInfo();\n    }\n\n    public static String generateUUID() {\n        return UUID.randomUUID().toString();\n    }\n\n    public String getVersionName() {\n        return getCachedInfo().versionName;\n    }\n\n    public String getOsName() {\n        return getCachedInfo().osName;\n    }\n\n    public String getOsVersion() {\n        return getCachedInfo().osVersion;\n    }\n\n    public String getBrand() {\n        return getCachedInfo().brand;\n    }\n\n    public String getManufacturer() {\n        return getCachedInfo().manufacturer;\n    }\n\n    public String getModel() {\n        return getCachedInfo().model;\n    }\n\n    public String getCarrier() {\n        return getCachedInfo().carrier;\n    }\n\n    public String getCountry() {\n        return getCachedInfo().country;\n    }\n\n    public String getLanguage() {\n        return getCachedInfo().language;\n    }\n\n    public String getAdvertisingId() {\n        return getCachedInfo().advertisingId;\n    }\n\n    public boolean isLimitAdTrackingEnabled() {\n        return getCachedInfo().limitAdTrackingEnabled;\n    }\n\n    public String getAppSetId() {\n        return getCachedInfo().appSetId;\n    }\n\n    public boolean isGooglePlayServicesEnabled() { return getCachedInfo().gpsEnabled; }\n\n    public Location getMostRecentLocation() {\n        if (!isLocationListening()) {\n            return null;\n        }\n\n        if (!Utils.checkLocationPermissionAllowed(context)) {\n            return null;\n        }\n\n        LocationManager locationManager = (LocationManager) context\n                .getSystemService(Context.LOCATION_SERVICE);\n\n        // Don't crash if the device does not have location services.\n        if (locationManager == null) {\n            return null;\n        }\n\n        // It's possible that the location service is running out of process\n        // and the remote getProviders call fails. Handle null provider lists.\n        List<String> providers = null;\n        try {\n            providers = locationManager.getProviders(true);\n        } catch (SecurityException e) {\n            // failed to get providers list\n        } catch (Exception e) {\n            // other causes\n        }\n        if (providers == null) {\n            return null;\n        }\n\n        List<Location> locations = new ArrayList<Location>();\n        for (String provider : providers) {\n            Location location = null;\n            try {\n                location = locationManager.getLastKnownLocation(provider);\n            } catch (SecurityException e) {\n                AmplitudeLog.getLogger().w(TAG, \"Failed to get most recent location\");\n            } catch (Exception e) {\n                AmplitudeLog.getLogger().w(TAG, \"Failed to get most recent location\");\n            }\n            if (location != null) {\n                locations.add(location);\n            }\n        }\n\n        long maximumTimestamp = -1;\n        Location bestLocation = null;\n        for (Location location : locations) {\n            if (location.getTime() > maximumTimestamp) {\n                maximumTimestamp = location.getTime();\n                bestLocation = location;\n            }\n        }\n\n        return bestLocation;\n    }\n\n    public boolean isLocationListening() {\n        return locationListening;\n    }\n\n    public void setLocationListening(boolean locationListening) {\n        this.locationListening = locationListening;\n    }\n\n    // @VisibleForTesting\n    protected Geocoder getGeocoder() {\n        return new Geocoder(context, Locale.ENGLISH);\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/api/Identify.java",
    "content": "package com.amplitude.api;\n\nimport org.json.JSONArray;\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport java.util.HashSet;\nimport java.util.Set;\n\n/**\n *  <h1>Identify</h1>\n *  Identify objects are a wrapper for user property operations. Each method adds a user\n *  property operation to the Identify object, and returns the same Identify object, allowing\n *  you to chain multiple method calls together, for example:\n *  {@code Identify identify = new Identify().set(\"color\", \"green\").add(\"karma\", 1);}\n *  <br><br>\n *  <b>Note:</b> if the same user property is used\n *  in multiple operations on a single Identify object, only the first operation on that\n *  property will be saved, and the rest will be ignored.\n *  <br><br>\n *  After creating an Identify object and setting the desired operations, send it to Amplitude\n *  servers by calling {@code Amplitude.getInstance().identify(identify);} and pass in the object.\n *\n *  @see <a href=\"https://github.com/amplitude/Amplitude-Android#user-properties-and-user-property-operations\">\n *      Android SDK README</a> for more information on the Identify API and user property operations.\n */\npublic class Identify {\n\n    /**\n     * The class identifier tag used in logging. TAG = {@code \"com.amplitude.api.Identify\";}\n     */\n    private static final String TAG = Identify.class.getName();\n\n    /**\n     * Internal {@code JSONObject} to hold all of the user property operations.\n     */\n    protected JSONObject userPropertiesOperations = new JSONObject();\n    /**\n     * Internal set to keep track of user property keys and test for duplicates.\n     */\n    protected Set<String> userProperties = new HashSet<String>();\n\n    /**\n     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user\n     * property will be ignored.\n     *\n     * @param property the user property to setOnce\n     * @param value    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify setOnce(String property, boolean value) {\n        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, value);\n        return this;\n    }\n\n    /**\n     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user\n     * property will be ignored.\n     *\n     * @param property the user property to setOnce\n     * @param value    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify setOnce(String property, double value) {\n        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, value);\n        return this;\n    }\n\n    /**\n     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user\n     * property will be ignored.\n     *\n     * @param property the user property to setOnce\n     * @param value    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify setOnce(String property, float value) {\n        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, value);\n        return this;\n    }\n\n    /**\n     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user\n     * property will be ignored.\n     *\n     * @param property the user property to setOnce\n     * @param value    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify setOnce(String property, int value) {\n        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, value);\n        return this;\n    }\n\n    /**\n     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user\n     * property will be ignored.\n     *\n     * @param property the user property to setOnce\n     * @param value    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify setOnce(String property, long value) {\n        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, value);\n        return this;\n    }\n\n    /**\n     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user\n     * property will be ignored.\n     *\n     * @param property the user property to setOnce\n     * @param value    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify setOnce(String property, String value) {\n        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, value);\n        return this;\n    }\n\n    /**\n     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user\n     * property will be ignored.\n     *\n     * @param property the user property to setOnce\n     * @param values    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify setOnce(String property, JSONArray values) {\n        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, values);\n        return this;\n    }\n\n    /**\n     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user\n     * property will be ignored.\n     *\n     * @param property the user property to setOnce\n     * @param values    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify setOnce(String property, JSONObject values) {\n        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, values);\n        return this;\n    }\n\n    /**\n     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user\n     * property will be ignored.\n     *\n     * @param property the user property to setOnce\n     * @param values    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify setOnce(String property, boolean[] values) {\n        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, booleanArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user\n     * property will be ignored.\n     *\n     * @param property the user property to setOnce\n     * @param values    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify setOnce(String property, double[] values) {\n        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, doubleArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user\n     * property will be ignored.\n     *\n     * @param property the user property to setOnce\n     * @param values    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify setOnce(String property, float[] values) {\n        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, floatArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user\n     * property will be ignored.\n     *\n     * @param property the user property to setOnce\n     * @param values    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify setOnce(String property, int[] values) {\n        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, intArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user\n     * property will be ignored.\n     *\n     * @param property the user property to setOnce\n     * @param values    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify setOnce(String property, long[] values) {\n        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, longArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user\n     * property will be ignored.\n     *\n     * @param property the user property to setOnce\n     * @param values    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify setOnce(String property, String[] values) {\n        addToUserProperties(Constants.AMP_OP_SET_ONCE, property, stringArrayToJSONArray(values));\n        return this;\n    }\n\n\n    /**\n     * Sets a user property value. Existing values for that user property will be overwritten.\n     *\n     * @param property the user property to set\n     * @param value    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify set(String property, boolean value) {\n        addToUserProperties(Constants.AMP_OP_SET, property, value);\n        return this;\n    }\n\n    /**\n     * Sets a user property value. Existing values for that user property will be overwritten.\n     *\n     * @param property the user property to set\n     * @param value    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify set(String property, double value) {\n        addToUserProperties(Constants.AMP_OP_SET, property, value);\n        return this;\n    }\n\n    /**\n     * Sets a user property value. Existing values for that user property will be overwritten.\n     *\n     * @param property the user property to set\n     * @param value    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify set(String property, float value) {\n        addToUserProperties(Constants.AMP_OP_SET, property, value);\n        return this;\n    }\n\n    /**\n     * Sets a user property value. Existing values for that user property will be overwritten.\n     *\n     * @param property the user property to set\n     * @param value    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify set(String property, int value) {\n        addToUserProperties(Constants.AMP_OP_SET, property, value);\n        return this;\n    }\n\n    /**\n     * Sets a user property value. Existing values for that user property will be overwritten.\n     *\n     * @param property the user property to set\n     * @param value    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify set(String property, long value) {\n        addToUserProperties(Constants.AMP_OP_SET, property, value);\n        return this;\n    }\n\n    /**\n     * Sets a user property value. Existing values for that user property will be overwritten.\n     *\n     * @param property the user property to set\n     * @param value    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify set(String property, String value) {\n        addToUserProperties(Constants.AMP_OP_SET, property, value);\n        return this;\n    }\n\n    /**\n     * Sets a user property value. Existing values for that user property will be overwritten.\n     *\n     * @param property the user property to set\n     * @param values    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify set(String property, JSONObject values) {\n        addToUserProperties(Constants.AMP_OP_SET, property, values);\n        return this;\n    }\n\n    /**\n     * Sets a user property value. Existing values for that user property will be overwritten.\n     *\n     * @param property the user property to set\n     * @param values    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify set(String property, JSONArray values) {\n        addToUserProperties(Constants.AMP_OP_SET, property, values);\n        return this;\n    }\n\n    /**\n     * Sets a user property value. Existing values for that user property will be overwritten.\n     *\n     * @param property the user property to set\n     * @param values    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify set(String property, boolean[] values) {\n        addToUserProperties(Constants.AMP_OP_SET, property, booleanArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Sets a user property value. Existing values for that user property will be overwritten.\n     *\n     * @param property the user property to set\n     * @param values    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify set(String property, double[] values) {\n        addToUserProperties(Constants.AMP_OP_SET, property, doubleArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Sets a user property value. Existing values for that user property will be overwritten.\n     *\n     * @param property the user property to set\n     * @param values    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify set(String property, float[] values) {\n        addToUserProperties(Constants.AMP_OP_SET, property, floatArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Sets a user property value. Existing values for that user property will be overwritten.\n     *\n     * @param property the user property to set\n     * @param values    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify set(String property, int[] values) {\n        addToUserProperties(Constants.AMP_OP_SET, property, intArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Sets a user property value. Existing values for that user property will be overwritten.\n     *\n     * @param property the user property to set\n     * @param values    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify set(String property, long[] values) {\n        addToUserProperties(Constants.AMP_OP_SET, property, longArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Sets a user property value. Existing values for that user property will be overwritten.\n     *\n     * @param property the user property to set\n     * @param values    the value of the user property\n     * @return the same Identify object\n     */\n    public Identify set(String property, String[] values) {\n        addToUserProperties(Constants.AMP_OP_SET, property, stringArrayToJSONArray(values));\n        return this;\n    }\n\n\n    /**\n     * Increment a user property by some numerical value. If the user property does not have\n     * a value set, it will be initialized to 0 before being incremented. Value can be\n     * negative to decrement a user property value.\n     *\n     * @param property the user property to increment\n     * @param value    the value (can be negative) to increment\n     * @return the same Identify object\n     */\n    public Identify add(String property, double value) {\n        addToUserProperties(Constants.AMP_OP_ADD, property, value);\n        return this;\n    }\n\n    /**\n     * Increment a user property by some numerical value. If the user property does not have\n     * a value set, it will be initialized to 0 before being incremented. Value can be\n     * negative to decrement a user property value.\n     *\n     * @param property the user property to increment\n     * @param value    the value (can be negative) to increment\n     * @return the same Identify object\n     */\n    public Identify add(String property, float value) {\n        addToUserProperties(Constants.AMP_OP_ADD, property, value);\n        return this;\n    }\n\n    /**\n     * Increment a user property by some numerical value. If the user property does not have\n     * a value set, it will be initialized to 0 before being incremented. Value can be\n     * negative to decrement a user property value.\n     *\n     * @param property the user property to increment\n     * @param value    the value (can be negative) to increment\n     * @return the same Identify object\n     */\n    public Identify add(String property, int value) {\n        addToUserProperties(Constants.AMP_OP_ADD, property, value);\n        return this;\n    }\n\n    /**\n     * Increment a user property by some numerical value. If the user property does not have\n     * a value set, it will be initialized to 0 before being incremented. Value can be\n     * negative to decrement a user property value.\n     *\n     * @param property the user property to increment\n     * @param value    the value (can be negative) to increment\n     * @return the same Identify object\n     */\n    public Identify add(String property, long value) {\n        addToUserProperties(Constants.AMP_OP_ADD, property, value);\n        return this;\n    }\n\n    /**\n     * Increment a user property by some numerical value. If the user property does not have\n     * a value set, it will be initialized to 0 before being incremented. Value can be\n     * negative to decrement a user property value.\n     *\n     * @param property the user property to increment\n     * @param value    the value (can be negative) to increment. Server-side we convert\n     *                 the string into a number if possible.\n     * @return the same Identify object\n     */\n    public Identify add(String property, String value) {\n        addToUserProperties(Constants.AMP_OP_ADD, property, value);\n        return this;\n    }\n\n    /**\n     * Increment a user property by some numerical value. If the user property does not have\n     * a value set, it will be initialized to 0 before being incremented. Value can be\n     * negative to decrement a user property value.\n     *\n     * @param property the user property to increment\n     * @param values    the value (can be negative) to increment. Server-side we flatten\n     *                 dictionaries and apply add to each flattened property value.\n     * @return the same Identify object\n     */\n    public Identify add(String property, JSONObject values) {\n        addToUserProperties(Constants.AMP_OP_ADD, property, values);\n        return this;\n    }\n\n\n    /**\n     * Append a value or values to a user property. If the user property does not have a value\n     * set, it will be initialized to an empty list before the new values are appended. If\n     * the user property has an existing value and it is not a list, it will be converted into\n     * a list with the new value(s) appended.\n     *\n     * @param property the user property property to which to append\n     * @param value    the value being appended\n     * @return the same Identify object\n     */\n    public Identify append(String property, boolean value) {\n        addToUserProperties(Constants.AMP_OP_APPEND, property, value);\n        return this;\n    }\n\n    /**\n     * Append a value or values to a user property. If the user property does not have a value\n     * set, it will be initialized to an empty list before the new values are appended. If\n     * the user property has an existing value and it is not a list, it will be converted into\n     * a list with the new value(s) appended.\n     *\n     * @param property the user property property to which to append\n     * @param value    the value being appended\n     * @return the same Identify object\n     */\n    public Identify append(String property, double value) {\n        addToUserProperties(Constants.AMP_OP_APPEND, property, value);\n        return this;\n    }\n\n    /**\n     * Append a value or values to a user property. If the user property does not have a value\n     * set, it will be initialized to an empty list before the new values are appended. If\n     * the user property has an existing value and it is not a list, it will be converted into\n     * a list with the new value(s) appended.\n     *\n     * @param property the user property property to which to append\n     * @param value    the value being appended\n     * @return the same Identify object\n     */\n    public Identify append(String property, float value) {\n        addToUserProperties(Constants.AMP_OP_APPEND, property, value);\n        return this;\n    }\n\n    /**\n     * Append a value or values to a user property. If the user property does not have a value\n     * set, it will be initialized to an empty list before the new values are appended. If\n     * the user property has an existing value and it is not a list, it will be converted into\n     * a list with the new value(s) appended.\n     *\n     * @param property the user property property to which to append\n     * @param value    the value being appended\n     * @return the same Identify object\n     */\n    public Identify append(String property, int value) {\n        addToUserProperties(Constants.AMP_OP_APPEND, property, value);\n        return this;\n    }\n\n    /**\n     * Append a value or values to a user property. If the user property does not have a value\n     * set, it will be initialized to an empty list before the new values are appended. If\n     * the user property has an existing value and it is not a list, it will be converted into\n     * a list with the new value(s) appended.\n     *\n     * @param property the user property property to which to append\n     * @param value    the value being appended\n     * @return the same Identify object\n     */\n    public Identify append(String property, long value) {\n        addToUserProperties(Constants.AMP_OP_APPEND, property, value);\n        return this;\n    }\n\n    /**\n     * Append a value or values to a user property. If the user property does not have a value\n     * set, it will be initialized to an empty list before the new values are appended. If\n     * the user property has an existing value and it is not a list, it will be converted into\n     * a list with the new value(s) appended.\n     *\n     * @param property the user property property to which to append\n     * @param value    the value being appended\n     * @return the same Identify object\n     */\n    public Identify append(String property, String value) {\n        addToUserProperties(Constants.AMP_OP_APPEND, property, value);\n        return this;\n    }\n\n    /**\n     * Append a value or values to a user property. If the user property does not have a value\n     * set, it will be initialized to an empty list before the new values are appended. If\n     * the user property has an existing value and it is not a list, it will be converted into\n     * a list with the new value(s) appended.\n     *\n     * @param property the user property property to which to append\n     * @param values    the values being appended\n     * @return the same Identify object\n     */\n    public Identify append(String property, JSONArray values) {\n        addToUserProperties(Constants.AMP_OP_APPEND, property, values);\n        return this;\n    }\n\n    /**\n     * Append a value or values to a user property. If the user property does not have a value\n     * set, it will be initialized to an empty list before the new values are appended. If\n     * the user property has an existing value and it is not a list, it will be converted into\n     * a list with the new value(s) appended.\n     *\n     * @param property the user property property to which to append\n     * @param values    the values being appended. Server-side we flatten dictionaries and apply\n     *                  append to each flattened property.\n     * @return the same Identify object\n     */\n    public Identify append(String property, JSONObject values) {\n        addToUserProperties(Constants.AMP_OP_APPEND, property, values);\n        return this;\n    }\n\n    /**\n     * Append a value or values to a user property. If the user property does not have a value\n     * set, it will be initialized to an empty list before the new values are appended. If\n     * the user property has an existing value and it is not a list, it will be converted into\n     * a list with the new value(s) appended.\n     *\n     * @param property the user property property to which to append\n     * @param values    the values being appended\n     * @return the same Identify object\n     */\n    public Identify append(String property, boolean[] values) {\n        addToUserProperties(Constants.AMP_OP_APPEND, property, booleanArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Append a value or values to a user property. If the user property does not have a value\n     * set, it will be initialized to an empty list before the new values are appended. If\n     * the user property has an existing value and it is not a list, it will be converted into\n     * a list with the new value(s) appended.\n     *\n     * @param property the user property property to which to append\n     * @param values    the values being appended\n     * @return the same Identify object\n     */\n    public Identify append(String property, double[] values) {\n        addToUserProperties(Constants.AMP_OP_APPEND, property, doubleArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Append a value or values to a user property. If the user property does not have a value\n     * set, it will be initialized to an empty list before the new values are appended. If\n     * the user property has an existing value and it is not a list, it will be converted into\n     * a list with the new value(s) appended.\n     *\n     * @param property the user property property to which to append\n     * @param values    the values being appended\n     * @return the same Identify object\n     */\n    public Identify append(String property, float[] values) {\n        addToUserProperties(Constants.AMP_OP_APPEND, property, floatArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Append a value or values to a user property. If the user property does not have a value\n     * set, it will be initialized to an empty list before the new values are appended. If\n     * the user property has an existing value and it is not a list, it will be converted into\n     * a list with the new value(s) appended.\n     *\n     * @param property the user property property to which to append\n     * @param values    the values being appended\n     * @return the same Identify object\n     */\n    public Identify append(String property, int[] values) {\n        addToUserProperties(Constants.AMP_OP_APPEND, property, intArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Append a value or values to a user property. If the user property does not have a value\n     * set, it will be initialized to an empty list before the new values are appended. If\n     * the user property has an existing value and it is not a list, it will be converted into\n     * a list with the new value(s) appended.\n     *\n     * @param property the user property property to which to append\n     * @param values    the values being appended\n     * @return the same Identify object\n     */\n    public Identify append(String property, long[] values) {\n        addToUserProperties(Constants.AMP_OP_APPEND, property, longArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Append a value or values to a user property. If the user property does not have a value\n     * set, it will be initialized to an empty list before the new values are appended. If\n     * the user property has an existing value and it is not a list, it will be converted into\n     * a list with the new value(s) appended.\n     *\n     * @param property the user property property to which to append\n     * @param values    the values being appended\n     * @return the same Identify object\n     */\n    public Identify append(String property, String[] values) {\n        addToUserProperties(Constants.AMP_OP_APPEND, property, stringArrayToJSONArray(values));\n        return this;\n    }\n\n\n    /**\n     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) prepended.\n     *\n     * @param property the user property to which to append\n     * @param value    the value being prepended\n     * @return the same Identify object\n     */\n    public Identify prepend(String property, boolean value) {\n        addToUserProperties(Constants.AMP_OP_PREPEND, property, value);\n        return this;\n    }\n\n    /**\n     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) prepended.\n     *\n     * @param property the user property to which to append\n     * @param value    the value being prepended\n     * @return the same Identify object\n     */\n    public Identify prepend(String property, double value) {\n        addToUserProperties(Constants.AMP_OP_PREPEND, property, value);\n        return this;\n    }\n\n    /**\n     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) prepended.\n     *\n     * @param property the user property to which to append\n     * @param value    the value being prepended\n     * @return the same Identify object\n     */\n    public Identify prepend(String property, float value) {\n        addToUserProperties(Constants.AMP_OP_PREPEND, property, value);\n        return this;\n    }\n\n    /**\n     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) prepended.\n     *\n     * @param property the user property to which to append\n     * @param value    the value being prepended\n     * @return the same Identify object\n     */\n    public Identify prepend(String property, int value) {\n        addToUserProperties(Constants.AMP_OP_PREPEND, property, value);\n        return this;\n    }\n\n    /**\n     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) prepended.\n     *\n     * @param property the user property to which to append\n     * @param value    the value being prepended\n     * @return the same Identify object\n     */\n    public Identify prepend(String property, long value) {\n        addToUserProperties(Constants.AMP_OP_PREPEND, property, value);\n        return this;\n    }\n\n    /**\n     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) prepended.\n     *\n     * @param property the user property to which to append\n     * @param value    the value being prepended\n     * @return the same Identify object\n     */\n    public Identify prepend(String property, String value) {\n        addToUserProperties(Constants.AMP_OP_PREPEND, property, value);\n        return this;\n    }\n\n    /**\n     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) prepended.\n     *\n     * @param property the user property to which to append\n     * @param values    the value being prepended\n     * @return the same Identify object\n     */\n    public Identify prepend(String property, JSONArray values) {\n        addToUserProperties(Constants.AMP_OP_PREPEND, property, values);\n        return this;\n    }\n\n    /**\n     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) prepended.\n     *\n     * @param property the user property to which to append\n     * @param values    the values being prepended. Server-side we flatten dictionaries and apply\n     *                  prepend to each flattened property.\n     * @return the same Identify object\n     */\n    public Identify prepend(String property, JSONObject values) {\n        addToUserProperties(Constants.AMP_OP_PREPEND, property, values);\n        return this;\n    }\n\n    /**\n     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) prepended.\n     *\n     * @param property the user property to which to append\n     * @param values    the values being prepended\n     * @return the same Identify object\n     */\n    public Identify prepend(String property, boolean[] values) {\n        addToUserProperties(Constants.AMP_OP_PREPEND, property, booleanArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) prepended.\n     *\n     * @param property the user property to which to append\n     * @param values    the values being prepended\n     * @return the same Identify object\n     */\n    public Identify prepend(String property, double[] values) {\n        addToUserProperties(Constants.AMP_OP_PREPEND, property, doubleArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) prepended.\n     *\n     * @param property the user property to which to append\n     * @param values    the values being prepended\n     * @return the same Identify object\n     */\n    public Identify prepend(String property, float[] values) {\n        addToUserProperties(Constants.AMP_OP_PREPEND, property, floatArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) prepended.\n     *\n     * @param property the user property to which to append\n     * @param values    the values being prepended\n     * @return the same Identify object\n     */\n    public Identify prepend(String property, int[] values) {\n        addToUserProperties(Constants.AMP_OP_PREPEND, property, intArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) prepended.\n     *\n     * @param property the user property to which to append\n     * @param values    the values being prepended\n     * @return the same Identify object\n     */\n    public Identify prepend(String property, long[] values) {\n        addToUserProperties(Constants.AMP_OP_PREPEND, property, longArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Prepend a value or values to a user property. Prepend means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) prepended.\n     *\n     * @param property the user property to which to append\n     * @param values    the values being prepended\n     * @return the same Identify object\n     */\n    public Identify prepend(String property, String[] values) {\n        addToUserProperties(Constants.AMP_OP_PREPEND, property, stringArrayToJSONArray(values));\n        return this;\n    }\n\n\n    /**\n     * Unset and remove a user property.\n     *\n     * @param property the user property to unset and remove.\n     * @return the same Identify object\n     */\n    public Identify unset(String property) {\n        addToUserProperties(Constants.AMP_OP_UNSET, property, \"-\");\n        return this;\n    }\n\n\n    /**\n     * Clear all user properties. <b>Note:</b> the result is irreversible! <b>Also Note:</b>\n     * clearAll needs to be be sent on its own Identify object without any other operations.\n     *\n     * @return the same Identify object.\n     */\n    public Identify clearAll() {\n        if (userPropertiesOperations.length() > 0) {\n            if (!userProperties.contains(Constants.AMP_OP_CLEAR_ALL)) {\n                AmplitudeLog.getLogger().w(TAG, String.format(\n                   \"Need to send $clearAll on its own Identify object without any other \" +\n                   \"operations, ignoring $clearAll\"\n                ));\n            }\n            return this;\n        }\n\n        try {\n            userPropertiesOperations.put(Constants.AMP_OP_CLEAR_ALL, \"-\");\n        } catch (JSONException e) {\n            AmplitudeLog.getLogger().e(TAG, e.toString());\n        }\n        return this;\n    }\n\n    /**\n     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to preInsert\n     * @param value    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify preInsert(String property, boolean value) {\n        addToUserProperties(Constants.AMP_OP_PREINSERT, property, value);\n        return this;\n    }\n\n    /**\n     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to preInsert\n     * @param value    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify preInsert(String property, double value) {\n        addToUserProperties(Constants.AMP_OP_PREINSERT, property, value);\n        return this;\n    }\n\n    /**\n     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to preInsert\n     * @param value    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify preInsert(String property, float value) {\n        addToUserProperties(Constants.AMP_OP_PREINSERT, property, value);\n        return this;\n    }\n\n    /**\n     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to preInsert\n     * @param value    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify preInsert(String property, int value) {\n        addToUserProperties(Constants.AMP_OP_PREINSERT, property, value);\n        return this;\n    }\n\n    /**\n     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to preInsert\n     * @param value    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify preInsert(String property, long value) {\n        addToUserProperties(Constants.AMP_OP_PREINSERT, property, value);\n        return this;\n    }\n\n    /**\n     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to preInsert\n     * @param value    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify preInsert(String property, String value) {\n        addToUserProperties(Constants.AMP_OP_PREINSERT, property, value);\n        return this;\n    }\n\n    /**\n     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to preInsert\n     * @param values    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify preInsert(String property, JSONArray values) {\n        addToUserProperties(Constants.AMP_OP_PREINSERT, property, values);\n        return this;\n    }\n\n    /**\n     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to preInsert\n     * @param values    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify preInsert(String property, JSONObject values) {\n        addToUserProperties(Constants.AMP_OP_PREINSERT, property, values);\n        return this;\n    }\n\n\n    /**\n     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to preInsert\n     * @param values    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify preInsert(String property, boolean[] values) {\n        addToUserProperties(Constants.AMP_OP_PREINSERT, property, booleanArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to preInsert\n     * @param values    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify preInsert(String property, double[] values) {\n        addToUserProperties(Constants.AMP_OP_PREINSERT, property, doubleArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to preInsert\n     * @param values    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify preInsert(String property, float[] values){\n        addToUserProperties(Constants.AMP_OP_PREINSERT, property, floatArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to preInsert\n     * @param values    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify preInsert(String property, int[] values){\n        addToUserProperties(Constants.AMP_OP_PREINSERT, property, intArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to preInsert\n     * @param values    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify preInsert(String property, long[] values) {\n        addToUserProperties(Constants.AMP_OP_PREINSERT, property, longArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the\n     * front of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) pre-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to preInsert\n     * @param values    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify preInsert(String property, String[] values) {\n        addToUserProperties(Constants.AMP_OP_PREINSERT, property, stringArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the\n     * end of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) post-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to postInsert\n     * @param value    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify postInsert(String property, boolean value) {\n        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, value);\n        return this;\n    }\n\n    /**\n     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the\n     * end of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) post-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to postInsert \n     * @param value    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify postInsert(String property, double value) {\n        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, value);\n        return this;\n    }\n\n    /**\n     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the\n     * end of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) post-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to postInsert\n     * @param value    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify postInsert(String property, float value) {\n        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, value);\n        return this;\n    }\n\n    /**\n     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the\n     * end of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) post-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to postInsert\n     * @param value    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify postInsert(String property, int value) {\n        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, value);\n        return this;\n    }\n\n    /**\n     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the\n     * end of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) post-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to postInsert\n     * @param value    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify postInsert(String property, long value) {\n        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, value);\n        return this;\n    }\n\n    /**\n     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the\n     * end of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) post-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to postInsert\n     * @param value    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify postInsert(String property, String value) {\n        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, value);\n        return this;\n    }\n\n    /**\n     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the\n     * end of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) post-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to postInsert\n     * @param values    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify postInsert(String property, JSONArray values) {\n        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, values);\n        return this;\n    }\n\n    /**\n     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the\n     * end of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) post-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to postInsert\n     * @param values    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify postInsert(String property, JSONObject values) {\n        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, values);\n        return this;\n    }\n\n    /**\n     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the\n     * end of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) post-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to postInsert\n     * @param values    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify postInsert(String property, boolean[] values) {\n        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, booleanArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the\n     * end of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) post-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to postInsert\n     * @param values    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify postInsert(String property, double[] values) {\n        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, doubleArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the\n     * end of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) post-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to postInsert\n     * @param values    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify postInsert(String property, float[] values){\n        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, floatArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the\n     * end of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) post-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to postInsert\n     * @param values    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify postInsert(String property, int[] values){\n        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, intArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the\n     * end of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) post-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to postInsert\n     * @param values    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify postInsert(String property, long[] values) {\n        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, longArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the\n     * end of a given list. if the user property does not have a value set, it will be\n     * initialized to an empty list before the new values are prepended. If the user property\n     * has an existing value and it is not a list, it will be converted into a list with the\n     * new value(s) post-insert. If the user property has an existing value, it will do no operation.\n     *\n     * @param property the user property to which to postInsert\n     * @param values    the values being preInsert\n     * @return the same Identify object\n     */\n    public Identify postInsert(String property, String[] values) {\n        addToUserProperties(Constants.AMP_OP_POSTINSERT, property, stringArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.\n     * If the user property has the matching value, it will remove that value from the given list.\n     * If the user property does not have that value set, it will do no operation.\n     *\n     * @param property the user property to which to remove\n     * @param value    the values being remove\n     * @return the same Identify object\n     */\n    public Identify remove(String property, boolean value) {\n        addToUserProperties(Constants.AMP_OP_REMOVE, property, value);\n        return this;\n    }\n\n    /**\n     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.\n     * If the user property has the matching value, it will remove that value from the given list.\n     * If the user property does not have that value set, it will do no operation.\n     *\n     * @param property the user property to which to remove\n     * @param value    the values being remove\n     * @return the same Identify object\n     */\n    public Identify remove(String property, double value) {\n        addToUserProperties(Constants.AMP_OP_REMOVE, property, value);\n        return this;\n    }\n\n    /**\n     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.\n     * If the user property has the matching value, it will remove that value from the given list.\n     * If the user property does not have that value set, it will do no operation.\n     *\n     * @param property the user property to which to remove\n     * @param value    the values being remove\n     * @return the same Identify object\n     */\n    public Identify remove(String property, float value) {\n        addToUserProperties(Constants.AMP_OP_REMOVE, property, value);\n        return this;\n    }\n\n    /**\n     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.\n     * If the user property has the matching value, it will remove that value from the given list.\n     * If the user property does not have that value set, it will do no operation.\n     *\n     * @param property the user property to which to remove\n     * @param value    the values being remove\n     * @return the same Identify object\n     */\n    public Identify remove(String property, int value) {\n        addToUserProperties(Constants.AMP_OP_REMOVE, property, value);\n        return this;\n    }\n\n    /**\n     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.\n     * If the user property has the matching value, it will remove that value from the given list.\n     * If the user property does not have that value set, it will do no operation.\n     *\n     * @param property the user property to which to remove\n     * @param value    the values being remove\n     * @return the same Identify object\n     */\n    public Identify remove(String property, long value) {\n        addToUserProperties(Constants.AMP_OP_REMOVE, property, value);\n        return this;\n    }\n\n    /**\n     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.\n     * If the user property has the matching value, it will remove that value from the given list.\n     * If the user property does not have that value set, it will do no operation.\n     *\n     * @param property the user property to which to remove\n     * @param value    the values being remove\n     * @return the same Identify object\n     */\n    public Identify remove(String property, String value) {\n        addToUserProperties(Constants.AMP_OP_REMOVE, property, value);\n        return this;\n    }\n\n    /**\n     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.\n     * If the user property has the matching value, it will remove that value from the given list.\n     * If the user property does not have that value set, it will do no operation.\n     *\n     * @param property the user property to which to remove\n     * @param values    the values being remove\n     * @return the same Identify object\n     */\n    public Identify remove(String property, JSONArray values) {\n        addToUserProperties(Constants.AMP_OP_REMOVE, property, values);\n        return this;\n    }\n\n    /**\n     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.\n     * If the user property has the matching value, it will remove that value from the given list.\n     * If the user property does not have that value set, it will do no operation.\n     *\n     * @param property the user property to which to remove\n     * @param values    the values being remove\n     * @return the same Identify object\n     */\n    public Identify remove(String property, JSONObject values) {\n        addToUserProperties(Constants.AMP_OP_REMOVE, property, values);\n        return this;\n    }\n\n    /**\n     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.\n     * If the user property has the matching value, it will remove that value from the given list.\n     * If the user property does not have that value set, it will do no operation.\n     *\n     * @param property the user property to which to remove\n     * @param values    the values being remove\n     * @return the same Identify object\n     */\n    public Identify remove(String property, boolean[] values) {\n        addToUserProperties(Constants.AMP_OP_REMOVE, property, booleanArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.\n     * If the user property has the matching value, it will remove that value from the given list.\n     * If the user property does not have that value set, it will do no operation.\n     *\n     * @param property the user property to which to remove\n     * @param values    the values being remove\n     * @return the same Identify object\n     */\n    public Identify remove(String property, double[] values) {\n        addToUserProperties(Constants.AMP_OP_REMOVE, property, doubleArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.\n     * If the user property has the matching value, it will remove that value from the given list.\n     * If the user property does not have that value set, it will do no operation.\n     *\n     * @param property the user property to which to remove\n     * @param values    the values being remove\n     * @return the same Identify object\n     */\n    public Identify remove(String property, float[] values){\n        addToUserProperties(Constants.AMP_OP_REMOVE, property, floatArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.\n     * If the user property has the matching value, it will remove that value from the given list.\n     * If the user property does not have that value set, it will do no operation.\n     *\n     * @param property the user property to which to remove\n     * @param values    the values being remove\n     * @return the same Identify object\n     */\n    public Identify remove(String property, int[] values){\n        addToUserProperties(Constants.AMP_OP_REMOVE, property, intArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.\n     * If the user property has the matching value, it will remove that value from the given list.\n     * If the user property does not have that value set, it will do no operation.\n     *\n     * @param property the user property to which to remove\n     * @param values    the values being remove\n     * @return the same Identify object\n     */\n    public Identify remove(String property, long[] values) {\n        addToUserProperties(Constants.AMP_OP_REMOVE, property, longArrayToJSONArray(values));\n        return this;\n    }\n\n    /**\n     * Remove a value or values to a user property. Remove means remove the value(s) from a given list.\n     * If the user property has the matching value, it will remove that value from the given list.\n     * If the user property does not have that value set, it will do no operation.\n     *\n     * @param property the user property to which to remove\n     * @param values    the values being remove\n     * @return the same Identify object\n     */\n    public Identify remove(String property, String[] values) {\n        addToUserProperties(Constants.AMP_OP_REMOVE, property, stringArrayToJSONArray(values));\n        return this;\n    }\n\n    private void addToUserProperties(String operation, String property, Object value) {\n        if (Utils.isEmptyString(property)) {\n            AmplitudeLog.getLogger().w(TAG, String.format(\n               \"Attempting to perform operation %s with a null or empty string property, ignoring\",\n                operation\n            ));\n            return;\n        }\n\n        if (value == null) {\n            AmplitudeLog.getLogger().w(TAG, String.format(\n                \"Attempting to perform operation %s with null value for property %s, ignoring\",\n                operation, property\n            ));\n            return;\n        }\n\n        // check that clearAll wasn't already used in this Identify\n        if (userPropertiesOperations.has(Constants.AMP_OP_CLEAR_ALL)) {\n            AmplitudeLog.getLogger().w(TAG, String.format(\n                \"This Identify already contains a $clearAll operation, ignoring operation %s\",\n                operation\n            ));\n            return;\n        }\n\n        // check if property already used in previous operation\n        if (userProperties.contains(property)) {\n            AmplitudeLog.getLogger().w(TAG, String.format(\n                \"Already used property %s in previous operation, ignoring operation %s\",\n                property, operation\n            ));\n            return;\n        }\n\n        try {\n            if (!userPropertiesOperations.has(operation)) {\n                userPropertiesOperations.put(operation, new JSONObject());\n            }\n            userPropertiesOperations.getJSONObject(operation).put(property, value);\n            userProperties.add(property);\n        } catch (JSONException e) {\n            AmplitudeLog.getLogger().e(TAG, e.toString());\n        }\n    }\n\n    private JSONArray booleanArrayToJSONArray(boolean[] values) {\n        JSONArray array = new JSONArray();\n        for (boolean value : values) array.put(value);\n        return array;\n    }\n\n    private JSONArray floatArrayToJSONArray(float[] values) {\n        JSONArray array = new JSONArray();\n        for (float value : values) {\n            try {\n                array.put(value);\n            } catch (JSONException e) {\n                AmplitudeLog.getLogger().e(TAG, String.format(\n                    \"Error converting float %f to JSON: %s\", value, e.toString()\n                ));\n            }\n        }\n        return array;\n    }\n\n    private JSONArray doubleArrayToJSONArray(double[] values) {\n        JSONArray array = new JSONArray();\n        for (double value : values) {\n            try {\n                array.put(value);\n            } catch (JSONException e) {\n                AmplitudeLog.getLogger().e(TAG, String.format(\n                    \"Error converting double %d to JSON: %s\", value, e.toString()\n                ));\n            }\n        }\n        return array;\n    }\n\n    private JSONArray intArrayToJSONArray(int[] values) {\n        JSONArray array = new JSONArray();\n        for (int value : values) array.put(value);\n        return array;\n    }\n\n    private JSONArray longArrayToJSONArray(long[] values) {\n        JSONArray array = new JSONArray();\n        for (long value : values) array.put(value);\n        return array;\n    }\n\n    private JSONArray stringArrayToJSONArray(String[] values) {\n        JSONArray array = new JSONArray();\n        for (String value : values) array.put(value);\n        return array;\n    }\n\n    /**\n     * Sets user property.\n     *\n     * @param property the property\n     * @param value    the value\n     * @return the user property\n     */\n    Identify setUserProperty(String property, Object value) {\n        addToUserProperties(Constants.AMP_OP_SET, property, value);\n        return this;\n    }\n\n    /**\n     * Sets a user property value only once. Subsequent @{code setOnce} operations on that user\n     * property will be ignored. <b>Note:</b> this method has been deprecated. Please use one\n     * with a different signature.\n     *\n     * @param property the user property to setOnce\n     * @param value    the value of the user property\n     * @return the same Identify object\n     * @deprecated\n     */\n    public Identify setOnce(String property, Object value) {\n        AmplitudeLog.getLogger().w(\n            TAG,\n            \"This version of setOnce is deprecated. Please use one with a different signature.\"\n        );\n        return this;\n    }\n\n    /**\n     * Sets a user property value. Existing values for that user property will be overwritten.\n     * <b>Note:</b> this method has been deprecated. Please use one with a different signature.\n     *\n     * @param property the user property to set\n     * @param value    the value of the user property\n     * @return the same Identify object\n     * @deprecated\n     */\n    public Identify set(String property, Object value) {\n        AmplitudeLog.getLogger().w(\n            TAG,\n            \"This version of set is deprecated. Please use one with a different signature.\"\n        );\n        return this;\n    }\n\n    /**\n     * Public method that exposes the user property operations JSON blob.\n      * @return a copy of the User Property Operations JSONObject. If copying fails, returns\n     *      an empty JSONObject\n     */\n    public JSONObject getUserPropertiesOperations() {\n        try {\n            return new JSONObject(userPropertiesOperations.toString());\n        } catch (JSONException e) {\n            AmplitudeLog.getLogger().e(TAG, e.toString());\n        }\n        return new JSONObject();\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/api/IdentifyInterceptor.java",
    "content": "package com.amplitude.api;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\n/**\n * IdentifyInterceptor\n * This is the internal class for handling identify events intercept and  optimize identify volumes.\n */\nclass IdentifyInterceptor {\n\n    private static final String TAG = IdentifyInterceptor.class.getName();\n\n    private final DatabaseHelper dbHelper;\n\n    private final WorkerThread logThread;\n\n    private long identifyBatchIntervalMillis;\n\n    private final AtomicBoolean transferScheduled = new AtomicBoolean(false);\n\n    private long lastIdentifyInterceptorId = -1;\n\n    private final AmplitudeClient client;\n\n    private String userId;\n    private String deviceId;\n    private final AtomicBoolean identitySet = new AtomicBoolean(false);\n\n    public  IdentifyInterceptor (\n            DatabaseHelper dbHelper,\n            WorkerThread logThread,\n            long identifyBatchIntervalMillis,\n            AmplitudeClient client\n    ) {\n        this.dbHelper = dbHelper;\n        this.logThread = logThread;\n        this.identifyBatchIntervalMillis = identifyBatchIntervalMillis;\n        if (dbHelper.getIdentifyInterceptorCount() > 0) {\n            lastIdentifyInterceptorId = dbHelper.getLastIdentifyInterceptorId();\n        }\n        this.client = client;\n    }\n\n    /**\n     * Intercept the event if it is identify with set action.\n     *\n     * @param eventType the event type\n     * @param event full event data after middleware run\n     * @return event with potentially more information or null if intercepted\n     */\n    public JSONObject intercept(String eventType, JSONObject event) {\n        if (isIdentityUpdated(event)) {\n            // if userId or deviceId is updated, send out the identify for older identity\n            transferInterceptedIdentify();\n        }\n        if (eventType.equals(Constants.IDENTIFY_EVENT)) {\n            if (isSetOnly(event) && !isSetGroups(event)) {\n                // intercept and  save user properties\n                lastIdentifyInterceptorId = saveIdentifyProperties(event);\n                scheduleTransfer();\n                return null;\n            } else if(isClearAll(event)){\n                // clear existing and return event\n                dbHelper.removeIdentifyInterceptors(lastIdentifyInterceptorId);\n                return event;\n            } else {\n                // send out the identify for older identity and event\n                transferInterceptedIdentify();\n                return event;\n            }\n        } else if (eventType.equals(Constants.GROUP_IDENTIFY_EVENT)) {\n            // no op\n            return event;\n        } else {\n            // send out the identify for older identity and event\n            transferInterceptedIdentify();\n            return event;\n        }\n    }\n\n    /**\n     * Sets min time for identify batch millis.\n     *\n     * @param identifyBatchIntervalMillis the time interval for identify batch interval\n     */\n    public void setIdentifyBatchIntervalMillis(long identifyBatchIntervalMillis) {\n        this.identifyBatchIntervalMillis = identifyBatchIntervalMillis;\n    }\n\n    private JSONObject getTransferIdentifyEvent() {\n        try {\n            List<JSONObject> identifys = dbHelper.getIdentifyInterceptors(lastIdentifyInterceptorId, -1);\n            if (identifys.isEmpty()) {\n                return null;\n            }\n            JSONObject identifyEvent = identifys.get(0);\n            JSONObject identifyEventUserProperties = identifyEvent.getJSONObject(\"user_properties\").getJSONObject(Constants.AMP_OP_SET);\n            JSONObject userProperties = mergeIdentifyInterceptList(identifys.subList(1, identifys.size()));\n            mergeUserProperties(identifyEventUserProperties, userProperties);\n            identifyEvent.getJSONObject(\"user_properties\").put(Constants.AMP_OP_SET, identifyEventUserProperties);\n            dbHelper.removeIdentifyInterceptors(lastIdentifyInterceptorId);\n            return identifyEvent;\n        } catch (Throwable e) {\n            AmplitudeLog.getLogger().w(TAG, \"Identify Merge error: \" + e.getMessage());\n        }\n        return null;\n    }\n\n    private void scheduleTransfer() {\n        if (transferScheduled.getAndSet(true)) {\n            return;\n        }\n\n        logThread.postDelayed(new Runnable() {\n            @Override\n            public void run() {\n                transferScheduled.set(false);\n                transferInterceptedIdentify();\n            }\n        }, identifyBatchIntervalMillis);\n    }\n\n    public void transferInterceptedIdentify() {\n        JSONObject identifyEvent = getTransferIdentifyEvent();\n        if (identifyEvent == null) {\n            return;\n        }\n        client.saveEvent(Constants.IDENTIFY_EVENT, identifyEvent);\n    }\n\n    private JSONObject mergeIdentifyInterceptList(List<JSONObject> identifys) throws JSONException {\n        JSONObject userProperties = new JSONObject();\n        for (JSONObject identify : identifys) {\n            JSONObject setUserProperties = identify.getJSONObject(\"user_properties\")\n                    .getJSONObject(Constants.AMP_OP_SET);\n            mergeUserProperties(userProperties, setUserProperties);\n        }\n        return userProperties;\n    }\n\n    private void mergeUserProperties(JSONObject userProperties, JSONObject userPropertiesToMerge) throws JSONException {\n        Iterator<?> keys = userPropertiesToMerge.keys();\n        while (keys.hasNext()) {\n            String key = (String) keys.next();\n            if (userPropertiesToMerge.get(key) != null && userPropertiesToMerge.get(key) != JSONObject.NULL) {\n                userProperties.put(key, userPropertiesToMerge.get(key));\n            }\n        }\n    }\n\n    private boolean isSetOnly(JSONObject event) {\n        return isActionOnly(event, Constants.AMP_OP_SET);\n    }\n\n    private boolean isClearAll(JSONObject event) {\n        return isActionOnly(event, Constants.AMP_OP_CLEAR_ALL);\n    }\n\n    private boolean isSetGroups(JSONObject event) {\n        try {\n            return event.getJSONObject(\"groups\").length() > 0;\n        } catch (JSONException e) {\n            return false;\n        }\n    }\n\n    private boolean isActionOnly(JSONObject event, String action) {\n        try {\n            JSONObject userProperties = event.getJSONObject(\"user_properties\");\n            return userProperties.length() == 1 && userProperties.has(action);\n        } catch (JSONException e) {\n            return false;\n        }\n    }\n\n    private long saveIdentifyProperties(JSONObject event) {\n        return dbHelper.addIdentifyInterceptor(event.toString());\n    }\n\n    private boolean isIdentityUpdated(JSONObject event) {\n        try {\n            if (!identitySet.getAndSet(true)) {\n                userId = event.getString(\"user_id\");\n                deviceId = event.getString(\"device_id\");\n                return true;\n            }\n            boolean isUpdated = false;\n            if (isIdUpdated(userId, event.getString(\"user_id\"))) {\n                userId = event.getString(\"user_id\");\n                isUpdated = true;\n            }\n            if (isIdUpdated(deviceId, event.getString(\"device_id\"))) {\n                deviceId = event.getString(\"device_id\");\n                isUpdated = true;\n            }\n            return isUpdated;\n        } catch (JSONException e) {\n            return true;\n        }\n    }\n\n    private boolean isIdUpdated(String id, String updateId) {\n        if (id == null && updateId == null) {\n            return false;\n        }\n        if (id == null || updateId == null) {\n            return true;\n        }\n        return !id.equals(updateId);\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/api/IngestionMetadata.java",
    "content": "package com.amplitude.api;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\npublic class IngestionMetadata {\n    private static final String TAG = IngestionMetadata.class.getName();\n    /**\n     * The source name, e.g. \"ampli\"\n     */\n    private String sourceName;\n    /**\n     * The source version, e.g. \"2.0.0\"\n     */\n    private String sourceVersion;\n\n    /**\n     * Set the ingestion metadata source name information.\n     * @param sourceName source name for ingestion metadata\n     * @return the same IngestionMetadata object\n     */\n    public IngestionMetadata setSourceName(String sourceName) {\n        this.sourceName = sourceName;\n        return this;\n    }\n\n    /**\n     * Set the ingestion metadata source version information.\n     * @param sourceVersion source version for ingestion metadata\n     * @return the same IngestionMetadata object\n     */\n    public IngestionMetadata setSourceVersion(String sourceVersion) {\n        this.sourceVersion = sourceVersion;\n        return this;\n    }\n\n    /**\n     * Get JSONObject of current ingestion metadata\n     * @return JSONObject including ingestion metadata information\n     */\n    protected JSONObject toJSONObject() {\n        JSONObject jsonObject = new JSONObject();\n        try {\n            if (!Utils.isEmptyString(sourceName)) {\n                jsonObject.put(Constants.AMP_INGESTION_METADATA_SOURCE_NAME, sourceName);\n            }\n            if (!Utils.isEmptyString(sourceVersion)) {\n                jsonObject.put(Constants.AMP_INGESTION_METADATA_SOURCE_VERSION, sourceVersion);\n            }\n        } catch (JSONException e) {\n            AmplitudeLog.getLogger().e(TAG, \"JSON Serialization of ingestion metadata object failed\");\n        }\n        return jsonObject;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/api/Middleware.java",
    "content": "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",
    "content": "package com.amplitude.api;\n\ninterface MiddlewareExtended extends Middleware {\n\tvoid flush();\n}"
  },
  {
    "path": "src/main/java/com/amplitude/api/MiddlewareExtra.java",
    "content": "package com.amplitude.api;\n\nimport java.util.Map;\nimport java.util.HashMap;\n\npublic class MiddlewareExtra extends HashMap<String, Object> {\n    public MiddlewareExtra() {\n        super();\n    }\n    public MiddlewareExtra(Map<String, Object> map) {\n        super(map);\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/api/MiddlewareNext.java",
    "content": "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",
    "content": "package com.amplitude.api;\n\nimport org.json.JSONObject;\n\npublic class MiddlewarePayload {\n    public JSONObject event;\n    public MiddlewareExtra extra;\n\n    public MiddlewarePayload(JSONObject event, MiddlewareExtra extra) {\n        this.event = event;\n        this.extra = extra;\n    }\n\n    public MiddlewarePayload(JSONObject event) {\n        this(event, null);\n    }\n}"
  },
  {
    "path": "src/main/java/com/amplitude/api/MiddlewareRunner.java",
    "content": "package com.amplitude.api;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.ConcurrentLinkedQueue;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\npublic class MiddlewareRunner {\n    private final ConcurrentLinkedQueue<Middleware> middlewares;\n\n    public MiddlewareRunner() {\n        middlewares = new ConcurrentLinkedQueue<>();\n    }\n\n    public void add(Middleware middleware) {\n        this.middlewares.add(middleware);\n    }\n\n    private void runMiddlewares(List<Middleware> middlewares, MiddlewarePayload payload, MiddlewareNext next) {\n        if (middlewares.size() == 0 ){\n            next.run(payload);\n            return;\n        }\n        middlewares.get(0).run(payload, new MiddlewareNext() {\n            @Override\n            public void run(MiddlewarePayload curPayload) {\n                runMiddlewares((middlewares.subList(1, middlewares.size())), curPayload, next);\n            }\n        });\n    }\n\n    public boolean run(MiddlewarePayload payload) {\n        AtomicBoolean middlewareCompleted = new AtomicBoolean(false);\n        this.run(payload, new MiddlewareNext() {\n            @Override\n            public void run(MiddlewarePayload curPayload) {\n                middlewareCompleted.set(true);\n            }\n        });\n        return middlewareCompleted.get();\n    }\n\n    public void run(MiddlewarePayload payload, MiddlewareNext next) {\n        List<Middleware> middlewareList = new ArrayList<>(this.middlewares);\n        runMiddlewares(middlewareList, payload, next);\n    }\n\n    void flush() {\n        for (Middleware middleware : middlewares) {\n            if (middleware instanceof MiddlewareExtended) {\n                ((MiddlewareExtended) middleware).flush();\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/main/java/com/amplitude/api/PinnedAmplitudeClient.java",
    "content": "package com.amplitude.api;\n\nimport android.content.Context;\n\nimport com.amplitude.util.DoubleCheck;\nimport com.amplitude.util.Provider;\n\nimport java.io.IOException;\nimport java.security.GeneralSecurityException;\nimport java.security.KeyStore;\nimport java.security.cert.CertificateFactory;\nimport java.security.cert.X509Certificate;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport javax.net.ssl.SSLContext;\nimport javax.net.ssl.SSLSocketFactory;\nimport javax.net.ssl.TrustManager;\nimport javax.net.ssl.TrustManagerFactory;\nimport javax.net.ssl.X509TrustManager;\n\nimport okhttp3.OkHttpClient;\nimport okio.Buffer;\nimport okio.ByteString;\n\n/**\n * <h1>PinnedAmplitudeClient</h1>\n * This is a version of the AmplitudeClient that supports SSL pinning for encrypted requests.\n * Please contact <a href=\"mailto:support@amplitude.com\">Amplitude Support</a> before you ship any\n * products with SSL pinning enabled so that we are aware and can provide documentation\n * and implementation help.\n */\npublic class PinnedAmplitudeClient extends AmplitudeClient {\n\n    /**\n     * The class identifier tag used in logging. TAG = {@code \"com.amplitude.api.PinnedAmplitudeClient\";}\n     */\n    private static final String TAG = PinnedAmplitudeClient.class.getName();\n\n    // CN=COMODO RSA Domain Validation Secure Server CA, O=COMODO CA Limited,\n    // L=Salford, ST=Greater Manchester, C=GB\n    private static final String CERTIFICATE_US = \"\"\n            + \"MIIGCDCCA/CgAwIBAgIQKy5u6tl1NmwUim7bo3yMBzANBgkqhkiG9w0BAQwFADCBhT\"\n            + \"ELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE\"\n            + \"BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIk\"\n            + \"NPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTQwMjEyMDAwMDAw\"\n            + \"WhcNMjkwMjExMjM1OTU5WjCBkDELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZX\"\n            + \"IgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENB\"\n            + \"IExpbWl0ZWQxNjA0BgNVBAMTLUNPTU9ETyBSU0EgRG9tYWluIFZhbGlkYXRpb24gU2\"\n            + \"VjdXJlIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAI7C\"\n            + \"AhnhoFmk6zg1jSz9AdDTScBkxwtiBUUWOqigwAwCfx3M28ShbXcDow+G+eMGnD4LgY\"\n            + \"qbSRutA776S9uMIO3Vzl5ljj4Nr0zCsLdFXlIvNN5IJGS0Qa4Al/e+Z96e0HqnU4A7\"\n            + \"fK31llVvl0cKfIWLIpeNs4TgllfQcBhglo/uLQeTnaG6ytHNe+nEKpooIZFNb5JPJa\"\n            + \"XyejXdJtxGpdCsWTWM/06RQ1A/WZMebFEh7lgUq/51UHg+TLAchhP6a5i84DuUHoVS\"\n            + \"3AOTJBhuyydRReZw3iVDpA3hSqXttn7IzW3uLh0nc13cRTCAquOyQQuvvUSH2rnlG5\"\n            + \"1/ruWFgqUCAwEAAaOCAWUwggFhMB8GA1UdIwQYMBaAFLuvfgI9+qbxPISOre44mOzZ\"\n            + \"MjLUMB0GA1UdDgQWBBSQr2o6lFoL2JDqElZz30O0Oija5zAOBgNVHQ8BAf8EBAMCAY\"\n            + \"YwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUH\"\n            + \"AwIwGwYDVR0gBBQwEjAGBgRVHSAAMAgGBmeBDAECATBMBgNVHR8ERTBDMEGgP6A9hj\"\n            + \"todHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9SU0FDZXJ0aWZpY2F0aW9uQXV0\"\n            + \"aG9yaXR5LmNybDBxBggrBgEFBQcBAQRlMGMwOwYIKwYBBQUHMAKGL2h0dHA6Ly9jcn\"\n            + \"QuY29tb2RvY2EuY29tL0NPTU9ET1JTQUFkZFRydXN0Q0EuY3J0MCQGCCsGAQUFBzAB\"\n            + \"hhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZIhvcNAQEMBQADggIBAE4rdk\"\n            + \"+SHGI2ibp3wScF9BzWRJ2pmj6q1WZmAT7qSeaiNbz69t2Vjpk1mA42GHWx3d1Qcnyu\"\n            + \"3HeIzg/3kCDKo2cuH1Z/e+FE6kKVxF0NAVBGFfKBiVlsit2M8RKhjTpCipj4SzR7Jz\"\n            + \"sItG8kO3KdY3RYPBpsP0/HEZrIqPW1N+8QRcZs2eBelSaz662jue5/DJpmNXMyYE7l\"\n            + \"3YphLG5SEXdoltMYdVEVABt0iN3hxzgEQyjpFv3ZBdRdRydg1vs4O2xyopT4Qhrf7W\"\n            + \"8GjEXCBgCq5Ojc2bXhc3js9iPc0d1sjhqPpepUfJa3w/5Vjo1JXvxku88+vZbrac2/\"\n            + \"4EjxYoIQ5QxGV/Iz2tDIY+3GH5QFlkoakdH368+PUq4NCNk+qKBR6cGHdNXJ93SrLl\"\n            + \"P7u3r7l+L4HyaPs9Kg4DdbKDsx5Q5XLVq4rXmsXiBmGqW5prU5wfWYQ//u+aen/e7K\"\n            + \"JD2AFsQXj4rBYKEMrltDR5FL1ZoXX/nUh8HCjLfn4g8wGTeGrODcQgPmlKidrv0PJF\"\n            + \"GUzpII0fxQ8ANAe4hZ7Q7drNJ3gjTcBpUC2JD5Leo31Rpg0Gcg19hCC0Wvgmje3WYk\"\n            + \"N5AplBlGGSW4gNfL1IYoakRwJiNiqZ+Gb7+6kHDSVneFeO/qJakXzlByjAA6quPbYz\"\n            + \"Sf+AZxAeKCINT+b72x\";\n\n    private static final String CERTIFICATE_EU = \"\"\n            + \"MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF\\n\"\n            + \"ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6\\n\"\n            + \"b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL\\n\"\n            + \"MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv\\n\"\n            + \"b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj\\n\"\n            + \"ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM\\n\"\n            + \"9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw\\n\"\n            + \"IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6\\n\"\n            + \"VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L\\n\"\n            + \"93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm\\n\"\n            + \"jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\\n\"\n            + \"AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA\\n\"\n            + \"A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI\\n\"\n            + \"U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs\\n\"\n            + \"N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv\\n\"\n            + \"o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU\\n\"\n            + \"5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy\\n\"\n            + \"rqXRfboQnoZsG4q5WTP468SQvvG5\";\n\n    private static final AmplitudeLog logger = AmplitudeLog.getLogger();\n\n    protected static String getCertificate(AmplitudeServerZone serverZone) {\n        return (serverZone == AmplitudeServerZone.EU) ? CERTIFICATE_EU : CERTIFICATE_US;\n    }\n\n    /**\n     * Pinned certificate chain for api.amplitude.com.\n     */\n    protected static SSLContextBuilder getPinnedCertificateChain(AmplitudeServerZone serverZone) {\n        String CERTIFICATE = getCertificate(serverZone);\n        return new SSLContextBuilder(serverZone).addCertificate(CERTIFICATE);\n    }\n\n    /**\n     * SSl context builder, used to generate the SSL context.\n     */\n    protected static class SSLContextBuilder {\n        private final List<String> certificateBase64s = new ArrayList<String>();\n        protected AmplitudeServerZone serverZone;\n\n        public SSLContextBuilder() {\n            this.serverZone = AmplitudeServerZone.US;\n        }\n        public SSLContextBuilder(AmplitudeServerZone serverZone) {\n            this.serverZone = serverZone;\n        }\n\n        /**\n         * Add certificate ssl context builder.\n         *\n         * @param certificateBase64 the certificate base 64\n         * @return the ssl context builder\n         */\n        public SSLContextBuilder addCertificate(String certificateBase64) {\n            certificateBase64s.add(certificateBase64);\n            return this;\n        }\n\n        /**\n         * Build ssl context.\n         *\n         * @return the ssl context\n         */\n        public SSLContext build() {\n            try {\n                CertificateFactory certificateFactory = CertificateFactory.getInstance(\"X.509\");\n                TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(\n                        TrustManagerFactory.getDefaultAlgorithm());\n                KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());\n                keyStore.load(null, null); // Use a null input stream + password to create an empty key store.\n\n                // Decode the certificates and add 'em to the key store.\n                int nextName = 1;\n                for (String certificateBase64 : certificateBase64s) {\n                    Buffer certificateBuffer = new Buffer().write(ByteString.decodeBase64(certificateBase64));\n                    X509Certificate certificate = (X509Certificate) certificateFactory.generateCertificate(\n                            certificateBuffer.inputStream());\n                    keyStore.setCertificateEntry(Integer.toString(nextName++), certificate);\n                }\n\n                // Create an SSL context that uses these certificates as its trust store.\n                trustManagerFactory.init(keyStore);\n                SSLContext sslContext = SSLContext.getInstance(\"TLS\");\n                sslContext.init(null, trustManagerFactory.getTrustManagers(), null);\n                return sslContext;\n            } catch (GeneralSecurityException e) {\n                logger.e(TAG, e.getMessage(), e);\n            } catch (IOException e) {\n                logger.e(TAG, e.getMessage(), e);\n            }\n            return null;\n        }\n    }\n\n    static Map<String, PinnedAmplitudeClient> instances = new HashMap<String, PinnedAmplitudeClient>();\n\n    /**\n     * Gets the default instance.\n     *\n     * @return the default instance\n     */\n    public static PinnedAmplitudeClient getInstance() {\n        return getInstance(null);\n    }\n\n    /**\n     * Gets the specified instance. If instance is null or empty string, fetches the default\n     * instance instead.\n     *\n     * @param instance name to get \"ex app 1\"\n     * @return the specified instance\n     */\n    public static synchronized PinnedAmplitudeClient getInstance(String instance) {\n        instance = Utils.normalizeInstanceName(instance);\n        PinnedAmplitudeClient client = instances.get(instance);\n        if (client == null) {\n            client = new PinnedAmplitudeClient(instance);\n            instances.put(instance, client);\n        }\n        return client;\n    }\n\n    /**\n     * The SSl socket factory.\n     */\n    protected SSLSocketFactory sslSocketFactory;\n\n    /**\n     * Instantiates a new Pinned amplitude client.\n     */\n    public PinnedAmplitudeClient(String instance) {\n        super(instance);\n    }\n\n    /**\n     * The Initialized ssl socket factory.\n     */\n    protected boolean initializedSSLSocketFactory = false;\n\n    public synchronized AmplitudeClient initializeInternal(\n            Context context,\n            String apiKey,\n            String userId,\n            Provider<OkHttpClient> clientProvider\n    ) {\n        super.initialize(context, apiKey, userId);\n        final PinnedAmplitudeClient client = this;\n        runOnLogThread(new Runnable() {\n            @Override\n            public void run() {\n                if (!client.initializedSSLSocketFactory) {\n                    SSLSocketFactory factory = getPinnedCertSslSocketFactory(client.getServerZone());\n                    if (factory != null) {\n                        try {\n                            CertificateFactory certificateFactory = CertificateFactory.getInstance(\"X.509\");\n                            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(\n                                    TrustManagerFactory.getDefaultAlgorithm());\n                            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());\n                            keyStore.load(null, null); // Use a null input stream + password to create an empty key store.\n\n                            List<String> certificateBase64s = new ArrayList<String>();\n                            String CERTIFICATE = getCertificate(client.getServerZone());\n                            certificateBase64s.add(CERTIFICATE);\n\n                            // Decode the certificates and add 'em to the key store.\n                            int nextName = 1;\n                            for (String certificateBase64 : certificateBase64s) {\n                                Buffer certificateBuffer = new Buffer().write(ByteString.decodeBase64(certificateBase64));\n                                X509Certificate certificate = (X509Certificate) certificateFactory.generateCertificate(\n                                        certificateBuffer.inputStream());\n                                keyStore.setCertificateEntry(Integer.toString(nextName++), certificate);\n                            }\n\n                            // Create an SSL context that uses these certificates as its trust store.\n                            trustManagerFactory.init(keyStore);\n\n                            TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();\n                            if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {\n                                throw new IllegalStateException(\"Unexpected default trust managers:\"\n                                        + Arrays.toString(trustManagers));\n                            }\n                            X509TrustManager trustManager = (X509TrustManager) trustManagers[0];\n                            final Provider<OkHttpClient> finalClientProvider = DoubleCheck.provider(() -> {\n                                final OkHttpClient.Builder builder;\n                                if (clientProvider != null) {\n                                    builder = clientProvider.get().newBuilder();\n                                } else {\n                                    builder = new OkHttpClient.Builder();\n                                }\n                                return builder.sslSocketFactory(factory, trustManager).build();\n                            });\n\n                            client.callFactory = request -> finalClientProvider.get().newCall(request);\n                        } catch (GeneralSecurityException e) {\n                            logger.e(TAG, e.getMessage(), e);\n                        } catch (IOException e) {\n                            logger.e(TAG, e.getMessage(), e);\n                        }\n                    } else {\n                        logger.e(TAG, \"Unable to pin SSL as requested. Will send data without SSL pinning.\");\n                    }\n                    client.initializedSSLSocketFactory = true;\n                }\n            }\n        });\n        return this;\n    }\n\n    // why not override base method?\n    @Override\n    public synchronized AmplitudeClient initialize(Context context, String apiKey, String userId) {\n        return initializeInternal(context, apiKey, userId, null);\n    }\n\n    public synchronized AmplitudeClient initialize(\n            Context context,\n            String apiKey,\n            String userId,\n            Provider<OkHttpClient> clientProvider) {\n        return initializeInternal(context, apiKey, userId, clientProvider);\n    }\n\n    /**\n     * Gets pinned cert ssl socket factory.\n     *\n     * @param serverZone the current server zone\n     * @return the pinned cert ssl socket factory\n     */\n    protected SSLSocketFactory getPinnedCertSslSocketFactory(AmplitudeServerZone serverZone) {\n        return getPinnedCertSslSocketFactory(getPinnedCertificateChain(serverZone));\n    }\n\n    /**\n     * Gets pinned cert ssl socket factory.\n     *\n     * @param context the context\n     * @return the pinned cert ssl socket factory\n     */\n    protected SSLSocketFactory getPinnedCertSslSocketFactory(SSLContextBuilder context) {\n        if (context == null) {\n            return null;\n        }\n        if (sslSocketFactory == null) {\n            try {\n                sslSocketFactory = context.build().getSocketFactory();\n                if (context.serverZone == AmplitudeServerZone.EU) {\n                    logger.i(TAG, \"Pinning SSL session using AWS Root CA Cert\");\n                } else {\n                    logger.i(TAG, \"Pinning SSL session using Comodo CA Cert\");\n                }\n            } catch (Exception e) {\n                logger.e(TAG, e.getMessage(), e);\n            }\n        }\n        return sslSocketFactory;\n    }\n\n    @Override\n    public AmplitudeClient setServerZone(AmplitudeServerZone serverZone) {\n        super.setServerZone(serverZone);\n        this.initializedSSLSocketFactory = false;\n        this.sslSocketFactory = null;\n        this.initialize(this.context, this.apiKey, this.userId);\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/api/Plan.java",
    "content": "package com.amplitude.api;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\npublic class Plan {\n    private static final String TAG = Plan.class.getName();\n    /**\n     * The tracking plan branch name e.g. \"main\"\n     */\n    private String branch;\n    /**\n     * The tracking plan source e.g. \"web\", \"mobile\"\n     */\n    private String source;\n    /**\n     * The tracking plan version e.g. \"1\", \"15\"\n     */\n    private String version;\n    /**\n     * The tracking plan version Id e.g. \"9ec23ba0-275f-468f-80d1-66b88bff9529\"\n     */\n    private String versionId;\n\n    /**\n     * Set the tracking plan branch information.\n     * @param branch The tracking plan branch name e.g. \"main\"\n     * @return the same Plan object\n     */\n    public Plan setBranch(String branch) {\n        this.branch = branch;\n        return this;\n    }\n\n    /**\n     * Set the tracking plan source information.\n     * @param source The tracking plan source e.g. \"web\", \"mobile\"\n     * @return the same Plan object\n     */\n    public Plan setSource(String source) {\n        this.source = source;\n        return this;\n    }\n\n    /**\n     * Set the tracking plan version information.\n     * @param version The tracking plan version e.g. \"1\", \"15\"\n     * @return the same Plan object\n     */\n    public Plan setVersion(String version) {\n        this.version = version;\n        return this;\n    }\n\n    /**\n     * Set the tracking plan version Id.\n     * @param version The tracking plan version e.g. \"9ec23ba0-275f-468f-80d1-66b88bff9529\"\n     * @return the same Plan object\n     */\n    public Plan setVersionId(String versionId) {\n        this.versionId = versionId;\n        return this;\n    }\n\n    /**\n     * Get JSONObject of current tacking plan\n     * @return JSONObject including plan information\n     */\n    protected JSONObject toJSONObject() {\n        JSONObject plan = new JSONObject();\n        try {\n            if (!Utils.isEmptyString(branch)) {\n                plan.put(Constants.AMP_PLAN_BRANCH, branch);\n            }\n            if (!Utils.isEmptyString(source)) {\n                plan.put(Constants.AMP_PLAN_SOURCE, source);\n            }\n            if (!Utils.isEmptyString(version)) {\n                plan.put(Constants.AMP_PLAN_VERSION, version);\n            }\n            if (!Utils.isEmptyString(versionId)) {\n                plan.put(Constants.AMP_PLAN_VERSION_ID, versionId);\n            }\n        } catch (JSONException e) {\n            AmplitudeLog.getLogger().e(TAG, \"JSON Serialization of tacking plan object failed\");\n        }\n        return plan;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/api/Revenue.java",
    "content": "package com.amplitude.api;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\n/**\n * <h1>Revenue</h1>\n * Revenue objects are a wrapper for revenue events and revenue properties. This should be used\n * in conjunction with {@code AmplitudeClient.logRevenueV2()} to record in-app transactions.\n * Each set method returns the same Revenue object, allowing\n * you to chain multiple set calls together, for example:\n * {@code Revenue revenue = new Revenue().setProductId(\"com.product.id\").setPrice(3.99);}\n * <br><br>\n * <b>Note:</b> {@code price} is a required field. If {@code quantity} is not\n * specified, it will default to 1. {@code productId}, {@code receipt} and {@code receiptSignature}\n * re required if you want to verify the revenue event.\n * <br><br>\n * <b>Note:</b> the total revenue amount is calculated as price * quantity.\n * <br><br>\n * After creating a Revenue object and setting the desired transaction properties, send it to\n * Amplitude servers by calling {@code Amplitude.getInstance().logRevenueV2(revenue);} and pass in\n * the object.\n *\n * @see <a href=\"https://github.com/amplitude/Amplitude-Android#tracking-revenue\">\n *     Android SDK README</a> for more information on logging revenue.\n */\npublic class Revenue {\n    /**\n     * The class identifier tag used in logging. TAG = {@code \"com.amplitude.api.Revenue\"}\n     */\n    private static final String TAG = Revenue.class.getName();\n    private static AmplitudeLog logger =  AmplitudeLog.getLogger();\n\n    /**\n     * The Product ID field.\n     */\n    protected String productId = null;\n    /**\n     * The Quantity field (defaults to 1).\n     */\n    protected int quantity = 1;\n    /**\n     * The Price field (required).\n     */\n    protected Double price = null;\n\n    /**\n     * The Revenue Type field (optional).\n     */\n    protected String revenueType = null;\n    /**\n     * The Receipt field (required if you want to verify the revenue event).\n     */\n    protected String receipt = null;\n    /**\n     * The Receipt Signature field (required if you want to verify the revenue event).\n     */\n    protected String receiptSig = null;\n    /**\n     * The Revenue Event Properties field (optional).\n     */\n    protected JSONObject properties = null;\n\n    /**\n     * Verifies that revenue object is valid and contains the required fields\n     *\n     * @return true if revenue object is valid, else false\n     */\n    protected boolean isValidRevenue() {\n        if (price == null) {\n            logger.w(TAG, \"Invalid revenue, need to set price\");\n            return false;\n        }\n        return true;\n    }\n\n    /**\n     * Set a value for the product identifier. Empty and invalid strings are ignored.\n     *\n     * @param productId the product id\n     * @return the same Revenue object\n     */\n    public Revenue setProductId(String productId) {\n        if (Utils.isEmptyString(productId)) {\n            logger.w(TAG, \"Invalid empty productId\");\n            return this;\n        }\n        this.productId = productId;\n        return this;\n    }\n\n    /**\n     * Set a value for the quantity. Note: revenue amount is calculated as price * quantity.\n     *\n     * @param quantity the quantity\n     * @return the same Revenue object\n     */\n    public Revenue setQuantity(int quantity) {\n        this.quantity = quantity;\n        return this;\n    }\n\n    /**\n     * Set a value for the price. Note: revenue amount is calculated as price * quantity.\n     *\n     * @param price the price\n     * @return the same Revenue object\n     */\n    public Revenue setPrice(double price) {\n        this.price = price;\n        return this;\n    }\n\n    /**\n     * Set a value for the revenue type.\n     *\n     * @param revenueType the revenue type\n     * @return the same Revenue object\n     */\n    public Revenue setRevenueType(String revenueType) {\n        this.revenueType = revenueType; // no input validation for optional field\n        return this;\n    }\n\n    /**\n     * Set the receipt and receipt signature. Both fields are required to verify the revenue event.\n     *\n     * @param receipt          the receipt\n     * @param receiptSignature the receipt signature\n     * @return the same Revenue object\n     */\n    public Revenue setReceipt(String receipt, String receiptSignature) {\n        this.receipt = receipt;\n        this.receiptSig = receiptSignature;\n        return this;\n    }\n\n    /**\n     * This is deprecated. RevenueProperties is a confusing name, should be EventProperties\n     *\n     * @param revenueProperties the revenue properties\n     * @return the same Revenue object\n     * @deprecated - use {@code Revenue.setEventProperties()} instead\n     */\n    public Revenue setRevenueProperties(JSONObject revenueProperties) {\n        logger.w(TAG, \"setRevenueProperties is deprecated, please use setEventProperties instead\");\n        return setEventProperties(revenueProperties);\n    }\n\n    /**\n     * Set event properties for the revenue event, like you would for an event during logEvent.\n     *\n     * @param eventProperties the event properties\n     * @return the same Revenue object\n     * @see <a href=\"https://github.com/amplitude/Amplitude-Android#setting-event-properties\">\n     *     Event Properties</a> for more information about logging event properties.\n     */\n    public Revenue setEventProperties(JSONObject eventProperties) {\n        this.properties = Utils.cloneJSONObject(eventProperties);\n        return this;\n    }\n\n    /**\n     * Converts Revenue object into a JSONObject to send to Amplitude servers\n     *\n     * @return the JSON representation of this Revenue object\n     */\n    protected JSONObject toJSONObject() {\n        JSONObject obj = properties == null ? new JSONObject() : properties;\n        try {\n            obj.put(Constants.AMP_REVENUE_PRODUCT_ID, productId);\n            obj.put(Constants.AMP_REVENUE_QUANTITY, quantity);\n            obj.put(Constants.AMP_REVENUE_PRICE, price);\n            obj.put(Constants.AMP_REVENUE_REVENUE_TYPE, revenueType);\n            obj.put(Constants.AMP_REVENUE_RECEIPT, receipt);\n            obj.put(Constants.AMP_REVENUE_RECEIPT_SIG, receiptSig);\n        } catch (JSONException e) {\n            logger.e(\n                TAG, String.format(\"Failed to convert revenue object to JSON: %s\", e.toString())\n            );\n        }\n\n        return obj;\n    }\n\n    /**\n     * Custom equals function to compare 2 revenue objects. 2 Revenue objects are equal if\n     * all of their fields are equal. Generated by Android Studio\n     *\n     * @param o the other object to compare to\n     * @return true if the two Revenue objects are equal, false otherwise\n     */\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n\n        Revenue revenue = (Revenue) o;\n\n        if (quantity != revenue.quantity) return false;\n        if (productId != null ? !productId.equals(revenue.productId) : revenue.productId != null)\n            return false;\n        if (price != null ? !price.equals(revenue.price) : revenue.price != null) return false;\n        if (revenueType != null ? !revenueType.equals(revenue.revenueType) : revenue.revenueType != null)\n            return false;\n        if (receipt != null ? !receipt.equals(revenue.receipt) : revenue.receipt != null)\n            return false;\n        if (receiptSig != null ? !receiptSig.equals(revenue.receiptSig) : revenue.receiptSig != null)\n            return false;\n        return !(properties != null ? !Utils.compareJSONObjects(properties, revenue.properties): revenue.properties != null);\n\n    }\n\n    /**\n     * Custom hashcode generator function for Revenue object. Generated by Android Studio.\n     *\n     * @return the hashcode for this Revenue instance~\n     */\n    @Override\n    public int hashCode() {\n        int result = productId != null ? productId.hashCode() : 0;\n        result = 31 * result + quantity;\n        result = 31 * result + (price != null ? price.hashCode() : 0);\n        result = 31 * result + (revenueType != null ? revenueType.hashCode() : 0);\n        result = 31 * result + (receipt != null ? receipt.hashCode() : 0);\n        result = 31 * result + (receiptSig != null ? receiptSig.hashCode() : 0);\n        result = 31 * result + (properties != null ? properties.hashCode() : 0);\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/api/TrackingOptions.java",
    "content": "package com.amplitude.api;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport java.util.HashSet;\nimport java.util.Set;\n\npublic class TrackingOptions {\n    private static final String TAG = TrackingOptions.class.getName();\n\n    private static String[] SERVER_SIDE_PROPERTIES = {\n            Constants.AMP_TRACKING_OPTION_CITY,\n            Constants.AMP_TRACKING_OPTION_COUNTRY,\n            Constants.AMP_TRACKING_OPTION_DMA,\n            Constants.AMP_TRACKING_OPTION_IP_ADDRESS,\n            Constants.AMP_TRACKING_OPTION_LAT_LNG,\n            Constants.AMP_TRACKING_OPTION_REGION,\n    };\n\n    private static String[] COPPA_CONTROL_PROPERTIES = {\n            Constants.AMP_TRACKING_OPTION_ADID,\n            Constants.AMP_TRACKING_OPTION_CITY,\n            Constants.AMP_TRACKING_OPTION_IP_ADDRESS,\n            Constants.AMP_TRACKING_OPTION_LAT_LNG,\n    };\n\n    Set<String> disabledFields = new HashSet<String>();\n\n    public TrackingOptions disableAdid() {\n        disableTrackingField(Constants.AMP_TRACKING_OPTION_ADID);\n        return this;\n    }\n\n    boolean shouldTrackAdid() {\n        return shouldTrackField(Constants.AMP_TRACKING_OPTION_ADID);\n    }\n\n    public TrackingOptions disableAppSetId() {\n        disableTrackingField(Constants.AMP_TRACKING_OPTION_APP_SET_ID);\n        return this;\n    }\n\n    boolean shouldTrackAppSetId() {\n        return shouldTrackField(Constants.AMP_TRACKING_OPTION_APP_SET_ID);\n    }\n\n    public TrackingOptions disableCarrier() {\n        disableTrackingField(Constants.AMP_TRACKING_OPTION_CARRIER);\n        return this;\n    }\n\n    boolean shouldTrackCarrier() {\n        return shouldTrackField(Constants.AMP_TRACKING_OPTION_CARRIER);\n    }\n\n    public TrackingOptions disableCity() {\n        disableTrackingField(Constants.AMP_TRACKING_OPTION_CITY);\n        return this;\n    }\n\n    boolean shouldTrackCity() {\n        return shouldTrackField(Constants.AMP_TRACKING_OPTION_CITY);\n    }\n\n    public TrackingOptions disableCountry() {\n        disableTrackingField(Constants.AMP_TRACKING_OPTION_COUNTRY);\n        return this;\n    }\n\n    boolean shouldTrackCountry() {\n        return shouldTrackField(Constants.AMP_TRACKING_OPTION_COUNTRY);\n    }\n\n    public TrackingOptions disableDeviceBrand() {\n        disableTrackingField(Constants.AMP_TRACKING_OPTION_DEVICE_BRAND);\n        return this;\n    }\n\n    boolean shouldTrackDeviceBrand() {\n        return shouldTrackField(Constants.AMP_TRACKING_OPTION_DEVICE_BRAND);\n    }\n\n    public TrackingOptions disableDeviceManufacturer() {\n        disableTrackingField(Constants.AMP_TRACKING_OPTION_DEVICE_MANUFACTURER);\n        return this;\n    }\n\n    boolean shouldTrackDeviceManufacturer() {\n        return shouldTrackField(Constants.AMP_TRACKING_OPTION_DEVICE_MANUFACTURER);\n    }\n\n    public TrackingOptions disableDeviceModel() {\n        disableTrackingField(Constants.AMP_TRACKING_OPTION_DEVICE_MODEL);\n        return this;\n    }\n\n    boolean shouldTrackDeviceModel() {\n        return shouldTrackField(Constants.AMP_TRACKING_OPTION_DEVICE_MODEL);\n    }\n\n    public TrackingOptions disableDma() {\n        disableTrackingField(Constants.AMP_TRACKING_OPTION_DMA);\n        return this;\n    }\n\n    boolean shouldTrackDma() {\n        return shouldTrackField(Constants.AMP_TRACKING_OPTION_DMA);\n    }\n\n    public TrackingOptions disableIpAddress() {\n        disableTrackingField(Constants.AMP_TRACKING_OPTION_IP_ADDRESS);\n        return this;\n    }\n\n    boolean shouldTrackIpAddress() {\n        return shouldTrackField(Constants.AMP_TRACKING_OPTION_IP_ADDRESS);\n    }\n\n    public TrackingOptions disableLanguage() {\n        disableTrackingField(Constants.AMP_TRACKING_OPTION_LANGUAGE);\n        return this;\n    }\n\n    boolean shouldTrackLanguage() {\n        return shouldTrackField(Constants.AMP_TRACKING_OPTION_LANGUAGE);\n    }\n\n    public TrackingOptions disableLatLng() {\n        disableTrackingField(Constants.AMP_TRACKING_OPTION_LAT_LNG);\n        return this;\n    }\n\n    boolean shouldTrackLatLng() {\n        return shouldTrackField(Constants.AMP_TRACKING_OPTION_LAT_LNG);\n    }\n\n    public TrackingOptions disableOsName() {\n        disableTrackingField(Constants.AMP_TRACKING_OPTION_OS_NAME);\n        return this;\n    }\n\n    boolean shouldTrackOsName() {\n        return shouldTrackField(Constants.AMP_TRACKING_OPTION_OS_NAME);\n    }\n\n    public TrackingOptions disableOsVersion() {\n        disableTrackingField(Constants.AMP_TRACKING_OPTION_OS_VERSION);\n        return this;\n    }\n\n    boolean shouldTrackOsVersion() {\n        return shouldTrackField(Constants.AMP_TRACKING_OPTION_OS_VERSION);\n    }\n\n    public TrackingOptions disableApiLevel() {\n        disableTrackingField(Constants.AMP_TRACKING_OPTION_API_LEVEL);\n        return this;\n    }\n\n    boolean shouldTrackApiLevel() {\n        return shouldTrackField(Constants.AMP_TRACKING_OPTION_API_LEVEL);\n    }\n\n    public TrackingOptions disablePlatform() {\n        disableTrackingField(Constants.AMP_TRACKING_OPTION_PLATFORM);\n        return this;\n    }\n\n    boolean shouldTrackPlatform() {\n        return shouldTrackField(Constants.AMP_TRACKING_OPTION_PLATFORM);\n    }\n\n    public TrackingOptions disableRegion() {\n        disableTrackingField(Constants.AMP_TRACKING_OPTION_REGION);\n        return this;\n    }\n\n    boolean shouldTrackRegion() {\n        return shouldTrackField(Constants.AMP_TRACKING_OPTION_REGION);\n    }\n\n    public TrackingOptions disableVersionName() {\n        disableTrackingField(Constants.AMP_TRACKING_OPTION_VERSION_NAME);\n        return this;\n    }\n\n    boolean shouldTrackVersionName() {\n        return shouldTrackField(Constants.AMP_TRACKING_OPTION_VERSION_NAME);\n    }\n\n    private void disableTrackingField(String field) {\n        disabledFields.add(field);\n    }\n\n    protected JSONObject getApiPropertiesTrackingOptions() {\n        JSONObject apiPropertiesTrackingOptions = new JSONObject();\n        if (disabledFields.isEmpty()) {\n            return apiPropertiesTrackingOptions;\n        }\n\n        for (String key : SERVER_SIDE_PROPERTIES) {\n            if (disabledFields.contains(key)) {\n                try {\n                    apiPropertiesTrackingOptions.put(key, false);\n                } catch (JSONException e) {\n                    AmplitudeLog.getLogger().e(TAG, e.toString());\n                }\n            }\n        }\n        return apiPropertiesTrackingOptions;\n    }\n\n    private boolean shouldTrackField(String field) {\n        return !disabledFields.contains(field);\n    }\n\n    TrackingOptions mergeIn(TrackingOptions other) {\n        for (String key : other.disabledFields) {\n            disableTrackingField(key);\n        }\n\n        return this;\n    }\n\n    static TrackingOptions copyOf(TrackingOptions other) {\n        TrackingOptions trackingOptions = new TrackingOptions();\n        for (String key : other.disabledFields) {\n            trackingOptions.disableTrackingField(key);\n        }\n\n        return trackingOptions;\n    }\n\n    static TrackingOptions forCoppaControl() {\n        TrackingOptions trackingOptions = new TrackingOptions();\n        for (String key : COPPA_CONTROL_PROPERTIES) {\n            trackingOptions.disableTrackingField(key);\n        }\n\n        return trackingOptions;\n    }\n\n    public boolean equals(Object other) {\n        if (this == other) {\n            return true;  // self check\n        }\n        if (other == null) {\n            return false;  // null check\n        }\n        if (getClass() != other.getClass()) {\n            return false;  // type check and cast\n        }\n\n        TrackingOptions options = (TrackingOptions) other;\n        return options.disabledFields.equals(this.disabledFields);\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/api/Utils.java",
    "content": "package com.amplitude.api;\n\nimport android.Manifest;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.content.SharedPreferences;\nimport android.content.pm.PackageManager;\n\nimport org.json.JSONArray;\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport java.util.Iterator;\n\npublic class Utils {\n    private static final String TAG = Utils.class.getName();\n\n    private static AmplitudeLog logger = AmplitudeLog.getLogger();\n\n    /**\n     * Do a shallow copy of a JSONObject. Takes a bit of code to avoid\n     * stringify and reparse given the API.\n     */\n    static JSONObject cloneJSONObject(final JSONObject obj) {\n        if (obj == null) {\n            return null;\n        }\n\n        if (obj.length() == 0) {\n            return new JSONObject();\n        }\n\n        // obj.names returns null if the json obj is empty.\n        JSONArray nameArray = null;\n        try {\n            nameArray = obj.names();\n        } catch (ArrayIndexOutOfBoundsException e) {\n            logger.e(TAG, e.toString());\n        }\n        int len = (nameArray != null ? nameArray.length() : 0);\n\n        String[] names = new String[len];\n        for (int i = 0; i < len; i++) {\n            names[i] = nameArray.optString(i);\n        }\n\n        try {\n            return new JSONObject(obj, names);\n        } catch (JSONException e) {\n            logger.e(TAG, e.toString());\n            return null;\n        }\n    }\n\n    static boolean compareJSONObjects(JSONObject o1, JSONObject o2) {\n        try {\n\n            if (o1 == o2) {\n                return true;\n            }\n\n            if ((o1 != null && o2 == null) || (o1 == null && o2 != null)) {\n                return false;\n            }\n\n            if (o1.length() != o2.length()) {\n                return false;\n            }\n\n            Iterator<?> keys = o1.keys();\n            while (keys.hasNext()) {\n                String key = (String) keys.next();\n                if (!o2.has(key)) {\n                    return false;\n                }\n\n                Object value1 = o1.get(key);\n                Object value2 = o2.get(key);\n\n                if (!value1.getClass().equals(value2.getClass())) {\n                    return false;\n                }\n\n                if (value1.getClass() == JSONObject.class) {\n                    if (!compareJSONObjects((JSONObject) value1, (JSONObject) value2)) {\n                        return false;\n                    }\n                } else if (!value1.equals(value2)) {\n                    return false;\n                }\n            }\n\n            return true;\n        } catch (JSONException e) {}\n        return false;\n    }\n\n    public static boolean isEmptyString(String s) {\n        return (s == null || s.length() == 0);\n    }\n\n    static String normalizeInstanceName(String instance) {\n        if (isEmptyString(instance)) {\n            instance = Constants.DEFAULT_INSTANCE;\n        }\n        return instance.toLowerCase();\n    }\n\n    static boolean checkLocationPermissionAllowed(Context context) {\n        return checkPermissionAllowed(context, Manifest.permission.ACCESS_COARSE_LOCATION) ||\n                checkPermissionAllowed(context, Manifest.permission.ACCESS_FINE_LOCATION);\n    }\n\n    static boolean checkPermissionAllowed(Context context, String permission) {\n        // ANDROID 6.0 AND UP!\n        if (android.os.Build.VERSION.SDK_INT >= 23) {\n            boolean hasPermission = false;\n            try {\n                // Invoke checkSelfPermission method from Android 6 (API 23 and UP)\n                java.lang.reflect.Method methodCheckPermission = Activity.class.getMethod(\"checkSelfPermission\", java.lang.String.class);\n                Object resultObj = methodCheckPermission.invoke(context, permission);\n                int result = Integer.parseInt(resultObj.toString());\n                hasPermission = (result == PackageManager.PERMISSION_GRANTED);\n            } catch (Exception ex) {\n\n            }\n\n            return hasPermission;\n        } else {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/api/WorkerThread.java",
    "content": "package com.amplitude.api;\n\nimport android.os.Handler;\nimport android.os.HandlerThread;\nimport android.os.Process;\n\npublic class WorkerThread extends HandlerThread {\n\t\n\tpublic WorkerThread(String name) {\n\t\tsuper(name, Process.THREAD_PRIORITY_BACKGROUND);\n\t}\n\n\tprivate Handler handler;\n\n\tHandler getHandler() {\n\t\treturn handler;\n\t}\n\t\n\tvoid post(Runnable r) {\n\t\twaitForInitialization();\n\t\thandler.post(r);\n\t}\n\n\tvoid postDelayed(Runnable r, long delayMillis) {\n\t\twaitForInitialization();\n\t\thandler.postDelayed(r, delayMillis);\n\t}\n\n\tvoid removeCallbacks(Runnable r) {\n\t\twaitForInitialization();\n\t\thandler.removeCallbacks(r);\n\t}\n\n\tprivate synchronized void waitForInitialization() {\n\t\tif (handler == null) {\n\t\t\thandler = new Handler(getLooper());\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/eventexplorer/EventExplorer.java",
    "content": "package com.amplitude.eventexplorer;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.content.res.Resources;\nimport android.graphics.PixelFormat;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.util.DisplayMetrics;\nimport android.view.Display;\nimport android.view.View;\nimport android.view.WindowManager;\n\nimport com.amplitude.R;\n\npublic class EventExplorer {\n    private String instanceName;\n    private View bubbleView;\n\n    public EventExplorer(String instanceName) {\n        this.instanceName = instanceName;\n    }\n\n    public void show(final Activity rootActivity) {\n        if (this.bubbleView == null) {\n            new Handler(Looper.getMainLooper()).post(() -> {\n                final WindowManager windowManager = rootActivity.getWindowManager();\n                final DisplayMetrics displayMetrics = new DisplayMetrics();\n\n                if (windowManager.getDefaultDisplay() != null) {\n                    windowManager.getDefaultDisplay().getMetrics(displayMetrics);\n                }\n\n                final WindowManager.LayoutParams layoutParams\n                        = prepareWindowManagerLayoutParams(rootActivity, displayMetrics);\n\n                this.bubbleView = rootActivity.getLayoutInflater().inflate(R.layout.amp_bubble_view, null);\n\n                windowManager.addView(this.bubbleView, layoutParams);\n\n                this.bubbleView.setOnTouchListener(new EventExplorerTouchHandler(windowManager, layoutParams, this.instanceName));\n            });\n        }\n    }\n\n    private WindowManager.LayoutParams prepareWindowManagerLayoutParams(Context context,\n                                                                        DisplayMetrics displayMetrics) {\n        int navbarHeight = 0;\n        Resources resources = context.getResources();\n        int resourceId = resources.getIdentifier(\"navigation_bar_height\", \"dimen\", \"android\");\n        if (resourceId > 0) {\n            navbarHeight = resources.getDimensionPixelSize(resourceId);\n        }\n\n        final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();\n        layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION;\n        layoutParams.format = PixelFormat.TRANSLUCENT;\n        layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;\n        layoutParams.y = (displayMetrics.heightPixels - navbarHeight) / 2;\n        layoutParams.x = (displayMetrics.widthPixels) / 2;\n\n        layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;\n        layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;\n\n        return layoutParams;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/eventexplorer/EventExplorerInfoActivity.java",
    "content": "package com.amplitude.eventexplorer;\n\nimport android.app.Activity;\nimport android.content.ClipData;\nimport android.content.ClipboardManager;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.os.Bundle;\nimport android.view.View;\nimport android.widget.Button;\nimport android.widget.ImageView;\nimport android.widget.TextView;\nimport android.widget.Toast;\n\nimport com.amplitude.R;\nimport com.amplitude.api.Amplitude;\n\npublic class EventExplorerInfoActivity extends Activity {\n    private ImageView closeImageView;\n    private Button deviceIdCopyButton;\n    private Button userIdCopyButton;\n\n    private TextView deviceIdTextView;\n    private TextView userIdTextView;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.amp_activity_eventexplorer_info);\n\n        this.closeImageView = findViewById(R.id.amp_eeInfo_iv_close);\n        this.closeImageView.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View view) {\n                onBackPressed();\n            }\n        });\n\n        this.deviceIdTextView = findViewById(R.id.amp_eeInfo_tv_deviceId);\n        this.userIdTextView = findViewById(R.id.amp_eeInfo_tv_userId);\n\n        Intent intent = getIntent();\n        String instanceName = intent.getExtras().getString(\"instanceName\");\n\n        String deviceId = Amplitude.getInstance(instanceName).getDeviceId();\n        String userId = Amplitude.getInstance(instanceName).getUserId();\n\n        this.deviceIdTextView.setText(deviceId != null ? deviceId : getString(R.string.amp_label_not_avail));\n        this.userIdTextView.setText(userId != null ? userId : getString(R.string.amp_label_not_avail));\n\n        this.deviceIdCopyButton = findViewById(R.id.amp_eeInfo_btn_copyDeviceId);\n        this.deviceIdCopyButton.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View view) {\n                copyText(view.getContext(), deviceId);\n            }\n        });\n\n        this.userIdCopyButton = findViewById(R.id.amp_eeInfo_btn_copyUserId);\n        this.userIdCopyButton.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View view) {\n                copyText(view.getContext(), userId);\n            }\n        });\n    }\n    \n    private void copyText(Context context, String text) {\n        if (text != null) {\n            ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);\n            ClipData clip = ClipData.newPlainText(\"copied text\", text);\n            clipboard.setPrimaryClip(clip);\n\n            Toast toast = Toast.makeText(context, getString(R.string.amp_label_copied), Toast.LENGTH_SHORT);\n            toast.show();\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/eventexplorer/EventExplorerTouchHandler.java",
    "content": "package com.amplitude.eventexplorer;\n\nimport android.content.Intent;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.WindowManager;\n\npublic class EventExplorerTouchHandler implements View.OnTouchListener {\n    private int initialX;\n    private float initialTouchX;\n\n    private int initialY;\n    private float initialTouchY;\n\n    private WindowManager.LayoutParams layoutParams;\n    private WindowManager windowManager;\n    private String instanceName;\n\n    EventExplorerTouchHandler(WindowManager windowManager,\n                              WindowManager.LayoutParams layoutParams,\n                              String instanceName) {\n        this.layoutParams = layoutParams;\n        this.windowManager = windowManager;\n        this.instanceName = instanceName;\n    }\n\n    @Override\n    public boolean onTouch(View v, MotionEvent event) {\n        switch (event.getAction()) {\n            case MotionEvent.ACTION_DOWN:\n                initialY = layoutParams.y;\n                initialX = layoutParams.x;\n                initialTouchX = event.getRawX();\n                initialTouchY = event.getRawY();\n                return true;\n            case MotionEvent.ACTION_MOVE:\n                layoutParams.y = initialY + (int) (event.getRawY() - initialTouchY);\n                layoutParams.x = initialX + (int) (event.getRawX() - initialTouchX);\n                windowManager.updateViewLayout(v, layoutParams);\n                return true;\n            case MotionEvent.ACTION_UP:\n                float endX = event.getRawX();\n                float endY = event.getRawY();\n                if (isAClick(initialTouchX, endX, initialTouchY, endY)) {\n                    v.performClick();\n\n                    Intent intent = new Intent(v.getContext(), EventExplorerInfoActivity.class);\n                    intent.putExtra(\"instanceName\", this.instanceName);\n                    v.getContext().startActivity(intent);\n                }\n                return true;\n        }\n        return false;\n    }\n\n    private boolean isAClick(float startX, float endX, float startY, float endY) {\n        float differenceX = Math.abs(startX - endX);\n        float differenceY = Math.abs(startY - endY);\n        return !(differenceX > 5 || differenceY > 5);\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/unity/plugins/AmplitudePlugin.java",
    "content": "package com.amplitude.unity.plugins;\n\nimport android.app.Application;\nimport android.content.Context;\n\nimport com.amplitude.api.Amplitude;\nimport com.amplitude.api.AmplitudeServerZone;\nimport com.amplitude.api.Identify;\nimport com.amplitude.api.Revenue;\nimport com.amplitude.api.TrackingOptions;\nimport com.amplitude.api.Utils;\n\nimport org.json.JSONArray;\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\npublic class AmplitudePlugin {\n\n    public static JSONObject ToJSONObject(String jsonString) {\n        JSONObject properties = null;\n        try {\n            properties = new JSONObject(jsonString);\n        } catch (JSONException e) {\n            e.printStackTrace();\n        }\n        return properties;\n    }\n\n    public static JSONArray ToJSONArray(String[] values) {\n        JSONArray result = new JSONArray();\n        for (String value : values) {\n            result.put(value);\n        }\n        return result;\n    }\n\n    public static void init(String instanceName, Context context, String apiKey) {\n        Amplitude.getInstance(instanceName).initialize(context, apiKey);\n    }\n\n    public static void init(String instanceName, Context context, String apiKey, String userId) {\n        Amplitude.getInstance(instanceName).initialize(context, apiKey, userId);\n    }\n\n    public static void setTrackingOptions(String instanceName, String trackingOptionsJson) {\n        JSONObject trackingOptionsDict = ToJSONObject(trackingOptionsJson);\n        TrackingOptions trackingOptions = new TrackingOptions();\n\n        if (trackingOptionsDict.optBoolean(\"disableADID\", false)) {\n            trackingOptions.disableAdid();\n        }\n        if (trackingOptionsDict.optBoolean(\"disableAppSetId\", false)) {\n            trackingOptions.disableAppSetId();\n        }\n        if (trackingOptionsDict.optBoolean(\"disableCarrier\", false)) {\n            trackingOptions.disableCarrier();\n        }\n        if (trackingOptionsDict.optBoolean(\"disableCity\", false)) {\n            trackingOptions.disableCity();\n        }\n        if (trackingOptionsDict.optBoolean(\"disableCountry\", false)) {\n            trackingOptions.disableCountry();\n        }\n        if (trackingOptionsDict.optBoolean(\"disableDeviceBrand\", false)) {\n            trackingOptions.disableDeviceBrand();\n        }\n        if (trackingOptionsDict.optBoolean(\"disableDeviceManufacturer\", false)) {\n            trackingOptions.disableDeviceManufacturer();\n        }\n        if (trackingOptionsDict.optBoolean(\"disableDeviceModel\", false)) {\n            trackingOptions.disableDeviceModel();\n        }\n        if (trackingOptionsDict.optBoolean(\"disableDMA\", false)) {\n            trackingOptions.disableDma();\n        }\n        if (trackingOptionsDict.optBoolean(\"disableIPAddress\", false)) {\n            trackingOptions.disableIpAddress();\n        }\n        if (trackingOptionsDict.optBoolean(\"disableLanguage\", false)) {\n            trackingOptions.disableLanguage();\n        }\n        if (trackingOptionsDict.optBoolean(\"disableLatLng\", false)) {\n            trackingOptions.disableLatLng();\n        }\n        if (trackingOptionsDict.optBoolean(\"disableOSName\", false)) {\n            trackingOptions.disableOsName();\n        }\n        if (trackingOptionsDict.optBoolean(\"disableOSVersion\", false)) {\n            trackingOptions.disableOsVersion();\n        }\n        if (trackingOptionsDict.optBoolean(\"disableApiLevel\", false)) {\n            trackingOptions.disableApiLevel();\n        }\n        if (trackingOptionsDict.optBoolean(\"disablePlatform\", false)) {\n            trackingOptions.disablePlatform();\n        }\n        if (trackingOptionsDict.optBoolean(\"disableRegion\", false)) {\n            trackingOptions.disableRegion();\n        }\n        if (trackingOptionsDict.optBoolean(\"disableVersionName\", false)) {\n            trackingOptions.disableVersionName();\n        }\n        Amplitude.getInstance(instanceName).setTrackingOptions(trackingOptions);\n    }\n\n    public static void enableForegroundTracking(String instanceName, Application app) {\n        Amplitude.getInstance(instanceName).enableForegroundTracking(app);\n    }\n\n    public static void enableCoppaControl(String instanceName) {\n        Amplitude.getInstance(instanceName).enableCoppaControl();\n    }\n\n    public static void disableCoppaControl(String instanceName) {\n        Amplitude.getInstance(instanceName).disableCoppaControl();\n    }\n\n    public static void setLibraryName(String instanceName, String libraryName) {\n        Amplitude.getInstance(instanceName).setLibraryName(libraryName);\n    }\n\n    public static void setLibraryVersion(String instanceName, String libraryVersion) {\n        Amplitude.getInstance(instanceName).setLibraryVersion(libraryVersion);\n    }\n\n    public static void setServerUrl(String instanceName, String serverUrl) {\n        Amplitude.getInstance(instanceName).setServerUrl(serverUrl);\n    }\n\n    public static void setServerZone(String instanceName, String serverZone, boolean updateServerUrl) {\n        AmplitudeServerZone amplitudeServerZone = AmplitudeServerZone.getServerZone(serverZone);\n        Amplitude.getInstance(instanceName).setServerZone(amplitudeServerZone, updateServerUrl);\n    }\n\n    public static void setUseDynamicConfig(String instanceName, boolean useDynamicConfig) {\n        Amplitude.getInstance(instanceName).setUseDynamicConfig(useDynamicConfig);\n    }\n\n    @Deprecated\n    public static void startSession() { return; }\n\n    @Deprecated\n    public static void endSession() { return; }\n\n    public static void logEvent(String instanceName, String event) {\n        Amplitude.getInstance(instanceName).logEvent(event);\n    }\n\n    public static void logEvent(String instanceName, String event, String jsonProperties) {\n        Amplitude.getInstance(instanceName).logEvent(event, ToJSONObject(jsonProperties));\n    }\n\n    public static void logEvent(String instanceName, String event, String jsonProperties, boolean outOfSession) {\n        Amplitude.getInstance(instanceName).logEvent(event, ToJSONObject(jsonProperties), outOfSession);\n    }\n\n    public static void uploadEvents(String instanceName) {\n        Amplitude.getInstance(instanceName).uploadEvents();\n    }\n\n    public static void useAdvertisingIdForDeviceId(String instanceName) {\n        Amplitude.getInstance(instanceName).useAdvertisingIdForDeviceId();\n    }\n\n    public static void useAppSetIdForDeviceId(String instanceName) {\n        Amplitude.getInstance(instanceName).useAppSetIdForDeviceId();\n    }\n\n    public static void setOffline(String instanceName, boolean offline) {\n        Amplitude.getInstance(instanceName).setOffline(offline);\n    }\n\n    public static void setUserId(String instanceName, String userId) {\n        Amplitude.getInstance(instanceName).setUserId(userId);\n    }\n\n    public static void setOptOut(String instanceName, boolean enabled) {\n        Amplitude.getInstance(instanceName).setOptOut(enabled);\n    }\n\n    public static void setMinTimeBetweenSessionsMillis(String instanceName, long minTimeBetweenSessionsMillis) {\n        Amplitude.getInstance(instanceName).setMinTimeBetweenSessionsMillis(minTimeBetweenSessionsMillis);\n    }\n\n    public static void setEventUploadPeriodMillis(String instanceName, int eventUploadPeriodMillis) {\n        Amplitude.getInstance(instanceName).setEventUploadPeriodMillis(eventUploadPeriodMillis);\n    }\n\n    public static void setUserProperties(String instanceName, String jsonProperties) {\n        Amplitude.getInstance(instanceName).setUserProperties(ToJSONObject(jsonProperties));\n    }\n\n    public static void setGroup(String instanceName, String groupType, String groupName) {\n        Amplitude.getInstance(instanceName).setGroup(groupType, groupName);\n    }\n\n    public static void setGroup(String instanceName, String groupType, String[] groupName) {\n        Amplitude.getInstance(instanceName).setGroup(groupType, ToJSONArray(groupName));\n    }\n\n    public static void logRevenue(String instanceName, double amount) {\n        Amplitude.getInstance(instanceName).logRevenue(amount);\n    }\n\n    public static void logRevenue(String instanceName, String productId, int quantity, double price) {\n        Amplitude.getInstance(instanceName).logRevenue(productId, quantity, price);\n    }\n\n    public static void logRevenue(String instanceName, String productId, int quantity, double price, String receipt, String receiptSignature) {\n        Amplitude.getInstance(instanceName).logRevenue(productId, quantity, price, receipt, receiptSignature);\n    }\n\n    public static void logRevenue(String instanceName, String productId, int quantity, double price, String receipt, String receiptSignature, String revenueType, String jsonProperties) {\n        Revenue revenue = new Revenue().setQuantity(quantity).setPrice(price);\n        if (!Utils.isEmptyString(productId)) {\n            revenue.setProductId(productId);\n        }\n        if (!Utils.isEmptyString(receipt) && !Utils.isEmptyString(receiptSignature)) {\n            revenue.setReceipt(receipt, receiptSignature);\n        }\n        if (!Utils.isEmptyString(revenueType)) {\n            revenue.setRevenueType(revenueType);\n        }\n        if (!Utils.isEmptyString(jsonProperties)) {\n            revenue.setEventProperties(ToJSONObject(jsonProperties));\n        }\n        Amplitude.getInstance(instanceName).logRevenueV2(revenue);\n    }\n\n    public static String getDeviceId(String instanceName) {\n        return Amplitude.getInstance(instanceName).getDeviceId();\n    }\n\n    public static void setDeviceId(String instanceName, String deviceId) {\n        Amplitude.getInstance(instanceName).setDeviceId(deviceId);\n    }\n\n    public static void regenerateDeviceId(String instanceName) { Amplitude.getInstance(instanceName).regenerateDeviceId(); }\n\n    public static void trackSessionEvents(String instanceName, boolean enabled) {\n        Amplitude.getInstance(instanceName).trackSessionEvents(enabled);\n    }\n\n    public static long getSessionId(String instanceName) { return Amplitude.getInstance(instanceName).getSessionId(); }\n\n    // User Property Operations\n\n    // clear user properties\n    public static void clearUserProperties(String instanceName) {\n        Amplitude.getInstance(instanceName).clearUserProperties();\n    }\n\n    // unset user property\n    public static void unsetUserProperty(String instanceName, String property) {\n        Amplitude.getInstance(instanceName).identify(new Identify().unset(property));\n    }\n\n    // setOnce user property\n    public static void setOnceUserProperty(String instanceName, String property, boolean value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().setOnce(property, value));\n    }\n\n    public static void setOnceUserProperty(String instanceName, String property, double value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().setOnce(property, value));\n    }\n\n    public static void setOnceUserProperty(String instanceName, String property, float value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().setOnce(property, value));\n    }\n\n    public static void setOnceUserProperty(String instanceName, String property, int value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().setOnce(property, value));\n    }\n\n    public static void setOnceUserProperty(String instanceName, String property, long value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().setOnce(property, value));\n    }\n\n    public static void setOnceUserProperty(String instanceName, String property, String value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().setOnce(property, value));\n    }\n\n    public static void setOnceUserPropertyDict(String instanceName, String property, String values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().setOnce(property, ToJSONObject(values)));\n    }\n\n    public static void setOnceUserPropertyList(String instanceName, String property, String values) {\n        JSONObject properties = ToJSONObject(values);\n        if (properties == null) {\n            return;\n        }\n        Amplitude.getInstance(instanceName).identify(new Identify().setOnce(\n            property, properties.optJSONArray(\"list\")\n        ));\n    }\n\n    public static void setOnceUserProperty(String instanceName, String property, boolean[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().setOnce(property, values));\n    }\n\n    public static void setOnceUserProperty(String instanceName, String property, double[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().setOnce(property, values));\n    }\n\n    public static void setOnceUserProperty(String instanceName, String property, float[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().setOnce(property, values));\n    }\n\n    public static void setOnceUserProperty(String instanceName, String property, int[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().setOnce(property, values));\n    }\n\n    public static void setOnceUserProperty(String instanceName, String property, long[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().setOnce(property, values));\n    }\n\n    public static void setOnceUserProperty(String instanceName, String property, String[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().setOnce(property, values));\n    }\n\n    // set user property\n    public static void setUserProperty(String instanceName, String property, boolean value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().set(property, value));\n    }\n\n    public static void setUserProperty(String instanceName, String property, double value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().set(property, value));\n    }\n\n    public static void setUserProperty(String instanceName, String property, float value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().set(property, value));\n    }\n\n    public static void setUserProperty(String instanceName, String property, int value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().set(property, value));\n    }\n\n    public static void setUserProperty(String instanceName, String property, long value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().set(property, value));\n    }\n\n    public static void setUserProperty(String instanceName, String property, String value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().set(property, value));\n    }\n\n    public static void setUserPropertyDict(String instanceName, String property, String values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().set(property, ToJSONObject(values)));\n    }\n\n    public static void setUserPropertyList(String instanceName, String property, String values) {\n        JSONObject properties = ToJSONObject(values);\n        if (properties == null) {\n            return;\n        }\n        Amplitude.getInstance(instanceName).identify(new Identify().set(\n                property, properties.optJSONArray(\"list\")\n        ));\n    }\n\n    public static void setUserProperty(String instanceName, String property, boolean[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().set(property, values));\n    }\n\n    public static void setUserProperty(String instanceName, String property, double[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().set(property, values));\n    }\n\n    public static void setUserProperty(String instanceName, String property, float[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().set(property, values));\n    }\n\n    public static void setUserProperty(String instanceName, String property, int[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().set(property, values));\n    }\n\n    public static void setUserProperty(String instanceName, String property, long[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().set(property, values));\n    }\n\n    public static void setUserProperty(String instanceName, String property, String[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().set(property, values));\n    }\n\n    // add\n    public static void addUserProperty(String instanceName, String property, double value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().add(property, value));\n    }\n\n    public static void addUserProperty(String instanceName, String property, float value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().add(property, value));\n    }\n\n    public static void addUserProperty(String instanceName, String property, int value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().add(property, value));\n    }\n\n    public static void addUserProperty(String instanceName, String property, long value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().add(property, value));\n    }\n\n    public static void addUserProperty(String instanceName, String property, String value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().add(property, value));\n    }\n\n    public static void addUserPropertyDict(String instanceName, String property, String values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().add(property, ToJSONObject(values)));\n    }\n\n    // append user property\n    public static void appendUserProperty(String instanceName, String property, boolean value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().append(property, value));\n    }\n\n    public static void appendUserProperty(String instanceName, String property, double value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().append(property, value));\n    }\n\n    public static void appendUserProperty(String instanceName, String property, float value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().append(property, value));\n    }\n\n    public static void appendUserProperty(String instanceName, String property, int value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().append(property, value));\n    }\n\n    public static void appendUserProperty(String instanceName, String property, long value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().append(property, value));\n    }\n\n    public static void appendUserProperty(String instanceName, String property, String value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().append(property, value));\n    }\n\n    public static void appendUserPropertyDict(String instanceName, String property, String values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().append(property, ToJSONObject(values)));\n    }\n\n    public static void appendUserPropertyList(String instanceName, String property, String values) {\n        JSONObject properties = ToJSONObject(values);\n        if (properties == null) {\n            return;\n        }\n        Amplitude.getInstance(instanceName).identify(new Identify().append(\n                property, properties.optJSONArray(\"list\")\n        ));\n    }\n\n    public static void appendUserProperty(String instanceName, String property, boolean[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().append(property, values));\n    }\n\n    public static void appendUserProperty(String instanceName, String property, double[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().append(property, values));\n    }\n\n    public static void appendUserProperty(String instanceName, String property, float[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().append(property, values));\n    }\n\n    public static void appendUserProperty(String instanceName, String property, int[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().append(property, values));\n    }\n\n    public static void appendUserProperty(String instanceName, String property, long[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().append(property, values));\n    }\n\n    public static void appendUserProperty(String instanceName, String property, String[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().append(property, values));\n    }\n\n    //prepend user property\n    public static void prependUserProperty(String instanceName, String property, boolean value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().prepend(property, value));\n    }\n\n    public static void prependUserProperty(String instanceName, String property, double value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().prepend(property, value));\n    }\n\n    public static void prependUserProperty(String instanceName, String property, float value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().prepend(property, value));\n    }\n\n    public static void prependUserProperty(String instanceName, String property, int value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().prepend(property, value));\n    }\n\n    public static void prependUserProperty(String instanceName, String property, long value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().prepend(property, value));\n    }\n\n    public static void prependUserProperty(String instanceName, String property, String value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().prepend(property, value));\n    }\n\n    public static void prependUserPropertyDict(String instanceName, String property, String values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().prepend(property, ToJSONObject(values)));\n    }\n\n    public static void prependUserPropertyList(String instanceName, String property, String values) {\n        JSONObject properties = ToJSONObject(values);\n        if (properties == null) {\n            return;\n        }\n        Amplitude.getInstance(instanceName).identify(new Identify().prepend(\n                property, properties.optJSONArray(\"list\")\n        ));\n    }\n\n    public static void prependUserProperty(String instanceName, String property, boolean[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().prepend(property, values));\n    }\n\n    public static void prependUserProperty(String instanceName, String property, double[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().prepend(property, values));\n    }\n\n    public static void prependUserProperty(String instanceName, String property, float[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().prepend(property, values));\n    }\n\n    public static void prependUserProperty(String instanceName, String property, int[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().prepend(property, values));\n    }\n\n    public static void prependUserProperty(String instanceName, String property, long[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().prepend(property, values));\n    }\n\n    public static void prependUserProperty(String instanceName, String property, String[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().prepend(property, values));\n    }\n\n    //preInsert user property\n    public static void preInsertUserProperty(String instanceName, String property, boolean value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().preInsert(property, value));\n    }\n\n    public static void preInsertUserProperty(String instanceName, String property, double value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().preInsert(property, value));\n    }\n\n    public static void preInsertUserProperty(String instanceName, String property, float value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().preInsert(property, value));\n    }\n\n    public static void preInsertUserProperty(String instanceName, String property, int value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().preInsert(property, value));\n    }\n\n    public static void preInsertUserProperty(String instanceName, String property, long value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().preInsert(property, value));\n    }\n\n    public static void preInsertUserProperty(String instanceName, String property, String value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().preInsert(property, value));\n    }\n\n    public static void preInsertUserPropertyDict(String instanceName, String property, String values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().preInsert(property, ToJSONObject(values)));\n    }\n\n    public static void preInsertUserPropertyList(String instanceName, String property, String values) {\n        JSONObject properties = ToJSONObject(values);\n        if (properties == null) {\n            return;\n        }\n        Amplitude.getInstance(instanceName).identify(new Identify().preInsert(\n                property, properties.optJSONArray(\"list\")\n        ));\n    }\n\n    public static void preInsertUserProperty(String instanceName, String property, boolean[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().preInsert(property, values));\n    }\n\n    public static void preInsertUserProperty(String instanceName, String property, double[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().preInsert(property, values));\n    }\n\n    public static void preInsertUserProperty(String instanceName, String property, float[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().preInsert(property, values));\n    }\n\n    public static void preInsertUserProperty(String instanceName, String property, int[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().preInsert(property, values));\n    }\n\n    public static void preInsertUserProperty(String instanceName, String property, long[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().preInsert(property, values));\n    }\n\n    public static void preInsertUserProperty(String instanceName, String property, String[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().preInsert(property, values));\n    }\n\n    //postInsert user property\n    public static void postInsertUserProperty(String instanceName, String property, boolean value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().postInsert(property, value));\n    }\n\n    public static void postInsertUserProperty(String instanceName, String property, double value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().postInsert(property, value));\n    }\n\n    public static void postInsertUserProperty(String instanceName, String property, float value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().postInsert(property, value));\n    }\n\n    public static void postInsertUserProperty(String instanceName, String property, int value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().postInsert(property, value));\n    }\n\n    public static void postInsertUserProperty(String instanceName, String property, long value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().postInsert(property, value));\n    }\n\n    public static void postInsertUserProperty(String instanceName, String property, String value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().postInsert(property, value));\n    }\n\n    public static void postInsertUserPropertyDict(String instanceName, String property, String values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().postInsert(property, ToJSONObject(values)));\n    }\n\n    public static void postInsertUserPropertyList(String instanceName, String property, String values) {\n        JSONObject properties = ToJSONObject(values);\n        if (properties == null) {\n            return;\n        }\n        Amplitude.getInstance(instanceName).identify(new Identify().postInsert(\n                property, properties.optJSONArray(\"list\")\n        ));\n    }\n\n    public static void postInsertUserProperty(String instanceName, String property, boolean[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().postInsert(property, values));\n    }\n\n    public static void postInsertUserProperty(String instanceName, String property, double[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().postInsert(property, values));\n    }\n\n    public static void postInsertUserProperty(String instanceName, String property, float[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().postInsert(property, values));\n    }\n\n    public static void postInsertUserProperty(String instanceName, String property, int[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().postInsert(property, values));\n    }\n\n    public static void postInsertUserProperty(String instanceName, String property, long[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().postInsert(property, values));\n    }\n\n    public static void postInsertUserProperty(String instanceName, String property, String[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().postInsert(property, values));\n    }\n\n    //remove user property\n    public static void removeUserProperty(String instanceName, String property, boolean value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().remove(property, value));\n    }\n\n    public static void removeUserProperty(String instanceName, String property, double value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().remove(property, value));\n    }\n\n    public static void removeUserProperty(String instanceName, String property, float value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().remove(property, value));\n    }\n\n    public static void removeUserProperty(String instanceName, String property, int value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().remove(property, value));\n    }\n\n    public static void removeUserProperty(String instanceName, String property, long value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().remove(property, value));\n    }\n\n    public static void removeUserProperty(String instanceName, String property, String value) {\n        Amplitude.getInstance(instanceName).identify(new Identify().remove(property, value));\n    }\n\n    public static void removeUserPropertyDict(String instanceName, String property, String values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().remove(property, ToJSONObject(values)));\n    }\n\n    public static void removeUserPropertyList(String instanceName, String property, String values) {\n        JSONObject properties = ToJSONObject(values);\n        if (properties == null) {\n            return;\n        }\n        Amplitude.getInstance(instanceName).identify(new Identify().remove(\n                property, properties.optJSONArray(\"list\")\n        ));\n    }\n\n    public static void removeUserProperty(String instanceName, String property, boolean[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().remove(property, values));\n    }\n\n    public static void removeUserProperty(String instanceName, String property, double[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().remove(property, values));\n    }\n\n    public static void removeUserProperty(String instanceName, String property, float[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().remove(property, values));\n    }\n\n    public static void removeUserProperty(String instanceName, String property, int[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().remove(property, values));\n    }\n\n    public static void removeUserProperty(String instanceName, String property, long[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().remove(property, values));\n    }\n\n    public static void removeUserProperty(String instanceName, String property, String[] values) {\n        Amplitude.getInstance(instanceName).identify(new Identify().remove(property, values));\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/util/DoubleCheck.java",
    "content": "package com.amplitude.util;\n\n\n/**\n * Copy from https://github.com/google/dagger/blob/master/java/dagger/internal/DoubleCheck.java\n *\n * Apache v2.0\n */\npublic class DoubleCheck<T> implements Provider<T> {\n    private static final Object UNINITIALIZED = new Object();\n\n    private volatile Provider<T> provider;\n    private volatile Object instance = UNINITIALIZED;\n\n    private DoubleCheck(Provider<T> provider) {\n        assert provider != null;\n        this.provider = provider;\n    }\n\n    @SuppressWarnings(\"unchecked\") // cast only happens when result comes from the provider\n    @Override\n    public T get() {\n        Object result = instance;\n        if (result == UNINITIALIZED) {\n            synchronized (this) {\n                result = instance;\n                if (result == UNINITIALIZED) {\n                    result = provider.get();\n                    instance = reentrantCheck(instance, result);\n                    /* Null out the reference to the provider. We are never going to need it again, so we\n                     * can make it eligible for GC. */\n                    provider = null;\n                }\n            }\n        }\n        return (T) result;\n    }\n\n    /**\n     * Checks to see if creating the new instance has resulted in a recursive call. If it has, and the\n     * new instance is the same as the current instance, return the instance. However, if the new\n     * instance differs from the current instance, an {@link IllegalStateException} is thrown.\n     */\n    public static Object reentrantCheck(Object currentInstance, Object newInstance) {\n        boolean isReentrant = !(currentInstance == UNINITIALIZED);\n\n        if (isReentrant && currentInstance != newInstance) {\n            throw new IllegalStateException(\"Scoped provider was invoked recursively returning \"\n                    + \"different results: \" + currentInstance + \" & \" + newInstance + \". This is likely \"\n                    + \"due to a circular dependency.\");\n        }\n        return newInstance;\n    }\n\n    /** Returns a {@link Provider} that caches the value from the given delegate provider. */\n    public static <P extends Provider<T>, T> Provider<T> provider(P delegate) {\n        if (delegate == null) {\n            throw new IllegalArgumentException(\"delegate cannot be null\");\n        }\n        if (delegate instanceof DoubleCheck) {\n            /* This should be a rare case, but if we have a scoped @Binds that delegates to a scoped\n             * binding, we shouldn't cache the value again. */\n            return delegate;\n        }\n        return new DoubleCheck<T>(delegate);\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/amplitude/util/Provider.java",
    "content": "package com.amplitude.util;\n\npublic interface Provider<T> {\n    T get();\n}"
  },
  {
    "path": "src/main/res/drawable/amp_button_bg.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <stroke android:width=\"3px\" android:color=\"@color/amp_light_gray_2\" />\n</shape>"
  },
  {
    "path": "src/main/res/layout/amp_activity_eventexplorer_info.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n\n    android:background=\"@color/amp_light_gray\">\n\n    <FrameLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"56dp\"\n        android:background=\"@android:color/white\">\n        <ImageView\n            android:src=\"@drawable/amp_banner\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\"/>\n        \n        <ImageView\n            android:id=\"@+id/amp_eeInfo_iv_close\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:src=\"@drawable/amp_cancel\"\n            android:layout_gravity=\"end|center_vertical\"\n            android:layout_marginRight=\"10dp\" />\n    </FrameLayout>\n\n    <LinearLayout\n        android:layout_marginTop=\"16dp\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\"\n        android:background=\"@android:color/white\">\n\n        <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/amp_label_user_info\"\n            android:textSize=\"18dp\"\n            android:textColor=\"@color/amp_dark_blue\"\n            android:layout_marginLeft=\"16dp\"\n            android:layout_marginTop=\"12dp\" />\n\n        <RelativeLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"44dp\"\n            android:layout_marginLeft=\"16dp\"\n            android:layout_marginTop=\"5dp\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:textColor=\"@color/amp_dark_blue\"\n                android:textSize=\"15dp\"\n                android:text=\"@string/amp_label_device_id\"/>\n\n            <TextView\n                android:id=\"@+id/amp_eeInfo_tv_deviceId\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:textColor=\"@color/amp_gray\"\n                android:singleLine=\"true\"\n                android:textSize=\"15dp\"\n                android:text=\"@string/amp_label_not_avail\"\n                android:layout_alignParentBottom=\"true\"\n                android:layout_toLeftOf=\"@id/amp_eeInfo_btn_copyDeviceId\"\n                android:layout_marginRight=\"10dp\" />\n\n            <Button\n                android:id=\"@+id/amp_eeInfo_btn_copyDeviceId\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"32dp\"\n                android:layout_centerInParent=\"true\"\n                android:gravity=\"center\"\n                android:layout_marginRight=\"19dp\"\n                android:background=\"@drawable/amp_button_bg\"\n                android:textColor=\"@color/amp_blue\"\n                android:text=\"@string/amp_label_copy\"\n                android:layout_alignParentRight=\"true\"/>\n        </RelativeLayout>\n\n        <RelativeLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"44dp\"\n            android:layout_marginLeft=\"16dp\"\n            android:layout_marginTop=\"10dp\"\n            android:layout_marginBottom=\"16dp\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:textColor=\"@color/amp_dark_blue\"\n                android:textSize=\"15dp\"\n                android:text=\"@string/amp_label_user_id\"/>\n\n            <TextView\n                android:id=\"@+id/amp_eeInfo_tv_userId\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:textColor=\"@color/amp_gray\"\n                android:singleLine=\"true\"\n                android:textSize=\"15dp\"\n                android:text=\"@string/amp_label_not_avail\"\n                android:layout_alignParentBottom=\"true\"\n                android:layout_toLeftOf=\"@id/amp_eeInfo_btn_copyUserId\"\n                android:layout_marginRight=\"10dp\"/>\n\n            <Button\n                android:id=\"@+id/amp_eeInfo_btn_copyUserId\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"32dp\"\n                android:layout_centerInParent=\"true\"\n                android:gravity=\"center\"\n                android:layout_marginRight=\"19dp\"\n                android:background=\"@drawable/amp_button_bg\"\n                android:textColor=\"@color/amp_blue\"\n                android:text=\"@string/amp_label_copy\"\n                android:layout_alignParentRight=\"true\"/>\n        </RelativeLayout>\n\n    </LinearLayout>\n</LinearLayout>"
  },
  {
    "path": "src/main/res/layout/amp_bubble_view.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"48dp\"\n    android:layout_height=\"48dp\"\n    android:layout_gravity=\"center\"\n    android:background=\"@color/amp_transparent\">\n\n    <ImageView\n        android:src=\"@drawable/amp_logo\"\n        android:layout_width=\"48dp\"\n        android:layout_height=\"48dp\" />\n\n</FrameLayout>"
  },
  {
    "path": "src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"amp_transparent\">#00000000</color>\n    <color name=\"amp_light_gray\">#E5E5E5</color>\n    <color name=\"amp_light_gray_2\">#C6D0D9</color>\n    <color name=\"amp_gray\">#60758B</color>\n    <color name=\"amp_dark_blue\">#0C2A4B</color>\n    <color name=\"amp_blue\">#005488</color>\n\n</resources>"
  },
  {
    "path": "src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"amp_label_copy\">Copy</string>\n    <string name=\"amp_label_copied\">Copied To Clipboard</string>\n    <string name=\"amp_label_user_info\">User Information</string>\n    <string name=\"amp_label_device_id\">DEVICE ID</string>\n    <string name=\"amp_label_user_id\">USER ID</string>\n    <string name=\"amp_label_not_avail\">N/A</string>\n</resources>\n"
  },
  {
    "path": "src/test/java/com/amplitude/api/AmplitudeClientTest.java",
    "content": "package com.amplitude.api;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.json.JSONArray;\nimport org.json.JSONException;\nimport org.json.JSONObject;\nimport org.junit.After;\nimport org.junit.Assert;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Matchers;\nimport org.powermock.api.mockito.PowerMockito;\nimport org.powermock.core.classloader.annotations.PrepareForTest;\nimport org.robolectric.Robolectric;\nimport org.robolectric.Shadows;\nimport org.robolectric.annotation.Config;\nimport org.robolectric.shadows.ShadowLooper;\n\nimport java.io.IOException;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport okhttp3.Call;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request;\nimport okhttp3.mockwebserver.MockResponse;\nimport okhttp3.mockwebserver.RecordedRequest;\n\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertNotEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertNull;\nimport static org.junit.Assert.assertTrue;\nimport static org.junit.Assert.fail;\nimport static org.robolectric.Shadows.shadowOf;\n\n@RunWith(AndroidJUnit4.class)\n@Config(manifest = Config.NONE)\npublic class AmplitudeClientTest extends BaseTest {\n\n    private String generateStringWithLength(int length, char c) {\n        if (length < 0) return \"\";\n        char [] array = new char[length];\n        Arrays.fill(array, c);\n        return new String(array);\n    }\n\n    @Before\n    public void setUp() throws Exception {\n        super.setUp();\n        amplitude.initialize(context, apiKey);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runOneTask();\n    }\n\n    @After\n    public void tearDown() throws Exception {\n        super.tearDown();\n    }\n\n    @Test\n    public void testConstructor() {\n        // verify that the constructor lowercases the instance name\n        AmplitudeClient a = new AmplitudeClient(\"APP1\");\n        AmplitudeClient b = new AmplitudeClient(\"New_App_2\");\n\n        assertEquals(a.instanceName, \"app1\");\n        assertEquals(b.instanceName, \"new_app_2\");\n    }\n\n    @Test\n    public void testSetUserId() {\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        String userId = \"user_id\";\n        amplitude.setUserId(userId);\n        looper.runToEndOfTasks();\n        assertEquals(userId, dbHelper.getValue(AmplitudeClient.USER_ID_KEY));\n        assertEquals(userId, amplitude.getUserId());\n\n        // try setting to null\n        amplitude.setUserId(null);\n        looper.runToEndOfTasks();\n        assertNull(dbHelper.getValue(AmplitudeClient.USER_ID_KEY));\n        assertNull(amplitude.getUserId());\n    }\n\n    @Test\n    public void testSetUserIdTwice() {\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        String userId1 = \"user_id1\";\n        String userId2 = \"user_id2\";\n\n        amplitude.setUserId(userId1);\n        looper.runToEndOfTasks();\n        assertEquals(amplitude.getUserId(), userId1);\n        amplitude.logEvent(\"event1\");\n        looper.runToEndOfTasks();\n\n        JSONObject event1 = getLastUnsentEvent();\n        assertEquals(event1.optString(\"event_type\"), \"event1\");\n        assertEquals(event1.optString(\"user_id\"), userId1);\n\n        amplitude.setUserId(userId2);\n        looper.runToEndOfTasks();\n        assertEquals(amplitude.getUserId(), userId2);\n        amplitude.logEvent(\"event2\");\n        looper.runToEndOfTasks();\n\n        JSONObject event2 = getLastUnsentEvent();\n        assertEquals(event2.optString(\"event_type\"), \"event2\");\n        assertEquals(event2.optString(\"user_id\"), userId2);\n    }\n\n    @Test\n    public void testSetDeviceId() {\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n\n        String deviceId = amplitude.getDeviceId(); // Randomly generated device ID\n        assertNotNull(deviceId);\n        assertEquals(deviceId.length(), 36 + 1); // 36 for UUID, + 1 for appended R\n        assertEquals(deviceId.charAt(36), 'R');\n        assertEquals(dbHelper.getValue(amplitude.DEVICE_ID_KEY), deviceId);\n\n\n        // test setting invalid device ids\n        amplitude.setDeviceId(null);\n        looper.runToEndOfTasks();\n        assertEquals(amplitude.getDeviceId(), deviceId);\n        assertEquals(dbHelper.getValue(amplitude.DEVICE_ID_KEY), deviceId);\n\n        amplitude.setDeviceId(\"\");\n        looper.runToEndOfTasks();\n        assertEquals(amplitude.getDeviceId(), deviceId);\n        assertEquals(dbHelper.getValue(amplitude.DEVICE_ID_KEY), deviceId);\n\n        amplitude.setDeviceId(\"9774d56d682e549c\");\n        looper.runToEndOfTasks();\n        assertEquals(amplitude.getDeviceId(), deviceId);\n        assertEquals(dbHelper.getValue(amplitude.DEVICE_ID_KEY), deviceId);\n\n        amplitude.setDeviceId(\"unknown\");\n        looper.runToEndOfTasks();\n        assertEquals(amplitude.getDeviceId(), deviceId);\n        assertEquals(dbHelper.getValue(amplitude.DEVICE_ID_KEY), deviceId);\n\n        amplitude.setDeviceId(\"000000000000000\");\n        looper.runToEndOfTasks();\n        assertEquals(amplitude.getDeviceId(), deviceId);\n        assertEquals(dbHelper.getValue(amplitude.DEVICE_ID_KEY), deviceId);\n\n        amplitude.setDeviceId(\"Android\");\n        looper.runToEndOfTasks();\n        assertEquals(amplitude.getDeviceId(), deviceId);\n        assertEquals(dbHelper.getValue(amplitude.DEVICE_ID_KEY), deviceId);\n\n        amplitude.setDeviceId(\"DEFACE\");\n        looper.runToEndOfTasks();\n        assertEquals(amplitude.getDeviceId(), deviceId);\n        assertEquals(dbHelper.getValue(amplitude.DEVICE_ID_KEY), deviceId);\n\n        amplitude.setDeviceId(\"00000000-0000-0000-0000-000000000000\");\n        assertEquals(amplitude.getDeviceId(), deviceId);\n        assertEquals(dbHelper.getValue(amplitude.DEVICE_ID_KEY), deviceId);\n\n        // set valid device id\n        String newDeviceId = UUID.randomUUID().toString();\n        amplitude.setDeviceId(newDeviceId);\n        looper.runToEndOfTasks();\n        assertEquals(amplitude.getDeviceId(), newDeviceId);\n        assertEquals(dbHelper.getValue(amplitude.DEVICE_ID_KEY), newDeviceId);\n\n        amplitude.logEvent(\"test\");\n        looper.runToEndOfTasks();\n        JSONObject event = getLastUnsentEvent();\n        assertEquals(event.optString(\"event_type\"), \"test\");\n        assertEquals(event.optString(\"device_id\"), newDeviceId);\n    }\n\n    @Test\n    public void testSetUserProperties() throws JSONException {\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n\n        // setting null or empty user properties does nothing\n        amplitude.setUserProperties(null);\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 0);\n        assertEquals(getUnsentIdentifyCount(), 0);\n        amplitude.setUserProperties(new JSONObject());\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 0);\n        assertEquals(getUnsentIdentifyCount(), 0);\n\n        JSONObject userProperties = new JSONObject().put(\"key1\", \"value1\").put(\"key2\", \"value2\");\n        amplitude.setUserProperties(userProperties);\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 0);\n        assertEquals(getUnsentIdentifyCount(), 1);\n        JSONObject event = getLastUnsentIdentify();\n        assertEquals(Constants.IDENTIFY_EVENT, event.optString(\"event_type\"));\n        assertTrue(Utils.compareJSONObjects(\n            event.optJSONObject(\"event_properties\"), new JSONObject()\n        ));\n\n        JSONObject userPropertiesOperations = event.optJSONObject(\"user_properties\");\n        assertEquals(userPropertiesOperations.length(), 1);\n        assertTrue(userPropertiesOperations.has(Constants.AMP_OP_SET));\n\n        JSONObject setOperations = userPropertiesOperations.optJSONObject(Constants.AMP_OP_SET);\n        assertTrue(Utils.compareJSONObjects(userProperties, setOperations));\n    }\n\n    @Test\n    public void testSetCustomLibrary() {\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        amplitude.setLibraryName(\"amplitude-unity\");\n        amplitude.setLibraryVersion(\"1.0.0\");\n        amplitude.logEvent(\"test\");\n        looper.runToEndOfTasks();\n\n        JSONObject event = getLastEvent();\n        assertNotNull(event);\n        try {\n            JSONObject library = event.getJSONObject(\"library\");\n            String libName = library.getString(\"name\");\n            String libVersion = library.getString(\"version\");\n            assertEquals(libName, \"amplitude-unity\");\n            assertEquals(libVersion, \"1.0.0\");\n        } catch (Exception e) {\n            Assert.fail();\n        }\n    }\n\n    @Test\n    public void testSetCustomLibraryWithNullValues() {\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        amplitude.setLibraryName(null);\n        amplitude.setLibraryVersion(null);\n        amplitude.logEvent(\"test\");\n        looper.runToEndOfTasks();\n\n        JSONObject event = getLastEvent();\n        assertNotNull(event);\n        try {\n            JSONObject library = event.getJSONObject(\"library\");\n            String libName = library.getString(\"name\");\n            String libVersion = library.getString(\"version\");\n            assertEquals(libName, \"unknown-library\");\n            assertEquals(libVersion, \"unknown-version\");\n        } catch (Exception e) {\n            Assert.fail();\n        }\n    }\n\n    @Test\n    public void testIdentifyMultipleOperations() throws JSONException {\n        String property1 = \"string value\";\n        String value1 = \"testValue\";\n\n        String property2 = \"double value\";\n        double value2 = 0.123;\n\n        String property3 = \"boolean value\";\n        boolean value3 = true;\n\n        String property4 = \"json value\";\n\n        Identify identify = new Identify().setOnce(property1, value1).add(property2, value2);\n        identify.set(property3, value3).unset(property4);\n\n        // identify should ignore this since duplicate key\n        identify.set(property4, value3);\n\n        amplitude.identify(identify);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        assertEquals(getUnsentIdentifyCount(), 1);\n        assertEquals(getUnsentEventCount(), 0);\n        JSONObject event = getLastUnsentIdentify();\n        assertEquals(Constants.IDENTIFY_EVENT, event.optString(\"event_type\"));\n\n        JSONObject userProperties = event.optJSONObject(\"user_properties\");\n        JSONObject expected = new JSONObject();\n        expected.put(Constants.AMP_OP_SET_ONCE, new JSONObject().put(property1, value1));\n        expected.put(Constants.AMP_OP_ADD, new JSONObject().put(property2, value2));\n        expected.put(Constants.AMP_OP_SET, new JSONObject().put(property3, value3));\n        expected.put(Constants.AMP_OP_UNSET, new JSONObject().put(property4, \"-\"));\n        assertTrue(Utils.compareJSONObjects(userProperties, expected));\n    }\n\n    @Test\n    public void testOptOut() {\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        ShadowLooper httplooper = Shadows.shadowOf(amplitude.httpThread.getLooper());\n\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        assertFalse(amplitude.isOptedOut());\n        assertNull(dbHelper.getLongValue(AmplitudeClient.OPT_OUT_KEY));\n\n        amplitude.setOptOut(true);\n        looper.runToEndOfTasks();\n        assertTrue(amplitude.isOptedOut());\n        assertEquals((long) dbHelper.getLongValue(AmplitudeClient.OPT_OUT_KEY), 1L);\n        RecordedRequest request = sendEvent(amplitude, \"test_opt_out\", null);\n        assertNull(request);\n\n        // Event shouldn't be sent event once opt out is turned off.\n        amplitude.setOptOut(false);\n        looper.runToEndOfTasks();\n        assertFalse(amplitude.isOptedOut());\n        assertEquals((long) dbHelper.getLongValue(AmplitudeClient.OPT_OUT_KEY), 0L);\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        httplooper.runToEndOfTasks();\n        assertNull(request);\n\n        request = sendEvent(amplitude, \"test_opt_out\", null);\n        assertNotNull(request);\n    }\n\n    @Test\n    public void testOffline() {\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        ShadowLooper httplooper = Shadows.shadowOf(amplitude.httpThread.getLooper());\n\n        amplitude.setOffline(true);\n        RecordedRequest request = sendEvent(amplitude, \"test_offline\", null);\n        assertNull(request);\n\n        // Events should be sent after offline is turned off.\n        amplitude.setOffline(false);\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        httplooper.runToEndOfTasks();\n\n        try {\n            request = server.takeRequest(1, SECONDS);\n        } catch (InterruptedException e) {\n        }\n        assertNotNull(request);\n    }\n\n    @Test\n    public void testLogEvent() {\n        RecordedRequest request = sendEvent(amplitude, \"test_event\", null);\n        assertNotNull(request);\n    }\n\n    @Test\n    public void testIdentify() throws JSONException {\n        long [] timestamps = {1000, 1001};\n        clock.setTimestamps(timestamps);\n\n        shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        amplitude.identify(new Identify().set(\"key\", \"value\"));\n        shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        JSONObject identifyIntercepted = getLastIdentifyInterceptor();\n\n        JSONObject expected = new JSONObject();\n        expected.put(\"key\", \"value\");\n        assertTrue(Utils.compareJSONObjects(identifyIntercepted.getJSONObject(\"user_properties\")\n                .getJSONObject(Constants.AMP_OP_SET), expected));\n\n        // verify db state\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        assertNull(dbHelper.getValue(AmplitudeClient.USER_ID_KEY));\n        assertNull(dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY));\n        assertNull(dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_ID_KEY));\n        assertEquals((long) dbHelper.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY), 1L);\n        assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY), timestamps[0]);\n\n        shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        RecordedRequest request = runRequest(amplitude);\n        JSONArray events = getEventsFromRequest(request);\n        assertEquals(events.length(), 1);\n        JSONObject identify = events.getJSONObject(0);\n        assertEquals(identify.getString(\"event_type\"), Constants.IDENTIFY_EVENT);\n        assertEquals(identify.getLong(\"event_id\"), 1);\n        assertEquals(identify.getLong(\"timestamp\"), timestamps[0]);\n        assertEquals(identify.getLong(\"sequence_number\"), 1);\n        JSONObject userProperties = identify.getJSONObject(\"user_properties\");\n        assertEquals(userProperties.length(), 1);\n        assertTrue(userProperties.has(Constants.AMP_OP_SET));\n        assertTrue(Utils.compareJSONObjects(userProperties.getJSONObject(Constants.AMP_OP_SET), expected));\n\n        // verify db state\n        assertNull(dbHelper.getValue(AmplitudeClient.USER_ID_KEY));\n        assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY), 1L);\n        assertNull(dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_ID_KEY));\n        assertEquals((long) dbHelper.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY), 1L);\n        assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY), timestamps[0]);\n        assertEquals((long)dbHelper.getLastIdentifyInterceptorId(), -1L);\n    }\n\n    @Test\n    public void testNullIdentify() {\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 0);\n        assertEquals(getUnsentIdentifyCount(), 0);\n\n        amplitude.identify(null);\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 0);\n        assertEquals(getUnsentIdentifyCount(), 0);\n    }\n\n    @Test\n    public void testLog3Events() throws InterruptedException {\n        long [] timestamps = {1, 2, 3, 4, 5, 6, 7};\n        clock.setTimestamps(timestamps);\n        Robolectric.getForegroundThreadScheduler().advanceTo(1);\n\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n\n\n        amplitude.logEvent(\"test_event1\");\n        amplitude.logEvent(\"test_event2\");\n        amplitude.logEvent(\"test_event3\");\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        assertEquals(getUnsentEventCount(), 3);\n        assertEquals(getUnsentIdentifyCount(), 0);\n        JSONArray events = getUnsentEvents(3);\n        for (int i = 0; i < 3; i++) {\n            assertEquals(events.optJSONObject(i).optString(\"event_type\"), \"test_event\" + (i+1));\n            assertEquals(events.optJSONObject(i).optLong(\"timestamp\"), timestamps[i]);\n            assertEquals(events.optJSONObject(i).optLong(\"sequence_number\"), i+1);\n        }\n\n        // send response and check that remove events works properly\n        runRequest(amplitude);\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 0);\n        assertEquals(getUnsentIdentifyCount(), 0);\n    }\n\n    @Test\n    public void testLog3Identifys() throws JSONException {\n        long [] timestamps = {1, 2, 3, 4, 5, 6, 7};\n        clock.setTimestamps(timestamps);\n        Robolectric.getForegroundThreadScheduler().advanceTo(1);\n\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n\n        Robolectric.getForegroundThreadScheduler().advanceTo(1);\n        amplitude.identify(new Identify().set(\"photo_count\", 1));\n        amplitude.identify(new Identify().add(\"karma\", 2));\n        amplitude.identify(new Identify().unset(\"gender\"));\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        assertEquals(getUnsentEventCount(), 0);\n        assertEquals(getUnsentIdentifyCount(), 3);\n        JSONArray events = getUnsentIdentifys(3);\n\n        JSONObject expectedIdentify1 = new JSONObject();\n        expectedIdentify1.put(Constants.AMP_OP_SET, new JSONObject().put(\"photo_count\", 1));\n        JSONObject expectedIdentify2 = new JSONObject();\n        expectedIdentify2.put(Constants.AMP_OP_ADD, new JSONObject().put(\"karma\", 2));\n        JSONObject expectedIdentify3 = new JSONObject();\n        expectedIdentify3.put(Constants.AMP_OP_UNSET, new JSONObject().put(\"gender\", \"-\"));\n\n        assertEquals(events.optJSONObject(0).optString(\"event_type\"), Constants.IDENTIFY_EVENT);\n        assertEquals(events.optJSONObject(0).optLong(\"timestamp\"), timestamps[0]);\n        assertEquals(events.optJSONObject(0).optLong(\"sequence_number\"), 1);\n        assertTrue(Utils.compareJSONObjects(\n                events.optJSONObject(0).optJSONObject(\"user_properties\"), expectedIdentify1\n        ));\n        assertEquals(events.optJSONObject(1).optString(\"event_type\"), Constants.IDENTIFY_EVENT);\n        assertEquals(events.optJSONObject(1).optLong(\"timestamp\"), timestamps[1]);\n        assertEquals(events.optJSONObject(1).optLong(\"sequence_number\"), 2);\n        assertTrue(Utils.compareJSONObjects(\n                events.optJSONObject(1).optJSONObject(\"user_properties\"), expectedIdentify2\n        ));\n        assertEquals(events.optJSONObject(2).optString(\"event_type\"), Constants.IDENTIFY_EVENT);\n        assertEquals(events.optJSONObject(2).optLong(\"timestamp\"), timestamps[2]);\n        assertEquals(events.optJSONObject(2).optLong(\"sequence_number\"), 3);\n        assertTrue(Utils.compareJSONObjects(\n                events.optJSONObject(2).optJSONObject(\"user_properties\"), expectedIdentify3\n        ));\n\n        // send response and check that remove events works properly\n        runRequest(amplitude);\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 0);\n        assertEquals(getUnsentIdentifyCount(), 0);\n    }\n\n    @Test\n    public void testLogEventAndIdentify() throws JSONException {\n        long [] timestamps = {1, 1, 2};\n        clock.setTimestamps(timestamps);\n        Robolectric.getForegroundThreadScheduler().advanceTo(1);\n\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n        amplitude.logEvent(\"test_event\");\n        amplitude.identify(new Identify().add(\"photo_count\", 1));\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        // verify some internal counters\n        assertEquals(getUnsentEventCount(), 1);\n        assertEquals(amplitude.lastEventId, 1);\n        assertEquals(getUnsentIdentifyCount(), 1);\n        assertEquals(amplitude.lastIdentifyId, 1);\n\n        JSONArray unsentEvents = getUnsentEvents(1);\n        assertEquals(unsentEvents.optJSONObject(0).optString(\"event_type\"), \"test_event\");\n        assertEquals(unsentEvents.optJSONObject(0).optLong(\"sequence_number\"), 1);\n\n        JSONObject expectedIdentify = new JSONObject();\n        expectedIdentify.put(Constants.AMP_OP_ADD, new JSONObject().put(\"photo_count\", 1));\n\n        JSONArray unsentIdentifys = getUnsentIdentifys(1);\n        assertEquals(unsentIdentifys.optJSONObject(0).optString(\"event_type\"), Constants.IDENTIFY_EVENT);\n        assertEquals(unsentIdentifys.optJSONObject(0).optLong(\"sequence_number\"), 2);\n        assertTrue(Utils.compareJSONObjects(\n            unsentIdentifys.optJSONObject(0).optJSONObject(\"user_properties\"), expectedIdentify\n        ));\n\n        // send response and check that remove events works properly\n        RecordedRequest request = runRequest(amplitude);\n        JSONArray events = getEventsFromRequest(request);\n        assertEquals(events.length(), 2);\n        assertEquals(events.optJSONObject(0).optString(\"event_type\"), \"test_event\");\n        assertEquals(events.optJSONObject(1).optString(\"event_type\"), Constants.IDENTIFY_EVENT);\n        assertTrue(Utils.compareJSONObjects(\n            events.optJSONObject(1).optJSONObject(\"user_properties\"), expectedIdentify\n        ));\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 0);\n        assertEquals(getUnsentIdentifyCount(), 0);\n    }\n\n    @Test\n    public void testMergeEventsAndIdentifys() throws JSONException {\n        long [] timestamps = {1, 2, 3, 4, 5, 5, 6, 7, 8, 9, 10};\n        clock.setTimestamps(timestamps);\n        Robolectric.getForegroundThreadScheduler().advanceTo(1);\n\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n\n        amplitude.logEvent(\"test_event1\");\n        amplitude.identify(new Identify().add(\"photo_count\", 1));\n        amplitude.logEvent(\"test_event2\");\n        amplitude.logEvent(\"test_event3\");\n        amplitude.logEvent(\"test_event4\");\n        amplitude.identify(new Identify().set(\"gender\", \"male\"));\n        amplitude.identify(new Identify().unset(\"karma\"));\n\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        // verify some internal counters\n        assertEquals(getUnsentEventCount(), 4);\n        assertEquals(amplitude.lastEventId, 4);\n        assertEquals(getUnsentIdentifyCount(), 3);\n        assertEquals(amplitude.lastIdentifyId, 3);\n\n        RecordedRequest request = runRequest(amplitude);\n        JSONArray events = getEventsFromRequest(request);\n        assertEquals(events.length(), 7);\n\n        JSONObject expectedIdentify1 = new JSONObject();\n        expectedIdentify1.put(Constants.AMP_OP_ADD, new JSONObject().put(\"photo_count\", 1));\n        JSONObject expectedIdentify2 = new JSONObject();\n        expectedIdentify2.put(Constants.AMP_OP_SET, new JSONObject().put(\"gender\", \"male\"));\n        JSONObject expectedIdentify3 = new JSONObject();\n        expectedIdentify3.put(Constants.AMP_OP_UNSET, new JSONObject().put(\"karma\", \"-\"));\n\n        assertEquals(events.getJSONObject(0).getString(\"event_type\"), \"test_event1\");\n        assertEquals(events.getJSONObject(0).getLong(\"event_id\"), 1);\n        assertEquals(events.getJSONObject(0).getLong(\"timestamp\"), timestamps[0]);\n        assertEquals(events.getJSONObject(0).getLong(\"sequence_number\"), 1);\n\n        assertEquals(events.getJSONObject(1).getString(\"event_type\"), Constants.IDENTIFY_EVENT);\n        assertEquals(events.getJSONObject(1).getLong(\"event_id\"), 1);\n        assertEquals(events.getJSONObject(1).getLong(\"timestamp\"), timestamps[1]);\n        assertEquals(events.getJSONObject(1).getLong(\"sequence_number\"), 2);\n        assertTrue(Utils.compareJSONObjects(\n                events.getJSONObject(1).getJSONObject(\"user_properties\"), expectedIdentify1\n        ));\n\n        assertEquals(events.getJSONObject(2).getString(\"event_type\"), \"test_event2\");\n        assertEquals(events.getJSONObject(2).getLong(\"event_id\"), 2);\n        assertEquals(events.getJSONObject(2).getLong(\"timestamp\"), timestamps[2]);\n        assertEquals(events.getJSONObject(2).getLong(\"sequence_number\"), 3);\n\n        assertEquals(events.getJSONObject(3).getString(\"event_type\"), \"test_event3\");\n        assertEquals(events.getJSONObject(3).getLong(\"event_id\"), 3);\n        assertEquals(events.getJSONObject(3).getLong(\"timestamp\"), timestamps[3]);\n        assertEquals(events.getJSONObject(3).getLong(\"sequence_number\"), 4);\n\n        // sequence number guarantees strict ordering regardless of timestamp\n        assertEquals(events.getJSONObject(4).getString(\"event_type\"), \"test_event4\");\n        assertEquals(events.getJSONObject(4).getLong(\"event_id\"), 4);\n        assertEquals(events.getJSONObject(4).getLong(\"timestamp\"), timestamps[4]);\n        assertEquals(events.getJSONObject(4).getLong(\"sequence_number\"), 5);\n\n        assertEquals(events.getJSONObject(5).getString(\"event_type\"), Constants.IDENTIFY_EVENT);\n        assertEquals(events.getJSONObject(5).getLong(\"event_id\"), 2);\n        assertEquals(events.getJSONObject(5).getLong(\"timestamp\"), timestamps[5]);\n        assertEquals(events.getJSONObject(5).getLong(\"sequence_number\"), 6);\n        assertTrue(Utils.compareJSONObjects(\n                events.getJSONObject(5).getJSONObject(\"user_properties\"), expectedIdentify2\n        ));\n\n        assertEquals(events.getJSONObject(6).getString(\"event_type\"), Constants.IDENTIFY_EVENT);\n        assertEquals(events.getJSONObject(6).getLong(\"event_id\"), 3);\n        assertEquals(events.getJSONObject(6).getLong(\"timestamp\"), timestamps[6]);\n        assertEquals(events.getJSONObject(6).getLong(\"sequence_number\"), 7);\n        assertTrue(Utils.compareJSONObjects(\n                events.getJSONObject(6).getJSONObject(\"user_properties\"), expectedIdentify3\n        ));\n\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 0);\n        assertEquals(getUnsentIdentifyCount(), 0);\n\n        // verify db state\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        assertNull(dbHelper.getValue(AmplitudeClient.USER_ID_KEY));\n        assertEquals((long) dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY), 3L);\n        assertEquals((long) dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_ID_KEY), 4L);\n        assertEquals((long) dbHelper.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY), 7L);\n        assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY), timestamps[6]);\n    }\n\n    @Test\n    public void testMergeEventBackwardsCompatible() throws JSONException {\n        amplitude.setEventUploadThreshold(4);\n        // eventst logged before v2.1.0 won't have a sequence number, should get priority\n        long [] timestamps = {1, 1, 2, 3};\n        clock.setTimestamps(timestamps);\n        Robolectric.getForegroundThreadScheduler().advanceTo(1);\n\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n\n        amplitude.uploadingCurrently.set(true);\n        amplitude.identify(new Identify().add(\"photo_count\", 1));\n        amplitude.logEvent(\"test_event1\");\n        amplitude.identify(new Identify().add(\"photo_count\", 2));\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        // need to delete sequence number from test event\n        JSONObject event = getUnsentEvents(1).getJSONObject(0);\n        assertEquals(event.getLong(\"event_id\"), 1);\n        event.remove(\"sequence_number\");\n        event.remove(\"event_id\");\n        // delete event from db and reinsert modified event\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        dbHelper.removeEvent(1);\n        dbHelper.addEvent(event.toString());\n        amplitude.uploadingCurrently.set(false);\n\n        // log another event to trigger upload\n        amplitude.logEvent(\"test_event2\");\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        // verify some internal counters\n        assertEquals(getUnsentEventCount(), 2);\n        assertEquals(amplitude.lastEventId, 3);\n        assertEquals(getUnsentIdentifyCount(), 2);\n        assertEquals(amplitude.lastIdentifyId, 2);\n\n        JSONObject expectedIdentify1 = new JSONObject();\n        expectedIdentify1.put(Constants.AMP_OP_ADD, new JSONObject().put(\"photo_count\", 1));\n        JSONObject expectedIdentify2 = new JSONObject();\n        expectedIdentify2.put(Constants.AMP_OP_ADD, new JSONObject().put(\"photo_count\", 2));\n\n        // send response and check that merging events correctly ordered events\n        RecordedRequest request = runRequest(amplitude);\n        JSONArray events = getEventsFromRequest(request);\n        assertEquals(events.length(), 4);\n        assertEquals(events.optJSONObject(0).optString(\"event_type\"), \"test_event1\");\n        assertFalse(events.optJSONObject(0).has(\"sequence_number\"));\n        assertEquals(events.optJSONObject(1).optString(\"event_type\"), Constants.IDENTIFY_EVENT);\n        assertEquals(events.optJSONObject(1).optLong(\"sequence_number\"), 1);\n        assertTrue(Utils.compareJSONObjects(\n                events.optJSONObject(1).optJSONObject(\"user_properties\"), expectedIdentify1\n        ));\n        assertEquals(events.optJSONObject(2).optString(\"event_type\"), Constants.IDENTIFY_EVENT);\n        assertEquals(events.optJSONObject(2).optLong(\"sequence_number\"), 3);\n        assertTrue(Utils.compareJSONObjects(\n                events.optJSONObject(2).optJSONObject(\"user_properties\"), expectedIdentify2\n        ));\n        assertEquals(events.optJSONObject(3).optString(\"event_type\"), \"test_event2\");\n        assertEquals(events.optJSONObject(3).optLong(\"sequence_number\"), 4);\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 0);\n        assertEquals(getUnsentIdentifyCount(), 0);\n    }\n\n    @Test\n    public void testRemoveAfterSuccessfulUpload() throws JSONException {\n        long [] timestamps = new long[Constants.EVENT_UPLOAD_MAX_BATCH_SIZE + 4];\n        for (int i = 0; i < timestamps.length; i++) timestamps[i] = i;\n        clock.setTimestamps(timestamps);\n        Robolectric.getForegroundThreadScheduler().advanceTo(1);\n\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n\n        for (int i = 0; i < Constants.EVENT_UPLOAD_THRESHOLD; i++) {\n            amplitude.logEvent(\"test_event\" + i);\n        }\n        amplitude.identify(new Identify().add(\"photo_count\", 1));\n        amplitude.identify(new Identify().add(\"photo_count\", 2));\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        assertEquals(getUnsentEventCount(), Constants.EVENT_UPLOAD_THRESHOLD);\n        assertEquals(getUnsentIdentifyCount(), 2);\n\n        RecordedRequest request = runRequest(amplitude);\n        JSONArray events = getEventsFromRequest(request);\n        for (int i = 0; i < events.length(); i++) {\n            assertEquals(events.optJSONObject(i).optString(\"event_type\"), \"test_event\" + i);\n        }\n\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 0);\n        assertEquals(getUnsentIdentifyCount(), 2); // should have 2 identifys left\n    }\n\n    @Test\n    public void testLogEventHasUUID() {\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        amplitude.logEvent(\"test_event\");\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        JSONObject event = getLastUnsentEvent();\n        assertTrue(event.has(\"uuid\"));\n        assertNotNull(event.optString(\"uuid\"));\n        assertTrue(event.optString(\"uuid\").length() > 0);\n    }\n\n    @Test\n    public void testLogRevenue() {\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n\n        JSONObject event, apiProps;\n\n        amplitude.logRevenue(10.99);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n\n        event = getLastUnsentEvent();\n        apiProps = event.optJSONObject(\"api_properties\");\n        assertEquals(Constants.AMP_REVENUE_EVENT, event.optString(\"event_type\"));\n        assertEquals(Constants.AMP_REVENUE_EVENT, apiProps.optString(\"special\"));\n        assertEquals(1, apiProps.optInt(\"quantity\"));\n        assertNull(apiProps.optString(\"productId\", null));\n        assertEquals(10.99, apiProps.optDouble(\"price\"), .01);\n        assertNull(apiProps.optString(\"receipt\", null));\n        assertNull(apiProps.optString(\"receiptSig\", null));\n\n        amplitude.logRevenue(\"ID1\", 2, 9.99);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n\n        event = getLastUnsentEvent();\n        apiProps = event.optJSONObject(\"api_properties\");;\n        assertEquals(Constants.AMP_REVENUE_EVENT, event.optString(\"event_type\"));\n        assertEquals(Constants.AMP_REVENUE_EVENT, apiProps.optString(\"special\"));\n        assertEquals(2, apiProps.optInt(\"quantity\"));\n        assertEquals(\"ID1\", apiProps.optString(\"productId\"));\n        assertEquals(9.99, apiProps.optDouble(\"price\"), .01);\n        assertNull(apiProps.optString(\"receipt\", null));\n        assertNull(apiProps.optString(\"receiptSig\", null));\n\n        amplitude.logRevenue(\"ID2\", 3, 8.99, \"RECEIPT\", \"SIG\");\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n\n        event = getLastUnsentEvent();\n        apiProps = event.optJSONObject(\"api_properties\");\n        assertEquals(Constants.AMP_REVENUE_EVENT, event.optString(\"event_type\"));\n        assertEquals(Constants.AMP_REVENUE_EVENT, apiProps.optString(\"special\"));\n        assertEquals(3, apiProps.optInt(\"quantity\"));\n        assertEquals(\"ID2\", apiProps.optString(\"productId\"));\n        assertEquals(8.99, apiProps.optDouble(\"price\"), .01);\n        assertEquals(\"RECEIPT\", apiProps.optString(\"receipt\"));\n        assertEquals(\"SIG\", apiProps.optString(\"receiptSig\"));\n\n        assertNotNull(runRequest(amplitude));\n    }\n\n    @Test\n    public void testLogRevenueV2() throws JSONException {\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n\n        // ignore invalid revenue objects\n        amplitude.logRevenueV2(null);\n        looper.runToEndOfTasks();\n        amplitude.logRevenueV2(new Revenue());\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 0);\n\n        // log valid revenue object\n        double price = 10.99;\n        int quantity = 15;\n        String productId = \"testProductId\";\n        String receipt = \"testReceipt\";\n        String receiptSig = \"testReceiptSig\";\n        String revenueType = \"testRevenueType\";\n        JSONObject props = new JSONObject().put(\"city\", \"Boston\");\n\n        Revenue revenue = new Revenue().setProductId(productId).setPrice(price);\n        revenue.setQuantity(quantity).setReceipt(receipt, receiptSig);\n        revenue.setRevenueType(revenueType).setRevenueProperties(props);\n\n        amplitude.logRevenueV2(revenue);\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 1);\n\n        JSONObject event = getLastUnsentEvent();\n        assertEquals(event.optString(\"event_type\"), \"revenue_amount\");\n\n        JSONObject obj = event.optJSONObject(\"event_properties\");\n        assertEquals(obj.optDouble(\"$price\"), price, 0);\n        assertEquals(obj.optInt(\"$quantity\"), 15);\n        assertEquals(obj.optString(\"$productId\"), productId);\n        assertEquals(obj.optString(\"$receipt\"), receipt);\n        assertEquals(obj.optString(\"$receiptSig\"), receiptSig);\n        assertEquals(obj.optString(\"$revenueType\"), revenueType);\n        assertEquals(obj.optString(\"city\"), \"Boston\");\n\n        // user properties should be empty\n        assertTrue(Utils.compareJSONObjects(\n            event.optJSONObject(\"user_properties\"), new JSONObject()\n        ));\n\n        // api properties should not have any revenue info\n        JSONObject apiProps = event.optJSONObject(\"api_properties\");\n        assertTrue(apiProps.length() > 0);\n        assertFalse(apiProps.has(\"special\"));\n        assertFalse(apiProps.has(\"productId\"));\n        assertFalse(apiProps.has(\"quantity\"));\n        assertFalse(apiProps.has(\"price\"));\n        assertFalse(apiProps.has(\"receipt\"));\n        assertFalse(apiProps.has(\"receiptSig\"));\n    }\n\n    @Test\n    public void testLogEventSync() {\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n\n        amplitude.logEventSync(\"test_event_sync\", null);\n\n        // Event should be in the database synchronously.\n        JSONObject event = getLastEvent();\n        assertEquals(\"test_event_sync\", event.optString(\"event_type\"));\n\n        looper.runToEndOfTasks();\n\n        server.enqueue(new MockResponse().setBody(\"success\"));\n        ShadowLooper httplooper = Shadows.shadowOf(amplitude.httpThread.getLooper());\n        httplooper.runToEndOfTasks();\n\n        try {\n            assertNotNull(server.takeRequest(1, SECONDS));\n        } catch (InterruptedException e) {\n            fail(e.toString());\n        }\n    }\n\n    /**\n     * Test for not excepting on empty event properties.\n     * See https://github.com/amplitude/Amplitude-Android/issues/35\n     */\n    @Test\n    public void testEmptyEventProps() {\n        RecordedRequest request = sendEvent(amplitude, \"test_event\", new JSONObject());\n        assertNotNull(request);\n    }\n\n    /**\n     * Test that resend failed events only occurs every 30 events.\n     */\n    @Test\n    public void testSaveEventLogic() {\n        amplitude.trackSessionEvents(true);\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 0);\n\n        for (int i = 0; i < Constants.EVENT_UPLOAD_THRESHOLD; i++) {\n            amplitude.logEvent(\"test\");\n        }\n        looper.runToEndOfTasks();\n        // unsent events will be threshold (+1 for start session)\n        assertEquals(getUnsentEventCount(), Constants.EVENT_UPLOAD_THRESHOLD + 1);\n\n        server.enqueue(new MockResponse().setResponseCode(400).setBody(\"invalid_api_key\"));\n        server.enqueue(new MockResponse().setResponseCode(400).setBody(\"bad_checksum\"));\n        ShadowLooper httpLooper = Shadows.shadowOf(amplitude.httpThread.getLooper());\n        httpLooper.runToEndOfTasks();\n\n        // no events sent, queue should be same size\n        assertEquals(getUnsentEventCount(), Constants.EVENT_UPLOAD_THRESHOLD + 1);\n\n        for (int i = 0; i < Constants.EVENT_UPLOAD_THRESHOLD; i++) {\n            amplitude.logEvent(\"test\");\n        }\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), Constants.EVENT_UPLOAD_THRESHOLD * 2 + 1);\n        httpLooper.runToEndOfTasks();\n\n        // sent 61 events, should have only made 2 requests\n        assertEquals(server.getRequestCount(), 2);\n    }\n\n    @Test\n    public void testRequestTooLargeBackoffLogic() {\n        amplitude.trackSessionEvents(true);\n        Robolectric.getForegroundThreadScheduler().advanceTo(1);\n\n        // verify event queue empty\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 0);\n\n        // 413 error force backoff with 2 events --> new upload limit will be 1\n        amplitude.logEvent(\"test\");\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 2); // 2 events: start session + test\n        server.enqueue(new MockResponse().setResponseCode(413));\n        ShadowLooper httpLooper = Shadows.shadowOf(amplitude.httpThread.getLooper());\n        httpLooper.runToEndOfTasks();\n\n        // 413 error with upload limit 1 will remove the top (start session) event\n        amplitude.logEvent(\"test\");\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 3);\n        server.enqueue(new MockResponse().setResponseCode(413));\n        httpLooper.runToEndOfTasks();\n\n        // verify only start session event removed\n        assertEquals(getUnsentEventCount(), 2);\n        JSONArray events = getUnsentEvents(2);\n        assertEquals(events.optJSONObject(0).optString(\"event_type\"), \"test\");\n        assertEquals(events.optJSONObject(1).optString(\"event_type\"), \"test\");\n\n        // upload limit persists until event count below threshold\n        server.enqueue(new MockResponse().setBody(\"success\"));\n        looper.runToEndOfTasks(); // retry uploading after removing large event\n        httpLooper.runToEndOfTasks(); // send success --> 1 event sent\n        looper.runToEndOfTasks(); // event count below threshold --> disable backoff\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 1);\n\n        // verify backoff disabled - queue 2 more events, see that all get uploaded\n        amplitude.logEvent(\"test\");\n        amplitude.logEvent(\"test\");\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 3);\n        server.enqueue(new MockResponse().setBody(\"success\"));\n        httpLooper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 0);\n    }\n\n    @Test\n    public void testUploadRemainingEvents() {\n        long [] timestamps = {1, 2, 3, 4, 5, 6, 7};\n        clock.setTimestamps(timestamps);\n        Robolectric.getForegroundThreadScheduler().advanceTo(1);\n\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 0);\n        assertEquals(getUnsentIdentifyCount(), 0);\n\n        amplitude.setEventUploadMaxBatchSize(2);\n        amplitude.setEventUploadThreshold(2);\n        amplitude.uploadingCurrently.set(true); // block uploading until we queue up enough events\n        for (int i = 0; i < 6; i++) {\n            amplitude.logEvent(String.format(\"test%d\", i));\n            looper.runToEndOfTasks();\n            looper.runToEndOfTasks();\n            assertEquals(dbHelper.getTotalEventCount(), i+1);\n        }\n        amplitude.uploadingCurrently.set(false);\n\n        // allow event uploads\n        // 7 events in queue, should upload 2, and then 2, and then 2, and then 2\n        amplitude.logEvent(\"test7\");\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        assertEquals(dbHelper.getEventCount(), 7);\n        assertEquals(dbHelper.getIdentifyCount(), 0);\n        assertEquals(dbHelper.getTotalEventCount(), 7);\n\n        // server response\n        server.enqueue(new MockResponse().setBody(\"success\"));\n        ShadowLooper httpLooper = Shadows.shadowOf(amplitude.httpThread.getLooper());\n        httpLooper.runToEndOfTasks();\n\n        // when receive success response, continue uploading\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks(); // remove uploaded events\n        assertEquals(dbHelper.getEventCount(), 5);\n        assertEquals(dbHelper.getIdentifyCount(), 0);\n        assertEquals(dbHelper.getTotalEventCount(), 5);\n\n        // 2nd server response\n        server.enqueue(new MockResponse().setBody(\"success\"));\n        httpLooper.runToEndOfTasks();\n        looper.runToEndOfTasks(); // remove uploaded events\n        assertEquals(dbHelper.getEventCount(), 3);\n        assertEquals(dbHelper.getIdentifyCount(), 0);\n        assertEquals(dbHelper.getTotalEventCount(), 3);\n\n        // 3rd server response\n        server.enqueue(new MockResponse().setBody(\"success\"));\n        httpLooper.runToEndOfTasks();\n        looper.runToEndOfTasks(); // remove uploaded events\n        looper.runToEndOfTasks();\n        assertEquals(dbHelper.getEventCount(), 1);\n        assertEquals(dbHelper.getIdentifyCount(), 0);\n        assertEquals(dbHelper.getTotalEventCount(), 1);\n    }\n\n    @Test\n    public void testBackoffRemoveIdentify() {\n        long [] timestamps = {1, 1, 2, 3, 4, 5};\n        clock.setTimestamps(timestamps);\n        Robolectric.getForegroundThreadScheduler().advanceTo(1);\n\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 0);\n        assertEquals(getUnsentIdentifyCount(), 0);\n\n        // 413 error force backoff with 2 events --> new upload limit will be 1\n        amplitude.identify(new Identify().add(\"photo_count\", 1));\n        amplitude.logEvent(\"test1\");\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        assertEquals(getUnsentIdentifyCount(), 1);\n        assertEquals(getUnsentEventCount(), 1);\n\n        server.enqueue(new MockResponse().setResponseCode(413));\n        ShadowLooper httpLooper = Shadows.shadowOf(amplitude.httpThread.getLooper());\n        httpLooper.runToEndOfTasks();\n\n        // 413 error with upload limit 1 will remove the top identify\n        amplitude.logEvent(\"test2\");\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 2);\n        assertEquals(getUnsentIdentifyCount(), 1);\n        server.enqueue(new MockResponse().setResponseCode(413));\n        httpLooper.runToEndOfTasks();\n\n        // verify only identify removed\n        assertEquals(getUnsentEventCount(), 2);\n        assertEquals(getUnsentIdentifyCount(), 0);\n        JSONArray events = getUnsentEvents(2);\n        assertEquals(events.optJSONObject(0).optString(\"event_type\"), \"test1\");\n        assertEquals(events.optJSONObject(1).optString(\"event_type\"), \"test2\");\n    }\n\n    @Test\n    public void testLimitTrackingEnabled() {\n        amplitude.logEvent(\"test\");\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        JSONObject apiProperties = getLastUnsentEvent().optJSONObject(\"api_properties\");\n        assertTrue(apiProperties.has(\"limit_ad_tracking\"));\n        assertFalse(apiProperties.optBoolean(\"limit_ad_tracking\"));\n        assertFalse(apiProperties.has(\"androidADID\"));\n    }\n\n    @Test\n    public void testTruncateString() {\n        String longString = generateStringWithLength(Constants.MAX_STRING_LENGTH * 2, 'c');\n        assertEquals(longString.length(), Constants.MAX_STRING_LENGTH * 2);\n        String truncatedString = amplitude.truncate(longString);\n        assertEquals(truncatedString.length(), Constants.MAX_STRING_LENGTH);\n        assertEquals(truncatedString, generateStringWithLength(Constants.MAX_STRING_LENGTH, 'c'));\n    }\n\n    @Test\n    public void testTruncateJSONObject() throws JSONException {\n        String longString = generateStringWithLength(Constants.MAX_STRING_LENGTH * 2, 'c');\n        String truncString = generateStringWithLength(Constants.MAX_STRING_LENGTH, 'c');\n        JSONObject object = new JSONObject();\n        object.put(\"int value\", 10);\n        object.put(\"bool value\", false);\n        object.put(\"long string\", longString);\n        object.put(\"array\", new JSONArray().put(longString).put(10));\n        object.put(\"jsonobject\", new JSONObject().put(\"long string\", longString));\n        object.put(Constants.AMP_REVENUE_RECEIPT, longString);\n        object.put(Constants.AMP_REVENUE_RECEIPT_SIG, longString);\n\n        object = amplitude.truncate(object);\n        assertEquals(object.optInt(\"int value\"), 10);\n        assertEquals(object.optBoolean(\"bool value\"), false);\n        assertEquals(object.optString(\"long string\"), truncString);\n        assertEquals(object.optJSONArray(\"array\").length(), 2);\n        assertEquals(object.optJSONArray(\"array\").getString(0), truncString);\n        assertEquals(object.optJSONArray(\"array\").getInt(1), 10);\n        assertEquals(object.optJSONObject(\"jsonobject\").length(), 1);\n        assertEquals(object.optJSONObject(\"jsonobject\").optString(\"long string\"), truncString);\n\n        // receipt and receipt sig should not be truncated\n        assertEquals(object.optString(Constants.AMP_REVENUE_RECEIPT), longString);\n        assertEquals(object.optString(Constants.AMP_REVENUE_RECEIPT_SIG), longString);\n    }\n\n    @Test\n    public void testTruncateNullJSONObject() throws JSONException {\n        assertTrue(Utils.compareJSONObjects(\n            amplitude.truncate((JSONObject) null), new JSONObject()\n        ));\n        assertEquals(amplitude.truncate((JSONArray) null).length(), 0);\n    }\n\n    @Test\n    public void testTruncateEventAndIdentify() throws JSONException {\n        String longString = generateStringWithLength(Constants.MAX_STRING_LENGTH * 2, 'c');\n        String truncString = generateStringWithLength(Constants.MAX_STRING_LENGTH, 'c');\n\n        long [] timestamps = {1, 1, 2, 3};\n        clock.setTimestamps(timestamps);\n        Robolectric.getForegroundThreadScheduler().advanceTo(1);\n\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n        amplitude.identify(new Identify().set(\"long_string\", longString));\n        amplitude.logEvent(\"test\", new JSONObject().put(\"long_string\", longString));\n\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        RecordedRequest request = runRequest(amplitude);\n        JSONArray events = getEventsFromRequest(request);\n        assertEquals(events.optJSONObject(0).optString(\"event_type\"), Constants.IDENTIFY_EVENT);\n        assertTrue(Utils.compareJSONObjects(\n            events.optJSONObject(0).optJSONObject(\"user_properties\").optJSONObject(Constants.AMP_OP_SET),\n            new JSONObject().put(\"long_string\", truncString)\n        ));\n        assertEquals(events.optJSONObject(1).optString(\"event_type\"), \"test\");\n        assertTrue(Utils.compareJSONObjects(\n            events.optJSONObject(1).optJSONObject(\"event_properties\"),\n            new JSONObject().put(\"long_string\", truncString)\n        ));\n    }\n\n    @Test\n    public void testAutoIncrementSequenceNumber() {\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        int limit = 10;\n        for (int i = 0; i < limit; i++) {\n            assertEquals(amplitude.getNextSequenceNumber(), i+1);\n            assertEquals(dbHelper.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY), Long.valueOf(i+1));\n        }\n    }\n\n    @Test\n    public void testSetOffline() throws JSONException {\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        amplitude.setOffline(true);\n\n        amplitude.logEvent(\"test1\");\n        amplitude.logEvent(\"test2\");\n        amplitude.identify(new Identify().unset(\"key1\"));\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 2);\n        assertEquals(getUnsentIdentifyCount(), 1);\n\n        amplitude.setOffline(false);\n        looper.runToEndOfTasks();\n        RecordedRequest request = runRequest(amplitude);\n        JSONArray events = getEventsFromRequest(request);\n        looper.runToEndOfTasks();\n\n        assertEquals(events.length(), 3);\n        assertEquals(getUnsentEventCount(), 0);\n        assertEquals(getUnsentIdentifyCount(), 0);\n    }\n\n    @Test\n    public void testSetOfflineTruncate() throws JSONException {\n        long [] timestamps = {1, 2, 3, 4, 5, 6, 7, 8, 9};\n        clock.setTimestamps(timestamps);\n        Robolectric.getForegroundThreadScheduler().advanceTo(1);\n\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        int eventMaxCount = 3;\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        amplitude.setEventMaxCount(eventMaxCount).setOffline(true);\n\n        amplitude.logEvent(\"test1\");\n        amplitude.logEvent(\"test2\");\n        amplitude.logEvent(\"test3\");\n        amplitude.identify(new Identify().unset(\"key1\"));\n        amplitude.identify(new Identify().unset(\"key2\"));\n        amplitude.identify(new Identify().unset(\"key3\"));\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), eventMaxCount);\n        assertEquals(getUnsentIdentifyCount(), eventMaxCount);\n\n        amplitude.logEvent(\"test4\");\n        amplitude.identify(new Identify().unset(\"key4\"));\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), eventMaxCount);\n        assertEquals(getUnsentIdentifyCount(), eventMaxCount);\n\n        List<JSONObject> events = dbHelper.getEvents(-1, -1);\n        assertEquals(events.size(), eventMaxCount);\n        assertEquals(events.get(0).optString(\"event_type\"), \"test2\");\n        assertEquals(events.get(1).optString(\"event_type\"), \"test3\");\n        assertEquals(events.get(2).optString(\"event_type\"), \"test4\");\n\n        List<JSONObject> identifys = dbHelper.getIdentifys(-1, -1);\n        assertEquals(identifys.size(), eventMaxCount);\n        assertEquals(identifys.get(0).optJSONObject(\"user_properties\").optJSONObject(\"$unset\").optString(\"key2\"), \"-\");\n        assertEquals(identifys.get(1).optJSONObject(\"user_properties\").optJSONObject(\"$unset\").optString(\"key3\"), \"-\");\n        assertEquals(identifys.get(2).optJSONObject(\"user_properties\").optJSONObject(\"$unset\").optString(\"key4\"), \"-\");\n    }\n\n    @Test\n    public void testTruncateEventsQueues() {\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        int eventMaxCount = 50;\n        assertTrue(eventMaxCount > Constants.EVENT_REMOVE_BATCH_SIZE);\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        amplitude.setEventMaxCount(eventMaxCount).setOffline(true);\n\n        for (int i = 0; i < eventMaxCount; i++) {\n            amplitude.logEvent(\"test\");\n        }\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), eventMaxCount);\n\n        amplitude.logEvent(\"test\");\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), eventMaxCount - (eventMaxCount/10) + 1);\n    }\n\n    @Test\n    public void testTruncateEventsQueuesWithOneEvent() {\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        int eventMaxCount = 1;\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        amplitude.setEventMaxCount(eventMaxCount).setOffline(true);\n\n        amplitude.logEvent(\"test1\");\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), eventMaxCount);\n\n        amplitude.logEvent(\"test2\");\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), eventMaxCount);\n\n        JSONObject event = getLastUnsentEvent();\n        assertEquals(event.optString(\"event_type\"), \"test2\");\n    }\n\n    @Test\n    public void testClearUserProperties() throws JSONException {\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n\n        amplitude.clearUserProperties();\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 0);\n        assertEquals(getUnsentIdentifyCount(), 1);\n        JSONObject event = getLastUnsentIdentify();\n        assertEquals(Constants.IDENTIFY_EVENT, event.optString(\"event_type\"));\n        assertTrue(Utils.compareJSONObjects(\n            event.optJSONObject(\"event_properties\"), new JSONObject()\n        ));\n\n        JSONObject userPropertiesOperations = event.optJSONObject(\"user_properties\");\n        assertEquals(userPropertiesOperations.length(), 1);\n        assertTrue(userPropertiesOperations.has(Constants.AMP_OP_CLEAR_ALL));\n\n        assertEquals(\n            \"-\", userPropertiesOperations.optString(Constants.AMP_OP_CLEAR_ALL)\n        );\n    }\n\n    @Test\n    public void testSetGroup() throws JSONException {\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n\n        amplitude.setGroup(\"orgId\", new JSONArray().put(10).put(15));\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        assertEquals(getUnsentEventCount(), 0);\n        assertEquals(getUnsentIdentifyCount(), 1);\n        JSONObject event = getLastUnsentIdentify();\n        assertEquals(Constants.IDENTIFY_EVENT, event.optString(\"event_type\"));\n        assertTrue(Utils.compareJSONObjects(\n            event.optJSONObject(\"event_properties\"), new JSONObject()\n        ));\n\n        JSONObject userPropertiesOperations = event.optJSONObject(\"user_properties\");\n        assertEquals(userPropertiesOperations.length(), 1);\n        assertTrue(userPropertiesOperations.has(Constants.AMP_OP_SET));\n\n        JSONObject groups = event.optJSONObject(\"groups\");\n        assertEquals(groups.length(), 1);\n        assertEquals(groups.optJSONArray(\"orgId\"), new JSONArray().put(10).put(15));\n\n        JSONObject setOperations = userPropertiesOperations.optJSONObject(Constants.AMP_OP_SET);\n        assertEquals(setOperations.length(), 1);\n        assertEquals(setOperations.optJSONArray(\"orgId\"), new JSONArray().put(10).put(15));\n    }\n\n    @Test\n    public void testLogEventWithGroups() throws JSONException {\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n\n        JSONObject groups = new JSONObject().put(\"orgId\", 10).put(\"sport\", \"tennis\");\n        amplitude.logEvent(\"test\", null, groups);\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        assertEquals(getUnsentEventCount(), 1);\n        assertEquals(getUnsentIdentifyCount(), 0);\n        JSONObject event = getLastUnsentEvent();\n        assertEquals(event.optString(\"event_type\"), \"test\");\n        assertTrue(Utils.compareJSONObjects(\n            event.optJSONObject(\"event_properties\"), new JSONObject()\n        ));\n        assertTrue(Utils.compareJSONObjects(\n            event.optJSONObject(\"user_properties\"), new JSONObject()\n        ));\n\n        JSONObject eventGroups = event.optJSONObject(\"groups\");\n        assertEquals(eventGroups.length(), 2);\n        assertEquals(eventGroups.optInt(\"orgId\"), 10);\n        assertEquals(eventGroups.optString(\"sport\"), \"tennis\");\n    }\n\n    @Test\n    public void testMergeEventsArrayIndexOutOfBounds() throws JSONException {\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n\n        amplitude.setOffline(true);\n\n        amplitude.logEvent(\"testEvent1\");\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        // force failure case\n        amplitude.setLastEventId(0);\n\n        amplitude.setOffline(false);\n        looper.runToEndOfTasks();\n\n        // make sure next upload succeeds\n        amplitude.setLastEventId(1);\n        amplitude.logEvent(\"testEvent2\");\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        RecordedRequest request = runRequest(amplitude);\n        JSONArray events = getEventsFromRequest(request);\n        assertEquals(events.length(), 2);\n\n        assertEquals(events.getJSONObject(0).optString(\"event_type\"), \"testEvent1\");\n        assertEquals(events.getJSONObject(0).optLong(\"event_id\"), 1);\n\n        assertEquals(events.getJSONObject(1).optString(\"event_type\"), \"testEvent2\");\n        assertEquals(events.getJSONObject(1).optLong(\"event_id\"), 2);\n    }\n\n    @Test\n    public void testCursorWindowAllocationException() {\n        Robolectric.getForegroundThreadScheduler().advanceTo(1);\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n\n        // log an event successfully\n        amplitude.logEvent(\"testEvent1\");\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 1);\n        assertEquals(getUnsentIdentifyCount(), 0);\n\n        // mock out database helper to force CursorWindowAllocationExceptions\n        DatabaseHelper.instances.put(Constants.DEFAULT_INSTANCE, new MockDatabaseHelper(context));\n\n        // force an upload and verify no request sent\n        // make sure we catch it during sending of events and defer sending\n        RecordedRequest request = runRequest(amplitude);\n        assertNull(request);\n        assertEquals(getUnsentEventCount(), 1);\n        assertEquals(getUnsentIdentifyCount(), 0);\n\n        // make sure we catch it during initialization and treat as uninitialized\n        amplitude.initialized = false;\n        amplitude.initialize(context, apiKey);\n        looper.runToEndOfTasks();\n        assertNull(amplitude.apiKey);\n\n        // since event meta data is loaded during initialize, in theory we should\n        // be able to log an event even if we can't query from it\n        amplitude.context = context;\n        amplitude.apiKey = apiKey;\n        Identify identify = new Identify().set(\"car\", \"blue\");\n        amplitude.identify(identify);\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 1);\n        assertEquals(getUnsentIdentifyCount(), 1);\n    }\n\n    @Test\n    public void testBlockTooManyEventUserProperties() throws JSONException {\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n\n        JSONObject eventProperties = new JSONObject();\n        JSONObject userProperties = new JSONObject();\n        Identify identify = new Identify();\n\n        for (int i = 0; i < Constants.MAX_PROPERTY_KEYS + 1; i++) {\n            eventProperties.put(String.valueOf(i), i);\n            userProperties.put(String.valueOf(i*2), i*2);\n            identify.setOnce(String.valueOf(i), i);\n        }\n\n        // verify user properties is filtered out\n        amplitude.setUserProperties(userProperties);\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentIdentifyCount(), 0);\n\n        // verify scrubbed from events\n        amplitude.logEvent(\"test event\", eventProperties);\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 1);\n        JSONObject event = getLastUnsentEvent();\n        assertEquals(event.optString(\"event_type\"), \"test event\");\n        assertTrue(Utils.compareJSONObjects(\n            event.optJSONObject(\"event_properties\"), new JSONObject()\n        ));\n\n        // verify scrubbed from identifys - but leaves an empty JSONObject\n        amplitude.identify(identify);\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentIdentifyCount(), 1);\n        JSONObject identifyEvent = getLastUnsentIdentify();\n        assertEquals(identifyEvent.optString(\"event_type\"), \"$identify\");\n        assertTrue(Utils.compareJSONObjects(\n            identifyEvent.optJSONObject(\"user_properties\"),\n            new JSONObject().put(\"$setOnce\", new JSONObject())\n        ));\n    }\n\n    @Test\n    public void testLogEventWithTimestamp() throws JSONException {\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n\n        amplitude.logEvent(\"test\", null, null, 1000, false);\n        looper.runToEndOfTasks();\n        JSONObject event = getLastUnsentEvent();\n        assertEquals(event.optLong(\"timestamp\"), 1000);\n\n        amplitude.logEventSync(\"test\", null, null, 2000, false);\n        looper.runToEndOfTasks();\n        event = getLastUnsentEvent();\n        assertEquals(event.optLong(\"timestamp\"), 2000);\n    }\n\n    @Test\n    public void testRegenerateDeviceId() {\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        String oldDeviceId = amplitude.getDeviceId();\n        assertEquals(oldDeviceId, dbHelper.getValue(\"device_id\"));\n\n        amplitude.regenerateDeviceId();\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        String newDeviceId = amplitude.getDeviceId();\n        assertNotEquals(oldDeviceId, newDeviceId);\n        assertEquals(newDeviceId, dbHelper.getValue(\"device_id\"));\n        assertTrue(newDeviceId.endsWith(\"R\"));\n    }\n\n    @Test\n    public void testSendNullEvents() throws JSONException {\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n\n        dbHelper.addEvent(null);\n        amplitude.setLastEventId(1);\n        amplitude.getNextSequenceNumber();\n        assertEquals(getUnsentEventCount(), 1);\n\n        amplitude.logEvent(\"test event\");\n        looper.runToEndOfTasks();\n\n        amplitude.updateServer();\n        RecordedRequest request = runRequest(amplitude);\n        JSONArray events = getEventsFromRequest(request);\n        assertEquals(events.length(), 1);\n        assertEquals(events.optJSONObject(0).optString(\"event_type\"), \"test event\");\n    }\n\n    @Test\n    @PrepareForTest(OkHttpClient.class)\n    public void testHandleUploadExceptions() throws Exception {\n        ShadowLooper logLooper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        ShadowLooper httpLooper = Shadows.shadowOf(amplitude.httpThread.getLooper());\n        IOException error = new IOException(\"test IO Exception\");\n\n        // mock out client\n        Call.Factory oldClient = amplitude.callFactory;\n        OkHttpClient mockClient = PowerMockito.mock(OkHttpClient.class);\n\n        // need to have mock client return mock call that throws exception\n        Call mockCall = PowerMockito.mock(Call.class);\n        PowerMockito.when(mockCall.execute()).thenThrow(error);\n        PowerMockito.when(mockClient.newCall(Matchers.any(Request.class))).thenReturn(mockCall);\n\n        // attach mock client to amplitude\n        amplitude.callFactory = mockClient;\n        amplitude.logEvent(\"test event\");\n        logLooper.runToEndOfTasks();\n        logLooper.runToEndOfTasks();\n        httpLooper.runToEndOfTasks();\n\n        assertEquals(amplitude.lastError, error);\n\n        // restore old client\n        amplitude.callFactory = oldClient;\n    }\n\n    @Test\n    public void testDefaultPlatform() throws InterruptedException {\n        long [] timestamps = {1, 2, 3, 4, 5, 6, 7};\n        clock.setTimestamps(timestamps);\n        Robolectric.getForegroundThreadScheduler().advanceTo(1);\n\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n\n        assertEquals(amplitude.platform, Constants.PLATFORM);\n\n        amplitude.logEvent(\"test_event1\");\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        assertEquals(getUnsentEventCount(), 1);\n        assertEquals(getUnsentIdentifyCount(), 0);\n        JSONArray events = getUnsentEvents(1);\n        for (int i = 0; i < 1; i++) {\n            assertEquals(events.optJSONObject(i).optString(\"event_type\"), \"test_event\" + (i+1));\n            assertEquals(events.optJSONObject(i).optLong(\"timestamp\"), timestamps[i]);\n            assertEquals(events.optJSONObject(i).optString(\"platform\"), Constants.PLATFORM);\n        }\n        runRequest(amplitude);\n    }\n\n    @Test\n    public void testOverridePlatform() throws InterruptedException {\n        long [] timestamps = {1, 2, 3, 4, 5, 6, 7};\n        clock.setTimestamps(timestamps);\n        Robolectric.getForegroundThreadScheduler().advanceTo(1);\n\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n\n        String customPlatform = \"test_custom_platform\";\n\n        // force re-initialize to override platform\n        amplitude.initialized = false;\n        amplitude.initialize(context, apiKey, null, customPlatform, false);\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        assertEquals(amplitude.platform, customPlatform);\n\n        amplitude.logEvent(\"test_event1\");\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        assertEquals(getUnsentEventCount(), 1);\n        assertEquals(getUnsentIdentifyCount(), 0);\n        JSONArray events = getUnsentEvents(1);\n        for (int i = 0; i < 1; i++) {\n            assertEquals(events.optJSONObject(i).optString(\"event_type\"), \"test_event\" + (i+1));\n            assertEquals(events.optJSONObject(i).optLong(\"timestamp\"), timestamps[i]);\n            assertEquals(events.optJSONObject(i).optString(\"platform\"), customPlatform);\n        }\n        runRequest(amplitude);\n    }\n\n    @Test\n    public void testSetTrackingConfig() throws JSONException {\n        long [] timestamps = {1, 2, 3, 4, 5, 6, 7};\n        clock.setTimestamps(timestamps);\n        Robolectric.getForegroundThreadScheduler().advanceTo(1);\n\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n\n        TrackingOptions options = new TrackingOptions().disableCity().disableCountry().disableIpAddress().disableLanguage().disableLatLng();\n        amplitude.setTrackingOptions(options);\n\n        assertEquals(amplitude.appliedTrackingOptions, options);\n        assertTrue(Utils.compareJSONObjects(amplitude.apiPropertiesTrackingOptions, options.getApiPropertiesTrackingOptions()));\n        assertFalse(amplitude.appliedTrackingOptions.shouldTrackCity());\n        assertFalse(amplitude.appliedTrackingOptions.shouldTrackCountry());\n        assertFalse(amplitude.appliedTrackingOptions.shouldTrackIpAddress());\n        assertFalse(amplitude.appliedTrackingOptions.shouldTrackLanguage());\n        assertFalse(amplitude.appliedTrackingOptions.shouldTrackLatLng());\n\n        amplitude.logEvent(\"test event\");\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        JSONArray events = getUnsentEvents(1);\n        assertEquals(events.length(), 1);\n        JSONObject event = events.getJSONObject(0);\n\n        // verify we do have platform and carrier since those were not filtered out\n        assertTrue(event.has(\"carrier\"));\n        assertTrue(event.has(\"platform\"));\n\n        // verify we do not have any of the filtered out fields\n        assertFalse(event.has(\"city\"));\n        assertFalse(event.has(\"country\"));\n        assertFalse(event.has(\"language\"));\n\n        // verify api properties contains tracking options for location filtering\n        JSONObject apiProperties = event.getJSONObject(\"api_properties\");\n        assertFalse(apiProperties.getBoolean(\"limit_ad_tracking\"));\n        assertFalse(apiProperties.getBoolean(\"gps_enabled\"));\n        assertTrue(apiProperties.has(\"tracking_options\"));\n\n        JSONObject trackingOptions = apiProperties.getJSONObject(\"tracking_options\");\n        assertEquals(trackingOptions.length(), 4);\n        assertFalse(trackingOptions.getBoolean(\"city\"));\n        assertFalse(trackingOptions.getBoolean(\"country\"));\n        assertFalse(trackingOptions.getBoolean(\"ip_address\"));\n        assertFalse(trackingOptions.getBoolean(\"lat_lng\"));\n    }\n\n    @Test\n    public void testEnableCoppaControl() throws JSONException {\n        long [] timestamps = {1, 2, 3, 4, 5, 6, 7};\n        clock.setTimestamps(timestamps);\n        Robolectric.getForegroundThreadScheduler().advanceTo(1);\n\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n\n        amplitude.disableCoppaControl();  // this shouldn't do anything\n\n        TrackingOptions options = new TrackingOptions();\n        assertEquals(amplitude.inputTrackingOptions, options);\n        assertEquals(amplitude.appliedTrackingOptions, options);\n        assertTrue(Utils.compareJSONObjects(amplitude.apiPropertiesTrackingOptions, options.getApiPropertiesTrackingOptions()));\n\n        // haven't merged in the privacy guard settings yet\n        assertTrue(amplitude.appliedTrackingOptions.shouldTrackLanguage());\n        assertTrue(amplitude.appliedTrackingOptions.shouldTrackCarrier());\n        assertTrue(amplitude.appliedTrackingOptions.shouldTrackIpAddress());\n        assertTrue(amplitude.appliedTrackingOptions.shouldTrackAdid());\n        assertTrue(amplitude.appliedTrackingOptions.shouldTrackCity());\n        assertTrue(amplitude.appliedTrackingOptions.shouldTrackLatLng());\n\n        amplitude.logEvent(\"test event\");\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        JSONArray events = getUnsentEvents(1);\n        assertEquals(events.length(), 1);\n        JSONObject event = events.getJSONObject(0);\n\n        // verify we do have platform and carrier since those were not filtered out\n        assertTrue(event.has(\"country\"));\n        assertTrue(event.has(\"platform\"));\n        assertTrue(event.has(\"carrier\"));\n        assertTrue(event.has(\"language\"));\n\n        // verify api properties contains tracking options for location filtering\n        JSONObject apiProperties = event.getJSONObject(\"api_properties\");\n        assertFalse(apiProperties.getBoolean(\"limit_ad_tracking\"));\n        assertFalse(apiProperties.getBoolean(\"gps_enabled\"));\n        assertFalse(apiProperties.has(\"tracking_options\"));\n\n        // test enabling privacy guard\n        amplitude.enableCoppaControl();\n        assertEquals(amplitude.inputTrackingOptions, options);\n        assertNotEquals(amplitude.appliedTrackingOptions, options);\n\n        // make sure we merge in the privacy guard options\n        assertFalse(amplitude.appliedTrackingOptions.shouldTrackIpAddress());\n        assertFalse(amplitude.appliedTrackingOptions.shouldTrackAdid());\n        assertFalse(amplitude.appliedTrackingOptions.shouldTrackCity());\n        assertFalse(amplitude.appliedTrackingOptions.shouldTrackLatLng());\n\n        amplitude.logEvent(\"test event 1\");\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        events = getUnsentEvents(2);\n        assertEquals(events.length(), 2);\n        event = events.getJSONObject(1);\n\n        // verify we do have platform and carrier since those were not filtered out\n        assertTrue(event.has(\"country\"));\n        assertTrue(event.has(\"platform\"));\n        assertTrue(event.has(\"carrier\"));\n        assertTrue(event.has(\"language\"));\n\n        // verify api properties contains tracking options for location filtering\n        apiProperties = event.getJSONObject(\"api_properties\");\n        assertFalse(apiProperties.getBoolean(\"limit_ad_tracking\"));\n        assertFalse(apiProperties.getBoolean(\"gps_enabled\"));\n        assertTrue(apiProperties.has(\"tracking_options\"));\n\n        JSONObject trackingOptions = apiProperties.getJSONObject(\"tracking_options\");\n        assertEquals(trackingOptions.length(), 3);\n        assertFalse(trackingOptions.getBoolean(\"ip_address\"));\n        assertFalse(trackingOptions.getBoolean(\"city\"));\n        assertFalse(trackingOptions.getBoolean(\"lat_lng\"));\n\n        // test disabling privacy guard\n        amplitude.disableCoppaControl();\n\n        assertEquals(amplitude.inputTrackingOptions, options);\n        assertEquals(amplitude.appliedTrackingOptions, options);\n        assertTrue(Utils.compareJSONObjects(amplitude.apiPropertiesTrackingOptions, options.getApiPropertiesTrackingOptions()));\n        assertTrue(amplitude.appliedTrackingOptions.shouldTrackLanguage());\n        assertTrue(amplitude.appliedTrackingOptions.shouldTrackCarrier());\n        assertTrue(amplitude.appliedTrackingOptions.shouldTrackIpAddress());\n        assertTrue(amplitude.appliedTrackingOptions.shouldTrackAdid());\n        assertTrue(amplitude.appliedTrackingOptions.shouldTrackCity());\n        assertTrue(amplitude.appliedTrackingOptions.shouldTrackLatLng());\n\n        amplitude.logEvent(\"test event 2\");\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        events = getUnsentEvents(3);\n        assertEquals(events.length(), 3);\n        event = events.getJSONObject(2);\n\n        // verify we do have platform and carrier since those were not filtered out\n        assertTrue(event.has(\"country\"));\n        assertTrue(event.has(\"platform\"));\n        assertTrue(event.has(\"carrier\"));\n        assertTrue(event.has(\"language\"));\n\n        // verify api properties contains tracking options for location filtering\n        apiProperties = event.getJSONObject(\"api_properties\");\n        assertFalse(apiProperties.getBoolean(\"limit_ad_tracking\"));\n        assertFalse(apiProperties.getBoolean(\"gps_enabled\"));\n        assertFalse(apiProperties.has(\"tracking_options\"));\n    }\n\n    @Test\n    public void testEnableCoppaControlWithOptions() throws JSONException {\n        long [] timestamps = {1, 2, 3, 4, 5, 6, 7};\n        clock.setTimestamps(timestamps);\n        Robolectric.getForegroundThreadScheduler().advanceTo(1);\n\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n\n        TrackingOptions options = new TrackingOptions().disableLanguage().disableCarrier().disableIpAddress();\n        amplitude.setTrackingOptions(options);\n\n        assertEquals(amplitude.inputTrackingOptions, options);\n        assertEquals(amplitude.appliedTrackingOptions, options);\n        assertTrue(Utils.compareJSONObjects(amplitude.apiPropertiesTrackingOptions, options.getApiPropertiesTrackingOptions()));\n        assertFalse(amplitude.appliedTrackingOptions.shouldTrackLanguage());\n        assertFalse(amplitude.appliedTrackingOptions.shouldTrackCarrier());\n        assertFalse(amplitude.appliedTrackingOptions.shouldTrackIpAddress());\n\n        // haven't merged in the privacy guard settings yet\n        assertTrue(amplitude.appliedTrackingOptions.shouldTrackAdid());\n        assertTrue(amplitude.appliedTrackingOptions.shouldTrackCity());\n        assertTrue(amplitude.appliedTrackingOptions.shouldTrackLatLng());\n\n        amplitude.logEvent(\"test event\");\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        JSONArray events = getUnsentEvents(1);\n        assertEquals(events.length(), 1);\n        JSONObject event = events.getJSONObject(0);\n\n        // verify we do have platform and carrier since those were not filtered out\n        assertTrue(event.has(\"country\"));\n        assertTrue(event.has(\"platform\"));\n\n        // verify we do not have any of the filtered out fields\n        assertFalse(event.has(\"carrier\"));\n        assertFalse(event.has(\"language\"));\n\n        // verify api properties contains tracking options for location filtering\n        JSONObject apiProperties = event.getJSONObject(\"api_properties\");\n        assertFalse(apiProperties.getBoolean(\"limit_ad_tracking\"));\n        assertFalse(apiProperties.getBoolean(\"gps_enabled\"));\n        assertTrue(apiProperties.has(\"tracking_options\"));\n\n        JSONObject trackingOptions = apiProperties.getJSONObject(\"tracking_options\");\n        assertEquals(trackingOptions.length(), 1);\n        assertFalse(trackingOptions.getBoolean(\"ip_address\"));\n\n        // when we enable privacy guard, make sure we maintain original tracking options\n        amplitude.enableCoppaControl();\n        assertEquals(amplitude.inputTrackingOptions, options);\n        assertNotEquals(amplitude.appliedTrackingOptions, options);\n\n        // also make sure we merge in the privacy guard options\n        assertFalse(amplitude.appliedTrackingOptions.shouldTrackLanguage());\n        assertFalse(amplitude.appliedTrackingOptions.shouldTrackCarrier());\n        assertFalse(amplitude.appliedTrackingOptions.shouldTrackIpAddress());\n        assertFalse(amplitude.appliedTrackingOptions.shouldTrackAdid());\n        assertFalse(amplitude.appliedTrackingOptions.shouldTrackCity());\n        assertFalse(amplitude.appliedTrackingOptions.shouldTrackLatLng());\n\n        amplitude.logEvent(\"test event 1\");\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        events = getUnsentEvents(2);\n        assertEquals(events.length(), 2);\n        event = events.getJSONObject(1);\n\n        // verify we do have platform and carrier since those were not filtered out\n        assertTrue(event.has(\"country\"));\n        assertTrue(event.has(\"platform\"));\n\n        // verify we do not have any of the filtered out fields\n        assertFalse(event.has(\"carrier\"));\n        assertFalse(event.has(\"language\"));\n\n        // verify api properties contains tracking options for location filtering\n        apiProperties = event.getJSONObject(\"api_properties\");\n        assertFalse(apiProperties.getBoolean(\"limit_ad_tracking\"));\n        assertFalse(apiProperties.getBoolean(\"gps_enabled\"));\n        assertTrue(apiProperties.has(\"tracking_options\"));\n\n        trackingOptions = apiProperties.getJSONObject(\"tracking_options\");\n        assertEquals(trackingOptions.length(), 3);\n        assertFalse(trackingOptions.getBoolean(\"ip_address\"));\n        assertFalse(trackingOptions.getBoolean(\"city\"));\n        assertFalse(trackingOptions.getBoolean(\"lat_lng\"));\n\n        // disable privacy guard and make sure original user input is maintained\n        amplitude.disableCoppaControl();\n\n        assertEquals(amplitude.inputTrackingOptions, options);\n        assertEquals(amplitude.appliedTrackingOptions, options);\n        assertTrue(Utils.compareJSONObjects(amplitude.apiPropertiesTrackingOptions, options.getApiPropertiesTrackingOptions()));\n        assertFalse(amplitude.appliedTrackingOptions.shouldTrackLanguage());\n        assertFalse(amplitude.appliedTrackingOptions.shouldTrackCarrier());\n        assertFalse(amplitude.appliedTrackingOptions.shouldTrackIpAddress());\n\n        // haven't merged in the privacy guard settings yet\n        assertTrue(amplitude.appliedTrackingOptions.shouldTrackAdid());\n        assertTrue(amplitude.appliedTrackingOptions.shouldTrackCity());\n        assertTrue(amplitude.appliedTrackingOptions.shouldTrackLatLng());\n\n        amplitude.logEvent(\"test event 2\");\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        events = getUnsentEvents(3);\n        assertEquals(events.length(), 3);\n        event = events.getJSONObject(2);\n\n        // verify we do have platform and carrier since those were not filtered out\n        assertTrue(event.has(\"country\"));\n        assertTrue(event.has(\"platform\"));\n\n        // verify we do not have any of the filtered out fields\n        assertFalse(event.has(\"carrier\"));\n        assertFalse(event.has(\"language\"));\n\n        // verify api properties contains tracking options for location filtering\n        apiProperties = event.getJSONObject(\"api_properties\");\n        assertFalse(apiProperties.getBoolean(\"limit_ad_tracking\"));\n        assertFalse(apiProperties.getBoolean(\"gps_enabled\"));\n        assertTrue(apiProperties.has(\"tracking_options\"));\n\n        trackingOptions = apiProperties.getJSONObject(\"tracking_options\");\n        assertEquals(trackingOptions.length(), 1);\n        assertFalse(trackingOptions.getBoolean(\"ip_address\"));\n    }\n\n    @Test\n    public void testGroupIdentifyMultipleOperations() throws JSONException {\n        String groupType = \"test group type\";\n        String groupName = \"test group name\";\n\n        String property1 = \"string value\";\n        String value1 = \"testValue\";\n\n        String property2 = \"double value\";\n        double value2 = 0.123;\n\n        String property3 = \"boolean value\";\n        boolean value3 = true;\n\n        String property4 = \"json value\";\n\n        Identify identify = new Identify().setOnce(property1, value1).add(property2, value2);\n        identify.set(property3, value3).unset(property4);\n\n        // identify should ignore this since duplicate key\n        identify.set(property4, value3);\n\n        amplitude.groupIdentify(groupType, groupName, identify);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        assertEquals(getUnsentIdentifyCount(), 1);\n        assertEquals(getUnsentEventCount(), 0);\n        assertEquals(0, getIdentifyInterceptorCount());\n        JSONObject event = getLastUnsentIdentify();\n        assertEquals(Constants.GROUP_IDENTIFY_EVENT, event.optString(\"event_type\"));\n\n        assertTrue(Utils.compareJSONObjects(event.optJSONObject(\"event_properties\"), new JSONObject()));\n        assertTrue(Utils.compareJSONObjects(event.optJSONObject(\"user_properties\"), new JSONObject()));\n\n        JSONObject groups = event.optJSONObject(\"groups\");\n        JSONObject expectedGroups = new JSONObject();\n        expectedGroups.put(groupType, groupName);\n        assertTrue(Utils.compareJSONObjects(groups, expectedGroups));\n\n        JSONObject groupProperties = event.optJSONObject(\"group_properties\");\n        JSONObject expected = new JSONObject();\n        expected.put(Constants.AMP_OP_SET_ONCE, new JSONObject().put(property1, value1));\n        expected.put(Constants.AMP_OP_ADD, new JSONObject().put(property2, value2));\n        expected.put(Constants.AMP_OP_SET, new JSONObject().put(property3, value3));\n        expected.put(Constants.AMP_OP_UNSET, new JSONObject().put(property4, \"-\"));\n        assertTrue(Utils.compareJSONObjects(groupProperties, expected));\n    }\n\n    @Test\n    public void testGroupIdentifyPropertiesObject() throws JSONException {\n        String groupType = \"test group type\";\n        String groupName = \"test group name\";\n\n        String property1 = \"string value\";\n        String value1 = \"testValue\";\n\n        String property2 = \"double value\";\n        double value2 = 0.123;\n\n        String property3 = \"boolean value\";\n        boolean value3 = true;\n\n        String property4 = \"json value\";\n\n        JSONObject properties = new JSONObject()\n                .put(property1, value1)\n                .put(property2, value2)\n                .put(property3, value3)\n                .put(property4, null);\n\n        amplitude.groupIdentify(groupType, groupName, properties, false, null);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        assertEquals(getUnsentIdentifyCount(), 1);\n        assertEquals(getUnsentEventCount(), 0);\n        JSONObject event = getLastUnsentIdentify();\n        assertEquals(Constants.GROUP_IDENTIFY_EVENT, event.optString(\"event_type\"));\n\n        assertTrue(Utils.compareJSONObjects(event.optJSONObject(\"event_properties\"), new JSONObject()));\n        assertTrue(Utils.compareJSONObjects(event.optJSONObject(\"user_properties\"), new JSONObject()));\n\n        JSONObject groups = event.optJSONObject(\"groups\");\n        JSONObject expectedGroups = new JSONObject();\n        expectedGroups.put(groupType, groupName);\n        assertTrue(Utils.compareJSONObjects(groups, expectedGroups));\n\n        JSONObject groupProperties = event.optJSONObject(\"group_properties\");\n        JSONObject expected = new JSONObject();\n        expected.put(Constants.AMP_OP_SET, new JSONObject()\n                .put(property1, value1)\n                .put(property2, value2)\n                .put(property3, value3));\n        assertTrue(Utils.compareJSONObjects(groupProperties, expected));\n    }\n\n    @Test\n    public void testSetLogCallback() {\n        class TestLogCallback implements AmplitudeLogCallback {\n            String errorMsg = null;\n\n            @Override\n            public void onError(String tag, String message) {\n                this.errorMsg = message;\n            }\n\n            private String getErrorMsg() {\n                return this.errorMsg;\n            }\n        }\n        TestLogCallback callback = new TestLogCallback();\n        amplitude.setLogCallback(callback);\n        assertNull(callback.getErrorMsg());\n        amplitude.validateLogEvent(\"\");\n        assertEquals(\"Argument eventType cannot be null or blank in logEvent()\", callback.getErrorMsg());\n    }\n\n    @Test\n    public void testSetPlan() {\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        String branch = \"main\";\n        String version = \"1.0.0\";\n        String versionId = \"9ec23ba0-275f-468f-80d1-66b88bff9529\";\n        Plan plan = new Plan().setBranch(branch).setVersion(version).setVersionId(versionId);\n        amplitude.setPlan(plan);\n        amplitude.logEvent(\"test\");\n        looper.runToEndOfTasks();\n\n        JSONObject event = getLastEvent();\n        assertNotNull(event);\n        try {\n            JSONObject planJsonObject = event.getJSONObject(\"plan\");\n            assertEquals(branch, planJsonObject.getString(\"branch\"));\n            assertEquals(version, planJsonObject.getString(\"version\"));\n            assertEquals(versionId, planJsonObject.getString(\"versionId\"));\n        } catch (Exception e) {\n            Assert.fail();\n        }\n    }\n\n    @Test\n    public void testSetIngestionMetadata() {\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        String sourceName = \"ampli\";\n        String sourceVersion = \"1.0.0\";\n        IngestionMetadata ingestionMetadata = new IngestionMetadata()\n                .setSourceName(sourceName)\n                .setSourceVersion(sourceVersion);\n        amplitude.setIngestionMetadata(ingestionMetadata);\n        amplitude.logEvent(\"test\");\n        looper.runToEndOfTasks();\n\n        JSONObject event = getLastEvent();\n        assertNotNull(event);\n        try {\n            JSONObject jsonObject = event.getJSONObject(\"ingestion_metadata\");\n            assertEquals(sourceName, jsonObject.getString(\"source_name\"));\n            assertEquals(sourceVersion, jsonObject.getString(\"source_version\"));\n        } catch (Exception e) {\n            Assert.fail();\n        }\n    }\n\n    @Test\n    public void testSetServerZoneWithoutUpdateServerUrl() {\n        String urlBeforeChange = amplitude.url;\n        AmplitudeServerZone euZone = AmplitudeServerZone.EU;\n        amplitude.setServerZone(euZone, false);\n        assertEquals(euZone, getPrivateFieldValueFromClient(amplitude, \"serverZone\"));\n        assertEquals(urlBeforeChange, amplitude.url);\n    }\n\n    @Test\n    public void testSetServerZoneAndUpdateServerUrl() {\n        AmplitudeServerZone euZone = AmplitudeServerZone.EU;\n        amplitude.setServerZone(euZone);\n        assertEquals(euZone, getPrivateFieldValueFromClient(amplitude, \"serverZone\"));\n        assertEquals(Constants.EVENT_LOG_EU_URL, amplitude.url);\n    }\n\n    @Test\n    public void testMiddlewareSupport() throws JSONException {\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n        Map<String, Object> extraMap = new HashMap<>();\n        extraMap.put(\"description\", \"extra description\");\n        MiddlewareExtra extra = new MiddlewareExtra(extraMap);\n        Middleware middleware = new Middleware() {\n            @Override\n            public void run(MiddlewarePayload payload, MiddlewareNext next) {\n                try {\n                    payload.event.optJSONObject(\"event_properties\").put(\"description\", \"extra description\");\n                } catch (JSONException e) {\n                    e.printStackTrace();\n                }\n\n                next.run(payload);\n            }\n        };\n        amplitude.addEventMiddleware(middleware);\n        amplitude.logEvent(\"middleware_event_type\", new JSONObject().put(\"user_id\", \"middleware_user\"), extra);\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        assertEquals(getUnsentEventCount(), 1);\n        JSONArray eventObject = getUnsentEvents(1);;\n        assertEquals(eventObject.optJSONObject(0).optString(\"event_type\"), \"middleware_event_type\");\n        assertEquals(eventObject.optJSONObject(0).optJSONObject(\"event_properties\").getString(\"description\"), \"extra description\");\n        assertEquals(eventObject.optJSONObject(0).optJSONObject(\"event_properties\").optString(\"user_id\"), \"middleware_user\");\n    }\n\n    @Test\n    public void testWithSwallowMiddleware() throws JSONException {\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n        Middleware middleware = new Middleware() {\n            @Override\n            public void run(MiddlewarePayload payload, MiddlewareNext next) {\n            }\n        };\n        amplitude.addEventMiddleware(middleware);\n        amplitude.logEvent(\"middleware_event_type\", new JSONObject().put(\"user_id\", \"middleware_user\"));\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        assertEquals(getUnsentEventCount(), 0);\n    }\n\n    @Test\n    public void setIdentifyBatchIntervalMillis() {\n        amplitude.setIdentifyBatchIntervalMillis(10000);\n        assertEquals(30000L, getPrivateFieldValueFromClient(amplitude, \"identifyBatchIntervalMillis\"));\n        amplitude.setIdentifyBatchIntervalMillis(40000);\n        assertEquals(40000L, getPrivateFieldValueFromClient(amplitude, \"identifyBatchIntervalMillis\"));\n    }\n\n    @Test\n    public void testMultipleIdentifyWithSetActionOnlySendOneIdentifyEvent() throws JSONException {\n        long [] timestamps = {1000, 1001, 1002, 1003};\n        clock.setTimestamps(timestamps);\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n        amplitude.identify(new Identify().set(\"key1\", \"key1-value1\").set(\"key2\", \"key2-value1\").set(\"key3\", \"key3-value1\"));\n        amplitude.identify(new Identify().set(\"key1\", \"key1-value2\").set(\"key4\", \"key4-value1\"));\n        amplitude.identify(new Identify().set(\"key2\", \"key2-value2\"));\n        amplitude.identify(new Identify().set(\"key3\", \"key3-value2\").set(\"key4\", \"key4-value2\"));\n        looper.runToEndOfTasks();\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        assertEquals(4L, getIdentifyInterceptorCount());\n        assertEquals(0, getUnsentIdentifyCount());\n        assertNull(dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY));\n        assertEquals((long) dbHelper.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY), 4L);\n        assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY), timestamps[3]);\n\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        RecordedRequest request = runRequest(amplitude);\n        JSONArray events = getEventsFromRequest(request);\n        assertEquals(events.length(), 1);\n        JSONObject identify = events.getJSONObject(0);\n        assertEquals(identify.getString(\"event_type\"), Constants.IDENTIFY_EVENT);\n        assertEquals(identify.getLong(\"event_id\"), 1);\n        assertEquals(identify.getLong(\"timestamp\"), timestamps[0]);\n        assertEquals(identify.getLong(\"sequence_number\"), 1);\n        JSONObject userProperties = identify.getJSONObject(\"user_properties\");\n        assertEquals(userProperties.length(), 1);\n        assertTrue(userProperties.has(Constants.AMP_OP_SET));\n        JSONObject expected = new JSONObject();\n        expected.put(\"key1\", \"key1-value2\");\n        expected.put(\"key2\", \"key2-value2\");\n        expected.put(\"key3\", \"key3-value2\");\n        expected.put(\"key4\", \"key4-value2\");\n        assertTrue(Utils.compareJSONObjects(userProperties.getJSONObject(Constants.AMP_OP_SET), expected));\n        assertEquals(0, getIdentifyInterceptorCount());\n        assertEquals((long)dbHelper.getLastIdentifyInterceptorId(), -1L);\n        assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY), 1L);\n    }\n\n    @Test\n    public void testMultipleIdentifyWithSetActionOnlyAndOneEvent() throws JSONException {\n        long [] timestamps = {1000, 1001, 1002, 1003, 1004};\n        clock.setTimestamps(timestamps);\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n        Middleware middleware = new Middleware() {\n            @Override\n            public void run(MiddlewarePayload payload, MiddlewareNext next) {\n                try {\n                    if (!payload.event.getString(\"event_type\").equals(Constants.IDENTIFY_EVENT)) {\n                        payload.event.optJSONObject(\"user_properties\").put(\"key1\", \"key1-value3\")\n                                .put(\"key2\", \"key2-value3\");\n                    }\n                } catch (JSONException e) {\n                    e.printStackTrace();\n                }\n\n                next.run(payload);\n            }\n        };\n        amplitude.addEventMiddleware(middleware);\n        amplitude.identify(new Identify().set(\"key1\", \"key1-value1\").set(\"key2\", \"key2-value1\").set(\"key3\", \"key3-value1\"));\n        amplitude.identify(new Identify().set(\"key1\", \"key1-value2\").set(\"key4\", \"key4-value1\"));\n        amplitude.identify(new Identify().set(\"key2\", \"key2-value2\"));\n        amplitude.identify(new Identify().set(\"key3\", \"key3-value2\").set(\"key4\", \"key4-value2\"));\n        amplitude.logEvent(\"test_event\", new JSONObject().put(\"test_event_prop_key\", \"test_event_prop_value\"));\n        looper.runToEndOfTasks();\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        assertEquals(1L, getUnsentIdentifyCount());\n        assertEquals(1L, getUnsentEventCount());\n        assertEquals(1L, (long)dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY));\n        assertEquals((long) dbHelper.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY), 5L);\n        assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY), timestamps[4]);\n\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        RecordedRequest request = runRequest(amplitude);\n        JSONArray events = getEventsFromRequest(request);\n        assertEquals(events.length(), 2);\n        JSONObject event = events.getJSONObject(0);\n        assertEquals(event.getString(\"event_type\"), Constants.IDENTIFY_EVENT);\n        assertEquals(event.getLong(\"event_id\"), 1);\n        assertEquals(event.getLong(\"timestamp\"), timestamps[0]);\n        assertEquals(event.getLong(\"sequence_number\"), 1);\n        JSONObject userProperties = event.getJSONObject(\"user_properties\").getJSONObject(Constants.AMP_OP_SET);\n        assertEquals(userProperties.length(), 4);\n        JSONObject expected = new JSONObject();\n        expected.put(\"key1\", \"key1-value2\");\n        expected.put(\"key2\", \"key2-value2\");\n        expected.put(\"key3\", \"key3-value2\");\n        expected.put(\"key4\", \"key4-value2\");\n        assertTrue(Utils.compareJSONObjects(userProperties, expected));\n\n        JSONObject event2 = events.getJSONObject(1);\n        assertEquals(event2.getString(\"event_type\"), \"test_event\");\n        assertEquals(event2.getLong(\"event_id\"), 1);\n        assertEquals(event2.getLong(\"timestamp\"), timestamps[4]);\n        assertEquals(event2.getLong(\"sequence_number\"), 5);\n        JSONObject userProperties2 = event2.getJSONObject(\"user_properties\");\n        assertEquals(userProperties2.length(), 2);\n        JSONObject expected2 = new JSONObject();\n        expected2.put(\"key1\", \"key1-value3\");\n        expected2.put(\"key2\", \"key2-value3\");\n        assertTrue(Utils.compareJSONObjects(userProperties2, expected2));\n        assertEquals(0, getIdentifyInterceptorCount());\n        assertEquals((long)dbHelper.getLastIdentifyInterceptorId(), -1L);\n        assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY), 1L);\n        assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_ID_KEY), 1L);\n    }\n\n    @Test\n    public void testMultipleIdentifyWithSetActionAndOneEventAndIdentify() throws JSONException {\n        long [] timestamps = {1000, 1001, 1002, 1003, 1004, 1005};\n        clock.setTimestamps(timestamps);\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n        Middleware middleware = new Middleware() {\n            @Override\n            public void run(MiddlewarePayload payload, MiddlewareNext next) {\n                try {\n                    if (!payload.event.getString(\"event_type\").equals(Constants.IDENTIFY_EVENT)) {\n                        payload.event.optJSONObject(\"user_properties\").put(\"key1\", \"key1-value3\")\n                                .put(\"key2\", \"key2-value3\");\n                    }\n                } catch (JSONException e) {\n                    e.printStackTrace();\n                }\n\n                next.run(payload);\n            }\n        };\n        amplitude.addEventMiddleware(middleware);\n        amplitude.identify(new Identify().set(\"key1\", \"key1-value1\").set(\"key2\", \"key2-value1\").set(\"key3\", \"key3-value1\"));\n        amplitude.identify(new Identify().set(\"key1\", \"key1-value2\").set(\"key4\", \"key4-value1\"));\n        amplitude.identify(new Identify().set(\"key2\", \"key2-value2\"));\n        amplitude.identify(new Identify().set(\"key3\", \"key3-value2\").set(\"key4\", \"key4-value2\"));\n        amplitude.logEvent(\"test_event\", new JSONObject().put(\"test_event_prop_key\", \"test_event_prop_value\"));\n        amplitude.setUserProperties(new JSONObject().put(\"key1\", \"key1-value4\"));\n        looper.runToEndOfTasks();\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        assertEquals(1L, getUnsentIdentifyCount());\n        assertEquals(1L, getUnsentEventCount());\n        assertEquals(1L, (long)dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY));\n        assertEquals((long) dbHelper.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY), 6L);\n        assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY), timestamps[5]);\n\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        RecordedRequest request = runRequest(amplitude);\n        JSONArray events = getEventsFromRequest(request);\n        assertEquals(events.length(), 3);\n        JSONObject event = events.getJSONObject(0);\n        assertEquals(event.getString(\"event_type\"), Constants.IDENTIFY_EVENT);\n        assertEquals(event.getLong(\"event_id\"), 1);\n        assertEquals(event.getLong(\"timestamp\"), timestamps[0]);\n        assertEquals(event.getLong(\"sequence_number\"), 1);\n        JSONObject userProperties = event.getJSONObject(\"user_properties\").getJSONObject(Constants.AMP_OP_SET);\n        assertEquals(userProperties.length(), 4);\n        JSONObject expected = new JSONObject();\n        expected.put(\"key1\", \"key1-value2\");\n        expected.put(\"key2\", \"key2-value2\");\n        expected.put(\"key3\", \"key3-value2\");\n        expected.put(\"key4\", \"key4-value2\");\n\n        JSONObject event2 = events.getJSONObject(1);\n        assertEquals(event2.getString(\"event_type\"), \"test_event\");\n        assertEquals(event2.getLong(\"event_id\"), 1);\n        assertEquals(event2.getLong(\"timestamp\"), timestamps[4]);\n        assertEquals(event2.getLong(\"sequence_number\"), 5);\n        JSONObject userProperties2 = event2.getJSONObject(\"user_properties\");\n        assertEquals(userProperties.length(), 4);\n        JSONObject expected2 = new JSONObject();\n        expected2.put(\"key1\", \"key1-value3\");\n        expected2.put(\"key2\", \"key2-value3\");\n        assertTrue(Utils.compareJSONObjects(userProperties2, expected2));\n\n        JSONObject event3 = events.getJSONObject(2);\n        assertEquals(event3.getString(\"event_type\"), Constants.IDENTIFY_EVENT);\n        assertEquals(event3.getLong(\"event_id\"), 2);\n        assertEquals(event3.getLong(\"timestamp\"), timestamps[5]);\n        assertEquals(event3.getLong(\"sequence_number\"), 6);\n        JSONObject userProperties3 = event3.getJSONObject(\"user_properties\").getJSONObject(Constants.AMP_OP_SET);\n        assertEquals(userProperties2.length(), 2);\n        JSONObject expected3 = new JSONObject();\n        expected3.put(\"key1\", \"key1-value4\");\n        assertTrue(Utils.compareJSONObjects(userProperties3, expected3));\n        assertEquals(0, getIdentifyInterceptorCount());\n        assertEquals((long)dbHelper.getLastIdentifyInterceptorId(), -1L);\n        assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY), 2L);\n        assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_ID_KEY), 1L);\n    }\n\n    @Test\n    public void testIdentifyInterceptWithSetAndClearAll() throws JSONException {\n        long [] timestamps = {1000, 1001, 1002, 1003, 1004};\n        clock.setTimestamps(timestamps);\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n        amplitude.identify(new Identify().set(\"key1\", \"key1-value1\").set(\"key2\", \"key2-value1\").set(\"key3\", \"key3-value1\"));\n        amplitude.identify(new Identify().set(\"key1\", \"key1-value2\").set(\"key4\", \"key4-value1\"));\n        amplitude.identify(new Identify().set(\"key2\", \"key2-value2\"));\n        amplitude.identify(new Identify().set(\"key3\", \"key3-value2\").set(\"key4\", \"key4-value2\"));\n        amplitude.identify(new Identify().clearAll());\n        looper.runToEndOfTasks();\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        assertEquals(0, getIdentifyInterceptorCount());\n        assertEquals(1, getUnsentIdentifyCount());\n        assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY), 1L);\n        assertEquals((long) dbHelper.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY), 5L);\n        assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY), timestamps[4]);\n\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        RecordedRequest request = runRequest(amplitude);\n        JSONArray events = getEventsFromRequest(request);\n        assertEquals(events.length(), 1);\n        JSONObject identify = events.getJSONObject(0);\n        assertEquals(identify.getString(\"event_type\"), Constants.IDENTIFY_EVENT);\n        assertEquals(identify.getLong(\"event_id\"), 1);\n        assertEquals(identify.getLong(\"timestamp\"), timestamps[4]);\n        assertEquals(identify.getLong(\"sequence_number\"), 5);\n        JSONObject userProperties = identify.getJSONObject(\"user_properties\");\n        assertEquals(userProperties.length(), 1);\n        assertTrue(userProperties.has(Constants.AMP_OP_CLEAR_ALL));\n        assertFalse(userProperties.has(Constants.AMP_OP_SET));\n        assertEquals(0, getIdentifyInterceptorCount());\n        assertEquals((long)dbHelper.getLastIdentifyInterceptorId(), -1L);\n    }\n\n    @Test\n    public void testMultipleIdentifyWithSetActionAndAnotherIdentify() throws JSONException {\n        long [] timestamps = {1000, 1001, 1002, 1003, 1004};\n        clock.setTimestamps(timestamps);\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n        amplitude.identify(new Identify().set(\"key1\", \"key1-value1\").set(\"key2\", \"key2-value1\").set(\"key3\", \"key3-value1\"));\n        amplitude.identify(new Identify().set(\"key1\", \"key1-value2\").set(\"key4\", \"key4-value1\"));\n        amplitude.identify(new Identify().set(\"key2\", \"key2-value2\"));\n        amplitude.identify(new Identify().set(\"key3\", \"key3-value2\").set(\"key4\", \"key4-value2\"));\n        amplitude.identify(new Identify().add(\"key5\", 2));\n        looper.runToEndOfTasks();\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        assertEquals(2L, getUnsentIdentifyCount());\n        assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY), 2L);\n        assertEquals((long) dbHelper.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY), 5L);\n        assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY), timestamps[4]);\n\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        RecordedRequest request = runRequest(amplitude);\n        JSONArray events = getEventsFromRequest(request);\n        assertEquals(events.length(), 2);\n        JSONObject event = events.getJSONObject(0);\n        assertEquals(event.getString(\"event_type\"), Constants.IDENTIFY_EVENT);\n        assertEquals(event.getLong(\"event_id\"), 1);\n        assertEquals(event.getLong(\"timestamp\"), timestamps[0]);\n        assertEquals(event.getLong(\"sequence_number\"), 1);\n        JSONObject userProperties = event.getJSONObject(\"user_properties\");\n        assertEquals(userProperties.length(), 1);\n        JSONObject expected = new JSONObject();\n        expected.put(\"key1\", \"key1-value2\");\n        expected.put(\"key2\", \"key2-value2\");\n        expected.put(\"key3\", \"key3-value2\");\n        expected.put(\"key4\", \"key4-value2\");\n        assertTrue(Utils.compareJSONObjects(userProperties.getJSONObject(Constants.AMP_OP_SET), expected));\n\n        JSONObject event2 = events.getJSONObject(1);\n        assertEquals(event2.getString(\"event_type\"), Constants.IDENTIFY_EVENT);\n        assertEquals(event2.getLong(\"event_id\"), 2);\n        assertEquals(event2.getLong(\"timestamp\"), timestamps[4]);\n        assertEquals(event2.getLong(\"sequence_number\"), 5);\n        JSONObject userProperties2 = event2.getJSONObject(\"user_properties\");\n        assertEquals(userProperties2.length(), 1);\n        JSONObject expectedAdd = new JSONObject();\n        expectedAdd.put(\"key5\", 2);\n        assertTrue(Utils.compareJSONObjects(userProperties2.getJSONObject(Constants.AMP_OP_ADD), expectedAdd));\n\n        assertEquals(0, getIdentifyInterceptorCount());\n        assertEquals((long)dbHelper.getLastIdentifyInterceptorId(), -1L);\n    }\n\n    @Test\n    public void testUploadEventsSendInterceptedIdentify() throws JSONException {\n        long [] timestamps = {1000, 1001, 1002, 1003};\n        clock.setTimestamps(timestamps);\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n        amplitude.identify(new Identify().set(\"key1\", \"key1-value1\").set(\"key2\", \"key2-value1\").set(\"key3\", \"key3-value1\"));\n        amplitude.identify(new Identify().set(\"key1\", \"key1-value2\").set(\"key4\", \"key4-value1\"));\n        amplitude.setUserProperties(new JSONObject().put(\"key2\", \"key2-value2\"));\n        amplitude.identify(new Identify().set(\"key3\", \"key3-value2\").set(\"key4\", \"key4-value2\"));\n        looper.runToEndOfTasks();\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        assertEquals(4L, getIdentifyInterceptorCount());\n        assertEquals(0, getUnsentIdentifyCount());\n        assertNull(dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY));\n        assertEquals((long) dbHelper.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY), 4L);\n        assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY), timestamps[3]);\n\n        amplitude.uploadEvents();\n        looper.runToEndOfTasks();\n        RecordedRequest request = runRequest(amplitude);\n        JSONArray events = getEventsFromRequest(request);\n        assertEquals(events.length(), 1);\n        JSONObject identify = events.getJSONObject(0);\n        assertEquals(identify.getString(\"event_type\"), Constants.IDENTIFY_EVENT);\n        assertEquals(identify.getLong(\"event_id\"), 1);\n        assertEquals(identify.getLong(\"timestamp\"), timestamps[0]);\n        assertEquals(identify.getLong(\"sequence_number\"), 1);\n        JSONObject userProperties = identify.getJSONObject(\"user_properties\");\n        assertEquals(userProperties.length(), 1);\n        assertTrue(userProperties.has(Constants.AMP_OP_SET));\n        JSONObject expected = new JSONObject();\n        expected.put(\"key1\", \"key1-value2\");\n        expected.put(\"key2\", \"key2-value2\");\n        expected.put(\"key3\", \"key3-value2\");\n        expected.put(\"key4\", \"key4-value2\");\n        assertTrue(Utils.compareJSONObjects(userProperties.getJSONObject(Constants.AMP_OP_SET), expected));\n        assertEquals(0, getIdentifyInterceptorCount());\n        assertEquals((long)dbHelper.getLastIdentifyInterceptorId(), -1L);\n        assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY), 1L);\n    }\n\n    @Test\n    public void testMultipleIdentifyWithSetActionAndSetGroup() throws JSONException {\n        long [] timestamps = {1000, 1001, 1002, 1003, 1004, 1005};\n        clock.setTimestamps(timestamps);\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n        amplitude.identify(new Identify().set(\"key1\", \"key1-value1\").set(\"key2\", \"key2-value1\").set(\"key3\", \"key3-value1\"));\n        amplitude.identify(new Identify().set(\"key1\", \"key1-value2\").set(\"key4\", \"key4-value1\"));\n        amplitude.identify(new Identify().set(\"key2\", \"key2-value2\"));\n        amplitude.identify(new Identify().set(\"key3\", \"key3-value2\").set(\"key4\", \"key4-value2\"));\n        amplitude.setGroup(\"test-group-type\", \"test-group-value\");\n        amplitude.identify(new Identify().set(\"key3\", \"key3-value3\").set(\"key4\", \"key4-value3\"));\n        looper.runToEndOfTasks();\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        assertEquals(2L, getUnsentIdentifyCount());\n        assertEquals(1L, getIdentifyInterceptorCount());\n        assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY), 2L);\n        assertEquals((long) dbHelper.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY), 6L);\n        assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY), timestamps[5]);\n\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        RecordedRequest request = runRequest(amplitude);\n        JSONArray events = getEventsFromRequest(request);\n        assertEquals(events.length(), 3);\n        JSONObject event = events.getJSONObject(0);\n        assertEquals(event.getString(\"event_type\"), Constants.IDENTIFY_EVENT);\n        assertEquals(event.getLong(\"event_id\"), 1);\n        assertEquals(event.getLong(\"timestamp\"), timestamps[0]);\n        assertEquals(event.getLong(\"sequence_number\"), 1);\n        JSONObject userProperties = event.getJSONObject(\"user_properties\");\n        assertEquals(userProperties.length(), 1);\n        JSONObject expected = new JSONObject();\n        expected.put(\"key1\", \"key1-value2\");\n        expected.put(\"key2\", \"key2-value2\");\n        expected.put(\"key3\", \"key3-value2\");\n        expected.put(\"key4\", \"key4-value2\");\n        assertTrue(Utils.compareJSONObjects(userProperties.getJSONObject(Constants.AMP_OP_SET), expected));\n\n        JSONObject event2 = events.getJSONObject(1);\n        assertEquals(event2.getString(\"event_type\"), Constants.IDENTIFY_EVENT);\n        assertEquals(event2.getLong(\"event_id\"), 2);\n        assertEquals(event2.getLong(\"timestamp\"), timestamps[4]);\n        assertEquals(event2.getLong(\"sequence_number\"), 5);\n        JSONObject userProperties2 = event2.getJSONObject(\"user_properties\");\n        assertEquals(userProperties2.length(), 1);\n        JSONObject expected2 = new JSONObject();\n        expected2.put(\"test-group-type\", \"test-group-value\");\n        JSONObject expectedGroups = new JSONObject();\n        expectedGroups.put(\"test-group-type\", \"test-group-value\");\n        assertTrue(Utils.compareJSONObjects(userProperties2.getJSONObject(Constants.AMP_OP_SET), expected2));\n        assertTrue(Utils.compareJSONObjects(event2.getJSONObject(\"groups\"), expectedGroups));\n\n        JSONObject event3 = events.getJSONObject(2);\n        assertEquals(event3.getString(\"event_type\"), Constants.IDENTIFY_EVENT);\n        assertEquals(event3.getLong(\"event_id\"), 3);\n        assertEquals(event3.getLong(\"timestamp\"), timestamps[5]);\n        assertEquals(event3.getLong(\"sequence_number\"), 6);\n        JSONObject userProperties3 = event3.getJSONObject(\"user_properties\");\n        assertEquals(userProperties3.length(), 1);\n        JSONObject expected3 = new JSONObject();\n        expected3.put(\"key3\", \"key3-value3\");\n        expected3.put(\"key4\", \"key4-value3\");\n        assertTrue(Utils.compareJSONObjects(userProperties3.getJSONObject(Constants.AMP_OP_SET), expected3));\n\n        assertEquals(0, getIdentifyInterceptorCount());\n        assertEquals((long)dbHelper.getLastIdentifyInterceptorId(), -1L);\n    }\n\n    @Test\n    public void testMultipleIdentifyWithSetActionAndUserIdUpdated() throws JSONException {\n        long [] timestamps = {1000, 1001, 1002, 1003, 1004};\n        clock.setTimestamps(timestamps);\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n        amplitude.identify(new Identify().set(\"key1\", \"key1-value1\").set(\"key2\", \"key2-value1\").set(\"key3\", \"key3-value1\"));\n        amplitude.identify(new Identify().set(\"key1\", \"key1-value2\").set(\"key4\", \"key4-value1\"));\n        amplitude.identify(new Identify().set(\"key2\", \"key2-value2\"));\n        amplitude.identify(new Identify().set(\"key3\", \"key3-value2\").set(\"key4\", \"key4-value2\"));\n        amplitude.setUserId(\"identify-user-id\");\n        amplitude.identify(new Identify().set(\"key3\", \"key3-value3\").set(\"key4\", \"key4-value3\"));\n        looper.runToEndOfTasks();\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        assertEquals(1L, getUnsentIdentifyCount());\n        assertEquals(1L, getIdentifyInterceptorCount());\n        assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY), 1L);\n        assertEquals((long) dbHelper.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY), 5L);\n        assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY), timestamps[4]);\n\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n        RecordedRequest request = runRequest(amplitude);\n        JSONArray events = getEventsFromRequest(request);\n        assertEquals(events.length(), 2);\n        JSONObject event = events.getJSONObject(0);\n        assertEquals(event.getString(\"event_type\"), Constants.IDENTIFY_EVENT);\n        assertEquals(event.getLong(\"event_id\"), 1);\n        assertEquals(event.getLong(\"timestamp\"), timestamps[0]);\n        assertEquals(event.getLong(\"sequence_number\"), 1);\n        assertEquals(event.getString(\"user_id\"), \"null\");\n        JSONObject userProperties = event.getJSONObject(\"user_properties\");\n        assertEquals(userProperties.length(), 1);\n        JSONObject expected = new JSONObject();\n        expected.put(\"key1\", \"key1-value2\");\n        expected.put(\"key2\", \"key2-value2\");\n        expected.put(\"key3\", \"key3-value2\");\n        expected.put(\"key4\", \"key4-value2\");\n        assertTrue(Utils.compareJSONObjects(userProperties.getJSONObject(Constants.AMP_OP_SET), expected));\n\n        JSONObject event2 = events.getJSONObject(1);\n        assertEquals(event2.getString(\"event_type\"), Constants.IDENTIFY_EVENT);\n        assertEquals(event2.getLong(\"event_id\"), 2);\n        assertEquals(event2.getLong(\"timestamp\"), timestamps[4]);\n        assertEquals(event2.getLong(\"sequence_number\"), 5);\n        assertEquals(event2.getString(\"user_id\"), \"identify-user-id\");\n        JSONObject userProperties2 = event2.getJSONObject(\"user_properties\");\n        assertEquals(userProperties2.length(), 1);\n        JSONObject expected2 = new JSONObject();\n        expected2.put(\"key3\", \"key3-value3\");\n        expected2.put(\"key4\", \"key4-value3\");\n        assertTrue(Utils.compareJSONObjects(userProperties2.getJSONObject(Constants.AMP_OP_SET), expected2));\n\n        assertEquals(0, getIdentifyInterceptorCount());\n        assertEquals((long)dbHelper.getLastIdentifyInterceptorId(), -1L);\n    }\n\n    @Test\n    public void testNullUserPropertyFilteredOut() throws JSONException {\n        long [] timestamps = {1000, 1001, 1002};\n        clock.setTimestamps(timestamps);\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n        amplitude.identify(new Identify().set(\"key1\", \"key1-value1\").set(\"key2\", \"key2-value1\").set(\"key3\", \"key3-value1\"));\n        amplitude.identify(new Identify().set(\"key1\", \"key1-value2\").set(\"key4\", \"key4-value1\"));\n        amplitude.setUserProperties(new JSONObject().put(\"key2\", null).put(\"key4\", JSONObject.NULL));\n        looper.runToEndOfTasks();\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        assertEquals(3L, getIdentifyInterceptorCount());\n        assertEquals(0, getUnsentIdentifyCount());\n        assertNull(dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY));\n        assertEquals((long) dbHelper.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY), 3L);\n        assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY), timestamps[2]);\n\n        amplitude.uploadEvents();\n        looper.runToEndOfTasks();\n        RecordedRequest request = runRequest(amplitude);\n        JSONArray events = getEventsFromRequest(request);\n        assertEquals(events.length(), 1);\n        JSONObject identify = events.getJSONObject(0);\n        assertEquals(identify.getString(\"event_type\"), Constants.IDENTIFY_EVENT);\n        assertEquals(identify.getLong(\"event_id\"), 1);\n        assertEquals(identify.getLong(\"timestamp\"), timestamps[0]);\n        assertEquals(identify.getLong(\"sequence_number\"), 1);\n        JSONObject userProperties = identify.getJSONObject(\"user_properties\");\n        assertEquals(userProperties.length(), 1);\n        assertTrue(userProperties.has(Constants.AMP_OP_SET));\n        JSONObject expected = new JSONObject();\n        expected.put(\"key1\", \"key1-value2\");\n        expected.put(\"key2\", \"key2-value1\");\n        expected.put(\"key3\", \"key3-value1\");\n        expected.put(\"key4\", \"key4-value1\");\n        System.out.println(userProperties.getJSONObject(Constants.AMP_OP_SET));\n        assertTrue(Utils.compareJSONObjects(userProperties.getJSONObject(Constants.AMP_OP_SET), expected));\n        assertEquals(0, getIdentifyInterceptorCount());\n        assertEquals((long)dbHelper.getLastIdentifyInterceptorId(), -1L);\n        assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY), 1L);\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/amplitude/api/AmplitudeServerZoneTest.java",
    "content": "package com.amplitude.api;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.junit.runners.Parameterized;\nimport org.robolectric.annotation.Config;\n\nimport java.util.Arrays;\nimport java.util.Collection;\n\nimport static org.junit.Assert.assertEquals;\n\n@RunWith(Parameterized.class)\n@Config(manifest=Config.NONE)\npublic class AmplitudeServerZoneTest {\n\n    @Parameterized.Parameters\n    public static Collection<Object[]> data() {\n        return Arrays.asList(new Object[][] {\n            { AmplitudeServerZone.US, Constants.EVENT_LOG_URL, Constants.DYNAMIC_CONFIG_URL, \"US\"},\n            { null, Constants.EVENT_LOG_URL, Constants.DYNAMIC_CONFIG_URL, \"\"},\n            { AmplitudeServerZone.EU, Constants.EVENT_LOG_EU_URL, Constants.DYNAMIC_CONFIG_EU_URL, \"EU\"}\n        });\n    }\n\n    private AmplitudeServerZone serverZone;\n    private String expectedEventLogUrl;\n    private String expectedDynamicConfigUrl;\n    private String serverZoneString;\n\n    public AmplitudeServerZoneTest(\n        AmplitudeServerZone serverZone,\n        String expectedEventLogUrl,\n        String expectedDynamicConfigUrl,\n        String serverZoneString\n    ) {\n        this.serverZone = serverZone;\n        this.expectedEventLogUrl = expectedEventLogUrl;\n        this.expectedDynamicConfigUrl = expectedDynamicConfigUrl;\n        this.serverZoneString = serverZoneString;\n    }\n\n    @Test\n    public void testGetCorrectUrlForAmplitudeServerZone() {\n        assertEquals(expectedEventLogUrl, AmplitudeServerZone.getEventLogApiForZone(serverZone));\n        assertEquals(expectedDynamicConfigUrl, AmplitudeServerZone.getDynamicConfigApi(serverZone));\n        AmplitudeServerZone expectedServerZone = serverZone != null ? serverZone : AmplitudeServerZone.US;\n        assertEquals(expectedServerZone, AmplitudeServerZone.getServerZone(serverZoneString));\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/amplitude/api/AmplitudeTest.java",
    "content": "package com.amplitude.api;\n\nimport android.content.Context;\nimport android.content.SharedPreferences;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.Shadows;\nimport org.robolectric.annotation.Config;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertNotSame;\nimport static org.junit.Assert.assertSame;\nimport static org.junit.Assert.assertTrue;\n\n@RunWith(AndroidJUnit4.class)\n@Config(manifest = Config.NONE)\npublic class AmplitudeTest extends BaseTest {\n\n    @Before\n    public void setUp() throws Exception {\n        super.setUp();\n    }\n\n    @After\n    public void tearDown() throws Exception {\n        super.tearDown();\n    }\n\n    @Test\n    public void testGetInstance() {\n        AmplitudeClient a = Amplitude.getInstance();\n        AmplitudeClient b = Amplitude.getInstance(\"\");\n        AmplitudeClient c = Amplitude.getInstance(null);\n        AmplitudeClient d = Amplitude.getInstance(Constants.DEFAULT_INSTANCE);\n        AmplitudeClient e = Amplitude.getInstance(\"app1\");\n        AmplitudeClient f = Amplitude.getInstance(\"app2\");\n\n        assertSame(a, b);\n        assertSame(b, c);\n        assertSame(c, d);\n        assertSame(d, Amplitude.getInstance());\n        assertNotSame(d, e);\n        assertSame(e, Amplitude.getInstance(\"app1\"));\n        assertNotSame(e, f);\n        assertSame(f, Amplitude.getInstance(\"app2\"));\n\n        // test for instance name case insensitivity\n        assertSame(e, Amplitude.getInstance(\"APP1\"));\n        assertSame(e, Amplitude.getInstance(\"App1\"));\n        assertSame(e, Amplitude.getInstance(\"aPP1\"));\n        assertSame(e, Amplitude.getInstance(\"apP1\"));\n\n        assertTrue(Amplitude.instances.size() == 3);\n        assertTrue(Amplitude.instances.containsKey(Constants.DEFAULT_INSTANCE));\n        assertTrue(Amplitude.instances.containsKey(\"app1\"));\n        assertTrue(Amplitude.instances.containsKey(\"app2\"));\n    }\n\n    @Test\n    public void testSeparateInstancesLogEventsSeparately() {\n        Amplitude.instances.clear();\n        DatabaseHelper.instances.clear();\n\n        String newInstance1 = \"newApp1\";\n        String newApiKey1 = \"1234567890\";\n        String newInstance2 = \"newApp2\";\n        String newApiKey2 = \"0987654321\";\n\n        DatabaseHelper oldDbHelper = DatabaseHelper.getDatabaseHelper(context);\n        DatabaseHelper newDbHelper1 = DatabaseHelper.getDatabaseHelper(context, newInstance1);\n        DatabaseHelper newDbHelper2 = DatabaseHelper.getDatabaseHelper(context, newInstance2);\n\n        // Setup existing Databasefile\n        oldDbHelper.insertOrReplaceKeyValue(\"device_id\", \"oldDeviceId\");\n        oldDbHelper.insertOrReplaceKeyLongValue(\"sequence_number\", 1000L);\n        oldDbHelper.addEvent(\"oldEvent1\");\n        oldDbHelper.addIdentify(\"oldIdentify1\");\n        oldDbHelper.addIdentify(\"oldIdentify2\");\n\n        // Verify persistence of old database file in default instance\n        Amplitude.getInstance().initialize(context, apiKey);\n        Shadows.shadowOf(Amplitude.getInstance().logThread.getLooper()).runToEndOfTasks();\n        assertEquals(Amplitude.getInstance().getDeviceId(), \"oldDeviceId\");\n        assertEquals(Amplitude.getInstance().getNextSequenceNumber(), 1001L);\n        assertTrue(oldDbHelper.dbFileExists());\n        assertFalse(newDbHelper1.dbFileExists());\n        assertFalse(newDbHelper2.dbFileExists());\n\n        // init first new app and verify separate database file\n        Amplitude.getInstance(newInstance1).initialize(context, newApiKey1);\n        Shadows.shadowOf(\n            Amplitude.getInstance(newInstance1).logThread.getLooper()\n        ).runToEndOfTasks();\n        assertTrue(newDbHelper1.dbFileExists()); // db file is created after deviceId initialization\n\n        assertFalse(newDbHelper1.getValue(\"device_id\").equals(\"oldDeviceId\"));\n        assertEquals(\n            newDbHelper1.getValue(\"device_id\"), Amplitude.getInstance(newInstance1).getDeviceId()\n        );\n        assertEquals(Amplitude.getInstance(newInstance1).getNextSequenceNumber(), 1L);\n        assertEquals(newDbHelper1.getEventCount(), 0);\n        assertEquals(newDbHelper1.getIdentifyCount(), 0);\n\n        // init second new app and verify separate database file\n        Amplitude.getInstance(newInstance2).initialize(context, newApiKey2);\n        Shadows.shadowOf(\n            Amplitude.getInstance(newInstance2).logThread.getLooper()\n        ).runToEndOfTasks();\n        assertTrue(newDbHelper2.dbFileExists()); // db file is created after deviceId initialization\n\n        assertFalse(newDbHelper2.getValue(\"device_id\").equals(\"oldDeviceId\"));\n        assertEquals(\n            newDbHelper2.getValue(\"device_id\"), Amplitude.getInstance(newInstance2).getDeviceId()\n        );\n        assertEquals(Amplitude.getInstance(newInstance2).getNextSequenceNumber(), 1L);\n        assertEquals(newDbHelper2.getEventCount(), 0);\n        assertEquals(newDbHelper2.getIdentifyCount(), 0);\n\n        // verify existing database still intact\n        assertTrue(oldDbHelper.dbFileExists());\n        assertEquals(oldDbHelper.getValue(\"device_id\"), \"oldDeviceId\");\n        assertEquals(oldDbHelper.getLongValue(\"sequence_number\").longValue(), 1001L);\n        assertEquals(oldDbHelper.getEventCount(), 1);\n        assertEquals(oldDbHelper.getIdentifyCount(), 2);\n\n        // verify both apps can modify their database independently and not affect old database\n        newDbHelper1.insertOrReplaceKeyValue(\"device_id\", \"fakeDeviceId\");\n        assertEquals(newDbHelper1.getValue(\"device_id\"), \"fakeDeviceId\");\n        assertFalse(newDbHelper2.getValue(\"device_id\").equals(\"fakeDeviceId\"));\n        assertEquals(oldDbHelper.getValue(\"device_id\"), \"oldDeviceId\");\n        newDbHelper1.addIdentify(\"testIdentify3\");\n        assertEquals(newDbHelper1.getIdentifyCount(), 1);\n        assertEquals(newDbHelper2.getIdentifyCount(), 0);\n        assertEquals(oldDbHelper.getIdentifyCount(), 2);\n\n        newDbHelper2.insertOrReplaceKeyValue(\"device_id\", \"brandNewDeviceId\");\n        assertEquals(newDbHelper1.getValue(\"device_id\"), \"fakeDeviceId\");\n        assertEquals(newDbHelper2.getValue(\"device_id\"), \"brandNewDeviceId\");\n        assertEquals(oldDbHelper.getValue(\"device_id\"), \"oldDeviceId\");\n        newDbHelper2.addEvent(\"testEvent2\");\n        newDbHelper2.addEvent(\"testEvent3\");\n        assertEquals(newDbHelper1.getEventCount(), 0);\n        assertEquals(newDbHelper2.getEventCount(), 2);\n        assertEquals(oldDbHelper.getEventCount(), 1);\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/com/amplitude/api/BaseTest.java",
    "content": "package com.amplitude.api;\n\nimport android.content.Context;\nimport android.content.pm.ApplicationInfo;\nimport android.content.pm.PackageInfo;\nimport android.content.pm.PackageManager;\nimport android.database.Cursor;\nimport android.database.sqlite.SQLiteDatabase;\n\nimport androidx.test.core.app.ApplicationProvider;\n\nimport org.json.JSONArray;\nimport org.json.JSONException;\nimport org.json.JSONObject;\nimport org.robolectric.RuntimeEnvironment;\nimport org.robolectric.shadows.ShadowLooper;\nimport org.robolectric.shadows.ShadowPackageManager;\n\nimport java.io.UnsupportedEncodingException;\nimport java.lang.reflect.Field;\nimport java.net.URLDecoder;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport okhttp3.mockwebserver.MockResponse;\nimport okhttp3.mockwebserver.MockWebServer;\nimport okhttp3.mockwebserver.RecordedRequest;\n\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.junit.Assert.fail;\nimport static org.robolectric.Shadows.shadowOf;\n\npublic class BaseTest {\n\n    protected class MockClock {\n        int index = 0;\n        long timestamps [];\n\n        public void setTimestamps(long [] timestamps) {\n            this.timestamps = timestamps;\n        }\n\n        public long currentTimeMillis() {\n            if (timestamps == null || index >= timestamps.length) {\n                return System.currentTimeMillis();\n            }\n            return timestamps[index++];\n        }\n    }\n\n    // override getCurrentTimeMillis to enforce time progression in tests\n    protected class AmplitudeClientWithTime extends AmplitudeClient {\n        MockClock mockClock;\n\n        public AmplitudeClientWithTime(MockClock mockClock) { this.mockClock = mockClock; }\n\n        @Override\n        protected long getCurrentTimeMillis() { return mockClock.currentTimeMillis(); }\n    }\n\n    // override AmplitudeDatabaseHelper to throw Cursor Allocation Exception\n    protected class MockDatabaseHelper extends DatabaseHelper {\n\n        protected MockDatabaseHelper(Context context) {\n            super(context);\n        }\n\n        @Override\n        Cursor queryDb(\n            SQLiteDatabase db, String table, String[] columns, String selection,\n            String[] selectionArgs, String groupBy, String having, String orderBy, String limit\n        ) {\n            // cannot import CursorWindowAllocationException, so we throw the base class instead\n            throw new RuntimeException(\"Cursor window allocation of 2048 kb failed.\");\n        }\n    }\n\n    private static final String TEST_PACKAGE_NAME = \"com.amplitude.test\";\n    private static final String TEST_VERSION_NAME = \"test_version\";\n\n    protected AmplitudeClient amplitude;\n    protected Context context;\n    protected MockWebServer server;\n    protected MockClock clock;\n    protected String apiKey = \"1cc2c1978ebab0f6451112a8f5df4f4e\";\n    protected String[] instanceNames = {Constants.DEFAULT_INSTANCE, \"app1\", \"app2\", \"newApp1\", \"newApp2\", \"new_app\"};\n\n    protected PackageManager packageManager;\n    protected ShadowPackageManager shadowPackageManager;\n\n    public void setUp() throws Exception {\n        setUp(true);\n    }\n\n    /**\n     * Handle common test setup for default cases. Specific cases can\n     * override the defaults by providing an amplitude object before\n     * calling this method or passing false for withServer.\n     */\n    public void setUp(boolean withServer) throws Exception {\n        context = ApplicationProvider.getApplicationContext();\n        packageManager = RuntimeEnvironment.application.getPackageManager();\n        shadowPackageManager = shadowOf(packageManager);\n\n        ApplicationInfo applicationInfo = new ApplicationInfo();\n        applicationInfo.packageName = TEST_PACKAGE_NAME;\n\n        PackageInfo packageInfo = new PackageInfo();\n        packageInfo.packageName = TEST_PACKAGE_NAME;\n        packageInfo.versionName = TEST_VERSION_NAME;\n        packageInfo.applicationInfo = applicationInfo;\n        shadowPackageManager.addPackage(packageInfo);\n\n        // Clear the database helper for each test. Better to have isolation.\n        // See https://github.com/robolectric/robolectric/issues/569\n        // and https://github.com/robolectric/robolectric/issues/1622\n        Amplitude.instances.clear();\n        DatabaseHelper.instances.clear();\n\n        if (withServer) {\n            server = new MockWebServer();\n            server.start();\n        }\n\n        if (clock == null) {\n            clock = new MockClock();\n        }\n\n        if (amplitude == null) {\n            amplitude = new AmplitudeClientWithTime(clock);\n            // this sometimes deadlocks with lock contention by logThread and httpThread for\n            // a ShadowWrangler instance and the ShadowLooper class\n            // Might be a sign of a bug, or just Robolectric's bug.\n        }\n\n        if (server != null) {\n            amplitude.url = server.url(\"/\").toString();\n        }\n    }\n\n    public void tearDown() throws Exception {\n        if (amplitude != null) {\n            amplitude.logThread.getLooper().quit();\n            amplitude.httpThread.getLooper().quit();\n            amplitude = null;\n        }\n\n        if (server != null) {\n            server.shutdown();\n        }\n\n        Amplitude.instances.clear();\n        DatabaseHelper.instances.clear();\n    }\n\n    public RecordedRequest runRequest(AmplitudeClient amplitude) {\n        server.enqueue(new MockResponse().setBody(\"success\"));\n        ShadowLooper httpLooper = shadowOf(amplitude.httpThread.getLooper());\n        httpLooper.runToEndOfTasks();\n\n        try {\n            return server.takeRequest(1, SECONDS);\n        } catch (InterruptedException e) {\n            return null;\n        }\n    }\n\n    public RecordedRequest sendEvent(AmplitudeClient amplitude, String name, JSONObject props) {\n        shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        amplitude.logEvent(name, props);\n        shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n\n        return runRequest(amplitude);\n    }\n\n    public RecordedRequest sendIdentify(AmplitudeClient amplitude, Identify identify) {\n        shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        amplitude.identify(identify);\n        shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n\n        return runRequest(amplitude);\n    }\n\n    public long getUnsentEventCount() {\n        return DatabaseHelper.getDatabaseHelper(context).getEventCount();\n    }\n\n    public long getUnsentIdentifyCount() {\n        return DatabaseHelper.getDatabaseHelper(context).getIdentifyCount();\n    }\n\n    public long getIdentifyInterceptorCount() {\n        return DatabaseHelper.getDatabaseHelper(context).getIdentifyInterceptorCount();\n    }\n\n    public JSONObject getLastUnsentEvent() {\n        JSONArray events = getUnsentEventsFromTable(DatabaseHelper.EVENT_TABLE_NAME, 1);\n        return (JSONObject)events.opt(events.length() - 1);\n    }\n\n    public JSONObject getLastUnsentIdentify() {\n        JSONArray events = getUnsentEventsFromTable(DatabaseHelper.IDENTIFY_TABLE_NAME, 1);\n        return (JSONObject)events.opt(events.length() - 1);\n    }\n\n    public JSONArray getUnsentEvents(int limit) {\n        return getUnsentEventsFromTable(DatabaseHelper.EVENT_TABLE_NAME, limit);\n    }\n\n    public JSONArray getUnsentIdentifys(int limit) {\n        return getUnsentEventsFromTable(DatabaseHelper.IDENTIFY_TABLE_NAME, limit);\n    }\n\n    public JSONArray getUnsentEventsFromTable(String table, int limit) {\n        try {\n            DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n            List<JSONObject> events = table.equals(DatabaseHelper.IDENTIFY_TABLE_NAME) ?\n                    dbHelper.getIdentifys(-1, -1) : dbHelper.getEvents(-1, -1);\n\n            JSONArray out = new JSONArray();\n            int start = Math.max(limit - events.size(), 0);\n            for (int i = start; i < limit; i++) {\n                out.put(i, events.get(events.size() - limit + i));\n            }\n            return out;\n        } catch (JSONException e) {\n            fail(e.toString());\n        }\n\n        return null;\n    }\n\n    public JSONObject getLastEvent() {\n        return getLastEventFromTable(DatabaseHelper.EVENT_TABLE_NAME);\n    }\n\n    public JSONObject getLastIdentify() {\n        return getLastEventFromTable(DatabaseHelper.IDENTIFY_TABLE_NAME);\n    }\n\n    public JSONObject getLastEventFromTable(String table) {\n        try {\n            DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n            List<JSONObject> events = table.equals(DatabaseHelper.IDENTIFY_TABLE_NAME) ?\n                    dbHelper.getIdentifys(-1, -1) : dbHelper.getEvents(-1, -1);\n            return events.get(events.size() - 1);\n        } catch (JSONException e) {\n            fail(e.toString());\n        }\n        return null;\n    }\n\n    public JSONObject getLastIdentifyInterceptor() {\n        try {\n            DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n            List<JSONObject> events = dbHelper.getIdentifyInterceptors(-1, -1);\n            return events.get(events.size() - 1);\n        } catch (JSONException e) {\n            fail(e.toString());\n        }\n        return null;\n    }\n\n    public JSONArray getEventsFromRequest(RecordedRequest request) throws JSONException {\n        Map<String, String> parsedBody = parseRequest(request.getUtf8Body());\n        if (parsedBody == null && !parsedBody.containsKey(\"e\")) {\n            return null;\n        }\n        return new JSONArray(parsedBody.get(\"e\"));\n    }\n\n    // parse request string into a key:value map\n    public static Map<String, String> parseRequest(String request) {\n        try {\n            Map<String, String> query_pairs = new LinkedHashMap<String, String>();\n            String[] pairs = request.split(\"&\");\n            for (String pair : pairs) {\n                int idx = pair.indexOf(\"=\");\n                query_pairs.put(URLDecoder.decode(pair.substring(0, idx), \"UTF-8\"), URLDecoder.decode(pair.substring(idx + 1), \"UTF-8\"));\n            }\n            return query_pairs;\n        } catch (UnsupportedEncodingException e) {\n            fail(e.toString());\n        }\n        return null;\n    }\n\n    protected Object getPrivateFieldValueFromClient(AmplitudeClient client, String field) {\n        try {\n            Field privateField = AmplitudeClient.class.getDeclaredField(field);\n            privateField.setAccessible(true);\n            return privateField.get(client);\n        } catch (IllegalAccessException e) {\n            fail(e.toString());\n        } catch (NoSuchFieldException e) {\n            fail(e.toString());\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/amplitude/api/ConfigManagerTest.java",
    "content": "package com.amplitude.api;\n\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport com.amplitude.api.util.MockHttpURLConnectionHelper;\nimport com.amplitude.api.util.MockURLStreamHandler;\n\nimport org.json.JSONObject;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.annotation.Config;\n\nimport java.net.HttpURLConnection;\nimport java.net.URL;\n\nimport static org.junit.Assert.assertEquals;\n\n@RunWith(AndroidJUnit4.class)\n@Config(manifest=Config.NONE)\npublic class ConfigManagerTest {\n    private final MockURLStreamHandler mockURLStreamHandler = MockURLStreamHandler.getInstance();\n\n    @Test\n    public void testRefreshForEU() throws Exception {\n        assertEquals(Constants.EVENT_LOG_URL, ConfigManager.getInstance().getIngestionEndpoint());\n        JSONObject responseObject = new JSONObject();\n        responseObject.put(\"code\", 200);\n        responseObject.put(\"ingestionEndpoint\", \"api.eu.amplitude.com\");\n\n        URL url = new URL(Constants.DYNAMIC_CONFIG_EU_URL);\n        AmplitudeServerZone euZone = AmplitudeServerZone.EU;\n        HttpURLConnection connection =\n                MockHttpURLConnectionHelper.getMockHttpURLConnection(200, responseObject.toString());\n        mockURLStreamHandler.setConnection(url, connection);\n\n        ConfigManager.getInstance().refresh(new ConfigManager.RefreshListener() {\n            @Override\n            public void onFinished() {\n                assertEquals(Constants.EVENT_LOG_EU_URL, ConfigManager.getInstance().getIngestionEndpoint() + \"/\");\n            }\n        }, euZone);\n    }\n}"
  },
  {
    "path": "src/test/java/com/amplitude/api/DatabaseHelperTest.java",
    "content": "package com.amplitude.api;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.annotation.Config;\n\nimport java.util.List;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotSame;\nimport static org.junit.Assert.assertNull;\nimport static org.junit.Assert.assertSame;\nimport static org.junit.Assert.assertTrue;\nimport static org.junit.Assert.fail;\n\n@RunWith(AndroidJUnit4.class)\n@Config(manifest = Config.NONE)\npublic class DatabaseHelperTest extends BaseTest {\n\n    protected DatabaseHelper dbInstance;\n\n    @Before\n    public void setUp() throws Exception {\n        super.setUp(false);\n        dbInstance = DatabaseHelper.getDatabaseHelper(context);\n    }\n\n    @After\n    public void tearDown() throws Exception {\n        super.tearDown();\n        dbInstance = null;\n    }\n\n    protected long addEvent(String type) {\n        return addEventToTable(DatabaseHelper.EVENT_TABLE_NAME, type, new JSONObject());\n    }\n\n    protected long addEventToTable(String table, String type, JSONObject props) {\n        try {\n            props.put(\"event_type\", type);\n            return table.equals(DatabaseHelper.IDENTIFY_TABLE_NAME) ?\n                    dbInstance.addIdentify(props.toString()) :\n                    dbInstance.addEvent(props.toString());\n        } catch (JSONException e) {\n            fail(e.toString());\n        }\n        return -1;\n    }\n\n    protected long addIdentify(String identifyEvent) {\n        return addEventToTable(DatabaseHelper.IDENTIFY_TABLE_NAME, identifyEvent, new JSONObject());\n    }\n\n    protected long addIdentifyInterceptor(String property) {\n        try {\n            JSONObject props = new JSONObject();\n            props.put(\"user_properties\", new JSONObject());\n            props.getJSONObject(\"user_properties\").put(\"test_prop\", property);\n            props.put(\"event_type\", Constants.IDENTIFY_EVENT);\n            return dbInstance.addIdentifyInterceptor(props.toString());\n        } catch (JSONException e) {\n            fail(e.toString());\n        }\n        return -1;\n    }\n\n    protected long insertOrReplaceKeyValue(String key, String value) {\n        return dbInstance.insertOrReplaceKeyValue(key, value);\n    }\n\n    protected long insertOrReplaceKeyLongValue(String key, Long value) {\n        return dbInstance.insertOrReplaceKeyLongValue(key, value);\n    }\n\n    protected String getValue(String key) {\n        return dbInstance.getValue(key);\n    }\n\n    protected Long getLongValue(String key) { return dbInstance.getLongValue(key); }\n\n    @Test\n    public void testCreate() {\n        dbInstance.onCreate(dbInstance.getWritableDatabase());\n        assertEquals(1, insertOrReplaceKeyValue(\"test_key\", \"test_value\"));\n        assertEquals(1, insertOrReplaceKeyLongValue(\"test_key\", 1L));\n        assertEquals(1, addEvent(\"test_create\"));\n        assertEquals(1, addIdentify(\"test_create\"));\n        assertEquals(1, addIdentifyInterceptor(\"test_create\"));\n    }\n\n    // need separate tests for different version to version upgrades since insertion failure\n    // triggers database deletion and recreation - need to refetch writable database as well\n    @Test\n    public void testUpgradeVersion1ToVersion2() {\n        // store table doesn't exist in v1, insert will fail\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.STORE_TABLE_NAME);\n        String key = \"test_key\";\n        String value = \"test_value\";\n        assertEquals(-1, insertOrReplaceKeyValue(key, value));\n\n        // long store table doesn't exist in v1, insert will fail\n        Long longValue = 1L;\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.LONG_STORE_TABLE_NAME);\n        assertEquals(-1, insertOrReplaceKeyLongValue(key, longValue));\n\n        // identify table doesn't exist in v1, insert will fail\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.IDENTIFY_TABLE_NAME);\n        assertEquals(-1, addIdentify(\"test_upgrade\"));\n\n        // identify interceptor table doesn't exist in v1, insert will fail\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.IDENTIFY_INTERCEPTOR_TABLE_NAME);\n        assertEquals(-1, addIdentifyInterceptor(\"test_upgrade\"));\n\n        // only event inserts will work\n        assertEquals(1, addEvent(\"test_upgrade\"));\n\n        // after v2 upgrade, can insert into store table\n        // still can't insert into identify table, long store table or identify interceptor table\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.STORE_TABLE_NAME);\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.IDENTIFY_TABLE_NAME);\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.LONG_STORE_TABLE_NAME);\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.IDENTIFY_INTERCEPTOR_TABLE_NAME);\n        dbInstance.onUpgrade(dbInstance.getWritableDatabase(), 1, 2);\n        assertEquals(2, addEvent(\"test_upgrade\"));\n        assertEquals(1, insertOrReplaceKeyValue(key, value));\n        assertEquals(-1, addIdentify(\"test_upgrade\"));\n        assertEquals(-1, addIdentifyInterceptor(\"test_upgrade\"));\n        assertEquals(-1, insertOrReplaceKeyLongValue(key, longValue));\n    }\n\n    @Test\n    public void testUpgradeVersion2ToVersion3() {\n        // identify table doesn't exist in v2, insert will fail\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.IDENTIFY_TABLE_NAME);\n        assertEquals(-1, addIdentify(\"test_upgrade\"));\n\n        // long store table doesn't exist in v2, insert will fail\n        String key = \"test_key\";\n        Long longValue = 1L;\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.LONG_STORE_TABLE_NAME);\n        assertEquals(-1, insertOrReplaceKeyLongValue(key, longValue));\n\n        // identify interceptor table doesn't exist in v2, insert will fail\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.IDENTIFY_INTERCEPTOR_TABLE_NAME);\n        assertEquals(-1, addIdentifyInterceptor(\"test_upgrade\"));\n\n        // events and store inserts will work\n        String value = \"test_value\";\n        assertEquals(1, insertOrReplaceKeyValue(key, value));\n        assertEquals(1, addEvent(\"test_upgrade\"));\n\n        // after v3 upgrade, can insert into identify table and long store\n        // cannot insert into identify interceptor table\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.IDENTIFY_TABLE_NAME);\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.IDENTIFY_INTERCEPTOR_TABLE_NAME);\n        dbInstance.onUpgrade(dbInstance.getWritableDatabase(), 2, 3);\n        assertEquals(2, addEvent(\"test_upgrade\"));\n        assertEquals(2, insertOrReplaceKeyValue(key, value));\n        assertEquals(1, addIdentify(\"test_upgrade\"));\n        assertEquals(1, insertOrReplaceKeyLongValue(key, longValue));\n        assertEquals(-1, addIdentifyInterceptor(\"test_upgrade\"));\n    }\n\n    @Test\n    public void testUpgradeVersion1ToVersion3() {\n        // store table doesn't exist in v1, insert will fail\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.STORE_TABLE_NAME);\n        String key = \"test_key\";\n        String value = \"test_value\";\n        assertEquals(-1, insertOrReplaceKeyValue(key, value));\n\n        // identify table doesn't exist in v1, insert will fail\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.IDENTIFY_TABLE_NAME);\n        assertEquals(-1, addIdentify(\"test_upgrade\"));\n\n        // long store table doesn't exist in v1, insert will fail\n        Long longValue = 1L;\n        dbInstance.getWritableDatabase().execSQL(\n            \"DROP TABLE IF EXISTS \" + DatabaseHelper.LONG_STORE_TABLE_NAME);\n        assertEquals(-1, insertOrReplaceKeyLongValue(key, longValue));\n\n        // identify interceptor table doesn't exist in v1, insert will fail\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.IDENTIFY_INTERCEPTOR_TABLE_NAME);\n        assertEquals(-1, addIdentifyInterceptor(\"test_upgrade\"));\n\n        // only event inserts will work\n        assertEquals(1, addEvent(\"test_upgrade\"));\n\n        // after v3 upgrade, can insert into store and identify tables\n        // cannot insert into identify interceptor table\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.STORE_TABLE_NAME);\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.IDENTIFY_TABLE_NAME);\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.LONG_STORE_TABLE_NAME);\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.IDENTIFY_INTERCEPTOR_TABLE_NAME);\n        dbInstance.onUpgrade(dbInstance.getWritableDatabase(), 1, 3);\n        assertEquals(2, addEvent(\"test_upgrade\"));\n        assertEquals(1, insertOrReplaceKeyValue(key, value));\n        assertEquals(1, addIdentify(\"test_upgrade\"));\n        assertEquals(1, insertOrReplaceKeyLongValue(key, longValue));\n        assertEquals(-1, addIdentifyInterceptor(\"test_upgrade\"));\n    }\n\n    @Test\n    public void testUpgradeVersion1ToVersion4() {\n        // store table doesn't exist in v1, insert will fail\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.STORE_TABLE_NAME);\n        String key = \"test_key\";\n        String value = \"test_value\";\n        assertEquals(-1, insertOrReplaceKeyValue(key, value));\n\n        // identify table doesn't exist in v1, insert will fail\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.IDENTIFY_TABLE_NAME);\n        assertEquals(-1, addIdentify(\"test_upgrade\"));\n\n        // long store table doesn't exist in v1, insert will fail\n        Long longValue = 1L;\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.LONG_STORE_TABLE_NAME);\n        assertEquals(-1, insertOrReplaceKeyLongValue(key, longValue));\n\n        // identify interceptor table doesn't exist in v1, insert will fail\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.IDENTIFY_INTERCEPTOR_TABLE_NAME);\n        assertEquals(-1, addIdentifyInterceptor(\"test_upgrade\"));\n\n        // only event inserts will work\n        assertEquals(1, addEvent(\"test_upgrade\"));\n\n        // after v4 upgrade, can insert into store, identify, and identify interceptor tables\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.STORE_TABLE_NAME);\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.IDENTIFY_TABLE_NAME);\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.LONG_STORE_TABLE_NAME);\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.IDENTIFY_INTERCEPTOR_TABLE_NAME);\n        dbInstance.onUpgrade(dbInstance.getWritableDatabase(), 1, 4);\n        assertEquals(2, addEvent(\"test_upgrade\"));\n        assertEquals(1, insertOrReplaceKeyValue(key, value));\n        assertEquals(1, addIdentify(\"test_upgrade\"));\n        assertEquals(1, insertOrReplaceKeyLongValue(key, longValue));\n        assertEquals(1, addIdentifyInterceptor(\"test_upgrade\"));\n    }\n\n    @Test\n    public void testUpgradeVersion2ToVersion4() {\n        // identify table doesn't exist in v2, insert will fail\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.IDENTIFY_TABLE_NAME);\n        assertEquals(-1, addIdentify(\"test_upgrade\"));\n\n        // long store table doesn't exist in v2, insert will fail\n        String key = \"test_key\";\n        Long longValue = 1L;\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.LONG_STORE_TABLE_NAME);\n        assertEquals(-1, insertOrReplaceKeyLongValue(key, longValue));\n\n        // identify interceptor table doesn't exist in v2, insert will fail\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.IDENTIFY_INTERCEPTOR_TABLE_NAME);\n        assertEquals(-1, addIdentifyInterceptor(\"test_upgrade\"));\n\n        // events and store inserts will work\n        String value = \"test_value\";\n        assertEquals(1, insertOrReplaceKeyValue(key, value));\n        assertEquals(1, addEvent(\"test_upgrade\"));\n\n        // after v4 upgrade, can insert into identify table, long store and identify interceptor table\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.IDENTIFY_TABLE_NAME);\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.IDENTIFY_INTERCEPTOR_TABLE_NAME);\n        dbInstance.onUpgrade(dbInstance.getWritableDatabase(), 2, 4);\n        assertEquals(2, addEvent(\"test_upgrade\"));\n        assertEquals(2, insertOrReplaceKeyValue(key, value));\n        assertEquals(1, addIdentify(\"test_upgrade\"));\n        assertEquals(1, insertOrReplaceKeyLongValue(key, longValue));\n        assertEquals(1, addIdentifyInterceptor(\"test_upgrade\"));\n    }\n\n    @Test\n    public void testUpgradeVersion3ToVersion4() {\n        // identify interceptor table doesn't exist in v3, insert will fail\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.IDENTIFY_INTERCEPTOR_TABLE_NAME);\n        assertEquals(-1, addIdentifyInterceptor(\"test_upgrade\"));\n\n        String key = \"test_key\";\n        Long longValue = 1L;\n        // events, store and identify inserts will work\n        String value = \"test_value\";\n        assertEquals(1, insertOrReplaceKeyValue(key, value));\n        assertEquals(1, addEvent(\"test_upgrade\"));\n        assertEquals(1, addIdentify(\"test_upgrade\"));\n\n        // after v4 upgrade, can insert into identify interceptor table\n        dbInstance.getWritableDatabase().execSQL(\n                \"DROP TABLE IF EXISTS \" + DatabaseHelper.IDENTIFY_INTERCEPTOR_TABLE_NAME);\n        dbInstance.onUpgrade(dbInstance.getWritableDatabase(), 3, 4);\n        assertEquals(2, addEvent(\"test_upgrade\"));\n        assertEquals(2, insertOrReplaceKeyValue(key, value));\n        assertEquals(2, addIdentify(\"test_upgrade\"));\n        assertEquals(1, insertOrReplaceKeyLongValue(key, longValue));\n    }\n\n    @Test\n    public void testInsertOrReplaceKeyValue() {\n        String key = \"test_key\";\n        String value1 = \"test_value1\";\n        String value2 = \"test_value2\";\n        assertEquals(null, getValue(key));\n\n        insertOrReplaceKeyValue(key, value1);\n        assertEquals(value1, getValue(key));\n\n        insertOrReplaceKeyValue(key, value2);\n        assertEquals(value2, getValue(key));\n    }\n\n    @Test\n    public void testInsertOrReplaceKeyLongValue() {\n        String key = \"test_key\";\n        Long value1 = 1L;\n        Long value2 = 2L;\n        assertEquals(null, getLongValue(key));\n\n        insertOrReplaceKeyLongValue(key, value1);\n        assertEquals(value1, getLongValue(key));\n\n        insertOrReplaceKeyLongValue(key, value2);\n        assertEquals(value2, getLongValue(key));\n    }\n\n    @Test\n    public void testInsertNullValues() {\n        String key = \"test_key\";\n\n        assertNull(getValue(key));\n        insertOrReplaceKeyValue(key, \"test\");\n        assertEquals(getValue(key), \"test\");\n        insertOrReplaceKeyValue(key, null);\n        assertNull(getValue(key));\n\n        assertNull(getLongValue(key));\n        insertOrReplaceKeyLongValue(key, 15L);\n        assertEquals((long)getLongValue(key), 15L);\n        insertOrReplaceKeyLongValue(key, null);\n        assertNull(getValue(key));\n    }\n\n    @Test\n    public void testAddEvent() {\n        assertEquals(1, addEvent(\"test_add_event\"));\n        assertEquals(1, getLastUnsentEvent().optLong(\"event_id\"));\n        assertEquals(2, addEvent(\"test_add_event\"));\n        assertEquals(2, getLastUnsentEvent().optLong(\"event_id\"));\n        assertEquals(3, addEvent(\"test_add_event\"));\n        assertEquals(3, getLastUnsentEvent().optLong(\"event_id\"));\n    }\n\n    @Test\n    public void testAddIdentify() {\n        assertEquals(1, addIdentify(\"test_add_identify\"));\n        assertEquals(1, getLastUnsentIdentify().optLong(\"event_id\"));\n        assertEquals(2, addIdentify(\"test_add_identify\"));\n        assertEquals(2, getLastUnsentIdentify().optLong(\"event_id\"));\n        assertEquals(3, addIdentify(\"test_add_identify\"));\n        assertEquals(3, getLastUnsentIdentify().optLong(\"event_id\"));\n    }\n\n    @Test\n     public void testGetEvents() {\n        assertEquals(1, addEvent(\"test_get_events_1\"));\n        assertEquals(2, addEvent(\"test_get_events_2\"));\n        assertEquals(3, addEvent(\"test_get_events_3\"));\n        assertEquals(4, addEvent(\"test_get_events_4\"));\n        assertEquals(5, addEvent(\"test_get_events_5\"));\n\n        try {\n            List<JSONObject> events;\n            assertEquals(5, dbInstance.getEventCount());\n\n            events = dbInstance.getEvents(-1, -1);\n            assertEquals(5, events.size());\n            assertEquals(1, (events.get(0)).getLong(\"event_id\"));\n            assertEquals(\"test_get_events_1\", (events.get(0)).getString(\"event_type\"));\n\n            events = dbInstance.getEvents(1, -1);\n            assertEquals(1, events.size());\n\n            events = dbInstance.getEvents(5, -1);\n            assertEquals(5, events.size());\n            assertEquals(5, (events.get(4)).getLong(\"event_id\"));\n            assertEquals(\"test_get_events_5\", (events.get(4)).getString(\"event_type\"));\n\n            events = dbInstance.getEvents(-1, 0);\n            assertEquals(0, events.size());\n\n            events = dbInstance.getEvents(-1, 1);\n            assertEquals(1, events.size());\n            assertEquals(1, (events.get(0)).getLong(\"event_id\"));\n            assertEquals(\"test_get_events_1\", (events.get(0)).getString(\"event_type\"));\n\n            events = dbInstance.getEvents(5, 1);\n            assertEquals(1, events.size());\n            assertEquals(1, (events.get(0)).getLong(\"event_id\"));\n            assertEquals(\"test_get_events_1\", (events.get(0)).getString(\"event_type\"));\n\n            dbInstance.removeEvent(1);\n            events = dbInstance.getEvents(5, 1);\n            assertEquals(1, events.size());\n            assertEquals(2, (events.get(0)).getLong(\"event_id\"));\n            assertEquals(\"test_get_events_2\", (events.get(0)).getString(\"event_type\"));\n\n            dbInstance.removeEvents(3);\n            events = dbInstance.getEvents(5, 1);\n            assertEquals(1, events.size());\n            assertEquals(4, (events.get(0)).getLong(\"event_id\"));\n            assertEquals(\"test_get_events_4\", (events.get(0)).getString(\"event_type\"));\n\n        } catch (JSONException e) {\n            fail(e.toString());\n        }\n    }\n\n    @Test\n    public void testGetIdentifys() {\n        assertEquals(1, addIdentify(\"test_get_identifys_1\"));\n        assertEquals(2, addIdentify(\"test_get_identifys_2\"));\n        assertEquals(3, addIdentify(\"test_get_identifys_3\"));\n        assertEquals(4, addIdentify(\"test_get_identifys_4\"));\n        assertEquals(5, addIdentify(\"test_get_identifys_5\"));\n\n        try {\n            List<JSONObject> events;\n            assertEquals(5, dbInstance.getIdentifyCount());\n\n            events = dbInstance.getIdentifys(-1, -1);\n            assertEquals(5, events.size());\n            assertEquals(1, (events.get(0)).getLong(\"event_id\"));\n            assertEquals(\"test_get_identifys_1\", (events.get(0)).getString(\"event_type\"));\n\n            events = dbInstance.getIdentifys(1, -1);\n            assertEquals(1, events.size());\n\n            events = dbInstance.getIdentifys(5, -1);\n            assertEquals(5, events.size());\n            assertEquals(5, (events.get(4)).getLong(\"event_id\"));\n            assertEquals(\"test_get_identifys_5\", (events.get(4)).getString(\"event_type\")\n            );\n\n            events = dbInstance.getIdentifys(-1, 0);\n            assertEquals(0, events.size());\n\n            events = dbInstance.getIdentifys(-1, 1);\n            assertEquals(1, events.size());\n            assertEquals(1, (events.get(0)).getLong(\"event_id\"));\n            assertEquals(\"test_get_identifys_1\", (events.get(0)).getString(\"event_type\"));\n\n            events = dbInstance.getIdentifys(5, 1);\n            assertEquals(1, events.size());\n            assertEquals(1, (events.get(0)).getLong(\"event_id\"));\n            assertEquals(\"test_get_identifys_1\", (events.get(0)).getString(\"event_type\"));\n\n            dbInstance.removeIdentify(1);\n            events = dbInstance.getIdentifys(5, 1);\n            assertEquals(1, events.size());\n            assertEquals(2, (events.get(0)).getLong(\"event_id\"));\n            assertEquals(\"test_get_identifys_2\", (events.get(0)).getString(\"event_type\")\n            );\n\n            dbInstance.removeIdentifys(3);\n            events = dbInstance.getIdentifys(5, 1);\n            assertEquals(1, events.size());\n            assertEquals(4, (events.get(0)).getLong(\"event_id\"));\n            assertEquals(\"test_get_identifys_4\", (events.get(0)).getString(\"event_type\")\n            );\n        } catch (JSONException e) {\n            fail(e.toString());\n        }\n    }\n\n    @Test\n     public void testGetEventCount() {\n        assertEquals(1, addEvent(\"test_get_event_count_1\"));\n        assertEquals(2, addEvent(\"test_get_event_count_2\"));\n        assertEquals(3, addEvent(\"test_get_event_count_3\"));\n        assertEquals(4, addEvent(\"test_get_event_count_4\"));\n        assertEquals(5, addEvent(\"test_get_event_count_5\"));\n\n        assertEquals(5, dbInstance.getEventCount());\n\n        dbInstance.removeEvent(1);\n        assertEquals(4, dbInstance.getEventCount());\n\n        dbInstance.removeEvents(3);\n        assertEquals(2, dbInstance.getEventCount());\n\n        dbInstance.removeEvents(10);\n        assertEquals(0, dbInstance.getEventCount());\n    }\n\n    @Test\n    public void testGetIdentifyCount() {\n        assertEquals(1, addIdentify(\"test_get_identify_count_1\"));\n        assertEquals(2, addIdentify(\"test_get_identify_count_2\"));\n        assertEquals(3, addIdentify(\"test_get_identify_count_3\"));\n        assertEquals(4, addIdentify(\"test_get_identify_count_4\"));\n        assertEquals(5, addIdentify(\"test_get_identify_count_5\"));\n\n        assertEquals(5, dbInstance.getIdentifyCount());\n\n        dbInstance.removeIdentify(1);\n        assertEquals(4, dbInstance.getIdentifyCount());\n\n        dbInstance.removeIdentifys(3);\n        assertEquals(2, dbInstance.getIdentifyCount());\n\n        dbInstance.removeIdentifys(10);\n        assertEquals(0, dbInstance.getIdentifyCount());\n    }\n\n    @Test\n    public void testGetNthEventId() {\n        assertEquals(1, addEvent(\"test_get_nth_event_id_1\"));\n        assertEquals(2, addEvent(\"test_get_nth_event_id_2\"));\n        assertEquals(3, addEvent(\"test_get_nth_event_id_3\"));\n        assertEquals(4, addEvent(\"test_get_nth_event_id_4\"));\n        assertEquals(5, addEvent(\"test_get_nth_event_id_5\"));\n\n        assertEquals(1, dbInstance.getNthEventId(0));\n        assertEquals(1, dbInstance.getNthEventId(1));\n        assertEquals(2, dbInstance.getNthEventId(2));\n        assertEquals(3, dbInstance.getNthEventId(3));\n        assertEquals(4, dbInstance.getNthEventId(4));\n        assertEquals(5, dbInstance.getNthEventId(5));\n\n        dbInstance.removeEvent(1);\n        assertEquals(2, dbInstance.getNthEventId(1));\n\n        dbInstance.removeEvents(3);\n        assertEquals(4, dbInstance.getNthEventId(1));\n\n        dbInstance.removeEvents(10);\n        assertEquals(-1, dbInstance.getNthEventId(1));\n    }\n\n    @Test\n    public void testGetNthIdentifyId() {\n        assertEquals(1, addIdentify(\"test_get_nth_identify_id_1\"));\n        assertEquals(2, addIdentify(\"test_get_nth_identify_id_2\"));\n        assertEquals(3, addIdentify(\"test_get_nth_identify_id_3\"));\n        assertEquals(4, addIdentify(\"test_get_nth_identify_id_4\"));\n        assertEquals(5, addIdentify(\"test_get_nth_identify_id_5\"));\n\n        assertEquals(1, dbInstance.getNthIdentifyId(0));\n        assertEquals(1, dbInstance.getNthIdentifyId(1));\n        assertEquals(2, dbInstance.getNthIdentifyId(2));\n        assertEquals(3, dbInstance.getNthIdentifyId(3));\n        assertEquals(4, dbInstance.getNthIdentifyId(4));\n        assertEquals(5, dbInstance.getNthIdentifyId(5));\n\n        dbInstance.removeIdentify(1);\n        assertEquals(2, dbInstance.getNthIdentifyId(1));\n\n        dbInstance.removeIdentifys(3);\n        assertEquals(4, dbInstance.getNthIdentifyId(1));\n\n        dbInstance.removeIdentifys(10);\n        assertEquals(-1, dbInstance.getNthIdentifyId(1));\n    }\n\n    @Test\n    public void testNoConflictBetweenEventsAndIdentifys() {\n        assertEquals(1, addEvent(\"test_add_event_id_1\"));\n        assertEquals(2, addEvent(\"test_add_event_id_2\"));\n        assertEquals(3, addEvent(\"test_add_event_id_3\"));\n        assertEquals(4, addEvent(\"test_add_event_id_4\"));\n        assertEquals(4, dbInstance.getEventCount());\n        assertEquals(0, dbInstance.getIdentifyCount());\n\n        assertEquals(1, addIdentify(\"test_add_identify_id_1\"));\n        assertEquals(2, addIdentify(\"test_add_identify_id_2\"));\n        assertEquals(4, dbInstance.getEventCount());\n        assertEquals(2, dbInstance.getIdentifyCount());\n\n        dbInstance.removeEvent(1);\n        assertEquals(3, dbInstance.getEventCount());\n        assertEquals(2, dbInstance.getIdentifyCount());\n\n        dbInstance.removeIdentify(1);\n        assertEquals(3, dbInstance.getEventCount());\n        assertEquals(1, dbInstance.getIdentifyCount());\n\n        dbInstance.removeEvents(4);\n        assertEquals(0, dbInstance.getEventCount());\n        assertEquals(1, dbInstance.getIdentifyCount());\n    }\n\n    @Test\n    public void testNullEventString() throws JSONException {\n        dbInstance.addEvent(null);\n        List<JSONObject> events = dbInstance.getEvents(-1, -1);\n        assertTrue(events.isEmpty());\n    }\n\n    @Test\n    public void testGetDatabaseHelper() {\n        assertEquals(DatabaseHelper.instances.size(), 1);\n        DatabaseHelper oldDbHelper = DatabaseHelper.getDatabaseHelper(context);\n        assertEquals(oldDbHelper.getEventCount(), 0);  // run query to initialize db file\n\n        assertSame(oldDbHelper, DatabaseHelper.getDatabaseHelper(context, null));\n        assertSame(oldDbHelper, DatabaseHelper.getDatabaseHelper(context, \"\"));\n        assertSame(\n            oldDbHelper, DatabaseHelper.getDatabaseHelper(context, Constants.DEFAULT_INSTANCE)\n        );\n        DatabaseHelper a = DatabaseHelper.getDatabaseHelper(context, \"a\");\n        DatabaseHelper b = DatabaseHelper.getDatabaseHelper(context, \"b\");\n        assertNotSame(oldDbHelper, a);\n        assertNotSame(oldDbHelper, b);\n        assertNotSame(a, b);\n        assertSame(a, DatabaseHelper.getDatabaseHelper(context, \"a\"));\n        assertSame(b, DatabaseHelper.getDatabaseHelper(context, \"b\"));\n\n        assertEquals(DatabaseHelper.instances.size(), 3);\n        assertTrue(DatabaseHelper.instances.containsKey(Constants.DEFAULT_INSTANCE));\n        assertTrue(DatabaseHelper.instances.containsKey(\"a\"));\n        assertTrue(DatabaseHelper.instances.containsKey(\"b\"));\n\n        // test for instance name case insensitivity\n        assertSame(a, DatabaseHelper.getDatabaseHelper(context, \"A\"));\n        assertSame(b, DatabaseHelper.getDatabaseHelper(context, \"B\"));\n        assertSame(\n            oldDbHelper,\n            DatabaseHelper.getDatabaseHelper(context, Constants.DEFAULT_INSTANCE.toUpperCase())\n        );\n\n        // assert defaultInstance maintains old database filename while new instances have new names\n        a.addEvent(\"testEvent1\");\n        b.addEvent(\"testEvent2\");\n        assertTrue(context.getDatabasePath(Constants.DATABASE_NAME).exists());\n        assertTrue(context.getDatabasePath(Constants.DATABASE_NAME + \"_a\").exists());\n        assertTrue(context.getDatabasePath(Constants.DATABASE_NAME + \"_b\").exists());\n    }\n\n    @Test\n    public void testSeparateInstances() {\n        DatabaseHelper dbHelper1 = DatabaseHelper.getDatabaseHelper(context, \"a\");\n        DatabaseHelper dbHelper2 = DatabaseHelper.getDatabaseHelper(context, \"b\");\n        DatabaseHelper dbHelper3 = DatabaseHelper.getDatabaseHelper(context, \"c\");\n\n        dbHelper1.insertOrReplaceKeyValue(\"device_id\", \"testDeviceId\");\n        assertEquals(dbHelper1.getValue(\"device_id\"), \"testDeviceId\");\n        assertNull(dbHelper2.getValue(\"device_id\"));\n        assertNull(dbHelper3.getValue(\"device_id\"));\n\n        dbHelper2.addEvent(\"test_event\");\n        assertEquals(dbHelper1.getEventCount(), 0);\n        assertEquals(dbHelper2.getEventCount(), 1);\n        assertEquals(dbHelper3.getEventCount(), 0);\n\n        dbHelper3.addIdentify(\"test_identify_1\");\n        assertEquals(dbHelper1.getIdentifyCount(), 0);\n        assertEquals(dbHelper2.getIdentifyCount(), 0);\n        assertEquals(dbHelper3.getIdentifyCount(), 1);\n    }\n\n    @Test\n    public void testGetLastIdentifyInterceptorId() {\n        assertEquals(1, addIdentifyInterceptor(\"test_get_last_identify_id_1\"));\n        assertEquals(2, addIdentifyInterceptor(\"test_get_last_identify_id_2\"));\n\n        assertEquals(2, dbInstance.getLastIdentifyInterceptorId());\n        dbInstance.removeIdentifyInterceptors(2);\n        assertEquals(-1, dbInstance.getLastIdentifyInterceptorId());\n\n        assertEquals(3, addIdentifyInterceptor(\"test_get_last_identify_id_3\"));\n        assertEquals(3, dbInstance.getLastIdentifyInterceptorId());\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/amplitude/api/DatabaseRecoveryTest.java",
    "content": "package com.amplitude.api;\n\nimport android.content.ContentValues;\nimport android.database.sqlite.SQLiteDatabase;\nimport android.database.sqlite.SQLiteException;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Matchers;\nimport org.powermock.api.mockito.PowerMockito;\nimport org.robolectric.Robolectric;\nimport org.robolectric.Shadows;\nimport org.robolectric.annotation.Config;\nimport org.robolectric.shadows.ShadowLooper;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.RandomAccessFile;\nimport java.util.List;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertNull;\nimport static org.junit.Assert.assertTrue;\nimport static org.mockito.Matchers.anyString;\nimport static org.mockito.Mockito.reset;\n\n@RunWith(AndroidJUnit4.class)\n@Config(manifest = Config.NONE)\npublic class DatabaseRecoveryTest extends BaseTest {\n\n    protected DatabaseHelper dbInstance;\n    protected ShadowLooper looper;\n    protected long startTime;\n\n    @Before\n    public void setUp() throws Exception {\n        super.setUp(false);\n\n        Robolectric.getForegroundThreadScheduler().advanceTo(1);\n        startTime = System.currentTimeMillis();\n\n        amplitude.setEventUploadPeriodMillis(10*60*1000);\n        amplitude.initialize(context, apiKey, null, null, true);\n\n        looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runOneTask();\n        dbInstance = DatabaseHelper.getDatabaseHelper(context);\n    }\n\n    @After\n    public void tearDown() throws Exception {\n        super.tearDown();\n\n        DatabaseHelper.instances.put(Constants.DEFAULT_INSTANCE, null);\n        dbInstance = null;\n    }\n\n    @Test\n    public void testRecoverFromDatabaseReset() {\n\n        // log an event normally, verify metadata updated in table\n        amplitude.logEvent(\"test\");\n        looper.runToEndOfTasks();\n\n        // metadata: deviceId, sessionId, sequence number, last_event_id, last_event_time, previous_session_id\n        assertEquals(dbInstance.getEventCount(), 1);\n        assertEquals(dbInstance.getTotalEventCount(), 1);\n        assertEquals(dbInstance.getNthEventId(1), 1);\n\n        String deviceId = dbInstance.getValue(AmplitudeClient.DEVICE_ID_KEY);\n        long previousSessionId = dbInstance.getLongValue(AmplitudeClient.PREVIOUS_SESSION_ID_KEY);\n        long sequenceNumber = dbInstance.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY);\n        long lastEventId = dbInstance.getLongValue(AmplitudeClient.LAST_EVENT_ID_KEY);\n        long lastEventTime = dbInstance.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY);\n        Long lastIdentifyId = dbInstance.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY);\n\n        assertNotNull(deviceId);\n        assertTrue(deviceId.endsWith(\"R\"));\n        assertTrue(previousSessionId > 0);\n        assertEquals(sequenceNumber, 1);\n        assertEquals(lastEventId, 1);\n        assertTrue(lastEventTime >= startTime);\n        assertNull(lastIdentifyId);\n\n        // difficult to mock out the SQLiteDatabase object inside DatabaseHelper since it's private\n        // add helper method specifically for mocking / testing\n        DatabaseHelper mockDbHelper = PowerMockito.spy(dbInstance);\n        PowerMockito.doThrow(new SQLiteException(\"test\")).when(mockDbHelper).insertEventContentValuesIntoTable(Matchers.any(SQLiteDatabase.class), anyString(), Matchers.any(ContentValues.class));\n        amplitude.dbHelper = mockDbHelper;\n\n        // log an event to trigger SQLException that we set up with mocks\n        amplitude.logEvent(\"test\");\n        looper.runToEndOfTasks();\n\n        // verify that the metadata has been persisted back into database\n        String newDeviceId = dbInstance.getValue(AmplitudeClient.DEVICE_ID_KEY);\n        long newPreviousSessionId = dbInstance.getLongValue(AmplitudeClient.PREVIOUS_SESSION_ID_KEY);\n        long newLastEventTime = dbInstance.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY);\n\n        // these do not get persisted and should be reset\n        Long newSequenceNumber = dbInstance.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY);\n        Long newLastEventId = dbInstance.getLongValue(AmplitudeClient.LAST_EVENT_ID_KEY);\n        Long newLastIdentifyId = dbInstance.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY);\n\n        assertEquals(newDeviceId, deviceId);\n        assertEquals(newPreviousSessionId, previousSessionId);\n        assertTrue(newLastEventTime >= lastEventTime);\n\n        assertNull(newSequenceNumber);\n        assertEquals(newLastEventId, Long.valueOf(-1));  // insert event fails, and returns -1\n        assertNull(newLastIdentifyId);\n    }\n\n    @Test\n    public void testDatabaseResetAvoidStackOverflow() {\n\n        // log an event normally, verify metadata updated in table\n        amplitude.logEvent(\"test\");\n        looper.runToEndOfTasks();\n\n        // metadata: deviceId, sessionId, sequence number, last_event_id, last_event_time, previous_session_id\n        assertEquals(dbInstance.getEventCount(), 1);\n        assertEquals(dbInstance.getTotalEventCount(), 1);\n        assertEquals(dbInstance.getNthEventId(1), 1);\n\n        String deviceId = dbInstance.getValue(AmplitudeClient.DEVICE_ID_KEY);\n        long previousSessionId = dbInstance.getLongValue(AmplitudeClient.PREVIOUS_SESSION_ID_KEY);\n        long sequenceNumber = dbInstance.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY);\n        long lastEventId = dbInstance.getLongValue(AmplitudeClient.LAST_EVENT_ID_KEY);\n        long lastEventTime = dbInstance.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY);\n        Long lastIdentifyId = dbInstance.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY);\n\n        assertNotNull(deviceId);\n        assertTrue(deviceId.endsWith(\"R\"));\n        assertTrue(previousSessionId > 0);\n        assertEquals(sequenceNumber, 1);\n        assertEquals(lastEventId, 1);\n        assertTrue(lastEventTime >= startTime);\n        assertNull(lastIdentifyId);\n\n        // difficult to mock out the SQLiteDatabase object inside DatabaseHelper since it's private\n        // add helper method specifically for mocking / testing\n        DatabaseHelper mockDbHelper = PowerMockito.spy(dbInstance);\n        PowerMockito.doThrow(new SQLiteException(\"test\")).when(mockDbHelper).insertKeyValueContentValuesIntoTable(Matchers.any(SQLiteDatabase.class), anyString(), Matchers.any(ContentValues.class));\n        amplitude.dbHelper = mockDbHelper;\n\n        // log an event to trigger SQLException that we set up with mocks\n        amplitude.logEvent(\"test\");\n        looper.runToEndOfTasks();\n\n        // the reset callback handler will keep retriggering the exception, so make sure we guard against stack overflows\n        // verify that the metadata has not been persisted back to database\n        String newDeviceId = dbInstance.getValue(AmplitudeClient.DEVICE_ID_KEY);\n        Long newPreviousSessionId = dbInstance.getLongValue(AmplitudeClient.PREVIOUS_SESSION_ID_KEY);\n        Long newLastEventTime = dbInstance.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY);\n        Long newSequenceNumber = dbInstance.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY);\n        Long newLastEventId = dbInstance.getLongValue(AmplitudeClient.LAST_EVENT_ID_KEY);\n        Long newLastIdentifyId = dbInstance.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY);\n\n        assertNull(newDeviceId);\n        assertNull(newPreviousSessionId);\n        assertNull(newLastEventTime);\n        assertNull(newSequenceNumber);\n        assertNull(newLastEventId);  // insert event fails, and returns -1\n        assertNull(newLastIdentifyId);\n    }\n\n    @Test\n    public void testCorruptingDatabaseFile() throws IOException, JSONException {\n\n        // log an event normally, verify metadata updated in table\n        amplitude.logEvent(\"test\");\n        looper.runToEndOfTasks();\n\n        // metadata: deviceId, sessionId, sequence number, last_event_id, last_event_time, previous_session_id\n        assertEquals(dbInstance.getEventCount(), 1);\n        assertEquals(dbInstance.getTotalEventCount(), 1);\n        assertEquals(dbInstance.getNthEventId(1), 1);\n\n        String deviceId = dbInstance.getValue(AmplitudeClient.DEVICE_ID_KEY);\n        long previousSessionId = dbInstance.getLongValue(AmplitudeClient.PREVIOUS_SESSION_ID_KEY);\n        long sequenceNumber = dbInstance.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY);\n        long lastEventId = dbInstance.getLongValue(AmplitudeClient.LAST_EVENT_ID_KEY);\n        long lastEventTime = dbInstance.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY);\n        Long lastIdentifyId = dbInstance.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY);\n\n        assertNotNull(deviceId);\n        assertTrue(deviceId.endsWith(\"R\"));\n        assertTrue(previousSessionId > 0);\n        assertEquals(sequenceNumber, 1);\n        assertEquals(lastEventId, 1);\n        assertTrue(lastEventTime >= startTime);\n        assertNull(lastIdentifyId);\n\n        // try to corrupt database file and then log another event\n        File file = dbInstance.file;\n        RandomAccessFile writer = new RandomAccessFile(file.getAbsolutePath(), \"rw\");\n        writer.seek(2);\n        writer.writeChars(\"corrupt database file with random string\");\n        writer.close();\n\n        amplitude.logEvent(\"test\");\n        looper.runToEndOfTasks();\n\n        // since events table recreated, the event id should have been reset back to 1\n        List<JSONObject> events = dbInstance.getEvents(5, 5);\n        assertEquals(events.size(), 1);\n        assertEquals(events.get(0).optInt(\"event_id\"), 1);\n\n        // verify metadata is re-inserted into database\n        String newDeviceId = dbInstance.getValue(AmplitudeClient.DEVICE_ID_KEY);\n        long newPreviousSessionId = dbInstance.getLongValue(AmplitudeClient.PREVIOUS_SESSION_ID_KEY);\n        long newLastEventTime = dbInstance.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY);\n        long newSequenceNumber = dbInstance.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY);\n        long newLastEventId = dbInstance.getLongValue(AmplitudeClient.LAST_EVENT_ID_KEY);\n        Long newLastIdentifyId = dbInstance.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY);\n\n        assertEquals(newDeviceId, deviceId);\n        assertEquals(newPreviousSessionId, previousSessionId);\n        assertTrue(newLastEventTime >= lastEventTime);\n        assertEquals(newSequenceNumber, 2);\n        assertEquals(newLastEventId, 1);\n        assertNull(newLastIdentifyId);\n    }\n\n    @Test\n    public void testDeletedDatabaseFile() throws IOException, JSONException {\n\n        // log an event normally, verify metadata updated in table\n        amplitude.logEvent(\"test\");\n        looper.runToEndOfTasks();\n\n        // metadata: deviceId, sessionId, sequence number, last_event_id, last_event_time, previous_session_id\n        assertEquals(dbInstance.getEventCount(), 1);\n        assertEquals(dbInstance.getTotalEventCount(), 1);\n        assertEquals(dbInstance.getNthEventId(1), 1);\n\n        String deviceId = dbInstance.getValue(AmplitudeClient.DEVICE_ID_KEY);\n        long previousSessionId = dbInstance.getLongValue(AmplitudeClient.PREVIOUS_SESSION_ID_KEY);\n        long sequenceNumber = dbInstance.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY);\n        long lastEventId = dbInstance.getLongValue(AmplitudeClient.LAST_EVENT_ID_KEY);\n        long lastEventTime = dbInstance.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY);\n        Long lastIdentifyId = dbInstance.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY);\n\n        assertNotNull(deviceId);\n        assertTrue(deviceId.endsWith(\"R\"));\n        assertTrue(previousSessionId > 0);\n        assertEquals(sequenceNumber, 1);\n        assertEquals(lastEventId, 1);\n        assertTrue(lastEventTime >= startTime);\n        assertNull(lastIdentifyId);\n\n        // try to delete database file and test logging event\n        File file = dbInstance.file;\n        context.deleteDatabase(file.getName());\n        amplitude.logEvent(\"test\");\n        looper.runToEndOfTasks();\n\n        // since events table recreated, the event id should have been reset back to 1\n        List<JSONObject> events = dbInstance.getEvents(5, 5);\n        assertEquals(events.size(), 1);\n        assertEquals(events.get(0).optInt(\"event_id\"), 1);\n\n        // verify metadata is re-inserted into database\n        String newDeviceId = dbInstance.getValue(AmplitudeClient.DEVICE_ID_KEY);\n        long newPreviousSessionId = dbInstance.getLongValue(AmplitudeClient.PREVIOUS_SESSION_ID_KEY);\n        long newLastEventTime = dbInstance.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY);\n        long newSequenceNumber = dbInstance.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY);\n        long newLastEventId = dbInstance.getLongValue(AmplitudeClient.LAST_EVENT_ID_KEY);\n        Long newLastIdentifyId = dbInstance.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY);\n\n        assertEquals(newDeviceId, deviceId);\n        assertEquals(newPreviousSessionId, previousSessionId);\n        assertTrue(newLastEventTime >= lastEventTime);\n        assertEquals(newSequenceNumber, 2);\n        assertEquals(newLastEventId, 1);\n        assertNull(newLastIdentifyId);\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/amplitude/api/DeviceInfoTest.java",
    "content": "package com.amplitude.api;\n\nimport android.content.ContentResolver;\nimport android.content.Context;\nimport android.content.res.Configuration;\nimport android.content.res.Resources;\nimport android.location.Location;\nimport android.os.Build;\nimport android.os.LocaleList;\nimport android.provider.Settings.Secure;\nimport android.telephony.TelephonyManager;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport com.google.android.gms.ads.identifier.AdvertisingIdClient;\nimport com.google.android.gms.common.ConnectionResult;\nimport com.google.android.gms.common.GooglePlayServicesUtil;\nimport com.google.android.gms.tasks.Task;\nimport com.google.android.gms.tasks.Tasks;\n\nimport org.json.JSONObject;\nimport org.junit.After;\nimport org.junit.Assert;\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mock;\nimport org.mockito.Mockito;\nimport org.mockito.internal.stubbing.answers.CallsRealMethods;\nimport org.powermock.api.mockito.PowerMockito;\nimport org.powermock.core.classloader.annotations.PowerMockIgnore;\nimport org.powermock.core.classloader.annotations.PrepareForTest;\nimport org.powermock.modules.junit4.PowerMockRunner;\nimport org.powermock.modules.junit4.rule.PowerMockRule;\nimport org.robolectric.Robolectric;\nimport org.robolectric.Shadows;\nimport org.robolectric.annotation.Config;\nimport org.robolectric.shadows.ShadowLooper;\nimport org.robolectric.shadows.ShadowTelephonyManager;\nimport org.robolectric.util.ReflectionHelpers;\n\nimport java.util.Locale;\nimport java.util.UUID;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertNotEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertNull;\nimport static org.junit.Assert.assertTrue;\nimport static org.junit.Assert.fail;\n\n\n@RunWith(AndroidJUnit4.class)\n@PowerMockIgnore({ \"org.mockito.*\", \"org.robolectric.*\", \"android.*\", \"androidx.*\", \"javax.net.ssl.*\", \"jdk.internal.reflect.*\" })\n@PrepareForTest({AdvertisingIdClient.class, GooglePlayServicesUtil.class})\n@Config(manifest = Config.NONE)\npublic class DeviceInfoTest extends BaseTest {\n    private DeviceInfo deviceInfo;\n    private static final String TEST_VERSION_NAME = \"test_version\";\n    private static final String TEST_BRAND = \"brand\";\n    private static final String TEST_MANUFACTURER = \"manufacturer\";\n    private static final String TEST_MODEL = \"model\";\n    private static final String TEST_CARRIER = \"carrier\";\n    private static final Locale TEST_LOCALE = Locale.FRANCE;\n    private static final String TEST_COUNTRY = \"FR\";\n    private static final String TEST_LANGUAGE = \"fr\";\n    private static final String TEST_NETWORK_COUNTRY = \"GB\";\n    private static final double TEST_LOCATION_LAT = 37.7749295;\n    private static final double TEST_LOCATION_LNG = -122.4194155;\n    private static final String TEST_GEO_COUNTRY = \"US\";\n\n    private static Location makeLocation(String provider, double lat, double lng) {\n        Location l = new Location(provider);\n        l.setLatitude(lat);\n        l.setLongitude(lng);\n        l.setTime(System.currentTimeMillis());\n        return l;\n    }\n\n    @Rule\n    public PowerMockRule rule = new PowerMockRule();\n\n    @Before\n    public void setUp() throws Exception {\n        super.setUp(false);\n\n        ReflectionHelpers.setStaticField(Build.class, \"BRAND\", TEST_BRAND);\n        ReflectionHelpers.setStaticField(Build.class, \"MANUFACTURER\", TEST_MANUFACTURER);\n        ReflectionHelpers.setStaticField(Build.class, \"MODEL\", TEST_MODEL);\n\n        Configuration c = context.getResources().getConfiguration();\n        Resources.getSystem().getConfiguration().setLocales(LocaleList.forLanguageTags(TEST_LOCALE.toLanguageTag()));\n\n        ShadowTelephonyManager manager = Shadows.shadowOf((TelephonyManager) context\n                .getSystemService(Context.TELEPHONY_SERVICE));\n        manager.setNetworkOperatorName(TEST_CARRIER);\n        deviceInfo = new DeviceInfo(context, true, false);\n    }\n\n    @After\n    public void tearDown() throws Exception {\n        super.tearDown();\n    }\n\n    @Test\n    public void testGetVersionName() {\n        assertEquals(TEST_VERSION_NAME, deviceInfo.getVersionName());\n    }\n\n    @Test\n    public void testGetBrand() {\n        assertEquals(TEST_BRAND, deviceInfo.getBrand());\n    }\n\n    @Test\n    public void testGetManufacturer() {\n        assertEquals(TEST_MANUFACTURER, deviceInfo.getManufacturer());\n    }\n\n    @Test\n    public void testGetModel() {\n        assertEquals(TEST_MODEL, deviceInfo.getModel());\n    }\n\n    @Test\n    public void testGetCarrier() {\n        assertEquals(TEST_CARRIER, deviceInfo.getCarrier());\n    }\n\n    @Test\n    public void testGetCountry() {\n        assertEquals(TEST_COUNTRY, deviceInfo.getCountry());\n    }\n\n    @Test\n    public void testGetCountryFromNetwork() {\n        ShadowTelephonyManager manager = Shadows.shadowOf((TelephonyManager) context\n                .getSystemService(Context.TELEPHONY_SERVICE));\n        manager.setNetworkCountryIso(TEST_NETWORK_COUNTRY);\n\n        DeviceInfo deviceInfo = new DeviceInfo(context, true, false);\n        assertEquals(TEST_NETWORK_COUNTRY, deviceInfo.getCountry());\n    }\n\n    // TODO: Consider move this test to android specific tests.\n//    @Test\n//    @Config(shadows = {MockGeocoder.class})\n//    public void testGetCountryFromLocation() {\n//        ShadowTelephonyManager telephonyManager = Shadows.shadowOf((TelephonyManager) context\n//                .getSystemService(Context.TELEPHONY_SERVICE));\n//        telephonyManager.setNetworkCountryIso(TEST_NETWORK_COUNTRY);\n//        ShadowLocationManager locationManager = Shadows.shadowOf((LocationManager) context\n//                .getSystemService(Context.LOCATION_SERVICE));\n//        locationManager.simulateLocation(makeLocation(LocationManager.NETWORK_PROVIDER,\n//                TEST_LOCATION_LAT, TEST_LOCATION_LNG));\n//        locationManager.setProviderEnabled(LocationManager.NETWORK_PROVIDER, true);\n//\n//        DeviceInfo deviceInfo = new DeviceInfo(context) {\n//            @Override\n//            protected Geocoder getGeocoder() {\n//                Geocoder geocoder = new Geocoder(context, Locale.ENGLISH);\n//                ShadowGeocoder shadowGeocoder = Shadow.extract(geocoder);\n//                shadowGeocoder.setSimulatedResponse(\"1 Dr Carlton B Goodlett Pl\", \"San Francisco\",\n//                        \"CA\", \"94506\", TEST_GEO_COUNTRY);\n//                return geocoder;\n//            }\n//        };\n//\n//        assertEquals(TEST_GEO_COUNTRY, deviceInfo.getCountry());\n//    }\n\n    @Test\n    public void testGetLanguage() {\n        assertEquals(TEST_LANGUAGE, deviceInfo.getLanguage());\n    }\n\n    @Test\n    public void testGetAdvertisingIdFromGoogleDevice() {\n        PowerMockito.mockStatic(AdvertisingIdClient.class);\n        String advertisingId = \"advertisingId\";\n        AdvertisingIdClient.Info info = new AdvertisingIdClient.Info(\n                advertisingId,\n                false\n        );\n\n        try {\n            Mockito.when(AdvertisingIdClient.getAdvertisingIdInfo(context)).thenReturn(info);\n        } catch (Exception e) {\n            fail(e.toString());\n        }\n        DeviceInfo deviceInfo = new DeviceInfo(context, true, true);\n\n        // still get advertisingId even if limit ad tracking disabled\n        assertEquals(advertisingId, deviceInfo.getAdvertisingId());\n        assertFalse(deviceInfo.isLimitAdTrackingEnabled());\n    }\n\n    @Test\n    public void testGetAdvertisingIdFromGoogleDeviceDisabledTrackAdid() {\n        PowerMockito.mockStatic(AdvertisingIdClient.class);\n\n        DeviceInfo deviceInfo = new DeviceInfo(context, true, false);\n\n        assertNull(deviceInfo.getAdvertisingId());\n        assertFalse(deviceInfo.isLimitAdTrackingEnabled());\n\n        PowerMockito.verifyStatic(Mockito.never());\n    }\n\n    @Test\n    public void testGetAdvertisingIdFromAmazonDevice() {\n        ReflectionHelpers.setStaticField(Build.class, \"MANUFACTURER\", \"Amazon\");\n\n        String advertisingId = \"advertisingId\";\n        ContentResolver cr = context.getContentResolver();\n\n        Secure.putInt(cr, \"limit_ad_tracking\", 1);\n        Secure.putString(cr, \"advertising_id\", advertisingId);\n\n        DeviceInfo deviceInfo = new DeviceInfo(context, true, true);\n\n        // still get advertisingID even if limit ad tracking enabled\n        assertEquals(advertisingId, deviceInfo.getAdvertisingId());\n        assertTrue(deviceInfo.isLimitAdTrackingEnabled());\n    }\n\n    @Test\n    public void testGetAdvertisingIdFromAmazonDeviceDisabledTrackAdid() {\n        ReflectionHelpers.setStaticField(Build.class, \"MANUFACTURER\", \"Amazon\");\n\n        String advertisingId = \"advertisingId\";\n        ContentResolver cr = context.getContentResolver();\n\n        Secure.putInt(cr, \"limit_ad_tracking\", 1);\n        Secure.putString(cr, \"advertising_id\", advertisingId);\n\n        DeviceInfo deviceInfo = new DeviceInfo(context, true, false);\n\n        assertNull(deviceInfo.getAdvertisingId());\n        assertFalse(deviceInfo.isLimitAdTrackingEnabled());\n    }\n\n    @Test\n    public void testGPSDisabled() {\n        // GPS not enabled\n        DeviceInfo deviceInfo = new DeviceInfo(context, true, false);\n        assertFalse(deviceInfo.isGooglePlayServicesEnabled());\n\n        // GPS bundled but not enabled, GooglePlayUtils.isAvailable returns non-0 value\n        PowerMockito.mockStatic(GooglePlayServicesUtil.class);\n        try {\n            Mockito.when(GooglePlayServicesUtil.isGooglePlayServicesAvailable(context))\n                    .thenReturn(1);\n        } catch (Exception e) {\n            fail(e.toString());\n        }\n        assertFalse(deviceInfo.isGooglePlayServicesEnabled());\n    }\n\n    @Test\n    public void testGPSEnabled() {\n        PowerMockito.mockStatic(GooglePlayServicesUtil.class);\n        try {\n            Mockito.when(GooglePlayServicesUtil.isGooglePlayServicesAvailable(context))\n                    .thenReturn(ConnectionResult.SUCCESS);\n        } catch (Exception e) {\n            fail(e.toString());\n        }\n        assertTrue(deviceInfo.isGooglePlayServicesEnabled());\n    }\n//    TODO: Consider move this test to android specific tests.\n//    @Test\n//    public void testGetMostRecentLocation() {\n//        DeviceInfo deviceInfo = new DeviceInfo(context);\n//        ShadowLocationManager locationManager = Shadows.shadowOf((LocationManager) context\n//                .getSystemService(Context.LOCATION_SERVICE));\n//        Location loc = makeLocation(LocationManager.NETWORK_PROVIDER, TEST_LOCATION_LAT,\n//                TEST_LOCATION_LNG);\n//        locationManager.simulateLocation(loc);\n//        locationManager.setProviderEnabled(LocationManager.NETWORK_PROVIDER, true);\n//        assertEquals(loc, deviceInfo.getMostRecentLocation());\n//    }\n\n    @Test\n    public void testNoLocation() {\n        DeviceInfo deviceInfo = new DeviceInfo(context, true, false);\n        Location recent = deviceInfo.getMostRecentLocation();\n        assertNull(recent);\n    }\n\n    @Test\n    public void testUseAdvertisingIdAsDeviceId() {\n        PowerMockito.mockStatic(AdvertisingIdClient.class);\n        String advertisingId = \"advertisingId\";\n        AdvertisingIdClient.Info info = new AdvertisingIdClient.Info(\n            advertisingId,\n            false\n        );\n\n        try {\n            Mockito.when(AdvertisingIdClient.getAdvertisingIdInfo(context)).thenReturn(info);\n        } catch (Exception e) {\n            fail(e.toString());\n        }\n\n        Robolectric.getForegroundThreadScheduler().advanceTo(1);\n\n        AmplitudeClient client = Amplitude.getInstance(\"ADID\");\n        client.useAdvertisingIdForDeviceId();\n        client.initialize(context, \"1cc2c1978ebab0f6451112a8f5df4f4e\");\n        ShadowLooper looper = Shadows.shadowOf(client.logThread.getLooper());\n        looper.runToEndOfTasks();\n\n        assertEquals(advertisingId, client.getDeviceId());\n    }\n\n    @Test\n    public void testDontUseAdvertisingIdAsDeviceId() {\n        PowerMockito.mockStatic(AdvertisingIdClient.class);\n        String advertisingId = \"advertisingId\";\n        AdvertisingIdClient.Info info = new AdvertisingIdClient.Info(\n            advertisingId,\n            true\n        );\n\n        try {\n            Mockito.when(AdvertisingIdClient.getAdvertisingIdInfo(context)).thenReturn(info);\n        } catch (Exception e) {\n            fail(e.toString());\n        }\n\n        Robolectric.getForegroundThreadScheduler().advanceTo(1);\n\n        AmplitudeClient client = Amplitude.getInstance(\"NoADID\");\n        client.useAdvertisingIdForDeviceId();\n        client.initialize(context, \"1cc2c1978ebab0f6451112a8f5df4f4e\");\n        ShadowLooper looper = Shadows.shadowOf(client.logThread.getLooper());\n        looper.runToEndOfTasks();\n\n        assertNotEquals(advertisingId, client.getDeviceId());\n        assertTrue(client.getDeviceId().endsWith(\"R\"));\n    }\n\n    @Test\n    public void testDeviceIdEqualsToAppSetId() {\n        //Set Advertising ID to be invalid to fallback to app set id\n        PowerMockito.mockStatic(AdvertisingIdClient.class);\n        String advertisingId = \"00000000-0000-0000-0000-000000000000\";\n        AdvertisingIdClient.Info info = new AdvertisingIdClient.Info(\n                advertisingId,\n                true\n        );\n        try {\n            Mockito.when(AdvertisingIdClient.getAdvertisingIdInfo(context)).thenReturn(info);\n        } catch (Exception e) {\n            fail(e.toString());\n        }\n\n        String mockAppSetId = \"5a8f0fd1-31a9-4a1f-bfad-cd5439ce533b\";\n        DeviceInfoAmplitudeClient client = Mockito.spy(new DeviceInfoAmplitudeClient(\"AppSetId\"));\n        DeviceInfo mockDeviceInfo = Mockito.mock(DeviceInfo.class, Mockito.CALLS_REAL_METHODS);\n        try {\n            Mockito.when(mockDeviceInfo.getAppSetId()).thenReturn(mockAppSetId);\n            Mockito.when(client.publicInitializeDeviceInfo()).thenReturn(mockDeviceInfo);\n        } catch (Exception e) {\n            Assert.fail(e.toString());\n        }\n\n        client.useAdvertisingIdForDeviceId();\n        client.useAppSetIdForDeviceId();\n\n        final String[] deviceIdCallbackResult = new String[1];\n        client.setDeviceIdCallback(deviceId -> deviceIdCallbackResult[0] = deviceId);\n\n        client.initialize(context, \"1cc2c1978ebab0f6451112a8f5df4f4e\");\n        ShadowLooper looper = Shadows.shadowOf(client.logThread.getLooper());\n        looper.runToEndOfTasks();\n\n        assertEquals(mockAppSetId + \"S\", client.getDeviceId());\n        assertEquals(mockAppSetId + \"S\", deviceIdCallbackResult[0]);\n    }\n\n    @Test\n    public void testToggleAppSetIdInEvents() {\n        String mockAppSetId = \"5a8f0fd1-31a9-4a1f-bfad-cd5439ce533b\";\n        amplitude = new DeviceInfoAmplitudeClient(\"\");\n        DeviceInfoAmplitudeClient client = Mockito.spy((DeviceInfoAmplitudeClient) amplitude);\n        DeviceInfo mockDeviceInfo = Mockito.mock(DeviceInfo.class, Mockito.CALLS_REAL_METHODS);\n        try {\n            Mockito.when(mockDeviceInfo.getAppSetId()).thenReturn(mockAppSetId);\n            Mockito.when(client.publicInitializeDeviceInfo()).thenReturn(mockDeviceInfo);\n        } catch (Exception e) {\n            Assert.fail(e.toString());\n        }\n\n        ShadowLooper looper = Shadows.shadowOf(client.logThread.getLooper());\n\n        client.useAppSetIdForDeviceId();\n        client.initialize(context, apiKey);\n        looper.runToEndOfTasks();\n        assertEquals(mockAppSetId + \"S\", client.getDeviceId());\n\n        client.logEvent(\"testSendAppSetIdInJson\");\n        looper.runToEndOfTasks();\n\n        JSONObject event = getLastEvent();\n        assertNotNull(event);\n        try {\n            assertEquals(\"testSendAppSetIdInJson\", event.getString(\"event_type\"));\n            JSONObject apiProps = event.getJSONObject(\"api_properties\");\n            String appSetId = apiProps.getString(\"android_app_set_id\");\n            assertEquals(mockAppSetId, appSetId);\n        } catch (Exception e) {\n            Assert.fail(e.toString());\n        }\n\n        TrackingOptions options = new TrackingOptions();\n        options.disableAppSetId();\n        client.setTrackingOptions(options);\n        client.logEvent(\"testSendAppSetIdInJson-2\");\n        looper.runToEndOfTasks();\n\n        event = getLastEvent();\n        assertNotNull(event);\n        try {\n            assertEquals(\"testSendAppSetIdInJson-2\", event.getString(\"event_type\"));\n            JSONObject apiProps = event.getJSONObject(\"api_properties\");\n            assertFalse(apiProps.has(\"android_app_set_id\"));\n        } catch (Exception e) {\n            Assert.fail(e.toString());\n        }\n    }\n\n    private class DeviceInfoAmplitudeClient extends AmplitudeClient {\n        protected DeviceInfo initializeDeviceInfo() {\n            return this.publicInitializeDeviceInfo();\n        }\n        public DeviceInfo publicInitializeDeviceInfo() {\n            return new DeviceInfo(context, true, false);\n        }\n        public DeviceInfoAmplitudeClient(String instance) {\n            super(instance);\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/com/amplitude/api/IdentifyTest.java",
    "content": "package com.amplitude.api;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.json.JSONArray;\nimport org.json.JSONException;\nimport org.json.JSONObject;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.annotation.Config;\n\nimport static org.junit.Assert.assertTrue;\n\n@RunWith(AndroidJUnit4.class)\n@Config(manifest = Config.NONE)\npublic class IdentifyTest extends BaseTest {\n\n    @Before\n    public void setUp() throws Exception { setUp(false); }\n\n    @After\n    public void tearDown() throws Exception {}\n\n    @Test\n    public void testUnsetProperty() throws JSONException {\n        String property1 = \"testProperty1\";\n        String property2 = \"testProperty2\";\n        Identify identify = new Identify().unset(property1).unset(property2).unset(property1);\n\n        JSONObject expected = new JSONObject();\n        JSONObject expectedOperations = new JSONObject().put(property1, \"-\").put(property2, \"-\");\n        expected.put(Constants.AMP_OP_UNSET, expectedOperations);\n        assertTrue(Utils.compareJSONObjects(expected, identify.userPropertiesOperations));\n    }\n\n    @Test\n    public void testSetProperty() throws JSONException {\n        String property1 = \"string value\";\n        String value1 = \"testValue\";\n\n        String property2 = \"double value\";\n        double value2 = 0.123;\n\n        String property3 = \"boolean value\";\n        boolean value3 = true;\n\n        String property4 = \"json value\";\n        JSONObject value4 = new JSONObject();\n\n        String property5 = \"boolean array\";\n        boolean[] value5 = new boolean[]{true, true, false};\n        JSONArray value5Expected = new JSONArray();\n        for (boolean value : value5) value5Expected.put(value);\n\n        Identify identify = new Identify().set(property1, value1).set(property2, value2);\n        identify.set(property3, value3).set(property4, value4).set(property5, value5);\n\n        // identify should ignore this since duplicate key\n        identify.set(property1, value3);\n\n        JSONObject expected = new JSONObject();\n        JSONObject expectedOperations = new JSONObject().put(property1, value1);\n        expectedOperations.put(property2, value2).put(property3, value3).put(property4, value4);\n        expectedOperations.put(property5, value5Expected);\n        expected.put(Constants.AMP_OP_SET, expectedOperations);\n        assertTrue(Utils.compareJSONObjects(expected, identify.userPropertiesOperations));\n    }\n\n    @Test\n    public void testSetOnceProperty() throws JSONException {\n        String property1 = \"string value\";\n        String value1 = \"testValue\";\n\n        String property2 = \"double value\";\n        double value2 = 0.123;\n\n        String property3 = \"boolean value\";\n        boolean value3 = true;\n\n        String property4 = \"json value\";\n        JSONObject value4 = new JSONObject();\n\n        String property5 = \"double array\";\n        double[] value5 = new double[]{1.2, 2.3, 3.4};\n        JSONArray value5Expected = new JSONArray();\n        for (double value : value5) value5Expected.put(value);\n\n        Identify identify = new Identify().setOnce(property1, value1).setOnce(property2, value2);\n        identify.setOnce(property3, value3).setOnce(property4, value4).setOnce(property5, value5);\n\n        // identify should ignore this since duplicate key\n        identify.setOnce(property1, value3);\n\n        JSONObject expected = new JSONObject();\n        JSONObject expectedOperations = new JSONObject().put(property1, value1);\n        expectedOperations.put(property2, value2).put(property3, value3).put(property4, value4);\n        expectedOperations.put(property5, value5Expected);\n        expected.put(Constants.AMP_OP_SET_ONCE, expectedOperations);\n        assertTrue(Utils.compareJSONObjects(expected, identify.userPropertiesOperations));\n    }\n\n    @Test\n    public void testAddProperty() throws JSONException {\n        String property1 = \"int value\";\n        int value1 = 5;\n\n        String property2 = \"double value\";\n        double value2 = 0.123;\n\n        String property3 = \"float value\";\n        double value3 = 0.625; // floats are actually promoted to long in JSONObject\n\n        String property4 = \"long value\";\n        long value4 = 18l;\n\n        String property5 = \"string value\";\n        String value5 = \"19\";\n\n        Identify identify = new Identify().add(property1, value1).add(property2, value2);\n        identify.add(property3, value3).add(property4, value4).add(property5, value5);\n\n        // identify should ignore this since duplicate key\n        identify.add(property1, value3);\n\n        JSONObject expected = new JSONObject();\n        JSONObject expectedOperations = new JSONObject().put(property1, value1);\n        expectedOperations.put(property2, value2).put(property3, value3).put(property4, value4);\n        expectedOperations.put(property5, value5);\n        expected.put(Constants.AMP_OP_ADD, expectedOperations);\n        assertTrue(Utils.compareJSONObjects(expected, identify.userPropertiesOperations));\n    }\n\n    @Test\n    public void testAppendProperty() throws JSONException {\n        String property1 = \"int value\";\n        int value1 = 5;\n\n        String property2 = \"double value\";\n        double value2 = 0.123;\n\n        String property3 = \"float value\";\n        double value3 = 0.625; // floats are actually promoted to long in JSONObject\n\n        String property4 = \"long value\";\n        long value4 = 18l;\n\n        String property5 = \"array value\";\n        JSONArray value5 = new JSONArray();\n        value5.put(1);\n        value5.put(2);\n        value5.put(3);\n\n        String property6 = \"float array\";\n        float[] value6 = new float[]{(float)1.2, (float)2.3, (float)3.4, (float)4.5};\n        JSONArray value6Expected = new JSONArray();\n        for (float value : value6) value6Expected.put(value);\n\n        String property7 = \"int array\";\n        int[] value7 = new int[]{10, 12, 14, 17};\n        JSONArray value7Expected = new JSONArray();\n        for (int value : value7) value7Expected.put(value);\n\n        String property8 = \"long array\";\n        long[] value8 = new long[]{20, 22, 24, 27};\n        JSONArray value8Expected = new JSONArray();\n        for (long value : value8) value8Expected.put(value);\n\n        String property9 = \"string array\";\n        String[] value9 = new String[]{\"test1\", \"test2\", \"test3\"};\n        JSONArray value9Expected = new JSONArray();\n        for (String value : value9) value9Expected.put(value);\n\n        Identify identify = new Identify().append(property1, value1).append(property2, value2);\n        identify.append(property3, value3).append(property4, value4).append(property5, value5);\n        identify.append(property6, value6).append(property7, value7).append(property8, value8);\n        identify.append(property9, value9);\n\n        // identify should ignore this since duplicate key\n        identify.add(property1, value3);\n\n        JSONObject expected = new JSONObject();\n        JSONObject expectedOperations = new JSONObject().put(property1, value1);\n        expectedOperations.put(property2, value2).put(property3, value3).put(property4, value4);\n        expectedOperations.put(property5, value5).put(property6, value6Expected);\n        expectedOperations.put(property7, value7Expected).put(property8, value8Expected);\n        expectedOperations.put(property9, value9Expected);\n        expected.put(Constants.AMP_OP_APPEND, expectedOperations);\n        assertTrue(Utils.compareJSONObjects(expected, identify.userPropertiesOperations));\n    }\n\n    @Test\n    public void testPrependProperty() throws JSONException {\n        String property1 = \"int value\";\n        int value1 = 5;\n\n        String property2 = \"double value\";\n        double value2 = 0.123;\n\n        String property3 = \"float value\";\n        double value3 = 0.625; // floats are actually promoted to long in JSONObject\n\n        String property4 = \"long value\";\n        long value4 = 18l;\n\n        String property5 = \"array value\";\n        JSONArray value5 = new JSONArray();\n        value5.put(1);\n        value5.put(2);\n        value5.put(3);\n\n        String property6 = \"float array\";\n        float[] value6 = new float[]{(float)1.2, (float)2.3, (float)3.4, (float)4.5};\n        JSONArray value6Expected = new JSONArray();\n        for (float value : value6) value6Expected.put(value);\n\n        String property7 = \"int array\";\n        int[] value7 = new int[]{10, 12, 14, 17};\n        JSONArray value7Expected = new JSONArray();\n        for (int value : value7) value7Expected.put(value);\n\n        String property8 = \"long array\";\n        long[] value8 = new long[]{20, 22, 24, 27};\n        JSONArray value8Expected = new JSONArray();\n        for (long value : value8) value8Expected.put(value);\n\n        String property9 = \"string array\";\n        String[] value9 = new String[]{\"test1\", \"test2\", \"test3\"};\n        JSONArray value9Expected = new JSONArray();\n        for (String value : value9) value9Expected.put(value);\n\n        Identify identify = new Identify().prepend(property1, value1).prepend(property2, value2);\n        identify.prepend(property3, value3).prepend(property4, value4).prepend(property5, value5);\n        identify.prepend(property6, value6).prepend(property7, value7).prepend(property8, value8);\n        identify.prepend(property9, value9);\n\n        // identify should ignore this since duplicate key\n        identify.add(property1, value3);\n\n        JSONObject expected = new JSONObject();\n        JSONObject expectedOperations = new JSONObject().put(property1, value1);\n        expectedOperations.put(property2, value2).put(property3, value3).put(property4, value4);\n        expectedOperations.put(property5, value5).put(property6, value6Expected);\n        expectedOperations.put(property7, value7Expected).put(property8, value8Expected);\n        expectedOperations.put(property9, value9Expected);\n        expected.put(Constants.AMP_OP_PREPEND, expectedOperations);\n        assertTrue(Utils.compareJSONObjects(expected, identify.userPropertiesOperations));\n    }\n\n    @Test\n    public void testPreInsertProperty() throws JSONException {\n        String property1 = \"int value\";\n        int value1 = 5;\n\n        String property2 = \"double value\";\n        double value2 = 0.123;\n\n        String property3 = \"float value\";\n        double value3 = 0.625; // floats are actually promoted to long in JSONObject\n\n        String property4 = \"long value\";\n        long value4 = 18l;\n\n        String property5 = \"array value\";\n        JSONArray value5 = new JSONArray();\n        value5.put(1);\n        value5.put(2);\n        value5.put(3);\n\n        String property6 = \"float array\";\n        float[] value6 = new float[]{(float)1.2, (float)2.3, (float)3.4, (float)4.5};\n        JSONArray value6Expected = new JSONArray();\n        for (float value : value6) value6Expected.put(value);\n\n        String property7 = \"int array\";\n        int[] value7 = new int[]{10, 12, 14, 17};\n        JSONArray value7Expected = new JSONArray();\n        for (int value : value7) value7Expected.put(value);\n\n        String property8 = \"long array\";\n        long[] value8 = new long[]{20, 22, 24, 27};\n        JSONArray value8Expected = new JSONArray();\n        for (long value : value8) value8Expected.put(value);\n\n        String property9 = \"string array\";\n        String[] value9 = new String[]{\"test1\", \"test2\", \"test3\"};\n        JSONArray value9Expected = new JSONArray();\n        for (String value : value9) value9Expected.put(value);\n\n        Identify identify = new Identify().preInsert(property1, value1).preInsert(property2, value2);\n        identify.preInsert(property3, value3).preInsert(property4, value4).preInsert(property5, value5);\n        identify.preInsert(property6, value6).preInsert(property7, value7).preInsert(property8, value8);\n        identify.preInsert(property9, value9);\n\n        // identify should ignore this since duplicate key\n        identify.add(property1, value3);\n\n        JSONObject expected = new JSONObject();\n        JSONObject expectedOperations = new JSONObject().put(property1, value1);\n        expectedOperations.put(property2, value2).put(property3, value3).put(property4, value4);\n        expectedOperations.put(property5, value5).put(property6, value6Expected);\n        expectedOperations.put(property7, value7Expected).put(property8, value8Expected);\n        expectedOperations.put(property9, value9Expected);\n        expected.put(Constants.AMP_OP_PREINSERT, expectedOperations);\n        assertTrue(Utils.compareJSONObjects(expected, identify.userPropertiesOperations));\n    }\n\n    @Test\n    public void testPostInsertProperty() throws JSONException {\n        String property1 = \"int value\";\n        int value1 = 5;\n\n        String property2 = \"double value\";\n        double value2 = 0.123;\n\n        String property3 = \"float value\";\n        double value3 = 0.625; // floats are actually promoted to long in JSONObject\n\n        String property4 = \"long value\";\n        long value4 = 18l;\n\n        String property5 = \"array value\";\n        JSONArray value5 = new JSONArray();\n        value5.put(1);\n        value5.put(2);\n        value5.put(3);\n\n        String property6 = \"float array\";\n        float[] value6 = new float[]{(float)1.2, (float)2.3, (float)3.4, (float)4.5};\n        JSONArray value6Expected = new JSONArray();\n        for (float value : value6) value6Expected.put(value);\n\n        String property7 = \"int array\";\n        int[] value7 = new int[]{10, 12, 14, 17};\n        JSONArray value7Expected = new JSONArray();\n        for (int value : value7) value7Expected.put(value);\n\n        String property8 = \"long array\";\n        long[] value8 = new long[]{20, 22, 24, 27};\n        JSONArray value8Expected = new JSONArray();\n        for (long value : value8) value8Expected.put(value);\n\n        String property9 = \"string array\";\n        String[] value9 = new String[]{\"test1\", \"test2\", \"test3\"};\n        JSONArray value9Expected = new JSONArray();\n        for (String value : value9) value9Expected.put(value);\n\n        Identify identify = new Identify().postInsert(property1, value1).postInsert(property2, value2);\n        identify.postInsert(property3, value3).postInsert(property4, value4).postInsert(property5, value5);\n        identify.postInsert(property6, value6).postInsert(property7, value7).postInsert(property8, value8);\n        identify.postInsert(property9, value9);\n\n        // identify should ignore this since duplicate key\n        identify.add(property1, value3);\n\n        JSONObject expected = new JSONObject();\n        JSONObject expectedOperations = new JSONObject().put(property1, value1);\n        expectedOperations.put(property2, value2).put(property3, value3).put(property4, value4);\n        expectedOperations.put(property5, value5).put(property6, value6Expected);\n        expectedOperations.put(property7, value7Expected).put(property8, value8Expected);\n        expectedOperations.put(property9, value9Expected);\n        expected.put(Constants.AMP_OP_POSTINSERT, expectedOperations);\n        assertTrue(Utils.compareJSONObjects(expected, identify.userPropertiesOperations));\n    }\n\n    @Test\n    public void testRemoveProperty() throws JSONException {\n        String property1 = \"int value\";\n        int value1 = 5;\n\n        String property2 = \"double value\";\n        double value2 = 0.123;\n\n        String property3 = \"float value\";\n        double value3 = 0.625; // floats are actually promoted to long in JSONObject\n\n        String property4 = \"long value\";\n        long value4 = 18l;\n\n        String property5 = \"array value\";\n        JSONArray value5 = new JSONArray();\n        value5.put(1);\n        value5.put(2);\n        value5.put(3);\n\n        String property6 = \"float array\";\n        float[] value6 = new float[]{(float)1.2, (float)2.3, (float)3.4, (float)4.5};\n        JSONArray value6Expected = new JSONArray();\n        for (float value : value6) value6Expected.put(value);\n\n        String property7 = \"int array\";\n        int[] value7 = new int[]{10, 12, 14, 17};\n        JSONArray value7Expected = new JSONArray();\n        for (int value : value7) value7Expected.put(value);\n\n        String property8 = \"long array\";\n        long[] value8 = new long[]{20, 22, 24, 27};\n        JSONArray value8Expected = new JSONArray();\n        for (long value : value8) value8Expected.put(value);\n\n        String property9 = \"string array\";\n        String[] value9 = new String[]{\"test1\", \"test2\", \"test3\"};\n        JSONArray value9Expected = new JSONArray();\n        for (String value : value9) value9Expected.put(value);\n\n        Identify identify = new Identify().remove(property1, value1).remove(property2, value2);\n        identify.remove(property3, value3).remove(property4, value4).remove(property5, value5);\n        identify.remove(property6, value6).remove(property7, value7).remove(property8, value8);\n        identify.remove(property9, value9);\n\n        // identify should ignore this since duplicate key\n        identify.add(property1, value3);\n\n        JSONObject expected = new JSONObject();\n        JSONObject expectedOperations = new JSONObject().put(property1, value1);\n        expectedOperations.put(property2, value2).put(property3, value3).put(property4, value4);\n        expectedOperations.put(property5, value5).put(property6, value6Expected);\n        expectedOperations.put(property7, value7Expected).put(property8, value8Expected);\n        expectedOperations.put(property9, value9Expected);\n        expected.put(Constants.AMP_OP_REMOVE, expectedOperations);\n        assertTrue(Utils.compareJSONObjects(expected, identify.userPropertiesOperations));\n    }\n\n    @Test\n    public void testMultipleOperations() throws JSONException {\n        String property1 = \"string value\";\n        String value1 = \"testValue\";\n\n        String property2 = \"double value\";\n        double value2 = 0.123;\n\n        String property3 = \"boolean value\";\n        boolean value3 = true;\n\n        String property4 = \"json value\";\n\n        String property5 = \"array value\";\n        JSONArray value5 = new JSONArray();\n        value5.put(15);\n        value5.put(25);\n\n        String property6 = \"int value\";\n        int value6 = 100;\n\n        String property7 = \"string value2\";\n        String value7 = \"testValue2\";\n\n        String property8 = \"double value2\";\n        double value8 = 0.123;\n\n        String property9 = \"boolean value2\";\n        boolean value9 = true; \n\n        Identify identify = new Identify().setOnce(property1, value1).add(property2, value2);\n        identify.set(property3, value3).unset(property4).append(property5, value5);\n        identify.prepend(property6, value6).preInsert(property7, value7);\n        identify.postInsert(property8, value8).remove(property9, value9);\n\n        // identify should ignore this since duplicate key\n        identify.set(property4, value3);\n\n        JSONObject expected = new JSONObject();\n        expected.put(Constants.AMP_OP_SET_ONCE, new JSONObject().put(property1, value1));\n        expected.put(Constants.AMP_OP_ADD, new JSONObject().put(property2, value2));\n        expected.put(Constants.AMP_OP_SET, new JSONObject().put(property3, value3));\n        expected.put(Constants.AMP_OP_UNSET, new JSONObject().put(property4, \"-\"));\n        expected.put(Constants.AMP_OP_APPEND, new JSONObject().put(property5, value5));\n        expected.put(Constants.AMP_OP_PREPEND, new JSONObject().put(property6, value6));\n        expected.put(Constants.AMP_OP_PREINSERT, new JSONObject().put(property7, value7));\n        expected.put(Constants.AMP_OP_POSTINSERT, new JSONObject().put(property8, value8));\n        expected.put(Constants.AMP_OP_REMOVE, new JSONObject().put(property9, value9));\n        assertTrue(Utils.compareJSONObjects(expected, identify.userPropertiesOperations));\n    }\n\n    @Test\n    public void testDisallowDuplicateProperties() throws JSONException {\n        String property = \"testProperty\";\n        String value1 = \"testValue\";\n        double value2 = 0.123;\n        boolean value3 = true;\n\n        Identify identify = new Identify().setOnce(property, value1).add(property, value2);\n        identify.set(property, value3).unset(property);\n\n        JSONObject expected = new JSONObject();\n        expected.put(Constants.AMP_OP_SET_ONCE, new JSONObject().put(property, value1));\n        assertTrue(Utils.compareJSONObjects(expected, identify.userPropertiesOperations));\n    }\n\n    @Test\n    public void testDisallowOtherOperationsOnClearAllIdentify() throws JSONException {\n        String property = \"testProperty\";\n        String value1 = \"testValue\";\n        double value2 = 0.123;\n        boolean value3 = true;\n\n        Identify identify = new Identify().clearAll().setOnce(property, value1);\n        identify.add(property, value2).set(property, value3).unset(property);\n\n        JSONObject expected = new JSONObject();\n        expected.put(Constants.AMP_OP_CLEAR_ALL, \"-\");\n        assertTrue(Utils.compareJSONObjects(expected, identify.userPropertiesOperations));\n    }\n\n    @Test\n    public void testDisallowClearAllOnIdentifysWithOtherOperations() throws JSONException {\n        String property = \"testProperty\";\n        String value1 = \"testValue\";\n        double value2 = 0.123;\n        boolean value3 = true;\n\n        Identify identify = new Identify().setOnce(property, value1).add(property, value2);\n        identify.set(property, value3).unset(property).clearAll();\n\n        JSONObject expected = new JSONObject();\n        expected.put(Constants.AMP_OP_SET_ONCE, new JSONObject().put(property, value1));\n        assertTrue(Utils.compareJSONObjects(expected, identify.userPropertiesOperations));\n    }\n\n    @Test\n    public void testGetUserPropertyOperations() throws JSONException {\n        String property1 = \"string value\";\n        String value1 = \"testValue\";\n\n        String property2 = \"double value\";\n        double value2 = 0.123;\n\n        String property3 = \"boolean value\";\n        boolean value3 = true;\n\n        String property4 = \"json value\";\n\n        String property5 = \"array value\";\n        JSONArray value5 = new JSONArray();\n        value5.put(15);\n        value5.put(25);\n\n        String property6 = \"int value\";\n        int value6 = 100;\n\n        String property7 = \"string value2\";\n        String value7 = \"testValue2\";\n\n        String property8 = \"double value2\";\n        double value8 = 0.123;\n\n        String property9 = \"boolean value2\";\n        boolean value9 = true; \n\n        Identify identify = new Identify().setOnce(property1, value1).add(property2, value2);\n        identify.set(property3, value3).unset(property4).append(property5, value5);\n        identify.prepend(property6, value6).preInsert(property7, value7);\n        identify.postInsert(property8, value8).remove(property9, value9);\n\n        // identify should ignore this since duplicate key\n        identify.set(property4, value3);\n\n        JSONObject expected = new JSONObject();\n        expected.put(Constants.AMP_OP_SET_ONCE, new JSONObject().put(property1, value1));\n        expected.put(Constants.AMP_OP_ADD, new JSONObject().put(property2, value2));\n        expected.put(Constants.AMP_OP_SET, new JSONObject().put(property3, value3));\n        expected.put(Constants.AMP_OP_UNSET, new JSONObject().put(property4, \"-\"));\n        expected.put(Constants.AMP_OP_APPEND, new JSONObject().put(property5, value5));\n        expected.put(Constants.AMP_OP_PREPEND, new JSONObject().put(property6, value6));\n        expected.put(Constants.AMP_OP_PREINSERT, new JSONObject().put(property7, value7));\n        expected.put(Constants.AMP_OP_POSTINSERT, new JSONObject().put(property8, value8));\n        expected.put(Constants.AMP_OP_REMOVE, new JSONObject().put(property9, value9));\n        assertTrue(Utils.compareJSONObjects(expected, identify.getUserPropertiesOperations()));\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/amplitude/api/IngestionMetadataTest.java",
    "content": "package com.amplitude.api;\n\nimport static org.junit.Assert.assertEquals;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.annotation.Config;\n\n@RunWith(AndroidJUnit4.class)\n@Config(manifest=Config.NONE)\npublic class IngestionMetadataTest {\n\n    @Test\n    public void testToJSONObject() throws JSONException {\n        IngestionMetadata ingestionMetadata = new IngestionMetadata();\n        String sourceName = \"ampli\";\n        String sourceVersion = \"1.0.0\";\n        ingestionMetadata.setSourceName(sourceName)\n                .setSourceVersion(sourceVersion);\n        JSONObject result = ingestionMetadata.toJSONObject();\n        assertEquals(sourceName, result.getString(Constants.AMP_INGESTION_METADATA_SOURCE_NAME));\n        assertEquals(sourceVersion, result.getString(Constants.AMP_INGESTION_METADATA_SOURCE_VERSION));\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/amplitude/api/InitializeTest.java",
    "content": "package com.amplitude.api;\n\nimport android.content.Context;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.json.JSONArray;\nimport org.json.JSONException;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.Shadows;\nimport org.robolectric.annotation.Config;\nimport org.robolectric.shadows.ShadowLooper;\n\nimport okhttp3.mockwebserver.RecordedRequest;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertNull;\nimport static org.junit.Assert.assertTrue;\n\n@RunWith(AndroidJUnit4.class)\n@Config(manifest = Config.NONE)\npublic class InitializeTest extends BaseTest {\n\n    @Before\n    public void setUp() throws Exception {\n        super.setUp();\n    }\n\n    @After\n    public void tearDown() throws Exception {\n        super.tearDown();\n    }\n\n    @Test\n    public void testInitializeUserId() {\n        // the userId passed to initialize should override any existing values\n\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        dbHelper.insertOrReplaceKeyValue(AmplitudeClient.USER_ID_KEY, \"oldUserId\");\n\n        String userId = \"newUserId\";\n        amplitude.initialize(context, apiKey, userId);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runOneTask();\n\n        // Test that the user id is set.\n        assertEquals(userId, amplitude.userId);\n        assertEquals(userId, dbHelper.getValue(AmplitudeClient.USER_ID_KEY));\n\n        // Test that events are logged.\n        RecordedRequest request = sendEvent(amplitude, \"init_test_event\", null);\n        assertNotNull(request);\n    }\n\n    @Test\n    public void testInitializeUserIdFromDb() {\n        // since user id already exists in database, ignore old value in shared prefs\n        String userId = \"testUserId\";\n\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        dbHelper.insertOrReplaceKeyValue(AmplitudeClient.USER_ID_KEY, userId);\n\n        amplitude.initialize(context, apiKey);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runOneTask();\n\n        // Test that the user id is set.\n        assertEquals(amplitude.userId, userId);\n        assertEquals(userId, dbHelper.getValue(AmplitudeClient.USER_ID_KEY));\n    }\n\n    @Test\n    public void testInitializeOptOut() {\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        assertNull(dbHelper.getLongValue(AmplitudeClient.OPT_OUT_KEY));\n\n        amplitude.initialize(context, apiKey);\n        looper.runOneTask();\n\n        assertFalse(amplitude.isOptedOut());\n\n        amplitude.setOptOut(true);\n        looper.runOneTask();\n\n        assertTrue(amplitude.isOptedOut());\n        assertEquals((long) dbHelper.getLongValue(AmplitudeClient.OPT_OUT_KEY), 1L);\n    }\n\n    @Test\n    public void testInitializeOptOutFromDB() {\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        dbHelper.insertOrReplaceKeyLongValue(AmplitudeClient.OPT_OUT_KEY, 0L);\n\n        amplitude.initialize(context, apiKey);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runOneTask();\n\n        assertFalse(amplitude.isOptedOut());\n        assertEquals((long) dbHelper.getLongValue(AmplitudeClient.OPT_OUT_KEY), 0L);\n    }\n\n\n    @Test\n    public void testInitializeLastEventId() throws JSONException {\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n\n        amplitude.initialize(context, apiKey);\n        amplitude.setLastEventId(3L);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runOneTask();\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runOneTask();\n\n        assertEquals(amplitude.lastEventId, 3L);\n        assertEquals((long) dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_ID_KEY), 3L);\n\n        amplitude.logEvent(\"testEvent\");\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n\n        RecordedRequest request = runRequest(amplitude);\n        JSONArray events = getEventsFromRequest(request);\n\n        assertEquals(events.getJSONObject(0).getLong(\"event_id\"), 1L);\n\n        assertEquals(amplitude.lastEventId, 1L);\n        assertEquals((long) dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_ID_KEY), 1L);\n    }\n\n    @Test\n    public void testInitializePreviousSessionId() {\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n\n        amplitude.initialize(context, apiKey);\n        amplitude.setPreviousSessionId(4000L);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n\n        assertEquals((long) dbHelper.getLongValue(AmplitudeClient.PREVIOUS_SESSION_ID_KEY), 4000L);\n    }\n\n    @Test\n    public void testInitializeLastEventTime() {\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        dbHelper.insertOrReplaceKeyLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY, 5000L);\n\n        amplitude.initialize(context, apiKey);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runOneTask();\n\n        assertEquals(amplitude.lastEventTime, 5000L);\n        assertEquals((long) dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY), 5000L);\n    }\n\n    @Test\n    public void testSkipSharedPrefsToDb() {\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        dbHelper.insertOrReplaceKeyValue(AmplitudeClient.DEVICE_ID_KEY, \"testDeviceId\");\n        dbHelper.insertOrReplaceKeyLongValue(AmplitudeClient.PREVIOUS_SESSION_ID_KEY, 1000L);\n        dbHelper.insertOrReplaceKeyLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY, 2000L);\n\n        assertNull(dbHelper.getValue(AmplitudeClient.USER_ID_KEY));\n        assertNull(dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_ID_KEY));\n        assertNull(dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY));\n        assertNull(dbHelper.getLongValue(AmplitudeClient.OPT_OUT_KEY));\n\n        amplitude.initialize(context, apiKey);\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runOneTask();\n        looper.runToEndOfTasks();\n\n        assertEquals(dbHelper.getValue(AmplitudeClient.DEVICE_ID_KEY), \"testDeviceId\");\n        assertEquals((long) dbHelper.getLongValue(AmplitudeClient.PREVIOUS_SESSION_ID_KEY), 1000L);\n        assertEquals((long) dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY), 2000L);\n        assertNull(dbHelper.getValue(AmplitudeClient.USER_ID_KEY));\n        assertNull(dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_ID_KEY));\n        assertNull(dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY));\n        assertNull(dbHelper.getLongValue(AmplitudeClient.OPT_OUT_KEY));\n\n        // after upgrade, pref values still there since they weren't deleted\n        assertEquals(amplitude.deviceId, \"testDeviceId\");\n        assertEquals(amplitude.previousSessionId, 1000L);\n        assertEquals(amplitude.lastEventTime, 2000L);\n        assertNull(amplitude.userId);\n    }\n\n    @Test\n    public void testInitializePreviousSessionIdLastEventTime() {\n        // set a previous session id & last event time\n        // log an event with timestamp such that same session is continued\n        // log second event with timestamp such that new session is started\n\n        amplitude.setSessionTimeoutMillis(5000); // 5s\n\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        dbHelper.insertOrReplaceKeyLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY, 7000L);\n\n        long [] timestamps = {8000, 14000};\n        clock.setTimestamps(timestamps);\n\n        amplitude.initialize(context, apiKey);\n        amplitude.setPreviousSessionId(6000);\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n\n        assertNull(amplitude.userId);\n\n        // log first event\n        amplitude.logEvent(\"testEvent1\");\n        looper.runToEndOfTasks();\n        assertEquals(amplitude.previousSessionId, 6000L);\n        assertEquals(amplitude.lastEventTime, 8000L);\n\n        // log second event\n        amplitude.logEvent(\"testEvent2\");\n        looper.runToEndOfTasks();\n        assertEquals(amplitude.previousSessionId, 14000L);\n        assertEquals(amplitude.lastEventTime, 14000L);\n    }\n\n    @Test\n    public void testReloadDeviceIdFromDatabase() {\n        String deviceId = \"test_device_id_from_database\";\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        DatabaseHelper.getDatabaseHelper(context).insertOrReplaceKeyValue(\n            AmplitudeClient.DEVICE_ID_KEY, deviceId\n        );\n\n        amplitude.initialize(context, apiKey);\n        looper.runToEndOfTasks();\n        assertEquals(deviceId, amplitude.getDeviceId());\n    }\n\n    @Test\n    public void testInitializeDeviceIdWithRandomUUID() {\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        amplitude.initialize(context, apiKey);\n        looper.runToEndOfTasks();\n\n        String deviceId = amplitude.getDeviceId();\n        assertEquals(37, deviceId.length());\n        assertTrue(deviceId.endsWith(\"R\"));\n        DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);\n        assertEquals(deviceId, dbHelper.getValue(AmplitudeClient.DEVICE_ID_KEY));\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/amplitude/api/MiddlewareRunnerTest.java",
    "content": "package com.amplitude.api;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.annotation.Config;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertTrue;\n\nimport java.util.concurrent.atomic.AtomicInteger;\n\n@RunWith(AndroidJUnit4.class)\n@Config(manifest= Config.NONE)\npublic class MiddlewareRunnerTest {\n    MiddlewareRunner middlewareRunner = new MiddlewareRunner();\n\n    @Test\n    public void testMiddlewareRun() throws JSONException {\n        String middlewareDevice = \"middleware_device\";\n        Middleware updateDeviceIdMiddleware = new Middleware() {\n            @Override\n            public void run(MiddlewarePayload payload, MiddlewareNext next) {\n                try {\n                    payload.event.put(\"device_model\", middlewareDevice);\n                } catch (JSONException e) {\n                    e.printStackTrace();\n                }\n                next.run(payload);\n            }\n        };\n        middlewareRunner.add(updateDeviceIdMiddleware);\n\n        JSONObject event = new JSONObject().put(\"device_model\", \"sample_device\");\n        boolean middlewareCompleted = middlewareRunner.run(new MiddlewarePayload(event, new MiddlewareExtra()));\n\n        assertTrue(middlewareCompleted);\n        assertEquals(event.getString(\"device_model\"), middlewareDevice);\n    }\n\n    @Test\n    public void testRunWithNotPassMiddleware() throws JSONException {\n        // first middleware\n        String middlewareDevice = \"middleware_device\";\n        Middleware updateDeviceIdMiddleware = new Middleware() {\n            @Override\n            public void run(MiddlewarePayload payload, MiddlewareNext next) {\n                try {\n                    payload.event.put(\"device_model\", middlewareDevice);\n                } catch (JSONException e) {\n                    e.printStackTrace();\n                }\n                next.run(payload);\n            }\n        };\n\n        // swallow middleware\n        String middlewareUser = \"middleware_user\";\n        Middleware swallowMiddleware = new Middleware() {\n            @Override\n            public void run(MiddlewarePayload payload, MiddlewareNext next) {\n                try {\n                    payload.event.put(\"user_id\", middlewareUser);\n                } catch (JSONException e) {\n                    e.printStackTrace();\n                }\n            }\n        };\n        middlewareRunner.add(updateDeviceIdMiddleware);\n        middlewareRunner.add(swallowMiddleware);\n\n        JSONObject event = new JSONObject().put(\"device_model\", \"sample_device\").put(\"user_id\", \"sample_user\");\n        boolean middlewareCompleted = middlewareRunner.run(new MiddlewarePayload(event));\n\n        assertFalse(middlewareCompleted);\n        assertEquals(event.getString(\"device_model\"), middlewareDevice);\n        assertEquals(event.getString(\"user_id\"), middlewareUser);\n    }\n\n    @Test\n    public void testMiddlewareFlush() throws JSONException {\n        AtomicInteger runCount = new AtomicInteger(0);\n        AtomicInteger flushCount = new AtomicInteger(0);\n\n        MiddlewareExtended flushMiddleware = new MiddlewareExtended() {\n            @Override\n            public void run(MiddlewarePayload payload, MiddlewareNext next) {\n                runCount.incrementAndGet();\n            }\n\n            @Override\n            public void flush() {\n                flushCount.incrementAndGet();\n            }\n        };\n\n        middlewareRunner.add(flushMiddleware);\n\n        middlewareRunner.flush();\n\n        assertEquals(flushCount.get(), 1);\n        assertEquals(runCount.get(), 0);\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/com/amplitude/api/MockGeocoder.java",
    "content": "package com.amplitude.api;\n\nimport android.location.Geocoder;\n\nimport org.robolectric.annotation.Implementation;\nimport org.robolectric.annotation.Implements;\nimport org.robolectric.shadows.maps.ShadowGeocoder;\n\n// mock for static Geocoder method\n@Implements(Geocoder.class)\npublic class MockGeocoder extends ShadowGeocoder {\n    @Implementation\n    public static boolean isPresent() {\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/amplitude/api/PinningTest.java",
    "content": "package com.amplitude.api;\n\nimport android.os.SystemClock;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Ignore;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.Shadows;\nimport org.robolectric.annotation.Config;\nimport org.robolectric.shadows.ShadowLooper;\n\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertNull;\n\n@RunWith(AndroidJUnit4.class)\n@Config(manifest = Config.NONE)\npublic class PinningTest extends BaseTest {\n\n    @Before\n    public void setUp() throws Exception {\n        super.setUp(false);\n        PinnedAmplitudeClient.instances.clear();\n        // need to set clock > 0 so that logThread posts in order\n        SystemClock.setCurrentTimeMillis(1000);\n    }\n\n    @After\n    public void tearDown() throws Exception {\n        super.tearDown();\n        PinnedAmplitudeClient.instances.clear();\n    }\n\n    @Test\n    @Ignore(\"This stopped working in github and skipping this for now since this SDK is \" +\n            \"under maintenance\")\n    public void testSslPinningUS() {\n        amplitude = PinnedAmplitudeClient.getInstance();\n        amplitude.initialize(context, \"1cc2c1978ebab0f6451112a8f5df4f4e\");\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runOneTask();\n        looper.runOneTask();\n\n        amplitude.logEvent(\"us_pinned_test_event\", null);\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        ShadowLooper httplooper = Shadows.shadowOf(amplitude.httpThread.getLooper());\n        httplooper.runToEndOfTasks();\n\n        assertNull(amplitude.lastError);\n    }\n\n    @Test\n    public void testSslPinningEU() {\n        amplitude = PinnedAmplitudeClient.getInstance();\n        amplitude.setServerZone(AmplitudeServerZone.EU);\n        amplitude.initialize(context, \"361e4558bb359e288ef75d1ae31437a0\");\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runOneTask();\n        looper.runOneTask();\n\n        amplitude.logEvent(\"eu_pinned_test_event\", null);\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        ShadowLooper httplooper = Shadows.shadowOf(amplitude.httpThread.getLooper());\n        httplooper.runToEndOfTasks();\n\n        assertNull(amplitude.lastError);\n    }\n\n    @Test\n    @Ignore(\"This stopped working in github and skipping this for now since this SDK is \" +\n            \"under maintenance\")\n    public void testSslPinningSwitch() {\n        amplitude = PinnedAmplitudeClient.getInstance();\n        amplitude.initialize(context, \"361e4558bb359e288ef75d1ae31437a0\");\n\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runOneTask();\n        looper.runOneTask();\n\n        amplitude.setServerZone(AmplitudeServerZone.EU);\n        amplitude.logEvent(\"eu_pinned_test_event\", null);\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        ShadowLooper httplooper = Shadows.shadowOf(amplitude.httpThread.getLooper());\n        httplooper.runToEndOfTasks();\n        assertNotNull(amplitude.lastError);\n    }\n\n    @Test\n    public void testSslPinningInvalid() {\n        amplitude = new InvalidPinnedAmplitudeClient();\n        amplitude.initialize(context, \"1cc2c1978ebab0f6451112a8f5df4f4e\");\n\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        looper.runToEndOfTasks();\n\n        amplitude.logEvent(\"pinned_test_event_invalid\", null);\n        looper.runToEndOfTasks();\n        looper.runToEndOfTasks();\n\n        ShadowLooper httplooper = Shadows.shadowOf(amplitude.httpThread.getLooper());\n        httplooper.runToEndOfTasks();\n\n        assertNotNull(amplitude.lastError);\n    }\n\n    private static class InvalidPinnedAmplitudeClient extends PinnedAmplitudeClient {\n        public static final SSLContextBuilder INVALID_SSL_CONTEXT = new SSLContextBuilder()\n          .addCertificate(\"\"\n              + \"MIIFVjCCBD6gAwIBAgIRAObsedhCFsMHaYL156gA4XAwDQYJKoZIhvcNAQELBQAwgZ\"\n              + \"AxCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNV\"\n              + \"BAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9ETyBDQSBMaW1pdGVkMTYwNAYDVQQDEy\"\n              + \"1DT01PRE8gUlNBIERvbWFpbiBWYWxpZGF0aW9uIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcN\"\n              + \"MTQwODE0MDAwMDAwWhcNMTkwODEzMjM1OTU5WjBcMSEwHwYDVQQLExhEb21haW4gQ2\"\n              + \"9udHJvbCBWYWxpZGF0ZWQxHTAbBgNVBAsTFFBvc2l0aXZlU1NMIFdpbGRjYXJkMRgw\"\n              + \"FgYDVQQDFA8qLnlpa3lha2FwaS5uZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwgg\"\n              + \"EKAoIBAQDANfb+9W5g48LTQezWOMdQlL6kE66mqAnR9GAM1Ron31WuHQFn52Y/A6KK\"\n              + \"EfUUHIcC/3vgLHRGzWlzPs8ctTHIMH++Tb2eS3uNhyeiQ2ZTALYFslNThZsdoh/kYu\"\n              + \"YD5qX55ZKP1DJxm2ftcR38XoyWH1mv/JsT1Hq6/ATsesJJxwzxjI2G4NZyPFm8c2w8\"\n              + \"EhfdBgDGyBliGo24TpM7uOVYmC01mAB/pZZS2EuBWjhA6Ny7pTLnjIBAx3jh8Vd3is\"\n              + \"cMxq5boq2DKD0rSVQWbWdYbFMvvMmvq8qnuX9IijqbykoHoHGKerE/LaDs3xDZTlpE\"\n              + \"AvAt8oFUyAllUNCairq3AgMBAAGjggHcMIIB2DAfBgNVHSMEGDAWgBSQr2o6lFoL2J\"\n              + \"DqElZz30O0Oija5zAdBgNVHQ4EFgQUMpYvhtltmd1BhVilRheMd6wqENYwDgYDVR0P\"\n              + \"AQH/BAQDAgWgMAwGA1UdEwEB/wQCMAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQ\"\n              + \"UFBwMCMFAGA1UdIARJMEcwOwYMKwYBBAGyMQECAQMEMCswKQYIKwYBBQUHAgEWHWh0\"\n              + \"dHBzOi8vc2VjdXJlLmNvbW9kby5uZXQvQ1BTMAgGBmeBDAECATBUBgNVHR8ETTBLME\"\n              + \"mgR6BFhkNodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9SU0FEb21haW5WYWxp\"\n              + \"ZGF0aW9uU2VjdXJlU2VydmVyQ0EuY3JsMIGFBggrBgEFBQcBAQR5MHcwTwYIKwYBBQ\"\n              + \"UHMAKGQ2h0dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NPTU9ET1JTQURvbWFpblZhbGlk\"\n              + \"YXRpb25TZWN1cmVTZXJ2ZXJDQS5jcnQwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLm\"\n              + \"NvbW9kb2NhLmNvbTApBgNVHREEIjAggg8qLnlpa3lha2FwaS5uZXSCDXlpa3lha2Fw\"\n              + \"aS5uZXQwDQYJKoZIhvcNAQELBQADggEBAEdw5iJwxvXcZlPQEbudu84VI48uwSYcGz\"\n              + \"xBVzOsfYPdLUc7HSYT8zwg2d5l89iLiapvHS6gQiASZRi7nzR/oqnARcVjWnvIKPlq\"\n              + \"+b3OUtNElnfSFXZnsgpxp3BlcCEfQs7faII89rTzxJqRf0fNo8Y4u3+k79zGF8xbon\"\n              + \"O8oXZt0ApxcYmFIQlhCddM20lgHLTeMx4yG5C2lHGJE3iUS7YVAq6ENRrgiVhcuf5R\"\n              + \"H1mWAYpFPJ7rOmpCReC6brxCho/7jg+fBqEUfCGyrMtYSRejCc9aZGBQmuz5v5iT6P\"\n              + \"XCBeVmjEX3kh4bkRPHJ5vyASNXUkF3nwVAe4cwOoLHN8o=\");\n\n        public InvalidPinnedAmplitudeClient() {\n            super(Constants.DEFAULT_INSTANCE);\n            super.getPinnedCertSslSocketFactory(INVALID_SSL_CONTEXT);\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/amplitude/api/PlanTest.java",
    "content": "package com.amplitude.api;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.annotation.Config;\n\nimport static org.junit.Assert.assertEquals;\n\n@RunWith(AndroidJUnit4.class)\n@Config(manifest=Config.NONE)\npublic class PlanTest {\n\n    @Test\n    public void testToJSONObject() throws JSONException {\n        Plan testPlan = new Plan();\n        String branch = \"main\";\n        String version = \"1.0.0\";\n        String source = \"mobile\";\n        String versionId = \"9ec23ba0-275f-468f-80d1-66b88bff9529\";\n        testPlan.setBranch(branch)\n                .setSource(source)\n                .setVersion(version)\n                .setVersionId(versionId);\n        JSONObject result = testPlan.toJSONObject();\n        assertEquals(branch, result.getString(Constants.AMP_PLAN_BRANCH));\n        assertEquals(source, result.getString(Constants.AMP_PLAN_SOURCE));\n        assertEquals(version, result.getString(Constants.AMP_PLAN_VERSION));\n        assertEquals(versionId, result.getString(Constants.AMP_PLAN_VERSION_ID));\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/amplitude/api/RevenueTest.java",
    "content": "package com.amplitude.api;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.annotation.Config;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertNotEquals;\nimport static org.junit.Assert.assertNull;\nimport static org.junit.Assert.assertTrue;\n\n@RunWith(AndroidJUnit4.class)\n@Config(manifest=Config.NONE)\npublic class RevenueTest extends BaseTest {\n\n    @Before\n    public void setUp() throws Exception { setUp(false); }\n\n    @After\n    public void tearDown() throws Exception {}\n\n    @Test\n    public void testProductId() {\n        Revenue revenue = new Revenue();\n        assertNull(revenue.productId);\n\n        String productId = \"testProductId\";\n        revenue.setProductId(productId);\n        assertEquals(revenue.productId, productId);\n\n        // test that ignore empty inputs\n        revenue.setProductId(null);\n        assertEquals(revenue.productId, productId);\n        revenue.setProductId(\"\");\n        assertEquals(revenue.productId, productId);\n\n        JSONObject obj = revenue.toJSONObject();\n        assertEquals(obj.optString(\"$productId\"), productId);\n    }\n\n    @Test\n    public void testQuantity() {\n        Revenue revenue = new Revenue();\n        assertEquals(revenue.quantity, 1);\n\n        int quantity = 100;\n        revenue.setQuantity(quantity);\n        assertEquals(revenue.quantity, quantity);\n\n        JSONObject obj = revenue.toJSONObject();\n        assertEquals(obj.optInt(\"$quantity\"), quantity);\n    }\n\n    @Test\n    public void testPrice() {\n        Revenue revenue = new Revenue();\n        assertNull(revenue.price);\n\n        double price = 10.99;\n        revenue.setPrice(price);\n        assertEquals(revenue.price.doubleValue(), price, 0);\n\n        JSONObject obj = revenue.toJSONObject();\n        assertEquals(obj.optDouble(\"$price\"), price, 0);\n    }\n\n    @Test\n    public void testRevenueType() {\n        Revenue revenue = new Revenue();\n        assertEquals(revenue.revenueType, null);\n\n        String revenueType = \"testRevenueType\";\n        revenue.setRevenueType(revenueType);\n        assertEquals(revenue.revenueType, revenueType);\n\n        // verify that null and empty strings allowed\n        revenue.setRevenueType(null);\n        assertNull(revenue.revenueType);\n        revenue.setRevenueType(\"\");\n        assertEquals(revenue.revenueType, \"\");\n\n        revenue.setRevenueType(revenueType);\n        assertEquals(revenue.revenueType, revenueType);\n\n        JSONObject obj = revenue.toJSONObject();\n        assertEquals(obj.optString(\"$revenueType\"), revenueType);\n    }\n\n    @Test\n    public void testReceipt() {\n        Revenue revenue = new Revenue();\n        assertNull(revenue.receipt);\n        assertNull(revenue.receiptSig);\n\n        String receipt = \"testReceipt\";\n        String receiptSig = \"testReceiptSig\";\n        revenue.setReceipt(receipt, receiptSig);\n        assertEquals(revenue.receipt, receipt);\n        assertEquals(revenue.receiptSig, receiptSig);\n\n        JSONObject obj = revenue.toJSONObject();\n        assertEquals(obj.optString(\"$receipt\"), receipt);\n        assertEquals(obj.optString(\"$receiptSig\"), receiptSig);\n    }\n\n    @Test\n    public void testRevenueProperties() throws JSONException {\n        Revenue revenue = new Revenue();\n        assertNull(revenue.properties);\n\n        JSONObject properties = new JSONObject().put(\"city\", \"san francisco\");\n        revenue.setRevenueProperties(properties);\n        assertTrue(Utils.compareJSONObjects(properties, revenue.properties));\n\n        JSONObject obj = revenue.toJSONObject();\n        assertEquals(obj.optString(\"city\"), \"san francisco\");\n        assertEquals(obj.optInt(\"$quantity\"), 1);\n\n        // assert original json object was not modified\n        assertFalse(properties.has(\"$quantity\"));\n    }\n\n    @Test\n    public void testEventProperties() throws JSONException {\n        Revenue revenue = new Revenue();\n        assertNull(revenue.properties);\n\n        JSONObject properties = new JSONObject().put(\"city\", \"san francisco\");\n        revenue.setEventProperties(properties);\n        assertTrue(Utils.compareJSONObjects(properties, revenue.properties));\n\n        JSONObject obj = revenue.toJSONObject();\n        assertEquals(obj.optString(\"city\"), \"san francisco\");\n        assertEquals(obj.optInt(\"$quantity\"), 1);\n\n        // assert original json object was not modified\n        assertFalse(properties.has(\"$quantity\"));\n    }\n\n    @Test\n    public void testValidRevenue() {\n        Revenue revenue = new Revenue();\n        assertFalse(revenue.isValidRevenue());\n        revenue.setProductId(\"testProductId\");\n        assertFalse(revenue.isValidRevenue());\n        revenue.setPrice(10.99);\n        assertTrue(revenue.isValidRevenue());\n\n        Revenue revenue2 = new Revenue();\n        assertFalse(revenue2.isValidRevenue());\n        revenue2.setPrice(10.99);\n        revenue2.setQuantity(15);\n        assertTrue(revenue2.isValidRevenue());\n        revenue2.setProductId(\"testProductId\");\n        assertTrue(revenue2.isValidRevenue());\n    }\n\n    @Test\n    public void testToJSONObject() throws JSONException {\n        double price = 10.99;\n        int quantity = 15;\n        String productId = \"testProductId\";\n        String receipt = \"testReceipt\";\n        String receiptSig = \"testReceiptSig\";\n        String revenueType = \"testRevenueType\";\n        JSONObject props = new JSONObject().put(\"city\", \"Boston\");\n\n        Revenue revenue = new Revenue().setProductId(productId).setPrice(price);\n        revenue.setQuantity(quantity).setReceipt(receipt, receiptSig);\n        revenue.setRevenueType(revenueType).setRevenueProperties(props);\n\n        JSONObject obj = revenue.toJSONObject();\n        assertEquals(obj.optDouble(\"$price\"), price, 0);\n        assertEquals(obj.optInt(\"$quantity\"), 15);\n        assertEquals(obj.optString(\"$productId\"), productId);\n        assertEquals(obj.optString(\"$receipt\"), receipt);\n        assertEquals(obj.optString(\"$receiptSig\"), receiptSig);\n        assertEquals(obj.optString(\"$revenueType\"), revenueType);\n        assertEquals(obj.optString(\"city\"), \"Boston\");\n    }\n\n    @Test\n    public void testEquals() throws JSONException {\n        Revenue r1 = new Revenue();\n        Revenue r2 = new Revenue();\n\n        r1.setPrice(10.00).setQuantity(2).setProductId(\"testProductId\");\n        r1.setEventProperties(new JSONObject().put(\"testProp\", \"testValue\"));\n        r2.setPrice(9.99).setQuantity(2).setProductId(\"testProductId\");\n        r2.setEventProperties(new JSONObject().put(\"testProp\", \"testValue\"));\n        assertFalse(r1.equals(r2));\n        r2.setPrice(10.00);\n        assertTrue(r1.equals(r2));\n        r2.setEventProperties(new JSONObject().put(\"testProp\", \"fakeValue\"));\n        assertFalse(r1.equals(r2));\n    }\n\n    @Test\n    public void testHashCode() {\n        Revenue r1 = new Revenue();\n        Revenue r2 = new Revenue();\n\n        r1.setPrice(10.00).setQuantity(2).setProductId(\"testProductId\");\n        r2.setPrice(9.99).setQuantity(2).setProductId(\"testProductId\");\n        assertNotEquals(r1.hashCode(), r2.hashCode());\n        r2.setPrice(10.00);\n        assertEquals(r1.hashCode(), r2.hashCode());\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/amplitude/api/SessionTest.java",
    "content": "package com.amplitude.api;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.json.JSONArray;\nimport org.json.JSONException;\nimport org.json.JSONObject;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.Robolectric;\nimport org.robolectric.Shadows;\nimport org.robolectric.annotation.Config;\nimport org.robolectric.shadows.ShadowLooper;\n\nimport okhttp3.mockwebserver.RecordedRequest;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertNull;\nimport static org.junit.Assert.assertTrue;\n\n@RunWith(AndroidJUnit4.class)\n@Config(manifest = Config.NONE)\npublic class SessionTest extends BaseTest {\n\n    // allows for control of System.currentTimeMillis\n    private class AmplitudeCallbacksWithTime extends AmplitudeCallbacks {\n\n        private int index;\n        private long [] timestamps = null;\n\n        public AmplitudeCallbacksWithTime(AmplitudeClient client, long [] timestamps) {\n            super(client);\n            this.index = 0;\n            this.timestamps = timestamps;\n        }\n\n        @Override\n        protected long getCurrentTimeMillis() {\n            return timestamps[index++ % timestamps.length];\n        }\n    }\n\n    @Before\n    public void setUp() throws Exception {\n        super.setUp(true);\n        amplitude.initialize(context, apiKey);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runOneTask();\n    }\n\n    @After\n    public void tearDown() throws Exception {\n        super.tearDown();\n    }\n\n    @Test\n    public void testDefaultStartSession() {\n        long timestamp = System.currentTimeMillis();\n        amplitude.logEventAsync(\"test\", null, null, null, null, null, timestamp, false);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n\n        // trackSessionEvents is false, no start_session event added\n        assertEquals(getUnsentEventCount(), 1);\n        JSONObject event = getLastUnsentEvent();\n        assertEquals(event.optString(\"event_type\"), \"test\");\n        assertEquals(event.optString(\"session_id\"), String.valueOf(timestamp));\n    }\n\n    @Test\n    public void testDefaultTriggerNewSession() {\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        long sessionTimeoutMillis = 5 * 1000; //5s\n        amplitude.setSessionTimeoutMillis(sessionTimeoutMillis);\n\n        // log 1st event, initialize first session\n        long timestamp1 = System.currentTimeMillis();\n        amplitude.logEventAsync(\"test1\", null, null, null, null, null, timestamp1, false);\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 1);\n\n        // log 2nd event past timeout, verify new session started\n        long timestamp2 = timestamp1 + sessionTimeoutMillis;\n        amplitude.logEventAsync(\"test2\", null, null, null, null, null, timestamp2, false);\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 2);\n\n        JSONArray events = getUnsentEvents(2);\n        JSONObject event1 = events.optJSONObject(0);\n        JSONObject event2 = events.optJSONObject(1);\n\n        assertEquals(event1.optString(\"event_type\"), \"test1\");\n        assertEquals(event1.optString(\"session_id\"), String.valueOf(timestamp1));\n        assertEquals(event2.optString(\"event_type\"), \"test2\");\n        assertEquals(event2.optString(\"session_id\"), String.valueOf(timestamp2));\n\n        // also test getSessionId\n        assertEquals(amplitude.getSessionId(), timestamp2);\n    }\n\n    @Test\n    public void testDefaultExtendSession() {\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        long sessionTimeoutMillis = 5 * 1000; //5s\n        amplitude.setSessionTimeoutMillis(sessionTimeoutMillis);\n\n        // log 3 events all just within session expiration window, verify all in same session\n        long timestamp1 = System.currentTimeMillis();\n        amplitude.logEventAsync(\"test1\", null, null, null, null, null, timestamp1, false);\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 1);\n\n        long timestamp2 = timestamp1 + sessionTimeoutMillis - 1;\n        amplitude.logEventAsync(\"test2\", null, null, null, null, null, timestamp2, false);\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 2);\n\n        long timestamp3 = timestamp2 + sessionTimeoutMillis - 1;\n        amplitude.logEventAsync(\"test3\", null, null, null, null, null, timestamp3, false);\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 3);\n\n        JSONArray events = getUnsentEvents(3);\n        JSONObject event1 = events.optJSONObject(0);\n        JSONObject event2 = events.optJSONObject(1);\n        JSONObject event3 = events.optJSONObject(2);\n\n        assertEquals(event1.optString(\"event_type\"), \"test1\");\n        assertEquals(event1.optString(\"session_id\"), String.valueOf(timestamp1));\n        assertEquals(event1.optString(\"timestamp\"), String.valueOf(timestamp1));\n\n        assertEquals(event2.optString(\"event_type\"), \"test2\");\n        assertEquals(event2.optString(\"session_id\"), String.valueOf(timestamp1));\n        assertEquals(event2.optString(\"timestamp\"), String.valueOf(timestamp2));\n\n        assertEquals(event3.optString(\"event_type\"), \"test3\");\n        assertEquals(event3.optString(\"session_id\"), String.valueOf(timestamp1));\n        assertEquals(event3.optString(\"timestamp\"), String.valueOf(timestamp3));\n    }\n\n    @Test\n    public void testDefaultStartSessionWithTracking() {\n        amplitude.trackSessionEvents(true);\n\n        long timestamp = System.currentTimeMillis();\n        amplitude.logEventAsync(\"test\", null, null, null, null, null, timestamp, false);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n\n        // trackSessions is true, start_session event is added\n        assertEquals(getUnsentEventCount(), 2);\n        JSONArray events = getUnsentEvents(2);\n        JSONObject session_event = events.optJSONObject(0);\n        JSONObject test_event = events.optJSONObject(1);\n\n        assertEquals(session_event.optString(\"event_type\"), AmplitudeClient.START_SESSION_EVENT);\n        assertEquals(\n                session_event.optJSONObject(\"api_properties\").optString(\"special\"),\n                AmplitudeClient.START_SESSION_EVENT\n        );\n        assertEquals(session_event.optString(\"session_id\"), String.valueOf(timestamp));\n\n        assertEquals(test_event.optString(\"event_type\"), \"test\");\n        assertEquals(test_event.optString(\"session_id\"), String.valueOf(timestamp));\n    }\n\n    @Test\n    public void testDefaultStartSessionWithTrackingSynchronous() {\n        amplitude.trackSessionEvents(true);\n\n        long timestamp = System.currentTimeMillis();\n        amplitude.logEventSync(\"test\", null, null, timestamp, false);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        // trackSessions is true, start_session event is added\n        assertEquals(getUnsentEventCount(), 2);\n\n        // verify order of synchronous events\n        JSONArray events = getUnsentEvents(2);\n        JSONObject session_event = events.optJSONObject(0);\n        JSONObject test_event = events.optJSONObject(1);\n\n        assertEquals(session_event.optString(\"event_type\"), AmplitudeClient.START_SESSION_EVENT);\n        assertEquals(\n                session_event.optJSONObject(\"api_properties\").optString(\"special\"),\n                AmplitudeClient.START_SESSION_EVENT\n        );\n        assertEquals(session_event.optString(\"session_id\"), String.valueOf(timestamp));\n\n        assertEquals(test_event.optString(\"event_type\"), \"test\");\n        assertEquals(test_event.optString(\"session_id\"), String.valueOf(timestamp));\n    }\n\n    @Test\n    public void testDefaultTriggerNewSessionWithTracking() {\n        amplitude.trackSessionEvents(true);\n\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        long sessionTimeoutMillis = 5 * 1000; //5s\n        amplitude.setSessionTimeoutMillis(sessionTimeoutMillis);\n\n        // log 1st event, initialize first session\n        long timestamp1 = System.currentTimeMillis();\n        amplitude.logEventAsync(\"test1\", null, null, null, null, null, timestamp1, false);\n        looper.runToEndOfTasks();\n        // trackSessions is true, start_session event is added\n        assertEquals(getUnsentEventCount(), 2);\n\n        // log 2nd event past timeout, verify new session started\n        long timestamp2 = timestamp1 + sessionTimeoutMillis;\n        amplitude.logEventAsync(\"test2\", null, null, null, null, null, timestamp2, false);\n        looper.runToEndOfTasks();\n        // trackSessions is true, end_session and start_session events are added\n        assertEquals(getUnsentEventCount(), 5);\n\n        JSONArray events = getUnsentEvents(5);\n        JSONObject startSession1 = events.optJSONObject(0);\n        JSONObject event1 = events.optJSONObject(1);\n        JSONObject endSession = events.optJSONObject(2);\n        JSONObject startSession2 = events.optJSONObject(3);\n        JSONObject event2 = events.optJSONObject(4);\n\n        assertEquals(startSession1.optString(\"event_type\"), AmplitudeClient.START_SESSION_EVENT);\n        assertEquals(\n                startSession1.optJSONObject(\"api_properties\").optString(\"special\"),\n                AmplitudeClient.START_SESSION_EVENT\n        );\n        assertEquals(startSession1.optString(\"session_id\"), String.valueOf(timestamp1));\n\n        assertEquals(event1.optString(\"event_type\"), \"test1\");\n        assertEquals(event1.optString(\"session_id\"), String.valueOf(timestamp1));\n        assertEquals(event1.optString(\"timestamp\"), String.valueOf(timestamp1));\n\n        assertEquals(endSession.optString(\"event_type\"), AmplitudeClient.END_SESSION_EVENT);\n        assertEquals(\n                endSession.optJSONObject(\"api_properties\").optString(\"special\"),\n                AmplitudeClient.END_SESSION_EVENT\n        );\n        assertEquals(endSession.optString(\"session_id\"), String.valueOf(timestamp1));\n\n        assertEquals(startSession2.optString(\"event_type\"), AmplitudeClient.START_SESSION_EVENT);\n        assertEquals(\n                startSession2.optJSONObject(\"api_properties\").optString(\"special\"),\n                AmplitudeClient.START_SESSION_EVENT\n        );\n        assertEquals(startSession2.optString(\"session_id\"), String.valueOf(timestamp2));\n\n        assertEquals(event2.optString(\"event_type\"), \"test2\");\n        assertEquals(event2.optString(\"session_id\"), String.valueOf(timestamp2));\n        assertEquals(event2.optString(\"timestamp\"), String.valueOf(timestamp2));\n    }\n\n    @Test\n    public void testDefaultTriggerNewSessionWithTrackingSynchronous() {\n        amplitude.trackSessionEvents(true);\n\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        long sessionTimeoutMillis = 5 * 1000; //5s\n        amplitude.setSessionTimeoutMillis(sessionTimeoutMillis);\n\n        // log 1st event, initialize first session\n        long timestamp1 = System.currentTimeMillis();\n        amplitude.logEventSync(\"test1\", null, null, timestamp1, false);\n        looper.runToEndOfTasks();\n        // trackSessions is true, start_session event is added\n        assertEquals(getUnsentEventCount(), 2);\n\n        // log 2nd event past timeout, verify new session started\n        long timestamp2 = timestamp1 + sessionTimeoutMillis;\n        amplitude.logEventSync(\"test2\", null, null, timestamp2, false);\n        looper.runToEndOfTasks();\n        // trackSessions is true, end_session and start_session events are added\n        assertEquals(getUnsentEventCount(), 5);\n\n        // verify order of synchronous events\n        JSONArray events = getUnsentEvents(5);\n        JSONObject startSession1 = events.optJSONObject(0);\n        JSONObject event1 = events.optJSONObject(1);\n        JSONObject endSession = events.optJSONObject(2);\n        JSONObject startSession2 = events.optJSONObject(3);\n        JSONObject event2 = events.optJSONObject(4);\n\n        assertEquals(startSession1.optString(\"event_type\"), AmplitudeClient.START_SESSION_EVENT);\n        assertEquals(\n                startSession1.optJSONObject(\"api_properties\").optString(\"special\"),\n                AmplitudeClient.START_SESSION_EVENT\n        );\n        assertEquals(startSession1.optString(\"session_id\"), String.valueOf(timestamp1));\n\n        assertEquals(event1.optString(\"event_type\"), \"test1\");\n        assertEquals(event1.optString(\"session_id\"), String.valueOf(timestamp1));\n        assertEquals(event1.optString(\"timestamp\"), String.valueOf(timestamp1));\n\n        assertEquals(endSession.optString(\"event_type\"), AmplitudeClient.END_SESSION_EVENT);\n        assertEquals(\n                endSession.optJSONObject(\"api_properties\").optString(\"special\"),\n                AmplitudeClient.END_SESSION_EVENT\n        );\n        assertEquals(endSession.optString(\"session_id\"), String.valueOf(timestamp1));\n\n        assertEquals(startSession2.optString(\"event_type\"), AmplitudeClient.START_SESSION_EVENT);\n        assertEquals(\n                startSession2.optJSONObject(\"api_properties\").optString(\"special\"),\n                AmplitudeClient.START_SESSION_EVENT\n        );\n        assertEquals(startSession2.optString(\"session_id\"), String.valueOf(timestamp2));\n\n        assertEquals(event2.optString(\"event_type\"), \"test2\");\n        assertEquals(event2.optString(\"session_id\"), String.valueOf(timestamp2));\n        assertEquals(event2.optString(\"timestamp\"), String.valueOf(timestamp2));\n    }\n\n    @Test\n    public void testDefaultExtendSessionWithTracking() {\n        amplitude.trackSessionEvents(true);\n\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        long sessionTimeoutMillis = 5 * 1000; //5s\n        amplitude.setSessionTimeoutMillis(sessionTimeoutMillis);\n\n        // log 3 events all just within session expiration window, verify all in same session\n        long timestamp1 = System.currentTimeMillis();\n        amplitude.logEventAsync(\"test1\", null, null, null, null, null, timestamp1, false);\n        looper.runToEndOfTasks();\n        // trackSessions is true, start_session event is added\n        assertEquals(getUnsentEventCount(), 2);\n\n        long timestamp2 = timestamp1 + sessionTimeoutMillis - 1;\n        amplitude.logEventAsync(\"test2\", null, null, null, null, null, timestamp2, false);\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 3);\n\n        long timestamp3 = timestamp2 + sessionTimeoutMillis - 1;\n        amplitude.logEventAsync(\"test3\", null, null, null, null, null, timestamp3, false);\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 4);\n\n        JSONArray events = getUnsentEvents(4);\n        JSONObject startSession = events.optJSONObject(0);\n        JSONObject event1 = events.optJSONObject(1);\n        JSONObject event2 = events.optJSONObject(2);\n        JSONObject event3 = events.optJSONObject(3);\n\n        assertEquals(startSession.optString(\"event_type\"), AmplitudeClient.START_SESSION_EVENT);\n        assertEquals(\n                startSession.optJSONObject(\"api_properties\").optString(\"special\"),\n                AmplitudeClient.START_SESSION_EVENT\n        );\n        assertEquals(startSession.optString(\"session_id\"), String.valueOf(timestamp1));\n\n        assertEquals(event1.optString(\"event_type\"), \"test1\");\n        assertEquals(event1.optString(\"session_id\"), String.valueOf(timestamp1));\n        assertEquals(event1.optString(\"timestamp\"), String.valueOf(timestamp1));\n\n        assertEquals(event2.optString(\"event_type\"), \"test2\");\n        assertEquals(event2.optString(\"session_id\"), String.valueOf(timestamp1));\n        assertEquals(event2.optString(\"timestamp\"), String.valueOf(timestamp2));\n\n        assertEquals(event3.optString(\"event_type\"), \"test3\");\n        assertEquals(event3.optString(\"session_id\"), String.valueOf(timestamp1));\n        assertEquals(event3.optString(\"timestamp\"), String.valueOf(timestamp3));\n    }\n\n    @Test\n    public void testDefaultExtendSessionWithTrackingSynchronous() {\n        amplitude.trackSessionEvents(true);\n\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        long sessionTimeoutMillis = 5 * 1000; //5s\n        amplitude.setSessionTimeoutMillis(sessionTimeoutMillis);\n\n        // log 3 events all just within session expiration window, verify all in same session\n        long timestamp1 = System.currentTimeMillis();\n        amplitude.logEventSync(\"test1\", null, null, timestamp1, false);\n        looper.runToEndOfTasks();\n        // trackSessions is true, start_session event is added\n        assertEquals(getUnsentEventCount(), 2);\n\n        long timestamp2 = timestamp1 + sessionTimeoutMillis - 1;\n        amplitude.logEventSync(\"test2\", null, null, timestamp2, false);\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 3);\n\n        long timestamp3 = timestamp2 + sessionTimeoutMillis - 1;\n        amplitude.logEventAsync(\"test3\", null, null, null, null, null, timestamp3, false);\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 4);\n\n        // verify order of synchronous events\n        JSONArray events = getUnsentEvents(4);\n        JSONObject startSession = events.optJSONObject(0);\n        JSONObject event1 = events.optJSONObject(1);\n        JSONObject event2 = events.optJSONObject(2);\n        JSONObject event3 = events.optJSONObject(3);\n\n        assertEquals(startSession.optString(\"event_type\"), AmplitudeClient.START_SESSION_EVENT);\n        assertEquals(\n                startSession.optJSONObject(\"api_properties\").optString(\"special\"),\n                AmplitudeClient.START_SESSION_EVENT\n        );\n        assertEquals(startSession.optString(\"session_id\"), String.valueOf(timestamp1));\n\n        assertEquals(event1.optString(\"event_type\"), \"test1\");\n        assertEquals(event1.optString(\"session_id\"), String.valueOf(timestamp1));\n        assertEquals(event1.optString(\"timestamp\"), String.valueOf(timestamp1));\n\n        assertEquals(event2.optString(\"event_type\"), \"test2\");\n        assertEquals(event2.optString(\"session_id\"), String.valueOf(timestamp1));\n        assertEquals(event2.optString(\"timestamp\"), String.valueOf(timestamp2));\n\n        assertEquals(event3.optString(\"event_type\"), \"test3\");\n        assertEquals(event3.optString(\"session_id\"), String.valueOf(timestamp1));\n        assertEquals(event3.optString(\"timestamp\"), String.valueOf(timestamp3));\n    }\n\n    @Test\n    public void testEnableAccurateTracking() {\n        assertFalse(amplitude.isUsingForegroundTracking());\n        AmplitudeCallbacks callBacks = new AmplitudeCallbacks(amplitude);\n        assertTrue(amplitude.isUsingForegroundTracking());\n    }\n\n    @Test\n    public void testAccurateOnResumeStartSession() {\n        long timestamp = System.currentTimeMillis();\n        long [] timestamps = {timestamp};\n        AmplitudeCallbacks callBacks = new AmplitudeCallbacksWithTime(amplitude, timestamps);\n\n        assertEquals(amplitude.previousSessionId, -1);\n        assertEquals(amplitude.lastEventId, -1);\n        assertEquals(amplitude.lastEventTime, -1);\n        assertFalse(amplitude.isInForeground());\n\n        callBacks.onActivityResumed(null);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        assertTrue(amplitude.isInForeground());\n        assertEquals(amplitude.previousSessionId, timestamp);\n        assertEquals(amplitude.lastEventId, -1);\n        assertEquals(amplitude.lastEventTime, timestamp);\n    }\n\n    @Test\n    public void testAccurateOnResumeStartSessionWithTracking() {\n        amplitude.trackSessionEvents(true);\n        long timestamp = System.currentTimeMillis();\n        long [] timestamps = {timestamp};\n        AmplitudeCallbacks callBacks = new AmplitudeCallbacksWithTime(amplitude, timestamps);\n\n        assertEquals(amplitude.previousSessionId, -1);\n        assertEquals(amplitude.lastEventId, -1);\n        assertEquals(amplitude.lastEventTime, -1);\n        assertFalse(amplitude.isInForeground());\n        assertEquals(getUnsentEventCount(), 0);\n\n        callBacks.onActivityResumed(null);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        assertTrue(amplitude.isInForeground());\n        assertEquals(amplitude.previousSessionId, timestamp);\n        assertEquals(amplitude.lastEventId, 1);\n        assertEquals(amplitude.lastEventTime, timestamp);\n\n        // verify that start session event sent\n        assertEquals(getUnsentEventCount(), 1);\n        JSONObject startSession = getLastUnsentEvent();\n        assertEquals(startSession.optString(\"event_type\"), AmplitudeClient.START_SESSION_EVENT);\n        assertEquals(\n                startSession.optJSONObject(\"api_properties\").optString(\"special\"),\n                AmplitudeClient.START_SESSION_EVENT\n        );\n        assertEquals(\n                startSession.optString(\"session_id\"),\n                String.valueOf(timestamp)\n        );\n        assertEquals(\n            startSession.optString(\"timestamp\"),\n            String.valueOf(timestamp)\n        );\n    }\n\n    @Test\n    public void testAccurateOnPauseRefreshTimestamp() {\n        long minTimeBetweenSessionsMillis = 5*1000; //5s\n        long timestamp = System.currentTimeMillis();\n        long [] timestamps = {timestamp, timestamp + minTimeBetweenSessionsMillis};\n        AmplitudeCallbacks callBacks = new AmplitudeCallbacksWithTime(amplitude, timestamps);\n\n        assertEquals(amplitude.previousSessionId, -1);\n        assertEquals(amplitude.lastEventId, -1);\n        assertEquals(amplitude.lastEventTime, -1);\n\n        callBacks.onActivityResumed(null);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        assertEquals(amplitude.previousSessionId, timestamps[0]);\n        assertEquals(amplitude.lastEventId, -1);\n        assertEquals(amplitude.lastEventTime, timestamps[0]);\n\n        callBacks.onActivityPaused(null);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        assertEquals(amplitude.previousSessionId, timestamps[0]);\n        assertEquals(amplitude.lastEventId, -1);\n        assertEquals(amplitude.lastEventTime, timestamps[1]);\n        assertFalse(amplitude.isInForeground());\n    }\n\n    @Test\n    public void testAccurateOnPauseRefreshTimestampWithTracking() {\n        amplitude.trackSessionEvents(true);\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        long minTimeBetweenSessionsMillis = 5*1000; //5s\n        amplitude.setMinTimeBetweenSessionsMillis(minTimeBetweenSessionsMillis);\n        long timestamp = System.currentTimeMillis();\n        long [] timestamps = {timestamp, timestamp + minTimeBetweenSessionsMillis};\n        AmplitudeCallbacks callBacks = new AmplitudeCallbacksWithTime(amplitude, timestamps);\n\n        assertEquals(amplitude.previousSessionId, -1);\n        assertEquals(amplitude.lastEventId, -1);\n        assertEquals(amplitude.lastEventTime, -1);\n        assertEquals(getUnsentEventCount(), 0);\n\n        callBacks.onActivityResumed(null);\n        looper.runToEndOfTasks();\n        assertEquals(amplitude.previousSessionId, timestamps[0]);\n        assertEquals(amplitude.lastEventId, 1);\n        assertEquals(amplitude.lastEventTime, timestamps[0]);\n        assertEquals(getUnsentEventCount(), 1);\n\n        // only refresh time, no session checking\n        callBacks.onActivityPaused(null);\n        looper.runToEndOfTasks();\n        assertEquals(amplitude.previousSessionId, timestamps[0]);\n        assertEquals(amplitude.lastEventId, 1);\n        assertEquals(amplitude.lastEventTime, timestamps[1]);\n        assertEquals(getUnsentEventCount(), 1);\n    }\n\n    @Test\n    public void testAccurateOnResumeTriggerNewSession() {\n        long minTimeBetweenSessionsMillis = 5*1000; //5s\n        amplitude.setMinTimeBetweenSessionsMillis(minTimeBetweenSessionsMillis);\n        long timestamp = System.currentTimeMillis();\n        long [] timestamps = {\n                timestamp,\n                timestamp + 1,\n                timestamp + 1 + minTimeBetweenSessionsMillis\n        };\n        AmplitudeCallbacks callBacks = new AmplitudeCallbacksWithTime(amplitude, timestamps);\n\n        assertEquals(amplitude.previousSessionId, -1);\n        assertEquals(amplitude.lastEventId, -1);\n        assertEquals(amplitude.lastEventTime, -1);\n        assertEquals(getUnsentEventCount(), 0);\n\n        callBacks.onActivityResumed(null);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        assertEquals(amplitude.previousSessionId, timestamps[0]);\n        assertEquals(amplitude.lastEventId, -1);\n        assertEquals(amplitude.lastEventTime, timestamps[0]);\n        assertEquals(getUnsentEventCount(), 0);\n        assertTrue(amplitude.isInForeground());\n\n        // only refresh time, no session checking\n        callBacks.onActivityPaused(null);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        assertEquals(amplitude.previousSessionId, timestamps[0]);\n        assertEquals(amplitude.lastEventId, -1);\n        assertEquals(amplitude.lastEventTime, timestamps[1]);\n        assertEquals(getUnsentEventCount(), 0);\n        assertFalse(amplitude.isInForeground());\n\n        // resume after min session expired window, verify new session started\n        callBacks.onActivityResumed(null);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        assertEquals(amplitude.previousSessionId, timestamps[2]);\n        assertEquals(amplitude.lastEventId, -1);\n        assertEquals(amplitude.lastEventTime, timestamps[2]);\n        assertEquals(getUnsentEventCount(), 0);\n        assertTrue(amplitude.isInForeground());\n    }\n\n    @Test\n    public void testAccurateOnResumeTriggerNewSessionWithTracking() {\n        amplitude.trackSessionEvents(true);\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        long minTimeBetweenSessionsMillis = 5*1000; //5s\n        amplitude.setMinTimeBetweenSessionsMillis(minTimeBetweenSessionsMillis);\n        long timestamp = System.currentTimeMillis();\n        long [] timestamps = {\n                timestamp,\n                timestamp + 1,\n                timestamp + 1 + minTimeBetweenSessionsMillis\n        };\n        AmplitudeCallbacks callBacks = new AmplitudeCallbacksWithTime(amplitude, timestamps);\n\n        assertEquals(amplitude.previousSessionId, -1);\n        assertEquals(amplitude.lastEventId, -1);\n        assertEquals(amplitude.lastEventTime, -1);\n        assertEquals(getUnsentEventCount(), 0);\n\n        callBacks.onActivityResumed(null);\n        looper.runToEndOfTasks();\n        assertEquals(amplitude.previousSessionId, timestamps[0]);\n        assertEquals(amplitude.lastEventId, 1);\n        assertEquals(amplitude.lastEventTime, timestamps[0]);\n        assertEquals(getUnsentEventCount(), 1);\n        assertTrue(amplitude.isInForeground());\n\n        // only refresh time, no session checking\n        callBacks.onActivityPaused(null);\n        looper.runToEndOfTasks();\n        assertEquals(amplitude.previousSessionId, timestamps[0]);\n        assertEquals(amplitude.lastEventId, 1);\n        assertEquals(amplitude.lastEventTime, timestamps[1]);\n        assertEquals(getUnsentEventCount(), 1);\n        assertFalse(amplitude.isInForeground());\n\n        // resume after min session expired window, verify new session started\n        callBacks.onActivityResumed(null);\n        looper.runToEndOfTasks();\n        assertEquals(amplitude.previousSessionId, timestamps[2]);\n        assertEquals(amplitude.lastEventId, 3);\n        assertEquals(amplitude.lastEventTime, timestamps[2]);\n        assertEquals(getUnsentEventCount(), 3);\n        assertTrue(amplitude.isInForeground());\n\n        JSONArray events = getUnsentEvents(3);\n        JSONObject startSession1 = events.optJSONObject(0);\n        JSONObject endSession = events.optJSONObject(1);\n        JSONObject startSession2 = events.optJSONObject(2);\n\n        assertEquals(startSession1.optString(\"event_type\"), AmplitudeClient.START_SESSION_EVENT);\n        assertEquals(\n            startSession1.optJSONObject(\"api_properties\").optString(\"special\"),\n            AmplitudeClient.START_SESSION_EVENT\n        );\n        assertEquals(startSession1.optString(\"session_id\"), String.valueOf(timestamps[0]));\n        assertEquals(startSession1.optString(\"timestamp\"), String.valueOf(timestamps[0]));\n\n        assertEquals(endSession.optString(\"event_type\"), AmplitudeClient.END_SESSION_EVENT);\n        assertEquals(\n                endSession.optJSONObject(\"api_properties\").optString(\"special\"),\n                AmplitudeClient.END_SESSION_EVENT\n        );\n        assertEquals(endSession.optString(\"session_id\"), String.valueOf(timestamps[0]));\n        assertEquals(endSession.optString(\"timestamp\"), String.valueOf(timestamps[1]));\n\n        assertEquals(startSession2.optString(\"event_type\"), AmplitudeClient.START_SESSION_EVENT);\n        assertEquals(\n                startSession2.optJSONObject(\"api_properties\").optString(\"special\"),\n                AmplitudeClient.START_SESSION_EVENT\n        );\n        assertEquals(startSession2.optString(\"session_id\"), String.valueOf(timestamps[2]));\n        assertEquals(startSession2.optString(\"timestamp\"), String.valueOf(timestamps[2]));\n    }\n\n    @Test\n    public void testAccurateOnResumeExtendSession() {\n        long minTimeBetweenSessionsMillis = 5*1000; //5s\n        amplitude.setMinTimeBetweenSessionsMillis(minTimeBetweenSessionsMillis);\n        long timestamp = System.currentTimeMillis();\n        long [] timestamps = {\n                timestamp,\n                timestamp + 1,\n                timestamp + 1 + minTimeBetweenSessionsMillis - 1  // just inside session exp window\n        };\n        AmplitudeCallbacks callBacks = new AmplitudeCallbacksWithTime(amplitude, timestamps);\n\n        assertEquals(amplitude.previousSessionId, -1);\n        assertEquals(amplitude.lastEventId, -1);\n        assertEquals(amplitude.lastEventTime, -1);\n\n        callBacks.onActivityResumed(null);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        assertEquals(amplitude.previousSessionId, timestamps[0]);\n        assertEquals(amplitude.lastEventId, -1);\n        assertEquals(amplitude.lastEventTime, timestamps[0]);\n\n        callBacks.onActivityPaused(null);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        assertEquals(amplitude.previousSessionId, timestamps[0]);\n        assertEquals(amplitude.lastEventId, -1);\n        assertEquals(amplitude.lastEventTime, timestamps[1]);\n        assertFalse(amplitude.isInForeground());\n\n        callBacks.onActivityResumed(null);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        assertEquals(amplitude.previousSessionId, timestamps[0]);\n        assertEquals(amplitude.lastEventId, -1);\n        assertEquals(amplitude.lastEventTime, timestamps[2]);\n        assertTrue(amplitude.isInForeground());\n    }\n\n    @Test\n    public void testAccurateOnResumeExtendSessionWithTracking() {\n        amplitude.trackSessionEvents(true);\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        long minTimeBetweenSessionsMillis = 5*1000; //5s\n        amplitude.setMinTimeBetweenSessionsMillis(minTimeBetweenSessionsMillis);\n        long timestamp = System.currentTimeMillis();\n        long [] timestamps = {\n                timestamp,\n                timestamp + 1,\n                timestamp + 1 + minTimeBetweenSessionsMillis - 1  // just inside session exp window\n        };\n        AmplitudeCallbacks callBacks = new AmplitudeCallbacksWithTime(amplitude, timestamps);\n\n        assertEquals(amplitude.previousSessionId, -1);\n        assertEquals(amplitude.lastEventId, -1);\n        assertEquals(amplitude.lastEventTime, -1);\n        assertEquals(getUnsentEventCount(), 0);\n\n        callBacks.onActivityResumed(null);\n        looper.runToEndOfTasks();\n        assertEquals(amplitude.previousSessionId, timestamps[0]);\n        assertEquals(amplitude.lastEventId, 1);\n        assertEquals(amplitude.lastEventTime, timestamps[0]);\n        assertEquals(getUnsentEventCount(), 1);\n\n        callBacks.onActivityPaused(null);\n        looper.runToEndOfTasks();\n        assertEquals(amplitude.previousSessionId, timestamps[0]);\n        assertEquals(amplitude.lastEventId, 1);\n        assertEquals(amplitude.lastEventTime, timestamps[1]);\n        assertFalse(amplitude.isInForeground());\n        assertEquals(getUnsentEventCount(), 1);\n\n        callBacks.onActivityResumed(null);\n        looper.runToEndOfTasks();\n        assertEquals(amplitude.previousSessionId, timestamps[0]);\n        assertEquals(amplitude.lastEventId, 1);\n        assertEquals(amplitude.lastEventTime, timestamps[2]);\n        assertTrue(amplitude.isInForeground());\n        assertEquals(getUnsentEventCount(), 1);\n\n        JSONObject event = getLastUnsentEvent();\n        assertEquals(event.optString(\"event_type\"), AmplitudeClient.START_SESSION_EVENT);\n        assertEquals(\n            event.optJSONObject(\"api_properties\").optString(\"special\"),\n            AmplitudeClient.START_SESSION_EVENT\n        );\n        assertEquals(event.optString(\"session_id\"), String.valueOf(timestamps[0]));\n        assertEquals(event.optString(\"timestamp\"), String.valueOf(timestamps[0]));\n    }\n\n    @Test\n    public void testAccurateLogAsyncEvent() {\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        long minTimeBetweenSessionsMillis = 5*1000; //5s\n        amplitude.setMinTimeBetweenSessionsMillis(minTimeBetweenSessionsMillis);\n        long timestamp = System.currentTimeMillis();\n        long [] timestamps = {timestamp + minTimeBetweenSessionsMillis - 1};\n        AmplitudeCallbacks callBacks = new AmplitudeCallbacksWithTime(amplitude, timestamps);\n\n        assertEquals(amplitude.previousSessionId, -1);\n        assertEquals(amplitude.lastEventId, -1);\n        assertEquals(amplitude.lastEventTime, -1);\n        assertEquals(getUnsentEventCount(), 0);\n        assertFalse(amplitude.isInForeground());\n\n        // logging an event before onResume will force a session check\n        amplitude.logEventAsync(\"test\", null, null, null, null, null, timestamp, false);\n        looper.runToEndOfTasks();\n        assertEquals(amplitude.previousSessionId, timestamp);\n        assertEquals(amplitude.lastEventId, 1);\n        assertEquals(amplitude.lastEventTime, timestamp);\n        assertEquals(getUnsentEventCount(), 1);\n\n        callBacks.onActivityResumed(null);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        assertEquals(amplitude.previousSessionId, timestamp);\n        assertEquals(amplitude.lastEventId, 1);\n        assertEquals(amplitude.lastEventTime, timestamps[0]);\n        assertEquals(getUnsentEventCount(), 1);\n        assertTrue(amplitude.isInForeground());\n\n        JSONObject event = getLastUnsentEvent();\n        assertEquals(event.optString(\"event_type\"), \"test\");\n        assertEquals(event.optString(\"session_id\"), String.valueOf(timestamp));\n        assertEquals(event.optString(\"timestamp\"), String.valueOf(timestamp));\n    }\n\n    @Test\n    public void testAccurateLogAsyncEventWithTracking() {\n        amplitude.trackSessionEvents(true);\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        long minTimeBetweenSessionsMillis = 5*1000; //5s\n        amplitude.setMinTimeBetweenSessionsMillis(minTimeBetweenSessionsMillis);\n        long timestamp = System.currentTimeMillis();\n        long [] timestamps = {timestamp + minTimeBetweenSessionsMillis};\n        AmplitudeCallbacks callBacks = new AmplitudeCallbacksWithTime(amplitude, timestamps);\n\n        assertEquals(amplitude.previousSessionId, -1);\n        assertEquals(amplitude.lastEventId, -1);\n        assertEquals(amplitude.lastEventTime, -1);\n        assertEquals(getUnsentEventCount(), 0);\n        assertFalse(amplitude.isInForeground());\n\n        // logging an event before onResume will force a session check\n        amplitude.logEventAsync(\"test\", null, null, null, null, null, timestamp, false);\n        looper.runToEndOfTasks();\n        assertEquals(amplitude.previousSessionId, timestamp);\n        assertEquals(amplitude.lastEventId, 2);\n        assertEquals(amplitude.lastEventTime, timestamp);\n        assertEquals(getUnsentEventCount(), 2);\n\n        // onResume after session expires will start new session\n        callBacks.onActivityResumed(null);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n        assertEquals(amplitude.previousSessionId, timestamps[0]);\n        assertEquals(amplitude.lastEventId, 4);\n        assertEquals(amplitude.lastEventTime, timestamps[0]);\n        assertEquals(getUnsentEventCount(), 4);\n        assertTrue(amplitude.isInForeground());\n\n        JSONArray events = getUnsentEvents(4);\n        JSONObject startSession1 = events.optJSONObject(0);\n        JSONObject event = events.optJSONObject(1);\n        JSONObject endSession = events.optJSONObject(2);\n        JSONObject startSession2 = events.optJSONObject(3);\n\n        assertEquals(startSession1.optString(\"event_type\"), AmplitudeClient.START_SESSION_EVENT);\n        assertEquals(\n            startSession1.optJSONObject(\"api_properties\").optString(\"special\"),\n            AmplitudeClient.START_SESSION_EVENT\n        );\n        assertEquals(startSession1.optString(\"session_id\"), String.valueOf(timestamp));\n        assertEquals(startSession1.optString(\"timestamp\"), String.valueOf(timestamp));\n\n        assertEquals(event.optString(\"event_type\"), \"test\");\n        assertEquals(event.optString(\"session_id\"), String.valueOf(timestamp));\n        assertEquals(event.optString(\"timestamp\"), String.valueOf(timestamp));\n\n        assertEquals(endSession.optString(\"event_type\"), AmplitudeClient.END_SESSION_EVENT);\n        assertEquals(\n                endSession.optJSONObject(\"api_properties\").optString(\"special\"),\n                AmplitudeClient.END_SESSION_EVENT\n        );\n        assertEquals(endSession.optString(\"session_id\"), String.valueOf(timestamp));\n        assertEquals(endSession.optString(\"timestamp\"), String.valueOf(timestamp));\n\n        assertEquals(startSession2.optString(\"event_type\"), AmplitudeClient.START_SESSION_EVENT);\n        assertEquals(\n                startSession2.optJSONObject(\"api_properties\").optString(\"special\"),\n                AmplitudeClient.START_SESSION_EVENT\n        );\n        assertEquals(startSession2.optString(\"session_id\"), String.valueOf(timestamps[0]));\n        assertEquals(startSession2.optString(\"timestamp\"), String.valueOf(timestamps[0]));\n    }\n\n\n    @Test\n    public void testLogOutOfSessionEvent() {\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        long sessionTimeoutMillis = 5*1000; //1s\n        amplitude.setSessionTimeoutMillis(sessionTimeoutMillis);\n\n        long timestamp1 = System.currentTimeMillis();\n        amplitude.logEventAsync(\"test1\", null, null, null, null, null, timestamp1, false);\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 1);\n\n        // log out of session event just within session expiration window\n        long timestamp2 = timestamp1 + sessionTimeoutMillis - 1;\n        amplitude.logEventAsync(\"outOfSession\", null, null, null, null, null, timestamp2, true);\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 2);\n\n        // out of session events do not extend session, 2nd event will start new session\n        long timestamp3 = timestamp1 + sessionTimeoutMillis;\n        amplitude.logEventAsync(\"test2\", null, null, null, null, null, timestamp3, false);\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 3);\n\n        JSONArray events = getUnsentEvents(3);\n        JSONObject event1 = events.optJSONObject(0);\n        JSONObject outOfSessionEvent = events.optJSONObject(1);\n        JSONObject event2 = events.optJSONObject(2);\n\n        assertEquals(event1.optString(\"event_type\"), \"test1\");\n        assertEquals(event1.optString(\"session_id\"), String.valueOf(timestamp1));\n        assertEquals(outOfSessionEvent.optString(\"event_type\"), \"outOfSession\");\n        assertEquals(outOfSessionEvent.optString(\"session_id\"), String.valueOf(-1));\n        assertEquals(event2.optString(\"event_type\"), \"test2\");\n        assertEquals(event2.optString(\"session_id\"), String.valueOf(timestamp3));\n    }\n\n    @Test\n    public void testOnPauseFlushEvents() throws JSONException {\n        long timestamp = System.currentTimeMillis();\n        long [] timestamps = {\n            timestamp, timestamp + 1, timestamp + 2,\n            timestamp + 3, timestamp + 4, timestamp + 5,\n        };\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        AmplitudeCallbacks callBacks = new AmplitudeCallbacksWithTime(amplitude, timestamps);\n        Robolectric.getForegroundThreadScheduler().advanceTo(1);\n\n        // log an event, should not be uploaded\n        amplitude.logEventAsync(\"testEvent\", null, null, null, null, null, timestamps[0], false);\n        looper.runOneTask();\n        looper.runOneTask();\n        assertEquals(getUnsentEventCount(), 1);\n\n        // force client into background and verify flushing of events\n        callBacks.onActivityPaused(null);\n        looper.runOneTask();  // run the update server\n        RecordedRequest request = runRequest(amplitude);\n        JSONArray events = getEventsFromRequest(request);\n        assertEquals(events.length(), 1);\n        assertEquals(events.getJSONObject(0).optString(\"event_type\"), \"testEvent\");\n\n        // verify that events have been cleared from client\n        looper.runOneTask();\n        assertEquals(getUnsentEventCount(), 0);\n    }\n\n    @Test\n    public void testOnPauseFlushEventsDisabled() throws JSONException {\n        long timestamp = System.currentTimeMillis();\n        long [] timestamps = {\n            timestamp, timestamp + 1, timestamp + 2,\n            timestamp + 3, timestamp + 4, timestamp + 5,\n        };\n        amplitude.setFlushEventsOnClose(false);\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        AmplitudeCallbacks callBacks = new AmplitudeCallbacksWithTime(amplitude, timestamps);\n        Robolectric.getForegroundThreadScheduler().advanceTo(1);\n\n        // log an event, should not be uploaded\n        amplitude.logEventAsync(\"testEvent\", null, null, null, null, null, timestamps[0], false);\n        looper.runOneTask();\n        assertEquals(getUnsentEventCount(), 1);\n\n        // force client into background and verify no flushing of events\n        callBacks.onActivityPaused(null);\n        looper.runOneTask();  // run the update server\n        RecordedRequest request = runRequest(amplitude);\n\n        // flushing disabled, so no request should be sent\n        assertNull(request);\n        assertEquals(getUnsentEventCount(), 1);\n    }\n\n    @Test\n    public void testIdentifyTriggerNewSession() throws JSONException {\n        amplitude.trackSessionEvents(true);\n\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        long sessionTimeoutMillis = 5 * 1000; //5s\n        amplitude.setSessionTimeoutMillis(sessionTimeoutMillis);\n\n        assertEquals(getUnsentEventCount(), 0);\n        assertEquals(getUnsentIdentifyCount(), 0);\n\n        // log 1st identify, initialize first session\n        Identify identify = new Identify().set(\"key\", \"value\");\n        amplitude.identify(identify);\n        looper.runToEndOfTasks();\n        // trackSessions is true, start_session event is added\n        assertEquals(getUnsentEventCount(), 1);\n        assertEquals(getUnsentIdentifyCount(), 0);\n\n        JSONArray events = getUnsentEvents(1);\n        assertEquals(\n            events.getJSONObject(0).optString(\"event_type\"), AmplitudeClient.START_SESSION_EVENT\n        );\n        looper.runToEndOfTasks();\n        JSONArray identifies = getUnsentIdentifys(1);\n        JSONObject expected = new JSONObject().put(\"$set\", new JSONObject().put(\"key\", \"value\"));\n        assertTrue(Utils.compareJSONObjects(\n            identifies.getJSONObject(0).getJSONObject(\"user_properties\"), expected\n        ));\n    }\n\n    @Test\n    public void testOutOfSessionIdentifyDoesNotTriggerNewSession() throws JSONException {\n        amplitude.trackSessionEvents(true);\n\n        ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());\n        long sessionTimeoutMillis = 5 * 1000; //5s\n        amplitude.setSessionTimeoutMillis(sessionTimeoutMillis);\n\n        assertEquals(getUnsentEventCount(), 0);\n        assertEquals(getUnsentIdentifyCount(), 0);\n\n        // log 1st identify, initialize first session\n        Identify identify = new Identify().set(\"key\", \"value\");\n        amplitude.identify(identify, true);\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentEventCount(), 0);  // out of session, start session is not added\n        assertEquals(getUnsentIdentifyCount(), 0);\n        looper.runToEndOfTasks();\n        assertEquals(getUnsentIdentifyCount(), 1);\n\n        JSONArray identifies = getUnsentIdentifys(1);\n        JSONObject expected = new JSONObject().put(\"$set\", new JSONObject().put(\"key\", \"value\"));\n        assertTrue(Utils.compareJSONObjects(\n            identifies.getJSONObject(0).getJSONObject(\"user_properties\"), expected\n        ));\n    }\n\n    @Test\n    public void testSetUserIdAndStartNewSessionWithTracking() {\n        amplitude.trackSessionEvents(true);\n\n        long timestamp = System.currentTimeMillis();\n        amplitude.logEventAsync(\"test\", null, null, null, null, null, timestamp, false);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n\n        // trackSessions is true, start_session event is added\n        assertEquals(getUnsentEventCount(), 2);\n\n        // set user id and validate session ended and new session started\n        amplitude.setUserId(\"test_new_user\", true);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n\n        // total of 4 events, start session, test event, end session, start session\n        assertEquals(getUnsentEventCount(), 4);\n        JSONArray events = getUnsentEvents(4);\n\n        // verify pre setUserId events\n        JSONObject session_event = events.optJSONObject(0);\n        JSONObject test_event = events.optJSONObject(1);\n        assertEquals(session_event.optString(\"event_type\"), AmplitudeClient.START_SESSION_EVENT);\n        assertEquals(session_event.optString(\"user_id\"), \"null\");\n        assertEquals(\n            session_event.optJSONObject(\"api_properties\").optString(\"special\"),\n            AmplitudeClient.START_SESSION_EVENT\n        );\n        assertEquals(session_event.optString(\"session_id\"), String.valueOf(timestamp));\n\n        assertEquals(test_event.optString(\"event_type\"), \"test\");\n        assertEquals(test_event.optString(\"session_id\"), String.valueOf(timestamp));\n        assertEquals(test_event.optString(\"user_id\"), \"null\");\n\n        // verify post setUserId events\n        session_event = events.optJSONObject(2);\n        assertEquals(session_event.optString(\"event_type\"), AmplitudeClient.END_SESSION_EVENT);\n        assertEquals(session_event.optString(\"user_id\"), \"null\");\n        assertEquals(\n            session_event.optJSONObject(\"api_properties\").optString(\"special\"),\n            AmplitudeClient.END_SESSION_EVENT\n        );\n        assertEquals(session_event.optString(\"session_id\"), String.valueOf(timestamp));\n\n        session_event = events.optJSONObject(3);\n        assertEquals(session_event.optString(\"event_type\"), AmplitudeClient.START_SESSION_EVENT);\n        assertEquals(session_event.optString(\"user_id\"), \"test_new_user\");\n        assertEquals(\n            session_event.optJSONObject(\"api_properties\").optString(\"special\"),\n            AmplitudeClient.START_SESSION_EVENT\n        );\n\n        // the new event should have a newer session id\n        assertTrue(session_event.optLong(\"session_id\") > timestamp);\n    }\n\n    @Test\n    public void testSetUserIdAndDoNotStartNewSessionWithTracking() {\n        amplitude.trackSessionEvents(true);\n\n        long timestamp = System.currentTimeMillis();\n        amplitude.logEventAsync(\"test\", null, null, null, null, null, timestamp, false);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n\n        // trackSessions is true, start_session event is added\n        assertEquals(getUnsentEventCount(), 2);\n\n        // set user id and validate session ended and new session started\n        amplitude.setUserId(\"test_new_user\", false);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n\n        // still only 2 events, start session, test event\n        assertEquals(getUnsentEventCount(), 2);\n        JSONArray events = getUnsentEvents(2);\n\n        // verify pre setUserId events\n        JSONObject session_event = events.optJSONObject(0);\n        JSONObject test_event = events.optJSONObject(1);\n        assertEquals(session_event.optString(\"event_type\"), AmplitudeClient.START_SESSION_EVENT);\n        assertEquals(session_event.optString(\"user_id\"), \"null\");\n        assertEquals(\n            session_event.optJSONObject(\"api_properties\").optString(\"special\"),\n            AmplitudeClient.START_SESSION_EVENT\n        );\n        assertEquals(session_event.optString(\"session_id\"), String.valueOf(timestamp));\n\n        assertEquals(test_event.optString(\"event_type\"), \"test\");\n        assertEquals(test_event.optString(\"session_id\"), String.valueOf(timestamp));\n        assertEquals(test_event.optString(\"user_id\"), \"null\");\n\n        // verify same session id\n        assertEquals(amplitude.sessionId, timestamp);\n    }\n\n    @Test\n    public void testSetUserIdAndStartNewSessionWithoutTracking() {\n        amplitude.trackSessionEvents(false);\n\n        long timestamp = System.currentTimeMillis();\n        amplitude.logEventAsync(\"test\", null, null, null, null, null, timestamp, false);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n\n        // trackSessions is false, there should only be 1 event\n        assertEquals(getUnsentEventCount(), 1);\n\n        // set user id and validate session ended and new session started\n        amplitude.setUserId(\"test_new_user\", true);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n\n        // still only 1 event1, test event\n        assertEquals(getUnsentEventCount(), 1);\n        JSONArray events = getUnsentEvents(1);\n\n        // verify pre setUserId events\n        JSONObject session_event = events.optJSONObject(0);\n        assertEquals(session_event.optString(\"event_type\"), \"test\");\n        assertEquals(session_event.optString(\"user_id\"), \"null\");\n        assertEquals(session_event.optString(\"session_id\"), String.valueOf(timestamp));\n\n        // log an event with new user id and session\n        amplitude.logEventAsync(\"test\", null, null, null, null, null, timestamp, false);\n        Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();\n\n        // verify post set user id\n        assertEquals(getUnsentEventCount(), 2);\n        JSONObject test_event = getLastEvent();\n        assertEquals(test_event.optString(\"event_type\"), \"test\");\n        assertEquals(test_event.optString(\"user_id\"), \"test_new_user\");\n        assertEquals(test_event.optLong(\"session_id\"), amplitude.sessionId);\n\n        // there should be a new session id at least\n        assertTrue(amplitude.sessionId > timestamp);\n        assertTrue(test_event.optLong(\"session_id\") > timestamp);\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/amplitude/api/TrackingOptionsTest.java",
    "content": "package com.amplitude.api;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.annotation.Config;\n\nimport java.util.HashSet;\nimport java.util.Set;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertTrue;\n\n@RunWith(AndroidJUnit4.class)\n@Config(manifest= Config.NONE)\npublic class TrackingOptionsTest extends BaseTest {\n\n    @Before\n    public void setUp() throws Exception { setUp(false); }\n\n    @After\n    public void tearDown() throws Exception {}\n\n    @Test\n    public void testDisableFields() {\n        TrackingOptions options = new TrackingOptions().disableCity().disableCountry().disableIpAddress().disableLanguage().disableLatLng();\n\n        Set<String> expectedDisabledFields = new HashSet<String>();\n        expectedDisabledFields.add(\"city\");\n        expectedDisabledFields.add(\"country\");\n        expectedDisabledFields.add(\"ip_address\");\n        expectedDisabledFields.add(\"language\");\n        expectedDisabledFields.add(\"lat_lng\");\n\n        assertEquals(options.disabledFields, expectedDisabledFields);\n        assertTrue(options.shouldTrackCarrier());\n        assertFalse(options.shouldTrackCity());\n        assertFalse(options.shouldTrackCountry());\n        assertTrue(options.shouldTrackDeviceBrand());\n        assertTrue(options.shouldTrackDeviceManufacturer());\n        assertTrue(options.shouldTrackDeviceModel());\n        assertTrue(options.shouldTrackDma());\n        assertFalse(options.shouldTrackIpAddress());\n        assertFalse(options.shouldTrackLanguage());\n        assertFalse(options.shouldTrackLatLng());\n        assertTrue(options.shouldTrackOsName());\n        assertTrue(options.shouldTrackOsVersion());\n        assertTrue(options.shouldTrackPlatform());\n        assertTrue(options.shouldTrackRegion());\n        assertTrue(options.shouldTrackVersionName());\n    }\n\n    @Test\n    public void testGetApiPropertiesTrackingOptions() throws JSONException {\n        TrackingOptions options = new TrackingOptions().disableCity().disableCountry().disableIpAddress().disableLanguage().disableLatLng();\n\n        JSONObject expectedOptions = new JSONObject();\n        expectedOptions.put(\"city\", false);\n        expectedOptions.put(\"country\", false);\n        expectedOptions.put(\"ip_address\", false);\n        expectedOptions.put(\"lat_lng\", false);\n\n        assertTrue(Utils.compareJSONObjects(options.getApiPropertiesTrackingOptions(), expectedOptions));\n    }\n\n    @Test\n    public void testGetCoppaControlTrackingOptions() {\n        TrackingOptions options = TrackingOptions.forCoppaControl();\n        assertFalse(options.shouldTrackAdid());\n        assertFalse(options.shouldTrackCity());\n        assertFalse(options.shouldTrackIpAddress());\n        assertFalse(options.shouldTrackLatLng());\n    }\n\n    @Test\n    public void testMerging() {\n        TrackingOptions options1 = TrackingOptions.forCoppaControl();\n        TrackingOptions options2 = new TrackingOptions().disableCountry().disableLanguage().disableAppSetId();\n        options1.mergeIn(options2);\n        assertFalse(options1.shouldTrackAdid());\n        assertFalse(options1.shouldTrackCity());\n        assertFalse(options1.shouldTrackIpAddress());\n        assertFalse(options1.shouldTrackCountry());\n        assertFalse(options1.shouldTrackLanguage());\n        assertFalse(options1.shouldTrackAppSetId());\n    }\n\n    @Test\n    public void testCopyOf() {\n        TrackingOptions options = TrackingOptions.copyOf(TrackingOptions.forCoppaControl());\n        assertFalse(options.shouldTrackAdid());\n        assertFalse(options.shouldTrackCity());\n        assertFalse(options.shouldTrackIpAddress());\n        assertFalse(options.shouldTrackLatLng());\n    }\n\n    @Test\n    public void testEquals() {\n        TrackingOptions options1 = new TrackingOptions();\n        options1.disableAdid().disableCarrier().disableAppSetId();\n        TrackingOptions options2 = new TrackingOptions();\n        options2.disableAdid().disableCarrier().disableAppSetId();\n        assertEquals(options1, options2);\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/amplitude/api/util/MockHttpURLConnectionHelper.java",
    "content": "package com.amplitude.api.util;\n\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.net.HttpURLConnection;\nimport java.nio.charset.StandardCharsets;\n\npublic class MockHttpURLConnectionHelper {\n\n    public static HttpURLConnection getMockHttpURLConnection(int code, String response)\n            throws IOException {\n        HttpURLConnection connection = mock(HttpURLConnection.class);\n        OutputStream outputStream = mock(OutputStream.class);\n        when(connection.getOutputStream()).thenReturn(outputStream);\n        when(connection.getResponseCode()).thenReturn(code);\n        InputStream inputStream = new ByteArrayInputStream(response.getBytes(StandardCharsets.UTF_8));\n        if (code == 200) {\n            when(connection.getInputStream()).thenReturn(inputStream);\n        } else {\n            when(connection.getErrorStream()).thenReturn(inputStream);\n        }\n        return connection;\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/amplitude/api/util/MockURLStreamHandler.java",
    "content": "package com.amplitude.api.util;\n\nimport java.io.IOException;\nimport java.net.URL;\nimport java.net.URLConnection;\nimport java.net.URLStreamHandler;\nimport java.net.URLStreamHandlerFactory;\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class MockURLStreamHandler extends URLStreamHandler implements URLStreamHandlerFactory {\n\n    private final Map<URL, URLConnection> connections = new HashMap<>();\n\n    private static final MockURLStreamHandler instance = new MockURLStreamHandler();\n\n    public static MockURLStreamHandler getInstance() {\n        return instance;\n    }\n\n    @Override\n    protected URLConnection openConnection(URL url) throws IOException {\n        return connections.get(url);\n    }\n\n    public void resetConnections() {\n        connections.clear();\n    }\n\n    public MockURLStreamHandler setConnection(URL url, URLConnection urlConnection) {\n        connections.put(url, urlConnection);\n        return this;\n    }\n\n    @Override\n    public URLStreamHandler createURLStreamHandler(String protocol) {\n        return getInstance();\n    }\n}\n"
  }
]