Repository: amplitude/Amplitude-Android
Branch: main
Commit: 089265eb1ff7
Files: 78
Total size: 691.8 KB
Directory structure:
gitextract_d5k27ryi/
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── Bug_report.md
│ │ ├── Feature_request.md
│ │ └── Question.md
│ └── workflows/
│ ├── codeql-analysis.yml
│ ├── jira-issue-create.yml
│ ├── release.yml
│ └── test.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── build.gradle
├── gradle.properties
├── gradlew
├── gradlew.bat
├── package.json
├── release.config.js
├── settings.gradle
└── src/
├── main/
│ ├── AndroidManifest.xml
│ ├── java/
│ │ └── com/
│ │ └── amplitude/
│ │ ├── api/
│ │ │ ├── Amplitude.java
│ │ │ ├── AmplitudeCallbacks.java
│ │ │ ├── AmplitudeClient.java
│ │ │ ├── AmplitudeDeviceIdCallback.java
│ │ │ ├── AmplitudeLog.java
│ │ │ ├── AmplitudeLogCallback.java
│ │ │ ├── AmplitudeServerZone.java
│ │ │ ├── ConfigManager.java
│ │ │ ├── Constants.java
│ │ │ ├── CursorWindowAllocationException.java
│ │ │ ├── DatabaseHelper.java
│ │ │ ├── DatabaseResetListener.java
│ │ │ ├── DeviceInfo.java
│ │ │ ├── Identify.java
│ │ │ ├── IdentifyInterceptor.java
│ │ │ ├── IngestionMetadata.java
│ │ │ ├── Middleware.java
│ │ │ ├── MiddlewareExtended.java
│ │ │ ├── MiddlewareExtra.java
│ │ │ ├── MiddlewareNext.java
│ │ │ ├── MiddlewarePayload.java
│ │ │ ├── MiddlewareRunner.java
│ │ │ ├── PinnedAmplitudeClient.java
│ │ │ ├── Plan.java
│ │ │ ├── Revenue.java
│ │ │ ├── TrackingOptions.java
│ │ │ ├── Utils.java
│ │ │ └── WorkerThread.java
│ │ ├── eventexplorer/
│ │ │ ├── EventExplorer.java
│ │ │ ├── EventExplorerInfoActivity.java
│ │ │ └── EventExplorerTouchHandler.java
│ │ ├── unity/
│ │ │ └── plugins/
│ │ │ └── AmplitudePlugin.java
│ │ └── util/
│ │ ├── DoubleCheck.java
│ │ └── Provider.java
│ └── res/
│ ├── drawable/
│ │ └── amp_button_bg.xml
│ ├── layout/
│ │ ├── amp_activity_eventexplorer_info.xml
│ │ └── amp_bubble_view.xml
│ └── values/
│ ├── colors.xml
│ └── strings.xml
└── test/
└── java/
└── com/
└── amplitude/
└── api/
├── AmplitudeClientTest.java
├── AmplitudeServerZoneTest.java
├── AmplitudeTest.java
├── BaseTest.java
├── ConfigManagerTest.java
├── DatabaseHelperTest.java
├── DatabaseRecoveryTest.java
├── DeviceInfoTest.java
├── IdentifyTest.java
├── IngestionMetadataTest.java
├── InitializeTest.java
├── MiddlewareRunnerTest.java
├── MockGeocoder.java
├── PinningTest.java
├── PlanTest.java
├── RevenueTest.java
├── SessionTest.java
├── TrackingOptionsTest.java
└── util/
├── MockHttpURLConnectionHelper.java
└── MockURLStreamHandler.java
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/ISSUE_TEMPLATE/Bug_report.md
================================================
---
name: Bug report 🐛
about: You're having technical issues
labels: 'bug'
---
## Expected Behavior
## Current Behavior
## Possible Solution
## Steps to Reproduce
1.
2.
3.
4.
## Environment
- SDK Version:
- Android API Level:
- Device:
================================================
FILE: .github/ISSUE_TEMPLATE/Feature_request.md
================================================
---
name: Feature Request 🚀
about: You'd like something added to the SDK
labels: 'feature request'
---
## Summary
## Motivations
================================================
FILE: .github/ISSUE_TEMPLATE/Question.md
================================================
---
name: Question ❓
about: Ask a question
labels: 'question'
---
## Summary
================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'java' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@b8d3b6e8af63cde30bdc382c0bc28114f4346c88 # v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@b8d3b6e8af63cde30bdc382c0bc28114f4346c88 # v2
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b8d3b6e8af63cde30bdc382c0bc28114f4346c88 # v2
================================================
FILE: .github/workflows/jira-issue-create.yml
================================================
# Creates jira tickets for new github issues to help triage
name: Jira Issue Creator For Android
on:
issues:
types: [opened]
workflow_call:
inputs:
label:
type: string
jobs:
call-workflow-passing-data:
uses: amplitude/Amplitude-TypeScript/.github/workflows/jira-issue-create-template.yml@8dadabbe62161729e3aa83c0d664e106b748c8cc # @amplitude/plugin-session-replay-react-native@0.4.9
with:
label: "Android"
subcomponent: "dx_legacy_android_sdk"
secrets:
JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
JIRA_PROJECT: ${{ secrets.JIRA_PROJECT }}
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
workflow_dispatch:
inputs:
dryRun:
description: 'Do a dry run to preview instead of a real release'
required: true
default: 'true'
jobs:
authorize:
name: Authorize
runs-on: ubuntu-latest
steps:
- name: ${{ github.actor }} permission check to do a release
uses: "lannonbr/repo-permission-check-action@2bb8c89ba8bf115c4bfab344d6a6f442b24c9a1f" # 2.0.2
with:
permission: "write"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release:
name: Release
runs-on: ubuntu-latest
needs: [authorize]
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set up JDK 8
uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4
with:
java-version: '8'
distribution: 'zulu'
- name: Build
run: ./gradlew build
- name: Test
run: ./gradlew test --info
- name: Configure GPG
env:
GPG_KEY_CONTENTS: ${{ secrets.GPG_KEY_CONTENTS }}
SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }}
run: |
sudo bash -c "echo '$GPG_KEY_CONTENTS' | base64 -d > '$SIGNING_SECRET_KEY_RING_FILE'"
- name: Configure Sonatype
env:
GRADLE_PROP_FILE: local.properties
run: |
echo "sonatypeUsername=${{ secrets.OSSRH_USERNAME }}" >> $GRADLE_PROP_FILE
echo "sonatypePassword=${{ secrets.OSSRH_PASSWORD }}" >> $GRADLE_PROP_FILE
echo "sonatypeStagingProfileId=${{ secrets.SONATYPE_STAGING_PROFILE_ID }}" >> $GRADLE_PROP_FILE
echo "signing.keyId=${{ secrets.SIGNING_KEY_ID }}" >> $GRADLE_PROP_FILE
echo "signing.password=${{ secrets.SIGNING_PASSWORD }}" >> $GRADLE_PROP_FILE
echo "signing.secretKeyRingFile=${{ secrets.SIGNING_SECRET_KEY_RING_FILE }}" >> $GRADLE_PROP_FILE
- name: Semantic Release --dry-run
if: ${{ github.event.inputs.dryRun == 'true'}}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GIT_AUTHOR_NAME: amplitude-sdk-bot
GIT_AUTHOR_EMAIL: amplitude-sdk-bot@users.noreply.github.com
GIT_COMMITTER_NAME: amplitude-sdk-bot
GIT_COMMITTER_EMAIL: amplitude-sdk-bot@users.noreply.github.com
run: |
npm ci
npm exec semantic-release -- --dry-run
- name: Semantic Release
if: ${{ github.event.inputs.dryRun == 'false'}}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GIT_AUTHOR_NAME: amplitude-sdk-bot
GIT_AUTHOR_EMAIL: amplitude-sdk-bot@users.noreply.github.com
GIT_COMMITTER_NAME: amplitude-sdk-bot
GIT_COMMITTER_EMAIL: amplitude-sdk-bot@users.noreply.github.com
run: |
npm ci
npm exec semantic-release
================================================
FILE: .github/workflows/test.yml
================================================
name: Build and Test
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
test:
name: Build and Test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set up JDK 8
uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4
with:
java-version: '8'
distribution: 'zulu'
- name: Build
run: ./gradlew build
- name: Test
run: ./gradlew test --info
================================================
FILE: .gitignore
================================================
# Gradle files
.gradle/
build/
distribution/
# Local configuration file (sdk path, etc)
local.properties
# Android Studio generated folders
.navigation/
captures/
.externalNativeBuild
caches/
daemon/
native/
wrapper/
# IntelliJ project files
*.iml
.idea/
# Java binary files
*.class
# Misc
.DS_Store
*.log
*.asc
*.bak
changes.txt
release.sh
node_modules/
================================================
FILE: CHANGELOG.md
================================================
## [2.40.3](https://github.com/amplitude/Amplitude-Android/compare/v2.40.2...v2.40.3) (2025-04-29)
### Bug Fixes
* default location listening to false ([#407](https://github.com/amplitude/Amplitude-Android/issues/407)) ([27d3758](https://github.com/amplitude/Amplitude-Android/commit/27d3758fd8427d44e15813356ada2e3234ad62b8))
## [2.40.2](https://github.com/amplitude/Amplitude-Android/compare/v2.40.1...v2.40.2) (2024-06-12)
### Bug Fixes
* catch all exceptions when trying to intercept identifies ([#402](https://github.com/amplitude/Amplitude-Android/issues/402)) ([f0350bf](https://github.com/amplitude/Amplitude-Android/commit/f0350bf5dff450fe34c1a9095b16082f2d6c5353))
## [2.40.1](https://github.com/amplitude/Amplitude-Android/compare/v2.40.0...v2.40.1) (2024-04-17)
### Bug Fixes
* always run middleware flush on updateServer ([#400](https://github.com/amplitude/Amplitude-Android/issues/400)) ([fbee357](https://github.com/amplitude/Amplitude-Android/commit/fbee357d6ba9eb540101cf09393f7eebafdfd49d))
# [2.40.0](https://github.com/amplitude/Amplitude-Android/compare/v2.39.9...v2.40.0) (2024-04-17)
### Features
* middleware session replay integration ([#399](https://github.com/amplitude/Amplitude-Android/issues/399)) ([28bbbe6](https://github.com/amplitude/Amplitude-Android/commit/28bbbe6ad2d0a0fe6424645e6105ecb8c2be7a4f))
## [2.39.9](https://github.com/amplitude/Amplitude-Android/compare/v2.39.8...v2.39.9) (2024-02-27)
### Bug Fixes
* move identifyInterceptor before eventBridge receiver is set ([#392](https://github.com/amplitude/Amplitude-Android/issues/392)) ([a6d4c9d](https://github.com/amplitude/Amplitude-Android/commit/a6d4c9d1ad8cfc99420e92ed39164feaf3e5eac1))
## [2.39.8](https://github.com/amplitude/Amplitude-Android/compare/v2.39.7...v2.39.8) (2023-07-21)
### Bug Fixes
* try to correctly handle session change for race condition ([#382](https://github.com/amplitude/Amplitude-Android/issues/382)) ([b0f4fea](https://github.com/amplitude/Amplitude-Android/commit/b0f4fea58d808129c0bd46a7624f8a1e541bc6c6))
## [2.39.7](https://github.com/amplitude/Amplitude-Android/compare/v2.39.6...v2.39.7) (2023-07-07)
### Bug Fixes
* try to reduce sqlite cursor memory usage ([#375](https://github.com/amplitude/Amplitude-Android/issues/375)) ([77e508d](https://github.com/amplitude/Amplitude-Android/commit/77e508d9b8cbaf36328f3176e89c04f502c606c6))
## [2.39.6](https://github.com/amplitude/Amplitude-Android/compare/v2.39.5...v2.39.6) (2023-07-06)
### Bug Fixes
* remove md5 usage ([#372](https://github.com/amplitude/Amplitude-Android/issues/372)) ([c849590](https://github.com/amplitude/Amplitude-Android/commit/c84959086f9a645f87a3175caaec3022154ee6bb))
## [2.39.5](https://github.com/amplitude/Amplitude-Android/compare/v2.39.4...v2.39.5) (2023-06-13)
### Bug Fixes
* do not fetch advertising Id if adid tracking is disabled ([#366](https://github.com/amplitude/Amplitude-Android/issues/366)) ([a1f8cc8](https://github.com/amplitude/Amplitude-Android/commit/a1f8cc80af0bbaf2b7edc3b47b87c3f518ea8433))
## [2.39.4](https://github.com/amplitude/Amplitude-Android/compare/v2.39.3...v2.39.4) (2023-06-01)
### Bug Fixes
* move inForeground reads/writes to main thread ([#362](https://github.com/amplitude/Amplitude-Android/issues/362)) ([15b4b35](https://github.com/amplitude/Amplitude-Android/commit/15b4b35187da132adeeb35e6726e5b76bc48f75f))
## [2.39.3](https://github.com/amplitude/Amplitude-Android/compare/v2.39.2...v2.39.3) (2023-04-29)
### Bug Fixes
* filter null value in identify user properties ([#356](https://github.com/amplitude/Amplitude-Android/issues/356)) ([779b6b3](https://github.com/amplitude/Amplitude-Android/commit/779b6b3246e01b0c408ecae7437b7f40c7067f22))
* update identify interceptor to identify only ([#357](https://github.com/amplitude/Amplitude-Android/issues/357)) ([afd3251](https://github.com/amplitude/Amplitude-Android/commit/afd3251f75c30b130a42805e49b31673916cedf1))
### As of September 21, 2020 CHANGELOG.md is no longer manually updated.
Please check the [releases page](https://github.com/amplitude/Amplitude-Android/releases) for up to date changes.
## 2.28.2 (Sep 13, 2020)
* Add `setMinTimeBetweenSessionsMillis` in plugin for Unity Plugin to use.
## 2.28.1 (Aug 26, 2020)
* Add `setOffline` in plugin for Unity Plugin to use.
## 2.28.0 (Aug 10, 2020)
* Introducing useDynamicConfig flag!! Turning this flag on will find the best server url automatically based on users' geo location.
* Note 1. If you have your own proxy server and use setServerUrl API, please leave this OFF.
* Note 2. If you have users in China Mainland, we suggest you turn this on.
* Note 3. By default, this feature is OFF. So you need to explicitly set it to ON to use it.
## 2.27.0 (Jul 14, 2020)
* Added setServerUrl to `AmplitudePlugin` to enable it for Unity SDK too.
* Fix an issue during location fetching.
## 2.26.1 (Jun 15, 2020)
* Fix the incorrect behavior of `disableLocationListening`. If you want to disable location listening over LocationManager. Please call called before initialization.
## 2.26.0 (Jun 2, 2020)
* Remove ComodoRSA certificate for SSL pinning.
## 2.25.2 (May 13, 2020)
* Add 3 APIs to `AmplitudePlugin` (`uploadEvents`, `useAdvertisingIdForDeviceId`, `setDeviceId`)
## 2.25.1 (Apr 3, 2020)
* Remove the declaration of location related permissions in manifest file.
## 2.25.0 (Mar 17, 2020)
* Added APIs to `AmplitudeClient` to let users set library name and version. This should be only used when you develop your own library which wraps Amplitude Android SDK.
## 2.24.2 (Feb 5, 2020)
* Now you can set auth token! Use `AmplitudeClient#setBearerToken(String token)` please!
## 2.24.1 (Jan 28, 2020)
* Fix the issue that `version` property shows old version.
## 2.24.0 (Jan 28, 2020)
* Now you can enable or disable COPPA (Children's Online Privacy Protection Act) restrictions on ADID, city, IP address and location tracking. `AmplitudeClient#enableCoppaControl()` and `AmplitudeClient#disableCoppaControl()`
## 2.23.2 (Aug 05, 2019)
* Catch exceptions when fetching most recent location.
## 2.23.1 (Jul 19, 2019)
* Handle SQLite database crashes caused by fetching events that exceed 2MB (max size of cursor window).
## 2.23.0 (Apr 22, 2019)
* Make `startNewSessionIfNeeded` a public method. Only call this if you know what you are doing. This may trigger a new session to start.
## 2.22.1 (Mar 21, 2019)
* Store deviceId in SharedPreferences as backup in case SQLite database fails or becomes corrupted.
## 2.22.0 (Jan 18, 2019)
* Add ability to set a custom server URL for uploading events using `setServerUrl`.
## 2.21.0 (Dec 05, 2018)
* Update SDK to better handle when the SQLite database file gets corrupted between interactions.
* Add optional diagnostic logging that tracks exceptions thrown in the SDK and sends to Amplitude.
## 2.20.0 (Oct 15, 2018)
* Add ability to set group properties via a new `groupIdentify` method that takes in an `Identify` object as well as a group type and group name.
## 2.19.1 (Aug 14, 2018)
* Update SDK to better handle SQLite Exceptions.
## 2.19.0 (Jul 24, 2018)
* Add `TrackingOptions` interface to customize the automatic tracking of user properties in the SDK (such as language, ip_address, platform, etc). See [Help Center Documentation](https://amplitude.zendesk.com/hc/en-us/articles/115002935588#disable-automatic-tracking-of-properties) for instructions on setting up this configuration.
## 2.18.2 (Jul 24, 2018)
* Use randomly generated device id if user has limitAdTracking enabled.
## 2.18.1 (May 07, 2018)
* Updating to [OkHttp 3.10.0](https://github.com/square/okhttp/blob/master/CHANGELOG.md#version-3100)
* Lowering event upload max batch size from 100 to 50. This should help to avoid out of memory issues on Android devices with low memory.
## 2.18.0 (Apr 19, 2018)
* Added a `setUserId` method with optional boolean argument `startNewSession`, which when `true` starts a new session after changing the userId.
## 2.17.0 (Feb 05, 2018)
* Add ability to specify a custom `platform` value during initialization as an input argument. If the value is `null` or an empty string then `platform` will default to `Android`.
## 2.16.0 (Nov 27, 2017)
* Expose a public `getUserPropertiesOperations` method on the `Identify` class.
* Handle exceptions when the LocationManager is not available for fetching location.
## 2.15.0 (Oct 04, 2017)
* Updating to latest version of OkHttp3 ([3.9.0](https://github.com/square/okhttp/blob/master/CHANGELOG.md#version-390))
## 2.14.1 (Jul 27, 2017)
* Switch to an internal implementation of `isEmptyString` instead of Android TextUtils.
## 2.14.0 (Jul 05, 2017)
* Add support for logging events to multiple Amplitude apps. See our [Help Center Documentation](https://amplitude.zendesk.com/hc/en-us/articles/115002935588#logging-events-to-multiple-projects) for details.
## 2.13.4 (May 09, 2017)
* Handle exceptions when fetching device carrier information. Thanks to @fkam-tt for the pull request.
* Copy userProperties on main thread in `setUserProperties` to prevent ConcurrentModificationExceptions.
* Migrating setup instructions and SDK documentation in the README file to Zendesk articles.
## 2.13.3 (Mar 13, 2017)
* Handle exceptions when reading from database. Only affects certain Fairphone and LG devices.
* Handle exceptions when building request to upload event data. Only affects certain Lenovo devices.
## 2.13.2 (Dec 22, 2016)
* Fix crash when pulling null unsent event strings during upload.
* Fix bug where unserializable events were being saved to unsent events table.
* Added more logging around JSON serialization errors when logging events.
## 2.13.1 (Dec 15, 2016)
* Fix bug where `regenerateDeviceId` was not being run on background thread. DeviceInfo.generateUUID() should be a static method.
## 2.13.0 (Dec 05, 2016)
* Add helper method to regenerate a new random deviceId. This can be used in conjunction with `setUserId(null)` to anonymize a user after they log out. Note this is not recommended unless you know what you are doing. See [Readme](https://github.com/amplitude/Amplitude-Android#logging-out-and-anonymous-users) for more information.
## 2.12.0 (Nov 07, 2016)
* Allow `logEvent` with a custom timestamp (milliseconds since epoch). See [documentation](https://rawgit.com/amplitude/Amplitude-Android/master/javadoc/com/amplitude/api/AmplitudeClient.html#logEvent-java.lang.String-org.json.JSONObject-org.json.JSONObject-org.json.JSONObject-org.json.JSONObject-long-boolean-) for more details.
## 2.11.0 (Oct 26, 2016)
* Add ability to log identify events outOfSession, this is useful for updating user properties without triggering session-handling logic. See [Readme](https://github.com/amplitude/Amplitude-Android#tracking-sessions) for more information.
## 2.10.0 (Oct 12, 2016)
* Catch and handle `CursorWindowAllocationException` thrown when the SDK is querying from the SQLite DB when app memory is low. If the exception is caught during `initialize`, then it is treated as if `initialize` was never called. If the exception is caught during the uploading of unsent events, then the upload is deferred to a later time.
* Block event property and user property dictionaries that have more than 1000 items. This is to block properties that are set unintentionally (for example in a loop). A single call to `logEvent` should not have more than 1000 event properties. Similarly a single call to `setUserProperties` should not have more than 1000 user properties.
* Handle IllegalArgumentException thrown by Android Geocoder for bad lat / lon values.
## 2.9.2 (Jul 14, 2016)
* Fix bug where `enableLocationListening` and `disableLocationListening` were not being run on background thread. Thanks to @elevenfive for PR.
* Update `Revenue` class to expose public `equals` and `hashCode` methods.
## 2.9.1 (Jul 11, 2016)
* Fix bug where `setOptOut` was not being run on background thread.
* `productId` is no longer a required field for `Revenue` logged via `logRevenueV2`.
* Fix bug where receipt and receiptSignature were being truncated if they were too long (exceeded 1024 characters).
## 2.9.0 (Jul 07, 2016)
* Add automatic flushing of unsent events on app close/minimize (through the Activity Lifecycle `onPause` callback). This only works if you call `Amplitude.getInstance().enableForegroundTracking(getApplication());`, which is recommended in the README by default for Setup. To disable you can call `Amplitude.getInstance().setFlushEventsOnClose(false);`
## 2.8.0 (Jun 29, 2016)
* Run the `initialize` logic on the background thread so that the SQLite database operations do not delay the main thread.
* Add support for Amazon Advertising ID (use in place of Google Advertising ID on Amazon devices). Thanks to @jcomo for the pull request.
## 2.7.2 (May 24, 2016)
* Add documentation for SDK functions. You can take a look [here](https://rawgit.com/amplitude/Amplitude-Android/master/javadoc/index.html). A link has also been added to the Readme.
* Fix bug where fetching the user's location on select devices throws a SecurityException, causing a crash.
## 2.7.1 (Apr 19, 2016)
* RevenueProperties is a confusing name and should actually be eventProperties. Deprecating Revenue.setRevenueProperties and replacing it with Revenue.setEventProperties, and clarified in Readme.
## 2.7.0 (Apr 19, 2016)
* Add support setting groups for users and events. See [Readme](https://github.com/amplitude/Amplitude-Android#setting-groups) for more information.
* Add helper method `getSessionId` to expose the current sessionId value.
* Add `logRevenueV2` and new `Revenue` class to support logging revenue events with properties, revenue type, and verified. See [Readme](https://github.com/amplitude/Amplitude-Android#tracking-revenue) for more info.
* Fix crash when trying to enableForegroundTracking with the PinnedAmplitudeClient. AmplitudeClient methods should be using `this` instead of static `instance` variable.
## 2.6.0 (Mar 29, 2016)
* Update to OKHttp v3.0.1.
* Add support for prepend user property operation.
* Fix bug where merging events for upload causes array index out of bounds exception.
* Migrate shared preferences (userId and event meta data) to Sqlite db to support apps with multiple processes.
## 2.5.1 (Mar 14, 2016)
* Fix bug where updateServer sets the wrong batchLimit when limit is false.
## 2.5.0 (Jan 15, 2016)
* Add ability to clear all user properties.
* Check that SDK is initialized when user calls enableForegroundTracking, identify, setUserProperties.
## 2.4.0 (Dec 15, 2015)
* Add support for append user property operation.
## 2.3.0 (Nov 30, 2015)
* Log if Google Play Services is enabled for the application.
## 2.2.0 (Oct 20, 2015)
* Removed all references to Apache HTTPClient to support Android M.
* Handle exceptions when fetching last known location from LocationManager.
* Add ability to set custom deviceId.
* Handle exception when cloning JSON object.
* Maintain only one instance of OKHttpClient.
* Add AmplitudeLog helper class that supports enabling and disabling of logging as well as setting of the log level.
* Fix bug where event and identify queues are not truncated if eventMaxCount is less than eventRemoveBatchSize.
## 2.1.0 (Oct 04, 2015)
* Add support for user properties operations (set, setOnce, add, unset).
* Fix bug where end session event was not being sent upon app reopen.
## 2.0.4 (Sep 23, 2015)
* Fix bug where deviceInfo was trying to use Geocoder if none present.
## 2.0.3 (Sep 22, 2015)
* Fix bug where deviceId was being fetched on main thread.
## 2.0.2 (Aug 24, 2015)
* Fix Maven jar, fixed build file.
## 2.0.1 (Aug 21, 2015)
* Catch all exceptions thrown by Android TelephonyManager and NullPointerExceptions thrown by geocoder during country lookup.
## 2.0.0 (Aug 20, 2015)
* Expose user ID with getUserId.
* Simplified session tracking. No longer need to call startSession and endSession. No longer send start/end session events by default. Added foreground tracking for sessions that uses Android activity lifecycles.
* The minimum supported API level is 9. API level 14 is required for foreground tracking.
* Always track Android advertising ID (ADID) regardless of limit ad tracking enabled.
* Track if limit ad tracking enabled as an API property for each logged event.
* Database upgraded to version 2: added a new store table for key value pairs.
* Device ID is now saved to and reloaded from the SQLite database (instead of SharedPrefs because SharedPrefs currently does not support multiple processes).
* MessageDigest.getInstance(String) is not threadsafe (known Android issue). Replaced with alternate MD5 implementation from http://org.rodage.com/pub/java/security/MD5.java.
* Create a copy of input userProperties JSONObject in setUserProperties to try and prevent ConcurrentModificationException.
## 1.7.0 (May 29, 2015)
* Enable configuration of eventUploadThreshold, eventMaxCount,
eventUploadMaxBatchSize, eventUploadPeriodSeconds, minTimeBetweenSessionsMillis,
and sessionTimeoutMillis.
## 1.6.3 (May 06, 2015)
* Add offline mode to turn off server uploading for a time.
* Add synchronous logging. Logs events to the DB synchronously to guarantee event persistence.
## 1.6.2 (Apr 17, 2015)
* Change protection on AmplitudeClient to public.
## 1.6.1 (Apr 13, 2015)
* Fix double class inclusion in jar distribution
## 1.6.0 (Apr 08, 2015)
* Fix crash under aggressive proguard optimizations.
* Fix device id being lost occasionally on app update.
* Fix exception when calling logEvent with empty JSONObject.
* Log a DEBUG message on each event.
## 1.5.0 (Mar 24, 2015)
* Add PinnedAmplitudeClient to support SSL pinning.
* Deprecate static methods on Amplitude. Switch to using Amplitude.getInstance().
* Upgrade HTTP client to okhttp.
## 1.4.6 (Mar 16, 2015)
* Fix bug when initializing with user id. Api key was not set properly.
## 1.4.4 (Mar 11, 2015)
* Expose setUserProperties(JSONObject, boolean) as a static
* Handle null edge cases in location request
* Add user opt out support
* Merge user properties in setUserProperties by default
* Refactor Amplitude to be a singleton to support tests
* Add option to disable fine-grained location tracking
* Fix crash: ConcurrentModificationException in HashMap
* Fix crash: CursorWindowAllocationException in SQLite
## 1.4.3 (Nov 13, 2014)
* Update field names, split platform and os, and send library information
## 1.4.2 (Nov 7, 2014)
* Don't log end session event if session isn't open
* Fix creating a new session id when the previous session id is invalid or non existant
## 1.4.1 (Jul 16, 2014)
* Hotfix extra class file in jar.
## 1.4.0 (Jul 1, 2014)
* Send androidADID with events
* Use Google Play Advertising ID instead of Android ID, if set. Default / fall back on using a random UUID
* Pull country from reverse geocode, then telephony network country, then locale
## 1.3.0 (Jun 4, 2014)
* Add getDeviceId to unity plugin
* Add additional logRevenue methods for receipt validation
* Make device ID public
* Fix bug where first event was getting skipped from upload
* Catch SQLiteExceptions
* Catch exceptions through by Apache HTTPClient
## 1.0.0 (May 1, 2014)
* Initial packaged release
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2014 Amplitude Analytics
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Announcement 📣
Amplitude is introducing a new [Android Kotlin SDK](https://github.com/amplitude/Amplitude-Kotlin). This new SDK provides improved developer experience, helps users instrument data more seamlessly and provide more control over data being instrumented using custom plugins.
To learn more about the new SDK, here are some useful links:
* Maven Central: https://search.maven.org/artifact/com.amplitude/analytics-android
* GitHub: https://github.com/amplitude/Amplitude-Kotlin
* Documentation: https://www.docs.developers.amplitude.com/data/sdks/android-kotlin
# Official Amplitude Android SDK
##### _February 17, 2023_ - [v2.39.2](https://github.com/amplitude/Amplitude-Android/releases/tag/v2.39.2)
## Amplitude and Ampli SDK
[Ampli SDK](https://www.docs.developers.amplitude.com/data/ampli) is autogenerated library based on your pre-defined [tracking plan](https://help.amplitude.com/hc/en-us/articles/5078731378203-Create-a-tracking-plan). The Ampli SDK, is a lightweight wrapper over the Amplitude SDK that provides type-safety, supports linting, and enables features like input validation. The code replicates the spec in the Tracking Plan and enforces its rules and requirements. This repository is about **Amplitude SDK**. To learn more about Ampli SDK, please refer to the [Ampli Android](https://www.docs.developers.amplitude.com/data/sdks/android-ampli/) and [examples](https://github.com/amplitude/ampli-examples).
## Installation and Quick Start
Please visit our :100:[Developer Center](https://www.docs.developers.amplitude.com/data/sdks/android/) for instructions on installing and using our the SDK.
## Javadoc
See our [Android SDK Reference](http://amplitude.github.io/Amplitude-Android/) for a list and description of all available SDK methods.
## Demo Applications
* A [demo application](https://github.com/amplitude/Android-Demo) showing the integration of our SDK using Gradle.
* A [demo application](https://github.com/amplitude/Segment-Android-Demo) showing the integration of our SDK using [Segment's](https://segment.com) Android SDK.
* A [demo application](https://github.com/amplitude/GTM-Android-Demo) demonstrating a potential integration with Google Tag Manager.
## Changelog
Click [here](https://github.com/amplitude/Amplitude-Android/wiki/Changelog) to view the Android SDK Changelog.
## Need Help?
If you have any problems or issues over our SDK, feel free to [create a github issue](https://github.com/amplitude/Amplitude-Android/issues/new) or submit a request on [Amplitude Help](https://help.amplitude.com/hc/en-us/requests/new).
================================================
FILE: build.gradle
================================================
group = ARTIFACT_GROUP
version = ARTIFACT_VERSION
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.2'
}
}
apply plugin: 'com.android.library'
apply plugin: 'maven'
apply plugin: 'signing'
ext {
artifactId = 'amplitude-android-sdk'
}
repositories {
// The order in which you list these repositories matter.
google()
jcenter()
}
android {
compileSdkVersion 28
buildToolsVersion '28.0.3'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion 14
// Note: Can't target to the latest 29 now, since running Robolectric tests on 29 will
// require Java 9 above. However, Android Studio will error out when setting up Java to 9
// above.
targetSdkVersion 28
buildConfigField "String", "AMPLITUDE_VERSION", "\"${version}\""
testInstrumentationRunner 'androidx.test.ext.junit.runners.AndroidJUnit4'
// The following argument makes the Android Test Orchestrator run its
// "pm clear" command after each test invocation. This command ensures
// that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true'
}
lintOptions {
abortOnError true
textReport true
warningsAsErrors false
}
testOptions {
unitTests.includeAndroidResources = true
}
}
dependencies {
implementation 'com.amplitude:analytics-connector:1.0.0'
implementation 'com.squareup.okhttp3:okhttp:4.2.2'
testImplementation 'com.squareup.okhttp3:mockwebserver:4.2.2'
testImplementation 'org.robolectric:robolectric:4.3.1'
testImplementation 'org.robolectric:shadows-maps:3.4-rc2'
testImplementation 'org.powermock:powermock-module-junit4:1.6.6'
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.6'
testImplementation 'org.powermock:powermock-api-mockito:1.6.6'
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.6'
testImplementation 'com.google.android:support-v4:r6'
testImplementation 'com.google.android.gms:play-services-ads:18.3.0'
testImplementation 'com.google.android.gms:play-services-base:17.1.0'
testImplementation 'org.json:json:20140107'
testImplementation "junit:junit:4.12"
// Core library
testImplementation 'androidx.test:core:1.2.0'
// AndroidJUnitRunner and JUnit Rules
testImplementation 'androidx.test:runner:1.2.0'
testImplementation 'androidx.test:rules:1.2.0'
// Assertions
testImplementation 'androidx.test.ext:junit:1.1.1'
}
File secretPropsFile = project.rootProject.file('local.properties')
if (secretPropsFile.exists()) {
// Read local.properties file first if it exists
Properties p = new Properties()
new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) }
p.each { name, value -> ext[name] = value }
}
// ======== For SDK Releases ========
uploadArchives {
repositories.mavenDeployer {
beforeDeployment {
MavenDeployment deployment -> signing.signPom(deployment)
}
pom.groupId = ARTIFACT_GROUP
pom.version = ARTIFACT_VERSION
pom.project {
name project.name
version ARTIFACT_VERSION
packaging POM_PACKAGING
description POM_DESCRIPTION
url POM_URL
scm {
url POM_SCM_URL
connection POM_SCM_CONNECTION
developerConnection POM_SCM_DEV_CONNECTION
}
licenses {
license {
name POM_LICENCE_NAME
url POM_LICENCE_URL
distribution POM_LICENCE_DIST
}
}
developers {
developer {
id POM_DEVELOPER_ID
name POM_DEVELOPER_NAME
email POM_DEVELOPER_EMAIL
organization POM_DEVELOPER_ORG
organizationUrl POM_DEVELOPER_ORG_URL
}
}
}
pom.whenConfigured { pom ->
pom.dependencies*.optional = true
pom.dependencies.find { dep ->
dep.groupId == 'com.amplitude' && dep.artifactId == 'analytics-connector'
}.optional = false
}
repository(url: RELEASE_REPOSITORY_URL) {
authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
}
snapshotRepository(url: SNAPSHOT_REPOSITORY_URL) {
authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
}
}
}
task install(type: Upload, dependsOn: assemble) {
repositories.mavenInstaller {
configuration = configurations.archives
pom.groupId = ARTIFACT_GROUP
pom.version = ARTIFACT_VERSION
pom.project {
name project.name
version ARTIFACT_VERSION
packaging POM_PACKAGING
description POM_DESCRIPTION
url POM_URL
scm {
url POM_SCM_URL
connection POM_SCM_CONNECTION
developerConnection POM_SCM_DEV_CONNECTION
}
licenses {
license {
name POM_LICENCE_NAME
url POM_LICENCE_URL
distribution POM_LICENCE_DIST
}
}
developers {
developer {
id POM_DEVELOPER_ID
name POM_DEVELOPER_NAME
email POM_DEVELOPER_EMAIL
organization POM_DEVELOPER_ORG
organizationUrl POM_DEVELOPER_ORG_URL
}
}
}
}
}
task androidJavadocs(type: Javadoc, dependsOn: ':generateReleaseBuildConfig') {
source = android.sourceSets.main.java.srcDirs
classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
classpath += project.files('build/generated/source/buildConfig/release')
exclude(
'**/R.*',
'**/security/**',
'**/unity/**',
'**/api/AmplitudeLog.java',
'**/api/Constants.java',
'**/api/DeviceInfo.java',
'**/api/Utils.java',
'**/api/WorkerThread.java',
'**/api/CursorWindowAllocationException.java'
)
options {
encoding = 'UTF-8'
docEncoding = 'UTF-8'
charSet = 'UTF-8'
}
failOnError false
}
task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) {
classifier = 'javadoc'
from androidJavadocs.destinationDir
}
task androidSourcesJar(type: Jar) {
classifier = 'sources'
from android.sourceSets.main.java.srcDirs
}
signing {
required { isReleaseBuild() && gradle.taskGraph.hasTask(":uploadArchives") }
sign configurations.archives
}
artifacts {
archives androidSourcesJar
archives androidJavadocsJar
}
def isReleaseBuild() {
return version.contains("SNAPSHOT") == false
}
def getRepositoryUsername() {
return hasProperty('sonatypeUsername') ? sonatypeUsername : ""
}
def getRepositoryPassword() {
return hasProperty('sonatypePassword') ? sonatypePassword : ""
}
================================================
FILE: gradle.properties
================================================
ARTIFACT_VERSION=2.40.3
ARTIFACT_GROUP=com.amplitude
POM_PACKAGING=aar
POM_DESCRIPTION=Amplitude Android SDK
POM_URL=https://github.com/amplitude/Amplitude-Android
POM_SCM_URL=https://github.com/amplitude/Amplitude-Android
POM_SCM_CONNECTION=scm:git:http://github.com/amplitude/Amplitude-Android
POM_SCM_DEV_CONNECTION=scm:git:git@github.com:amplitude/Amplitude-Android.git
POM_LICENCE_NAME=The MIT License
POM_LICENCE_URL=http://www.opensource.org/licenses/mit-license.php
POM_LICENCE_DIST=repo
POM_DEVELOPER_ID=amplitude_sdk_dev
POM_DEVELOPER_NAME=Amplitude SDK Developers
POM_DEVELOPER_EMAIL=sdk.dev@amplitude.com
POM_DEVELOPER_ORG=Amplitude
POM_DEVELOPER_ORG_URL=https://amplitude.com/
RELEASE_REPOSITORY_URL=https://oss.sonatype.org/service/local/staging/deploy/maven2/
SNAPSHOT_REPOSITORY_URL=https://oss.sonatype.org/content/repositories/snapshots/
================================================
FILE: gradlew
================================================
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
================================================
FILE: gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: package.json
================================================
{
"private": true,
"dependencies": {
"lodash": "4.17.21",
"semantic-release": "17.4.7",
"@semantic-release/changelog": "5.0.1",
"@semantic-release/git": "9.0.1",
"@google/semantic-release-replace-plugin": "1.2.0",
"@semantic-release/exec": "5.0.0"
}
}
================================================
FILE: release.config.js
================================================
module.exports = {
"branches": [
{name: 'beta', prerelease: true},
"main"
],
"tagFormat": ["v${version}"],
"plugins": [
["@semantic-release/commit-analyzer", {
"preset": "angular",
"parserOpts": {
"noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES", "BREAKING"]
}
}],
["@semantic-release/release-notes-generator", {
"preset": "angular",
}],
["@semantic-release/changelog", {
"changelogFile": "CHANGELOG.md"
}],
"@semantic-release/github",
[
"@google/semantic-release-replace-plugin",
{
"replacements": [
{
"files": ["gradle.properties"],
"from": "ARTIFACT_VERSION=.*",
"to": "ARTIFACT_VERSION=${nextRelease.version}",
"results": [
{
"file": "gradle.properties",
"hasChanged": true,
"numMatches": 1,
"numReplacements": 1
}
],
"countMatches": true
},
]
}
],
["@semantic-release/git", {
"assets": ["gradle.properties", "CHANGELOG.md"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}],
["@semantic-release/exec", {
"publishCmd": "./gradlew uploadArchives",
}],
],
}
================================================
FILE: settings.gradle
================================================
rootProject.name = 'android-sdk'
================================================
FILE: src/main/AndroidManifest.xml
================================================
================================================
FILE: src/main/java/com/amplitude/api/Amplitude.java
================================================
package com.amplitude.api;
import android.content.Context;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.Map;
/**
*
Amplitude
* This is the main Amplitude class that manages SDK instances.
* NOTE: All of the methods except {@code getInstance()} have been deprecated.
* Please call those methods on the AmplitudeClient instance instead, for example:
* {@code Amplitude.getInstance().logEvent();}
*
* @see com.amplitude.api.AmplitudeClient AmplitudeClient
*/
public class Amplitude {
static final Map instances = new HashMap();
/**
* Gets the default instance.
*
* @return the default instance
*/
public static AmplitudeClient getInstance() {
return getInstance(null);
}
/**
* Gets the specified instance. If instance is null or empty string, fetches the default
* instance instead.
*
* @param instance name to get "ex app 1"
* @return the specified instance
*/
public static synchronized AmplitudeClient getInstance(String instance) {
instance = Utils.normalizeInstanceName(instance);
AmplitudeClient client = instances.get(instance);
if (client == null) {
client = new AmplitudeClient(instance);
instances.put(instance, client);
}
return client;
}
/**
* Initialize the SDK with the Android app context and Amplitude API key.
* Initializing is required before calling other methods such as {@code logEvent();}.
*
* @param context the context
* @param apiKey the api key
*/
@Deprecated
public static void initialize(Context context, String apiKey) {
getInstance().initialize(context, apiKey);
}
/**
* Initialize the SDK with the Android app context, Amplitude API key, and a user Id.
* Initializing is required before calling other methods such as {@code logEvent();}.
*
* @param context the context
* @param apiKey the api key
* @param userId the user id
*/
@Deprecated
public static void initialize(Context context, String apiKey, String userId) {
getInstance().initialize(context, apiKey, userId);
}
/**
* Enable new device id per install.
*
* @param newDeviceIdPerInstall the new device id per install
*/
@Deprecated
public static void enableNewDeviceIdPerInstall(boolean newDeviceIdPerInstall) {
getInstance().enableNewDeviceIdPerInstall(newDeviceIdPerInstall);
}
/**
* Use advertising id for device id.
*/
@Deprecated
public static void useAdvertisingIdForDeviceId() {
getInstance().useAdvertisingIdForDeviceId();
}
/**
* Enable location listening.
*/
@Deprecated
public static void enableLocationListening() {
getInstance().enableLocationListening();
}
/**
* Disable location listening.
*/
@Deprecated
public static void disableLocationListening() {
getInstance().disableLocationListening();
}
/**
* Sets session timeout millis.
*
* @param sessionTimeoutMillis the session timeout millis
*/
@Deprecated
public static void setSessionTimeoutMillis(long sessionTimeoutMillis) {
getInstance().setSessionTimeoutMillis(sessionTimeoutMillis);
}
/**
* Sets opt out.
*
* @param optOut the opt out
*/
@Deprecated
public static void setOptOut(boolean optOut) {
getInstance().setOptOut(optOut);
}
/**
* Log event.
*
* @param eventType the event type
*/
@Deprecated
public static void logEvent(String eventType) {
getInstance().logEvent(eventType);
}
/**
* Log event.
*
* @param eventType the event type
* @param eventProperties the event properties
*/
@Deprecated
public static void logEvent(String eventType, JSONObject eventProperties) {
getInstance().logEvent(eventType, eventProperties);
}
/**
* Upload events.
*/
@Deprecated
public static void uploadEvents() {
getInstance().uploadEvents();
}
/**
* Start session.
*/
@Deprecated
public static void startSession() { return; }
/**
* End session.
*/
@Deprecated
public static void endSession() { return; }
/**
* Log revenue.
*
* @param amount the amount
*/
@Deprecated
public static void logRevenue(double amount) {
getInstance().logRevenue(amount);
}
/**
* Log revenue.
*
* @param productId the product id
* @param quantity the quantity
* @param price the price
*/
@Deprecated
public static void logRevenue(String productId, int quantity, double price) {
getInstance().logRevenue(productId, quantity, price);
}
/**
* Log revenue.
*
* @param productId the product id
* @param quantity the quantity
* @param price the price
* @param receipt the receipt
* @param receiptSignature the receipt signature
*/
@Deprecated
public static void logRevenue(String productId, int quantity, double price, String receipt,
String receiptSignature) {
getInstance().logRevenue(productId, quantity, price, receipt, receiptSignature);
}
/**
* Sets user properties.
*
* @param userProperties the user properties
*/
@Deprecated
public static void setUserProperties(JSONObject userProperties) {
getInstance().setUserProperties(userProperties);
}
/**
* Sets user properties.
*
* @param userProperties the user properties
* @param replace the replace
*/
@Deprecated
public static void setUserProperties(JSONObject userProperties, boolean replace) {
getInstance().setUserProperties(userProperties, replace);
}
/**
* Sets user id.
*
* @param userId the user id
*/
@Deprecated
public static void setUserId(String userId) {
getInstance().setUserId(userId);
}
/**
* Gets device id.
*
* @return the device id
*/
@Deprecated
public static String getDeviceId() {
return getInstance().getDeviceId();
}
}
================================================
FILE: src/main/java/com/amplitude/api/AmplitudeCallbacks.java
================================================
package com.amplitude.api;
import android.app.Activity;
import android.app.Application;
import android.os.Bundle;
class AmplitudeCallbacks implements Application.ActivityLifecycleCallbacks {
private static final String TAG = AmplitudeCallbacks.class.getName();
private static final String NULLMSG = "Need to initialize AmplitudeCallbacks with AmplitudeClient instance";
private AmplitudeClient clientInstance = null;
private static AmplitudeLog logger = AmplitudeLog.getLogger();
public AmplitudeCallbacks(AmplitudeClient clientInstance) {
if (clientInstance == null) {
logger.e(TAG, NULLMSG);
return;
}
this.clientInstance = clientInstance;
clientInstance.useForegroundTracking();
}
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {}
@Override
public void onActivityDestroyed(Activity activity) {}
@Override
public void onActivityPaused(Activity activity) {
if (clientInstance == null) {
logger.e(TAG, NULLMSG);
return;
}
clientInstance.onExitForeground(getCurrentTimeMillis());
}
@Override
public void onActivityResumed(Activity activity) {
if (clientInstance == null) {
logger.e(TAG, NULLMSG);
return;
}
clientInstance.onEnterForeground(getCurrentTimeMillis());
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outstate) {}
@Override
public void onActivityStarted(Activity activity) {}
@Override
public void onActivityStopped(Activity activity) {}
protected long getCurrentTimeMillis() {
return System.currentTimeMillis();
}
}
================================================
FILE: src/main/java/com/amplitude/api/AmplitudeClient.java
================================================
package com.amplitude.api;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.location.Location;
import android.os.Build;
import android.util.Pair;
import com.amplitude.analytics.connector.AnalyticsConnector;
import com.amplitude.analytics.connector.Identity;
import com.amplitude.analytics.connector.util.JSONUtil;
import com.amplitude.eventexplorer.EventExplorer;
import com.amplitude.util.DoubleCheck;
import com.amplitude.util.Provider;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import kotlin.Unit;
import okhttp3.Call;
import okhttp3.FormBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
/**
*
AmplitudeClient
* This is the SDK instance class that contains all of the SDK functionality.
* Note: call the methods on the default shared instance in the Amplitude class,
* for example: {@code Amplitude.getInstance().logEvent();}
* Many of the SDK functions return the SDK instance back, allowing you to chain multiple method
* calls together, for example: {@code Amplitude.getInstance().initialize(this, "APIKEY").enableForegroundTracking(getApplication())}
*/
public class AmplitudeClient {
/**
* The class identifier tag used in logging. TAG = {@code "com.amplitude.api.AmplitudeClient";}
*/
private static final String TAG = AmplitudeClient.class.getName();
/**
* The event type for start session events.
*/
public static final String START_SESSION_EVENT = "session_start";
/**
* The event type for end session events.
*/
public static final String END_SESSION_EVENT = "session_end";
/**
* The pref/database key for the device ID value.
*/
public static final String DEVICE_ID_KEY = "device_id";
/**
* The pref/database key for the user ID value.
*/
public static final String USER_ID_KEY = "user_id";
/**
* The pref/database key for the opt out flag.
*/
public static final String OPT_OUT_KEY = "opt_out";
/**
* The pref/database key for the sequence number.
*/
public static final String SEQUENCE_NUMBER_KEY = "sequence_number";
/**
* The pref/database key for the last event time.
*/
public static final String LAST_EVENT_TIME_KEY = "last_event_time";
/**
* The pref/database key for the last event ID value.
*/
public static final String LAST_EVENT_ID_KEY = "last_event_id";
/**
* The pref/database key for the last identify ID value.
*/
public static final String LAST_IDENTIFY_ID_KEY = "last_identify_id";
/**
* The pref/database key for the previous session ID value.
*/
public static final String PREVIOUS_SESSION_ID_KEY = "previous_session_id";
private static final AmplitudeLog logger = AmplitudeLog.getLogger();
/**
* The Android App Context.
*/
protected Context context;
/**
* The shared OkHTTPClient instance.
*/
protected Call.Factory callFactory;
/**
* The shared Amplitude database helper instance.
*/
protected DatabaseHelper dbHelper;
/**
* The Amplitude App API key.
*/
protected String apiKey;
/**
* The name for this instance of AmplitudeClient.
*/
protected String instanceName;
/**
* The user's ID value.
*/
protected String userId;
/**
* The user's Device ID value.
*/
protected String deviceId;
private boolean newDeviceIdPerInstall = false;
private boolean useAdvertisingIdForDeviceId = false;
private boolean useAppSetIdForDeviceId = false;
protected boolean initialized = false;
private AmplitudeDeviceIdCallback deviceIdCallback;
private boolean optOut = false;
private boolean offline = false;
TrackingOptions inputTrackingOptions = new TrackingOptions();
TrackingOptions appliedTrackingOptions = TrackingOptions.copyOf(inputTrackingOptions);
JSONObject apiPropertiesTrackingOptions = appliedTrackingOptions.getApiPropertiesTrackingOptions();
private boolean coppaControlEnabled = false;
private boolean locationListening = false;
private EventExplorer eventExplorer;
private Plan plan;
private IdentifyInterceptor identifyInterceptor;
/**
* The ingestion metadata.
*/
private IngestionMetadata ingestionMetadata;
/**
* Amplitude Server Zone
*/
private AmplitudeServerZone serverZone = AmplitudeServerZone.US;
/**
* The device's Platform value.
*/
protected String platform;
/**
* Event metadata
*/
long sessionId = -1;
long sequenceNumber = 0;
long lastEventId = -1;
long lastIdentifyId = -1;
long lastEventTime = -1;
long previousSessionId = -1;
protected DeviceInfo deviceInfo;
/**
* The current session ID value.
*/
private int eventUploadThreshold = Constants.EVENT_UPLOAD_THRESHOLD;
private int eventUploadMaxBatchSize = Constants.EVENT_UPLOAD_MAX_BATCH_SIZE;
private int eventMaxCount = Constants.EVENT_MAX_COUNT;
private long eventUploadPeriodMillis = Constants.EVENT_UPLOAD_PERIOD_MILLIS;
private long minTimeBetweenSessionsMillis = Constants.MIN_TIME_BETWEEN_SESSIONS_MILLIS;
private long identifyBatchIntervalMillis= Constants.IDENTIFY_BATCH_INTERVAL_MILLIS;
private long sessionTimeoutMillis = Constants.SESSION_TIMEOUT_MILLIS;
private boolean backoffUpload = false;
private int backoffUploadBatchSize = eventUploadMaxBatchSize;
private boolean usingForegroundTracking = false;
private boolean trackingSessionEvents = false;
private boolean inForeground = false;
private boolean isEnteringForeground = false;
private boolean flushEventsOnClose = true;
private String libraryName = Constants.LIBRARY;
private String libraryVersion = Constants.VERSION;
private boolean useDynamicConfig = false;
private AtomicBoolean updateScheduled = new AtomicBoolean(false);
/**
* Whether or not the SDK is in the process of uploading events.
*/
AtomicBoolean uploadingCurrently = new AtomicBoolean(false);
/**
* The last SDK error - used for testing.
*/
Throwable lastError;
/**
* The url for Amplitude API endpoint
*/
String url = Constants.EVENT_LOG_URL;
/**
* The Bearer Token for authentication
*/
String bearerToken = null;
/**
* The background event logging worker thread instance.
*/
WorkerThread logThread = new WorkerThread("logThread");
/**
* The background event uploading worker thread instance.
*/
WorkerThread httpThread = new WorkerThread("httpThread");
/**
* The core package for integrating with the Experiment SDK.
*/
final AnalyticsConnector connector;
/**
* The runner for middleware
*/
MiddlewareRunner middlewareRunner = new MiddlewareRunner();
/**
* Instantiates a new default instance AmplitudeClient and starts worker threads.
*/
public AmplitudeClient() {
this(null);
}
/**
* Instantiates a new AmplitudeClient with instance name and starts worker threads.
* @param instance
*/
public AmplitudeClient(String instance) {
this.instanceName = Utils.normalizeInstanceName(instance);
logThread.start();
httpThread.start();
this.connector = AnalyticsConnector.getInstance(this.instanceName);
}
/**
* Initialize the Amplitude SDK with the Android application context and your Amplitude
* App API key. Note: initialization is required before you log events and modify
* user properties.
*
* @param context the Android application context
* @param apiKey your Amplitude App API key
* @return the AmplitudeClient
*/
public AmplitudeClient initialize(Context context, String apiKey) {
return initialize(context, apiKey, null);
}
/**
* Initialize the Amplitude SDK with the Android application context, your Amplitude App API
* key, and a user ID for the current user. Note: initialization is required before
* you log events and modify user properties.
*
* @param context the Android application context
* @param apiKey your Amplitude App API key
* @param userId the user id to set
* @return the AmplitudeClient
*/
public AmplitudeClient initialize(Context context, String apiKey, String userId) {
return initialize(context, apiKey, userId, null, false);
}
/**
* Initialize the Amplitude SDK with the Android application context, your Amplitude App API
* key, a user ID for the current user, and a custom platform value.
* Note: initialization is required before you log events and modify user properties.
*
* @param context the Android application context
* @param apiKey your Amplitude App API key
* @param userId the user id to set
* @param
* @return the AmplitudeClient
*/
public synchronized AmplitudeClient initialize(
final Context context,
final String apiKey,
final String userId,
final String platform,
final boolean enableDiagnosticLogging
) {
return this.initializeInternal(
context,
apiKey,
userId,
platform,
enableDiagnosticLogging,
null);
}
/**
* Initialize the Amplitude SDK with the Android application context, your Amplitude App API
* key, a user ID for the current user, and a custom platform value.
* Note: initialization is required before you log events and modify user properties.
*
* @param context the Android application context
* @param apiKey your Amplitude App API key
* @param userId the user id to set
* @param callFactory the call factory that used by Amplitude to make http request
* @return the AmplitudeClient
*/
public synchronized AmplitudeClient initialize(
final Context context,
final String apiKey,
final String userId,
final String platform,
final boolean enableDiagnosticLogging,
final Call.Factory callFactory
) {
return this.initializeInternal(
context,
apiKey,
userId,
platform,
enableDiagnosticLogging,
callFactory);
}
/**
* Initialize the Amplitude SDK with the Android application context, your Amplitude App API
* key, a user ID for the current user, and a custom platform value.
* Note: initialization is required before you log events and modify user properties.
*
* @param context the Android application context
* @param apiKey your Amplitude App API key
* @param userId the user id to set
* @param
* @return the AmplitudeClient
*/
public synchronized AmplitudeClient initializeInternal(
final Context context,
final String apiKey,
final String userId,
final String platform,
final boolean enableDiagnosticLogging,
final Call.Factory callFactory
) {
if (context == null) {
logger.e(TAG, "Argument context cannot be null in initialize()");
return this;
}
if (Utils.isEmptyString(apiKey)) {
logger.e(TAG, "Argument apiKey cannot be null or blank in initialize()");
return this;
}
this.context = context.getApplicationContext();
this.apiKey = apiKey;
this.dbHelper = DatabaseHelper.getDatabaseHelper(this.context, this.instanceName);
this.platform = Utils.isEmptyString(platform) ? Constants.PLATFORM : platform;
final AmplitudeClient client = this;
runOnLogThread(() -> {
if (!initialized) {
// this try block is idempotent, so it's safe to retry initialize if failed
try {
if (callFactory == null) {
// defer OkHttp client to first call
final Provider callProvider
= DoubleCheck.provider(OkHttpClient::new);
this.callFactory = request -> callProvider.get().newCall(request);
} else {
this.callFactory = callFactory;
}
if (useDynamicConfig) {
ConfigManager.getInstance().refresh(new ConfigManager.RefreshListener() {
@Override
public void onFinished() {
url = ConfigManager.getInstance().getIngestionEndpoint();
}
}, serverZone);
}
deviceInfo = initializeDeviceInfo();
deviceId = initializeDeviceId();
if (this.deviceIdCallback != null) {
this.deviceIdCallback.onDeviceIdReady(deviceId);
}
if (userId != null) {
client.userId = userId;
dbHelper.insertOrReplaceKeyValue(USER_ID_KEY, userId);
} else {
client.userId = dbHelper.getValue(USER_ID_KEY);
}
identifyInterceptor = new IdentifyInterceptor(dbHelper, logThread, identifyBatchIntervalMillis, this);
// set up listener to core package to receive exposure events from Experiment
connector.getEventBridge().setEventReceiver(analyticsEvent -> {
String eventType = analyticsEvent.getEventType();
JSONObject eventProperties = JSONUtil.toJSONObject(analyticsEvent.getEventProperties());
JSONObject userProperties = JSONUtil.toJSONObject(analyticsEvent.getUserProperties());
logEventAsync(eventType, eventProperties, null, userProperties,
null, null, getCurrentTimeMillis(), false);
return Unit.INSTANCE;
});
// Set user ID and device ID in core identity store for use in Experiment SDK
connector.getIdentityStore().setIdentity(new Identity(userId, deviceId, new HashMap<>()));
// May take some time...
deviceInfo.prefetch();
final Long optOutLong = dbHelper.getLongValue(OPT_OUT_KEY);
optOut = optOutLong != null && optOutLong == 1;
// try to restore previous session id
previousSessionId = getLongvalue(PREVIOUS_SESSION_ID_KEY, -1);
if (previousSessionId >= 0) {
sessionId = previousSessionId;
}
// reload event meta data
sequenceNumber = getLongvalue(SEQUENCE_NUMBER_KEY, 0);
lastEventId = getLongvalue(LAST_EVENT_ID_KEY, -1);
lastIdentifyId = getLongvalue(LAST_IDENTIFY_ID_KEY, -1);
lastEventTime = getLongvalue(LAST_EVENT_TIME_KEY, -1);
// install database reset listener to re-insert metadata in memory
dbHelper.setDatabaseResetListener(new DatabaseResetListener() {
@Override
public void onDatabaseReset(SQLiteDatabase db) {
dbHelper.insertOrReplaceKeyValueToTable(db, DatabaseHelper.STORE_TABLE_NAME, DEVICE_ID_KEY, client.deviceId);
dbHelper.insertOrReplaceKeyValueToTable(db, DatabaseHelper.STORE_TABLE_NAME, USER_ID_KEY, client.userId);
dbHelper.insertOrReplaceKeyValueToTable(db, DatabaseHelper.LONG_STORE_TABLE_NAME, OPT_OUT_KEY, client.optOut ? 1L : 0L);
dbHelper.insertOrReplaceKeyValueToTable(db, DatabaseHelper.LONG_STORE_TABLE_NAME, PREVIOUS_SESSION_ID_KEY, client.sessionId);
dbHelper.insertOrReplaceKeyValueToTable(db, DatabaseHelper.LONG_STORE_TABLE_NAME, LAST_EVENT_TIME_KEY, client.lastEventTime);
}
});
initialized = true;
} catch (CursorWindowAllocationException e) { // treat as uninitialized SDK
logger.e(TAG, String.format(
"Failed to initialize Amplitude SDK due to: %s", e.getMessage()
));
client.apiKey = null;
}
}
});
return this;
}
/**
* Enable foreground tracking for the SDK. This is HIGHLY RECOMMENDED, and will allow
* for accurate session tracking.
*
* @param app the Android application
* @return the AmplitudeClient
* @see
* Tracking Sessions
*/
public AmplitudeClient enableForegroundTracking(Application app) {
if (usingForegroundTracking || !contextAndApiKeySet("enableForegroundTracking()")) {
return this;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
app.registerActivityLifecycleCallbacks(new AmplitudeCallbacks(this));
}
return this;
}
/**
* @deprecated - We removed Diagnostics class and this function has no-op.
* Will completely remove it in the near future.
*/
public AmplitudeClient enableDiagnosticLogging() {
return this;
}
/**
* @deprecated - We removed Diagnostics class and this function has no-op.
* Will completely remove it in the near future.
*/
public AmplitudeClient disableDiagnosticLogging() {
return this;
}
/**
* @deprecated - We removed Diagnostics class and this function has no-op.
* Will completely remove it in the near future.
*/
public AmplitudeClient setDiagnosticEventMaxCount(int eventMaxCount) {
return this;
}
/**
* Whether to set a new device ID per install. If true, then the SDK will always generate a new
* device ID on app install (as opposed to re-using an existing value like ADID).
*
* @param newDeviceIdPerInstall whether to set a new device ID on app install.
* @return the AmplitudeClient
* @deprecated
*/
public AmplitudeClient enableNewDeviceIdPerInstall(boolean newDeviceIdPerInstall) {
this.newDeviceIdPerInstall = newDeviceIdPerInstall;
return this;
}
/**
* Whether to use the Android advertising ID (ADID) as the user's device ID.
*
* @return the AmplitudeClient
*/
public AmplitudeClient useAdvertisingIdForDeviceId() {
useAdvertisingIdForDeviceId = true;
return this;
}
/**
* Use Android app set id as the user's device ID.
*
* @return the AmplitudeClient
*/
public AmplitudeClient useAppSetIdForDeviceId() {
useAppSetIdForDeviceId = true;
return this;
}
/**
* Enable location listening in the SDK. This will add the user's current lat/lon coordinates
* to every event logged.
*
* This function should be called before SDK initialization, e.g. {@link #initialize(Context, String)}.
*
* @return the AmplitudeClient
*/
public AmplitudeClient enableLocationListening() {
this.locationListening = true;
if (this.deviceInfo != null) {
this.deviceInfo.setLocationListening(true);
}
return this;
}
/**
* Disable location listening in the SDK. This will stop the sending of the user's current
* lat/lon coordinates.
*
* This function should be called before SDK initialization, e.g. {@link #initialize(Context, String)}.
*
* @return the AmplitudeClient
*/
public AmplitudeClient disableLocationListening() {
this.locationListening = false;
if (this.deviceInfo != null) {
this.deviceInfo.setLocationListening(false);
}
return this;
}
/**
* Sets event upload threshold. The SDK will attempt to batch upload unsent events
* every eventUploadPeriodMillis milliseconds, or if the unsent event count exceeds the
* event upload threshold.
*
* @param eventUploadThreshold the event upload threshold
* @return the AmplitudeClient
*/
public AmplitudeClient setEventUploadThreshold(int eventUploadThreshold) {
this.eventUploadThreshold = eventUploadThreshold;
return this;
}
/**
* Sets event upload max batch size. This controls the maximum number of events sent with
* each upload request.
*
* @param eventUploadMaxBatchSize the event upload max batch size
* @return the AmplitudeClient
*/
public AmplitudeClient setEventUploadMaxBatchSize(int eventUploadMaxBatchSize) {
this.eventUploadMaxBatchSize = eventUploadMaxBatchSize;
this.backoffUploadBatchSize = eventUploadMaxBatchSize;
return this;
}
/**
* Sets event max count. This is the maximum number of unsent events to keep on the device
* (for example if the device does not have internet connectivity and cannot upload events).
* If the number of unsent events exceeds the max count, then the SDK begins dropping events,
* starting from the earliest logged.
*
* @param eventMaxCount the event max count
* @return the AmplitudeClient
*/
public AmplitudeClient setEventMaxCount(int eventMaxCount) {
this.eventMaxCount = eventMaxCount;
return this;
}
/**
* Sets event upload period millis. The SDK will attempt to batch upload unsent events
* every eventUploadPeriodMillis milliseconds, or if the unsent event count exceeds the
* event upload threshold.
*
* @param eventUploadPeriodMillis the event upload period millis
* @return the AmplitudeClient
*/
public AmplitudeClient setEventUploadPeriodMillis(int eventUploadPeriodMillis) {
this.eventUploadPeriodMillis = eventUploadPeriodMillis;
return this;
}
/**
* Sets min time between sessions millis.
*
* @param minTimeBetweenSessionsMillis the min time between sessions millis
* @return the min time between sessions millis
*/
public AmplitudeClient setMinTimeBetweenSessionsMillis(long minTimeBetweenSessionsMillis) {
this.minTimeBetweenSessionsMillis = minTimeBetweenSessionsMillis;
return this;
}
/**
* Sets min time for identify batch millis.
*
* @param identifyBatchIntervalMillis the time interval for identify batch interval
* @return the AmplitudeClient
*/
public AmplitudeClient setIdentifyBatchIntervalMillis(long identifyBatchIntervalMillis) {
if (identifyBatchIntervalMillis < eventUploadPeriodMillis) {
logger.w(TAG, "Warning: minimum batch interval is event upload period.");
return this;
}
this.identifyBatchIntervalMillis = identifyBatchIntervalMillis;
if (this.identifyInterceptor != null) {
identifyInterceptor.setIdentifyBatchIntervalMillis(identifyBatchIntervalMillis);
}
return this;
}
/**
* Sets a custom server url for event upload.
*
* We now have a new method setServerZone. To send data to Amplitude's EU servers, recommend to
* use setServerZone method like client.setServerZone(AmplitudeServerZone.EU);
*
* @param serverUrl - a string url for event upload.
* @return the AmplitudeClient
*/
public AmplitudeClient setServerUrl(String serverUrl) {
if (!Utils.isEmptyString(serverUrl)) {
url = serverUrl;
}
return this;
}
/**
* Set Bearer Token to be included in request header.
* @param token
* @return the AmplitudeClient
*/
public AmplitudeClient setBearerToken(String token) {
this.bearerToken = token;
return this;
}
/**
* Sets session timeout millis. If foreground tracking has not been enabled with
* @{code enableForegroundTracking()}, then new sessions will be started after
* sessionTimeoutMillis milliseconds have passed since the last event logged.
*
* @param sessionTimeoutMillis the session timeout millis
* @return the AmplitudeClient
*/
public AmplitudeClient setSessionTimeoutMillis(long sessionTimeoutMillis) {
this.sessionTimeoutMillis = sessionTimeoutMillis;
return this;
}
public AmplitudeClient setTrackingOptions(TrackingOptions trackingOptions) {
inputTrackingOptions = trackingOptions;
appliedTrackingOptions = TrackingOptions.copyOf(inputTrackingOptions);
if (coppaControlEnabled) {
appliedTrackingOptions.mergeIn(TrackingOptions.forCoppaControl());
}
apiPropertiesTrackingOptions = appliedTrackingOptions.getApiPropertiesTrackingOptions();
return this;
}
/**
* Enable COPPA (Children's Online Privacy Protection Act) restrictions on ADID, city, IP address and location tracking.
* This can be used by any customer that does not want to collect ADID, city, IP address and location tracking.
*/
public AmplitudeClient enableCoppaControl() {
coppaControlEnabled = true;
appliedTrackingOptions.mergeIn(TrackingOptions.forCoppaControl());
apiPropertiesTrackingOptions = appliedTrackingOptions.getApiPropertiesTrackingOptions();
return this;
}
/**
* Disable COPPA (Children's Online Privacy Protection Act) restrictions on ADID, city, IP address and location tracking.
*/
public AmplitudeClient disableCoppaControl() {
coppaControlEnabled = false;
appliedTrackingOptions = TrackingOptions.copyOf(inputTrackingOptions);
apiPropertiesTrackingOptions = appliedTrackingOptions.getApiPropertiesTrackingOptions();
return this;
}
/**
* Sets opt out. If true then the SDK does not track any events for the user.
*
* @param optOut whether or not to opt the user out of tracking
* @return the AmplitudeClient
*/
public AmplitudeClient setOptOut(final boolean optOut) {
if (!contextAndApiKeySet("setOptOut()")) {
return this;
}
final AmplitudeClient client = this;
runOnLogThread(new Runnable() {
@Override
public void run() {
if (Utils.isEmptyString(apiKey)) { // in case initialization failed
return;
}
client.optOut = optOut;
dbHelper.insertOrReplaceKeyLongValue(OPT_OUT_KEY, optOut ? 1L : 0L);
}
});
return this;
}
public Boolean getOptOut() {
return optOut;
}
/**
* Library name is default as `amplitude-android`.
* Notice: You will only want to set it when following conditions are met.
* 1. You develop your own library which bridges Amplitude Android native library.
* 2. You want to track your library as one of the data sources.
*/
public AmplitudeClient setLibraryName(final String libraryName) {
this.libraryName = libraryName;
return this;
}
/**
* Library version is default as the latest Amplitude Android SDK version.
* Notice: You will only want to set it when following conditions are met.
* 1. You develop your own library which bridges Amplitude Android native library.
* 2. You want to track your library as one of the data sources.
*/
public AmplitudeClient setLibraryVersion(final String libraryVersion) {
this.libraryVersion = libraryVersion;
return this;
}
/**
* Returns whether or not the user is opted out of tracking.
*
* @return the optOut flag value
*/
public boolean isOptedOut() {
return optOut;
}
/**
* Enable/disable message logging by the SDK.
*
* @param enableLogging whether to enable message logging by the SDK.
* @return the AmplitudeClient
*/
public AmplitudeClient enableLogging(boolean enableLogging) {
logger.setEnableLogging(enableLogging);
return this;
}
/**
* Sets the logging level. Logging messages will only appear if they are the same severity
* level or higher than the set log level.
*
* @param logLevel the log level
* @return the AmplitudeClient
*/
public AmplitudeClient setLogLevel(int logLevel) {
logger.setLogLevel(logLevel);
return this;
}
/**
* Set log callback, it can help read and collect error message from sdk
*
* @param callback
* @return the AmplitudeClient
*/
public AmplitudeClient setLogCallback(AmplitudeLogCallback callback) {
logger.setAmplitudeLogCallback(callback);
return this;
}
/**
* Sets offline. If offline is true, then the SDK will not upload events to Amplitude servers;
* however, it will still log events.
*
* @param offline whether or not the SDK should be offline
* @return the AmplitudeClient
*/
public AmplitudeClient setOffline(boolean offline) {
this.offline = offline;
// Try to update to the server once offline mode is disabled.
if (!offline) {
uploadEvents();
}
return this;
}
/**
* Enable/disable flushing of unsent events on app close (enabled by default).
*
* @param flushEventsOnClose whether to flush unsent events on app close
* @return the AmplitudeClient
*/
public AmplitudeClient setFlushEventsOnClose(boolean flushEventsOnClose) {
this.flushEventsOnClose = flushEventsOnClose;
return this;
}
/**
* Track session events amplitude client. If enabled then the SDK will automatically send
* start and end session events to mark the start and end of the user's sessions.
*
* @param trackingSessionEvents whether to enable tracking of session events
* @return the AmplitudeClient
*/
public AmplitudeClient trackSessionEvents(boolean trackingSessionEvents) {
this.trackingSessionEvents = trackingSessionEvents;
return this;
}
/**
* Turning this flag on will find the best server url automatically based on users' geo location.
* Note:
* 1. If you have your own proxy server and use `setServerUrl` API, please leave this off.
* 2. If you have users in China Mainland, we suggest you turn this on.
*
* @param useDynamicConfig whether to enable dynamic config
* @return the AmplitudeClient
*/
public AmplitudeClient setUseDynamicConfig(boolean useDynamicConfig) {
this.useDynamicConfig = useDynamicConfig;
return this;
}
/**
* Show Amplitude Event Explorer for the given activity.
*
* @param activity root activity
*/
public void showEventExplorer(Activity activity) {
if (this.eventExplorer == null) {
this.eventExplorer = new EventExplorer(this.instanceName);
}
this.eventExplorer.show(activity);
}
/**
* Set foreground tracking to true.
*/
void useForegroundTracking() {
usingForegroundTracking = true;
}
/**
* Whether foreground tracking is enabled.
*
* @return whether foreground tracking is enabled
*/
boolean isUsingForegroundTracking() { return usingForegroundTracking; }
/**
* Add middleware to the middleware runner
*/
public void addEventMiddleware(Middleware middleware) {
middlewareRunner.add(middleware);
}
/**
* Whether app is in the foreground.
*
* @return whether app is in the foreground
*/
boolean isInForeground() { return inForeground; }
/**
* Log an event with the specified event type.
* Note: this is asynchronous and happens on a background thread.
*
* @param eventType the event type
*/
public void logEvent(String eventType) {
logEvent(eventType, null);
}
/**
* Log an event with the specified event type and event properties.
* Note: this is asynchronous and happens on a background thread.
*
* @param eventType the event type
* @param eventProperties the event properties
*/
public void logEvent(String eventType, JSONObject eventProperties) {
logEvent(eventType, eventProperties, false);
}
/**
* Log an event with the specified event type, event properties, with optional out of session
* flag. If out of session is true, then the sessionId will be -1 for the event, indicating
* that it is not part of the current session. Note: this might be useful when logging events
* for notifications received.
* Note: this is asynchronous and happens on a background thread.
*
* @param eventType the event type
* @param eventProperties the event properties
* @param extra the extra unstructured data for middleware
*/
public void logEvent(String eventType, JSONObject eventProperties, MiddlewareExtra extra) {
logEvent(eventType, eventProperties, null, getCurrentTimeMillis(), false, extra);
}
/**
* Log an event with the specified event type, event properties, with optional out of session
* flag. If out of session is true, then the sessionId will be -1 for the event, indicating
* that it is not part of the current session. Note: this might be useful when logging events
* for notifications received.
* Note: this is asynchronous and happens on a background thread.
*
* @param eventType the event type
* @param eventProperties the event properties
* @param outOfSession the out of session
*/
public void logEvent(String eventType, JSONObject eventProperties, boolean outOfSession) {
logEvent(eventType, eventProperties, null, outOfSession);
}
/**
* Log an event with the specified event type, event properties, and groups. Use this to set
* event-level groups, meaning the group(s) set only apply for this specific event and does
* not persist on the user.
* Note: this is asynchronous and happens on a background thread.
*
* @param eventType the event type
* @param eventProperties the event properties
* @param groups the groups
*/
public void logEvent(String eventType, JSONObject eventProperties, JSONObject groups) {
logEvent(eventType, eventProperties, groups, false);
}
/**
* Log event with the specified event type, event properties, groups, with optional out of
* session flag. If out of session is true, then the sessionId will be -1 for the event,
* indicating that it is not part of the current session. Note: this might be useful when
* logging events for notifications received.
* Note: this is asynchronous and happens on a background thread.
*
* @param eventType the event type
* @param eventProperties the event properties
* @param groups the groups
* @param outOfSession the out of session
*/
public void logEvent(String eventType, JSONObject eventProperties, JSONObject groups, boolean outOfSession) {
logEvent(eventType, eventProperties, groups, getCurrentTimeMillis(), outOfSession);
}
/**
* Log event with the specified event type, event properties, groups, timestamp, with optional
* out of session flag. If out of session is true, then the sessionId will be -1 for the event,
* indicating that it is not part of the current session. Note: this might be useful when
* logging events for notifications received.
* Note: this is asynchronous and happens on a background thread.
*
* @param eventType the event type
* @param eventProperties the event properties
* @param groups the groups
* @param timestamp the timestamp in millisecond since epoch
* @param outOfSession the out of session
* @see
* Setting Event Properties
* @see
* Setting Groups
* @see
* Tracking Sessions
*/
public void logEvent(String eventType, JSONObject eventProperties, JSONObject groups, long timestamp, boolean outOfSession) {
logEvent(eventType, eventProperties, groups,
timestamp, outOfSession, null);
}
public void logEvent(String eventType, JSONObject eventProperties, JSONObject groups, long timestamp, boolean outOfSession, MiddlewareExtra extra) {
if (validateLogEvent(eventType)) {
logEventAsync(
eventType, eventProperties, null, null, groups, null,
timestamp, outOfSession, extra);
}
}
/**
* Log an event with the specified event type.
* Note: this is version is synchronous and blocks the main thread until done.
*
* @param eventType the event type
*/
public void logEventSync(String eventType) {
logEventSync(eventType, null);
}
/**
* Log an event with the specified event type and event properties.
* Note: this is version is synchronous and blocks the main thread until done.
*
* @param eventType the event type
* @param eventProperties the event properties
* @see
* Setting Event Properties
*/
public void logEventSync(String eventType, JSONObject eventProperties) {
logEventSync(eventType, eventProperties, false);
}
/**
* Log an event with the specified event type, event properties, with optional out of session
* flag. If out of session is true, then the sessionId will be -1 for the event, indicating
* that it is not part of the current session. Note: this might be useful when logging events
* for notifications received.
* Note: this is version is synchronous and blocks the main thread until done.
*
* @param eventType the event type
* @param eventProperties the event properties
* @param outOfSession the out of session
*/
public void logEventSync(String eventType, JSONObject eventProperties, boolean outOfSession) {
logEventSync(eventType, eventProperties, null, outOfSession);
}
/**
* Log an event with the specified event type, event properties, and groups. Use this to set
* event-level groups, meaning the group(s) set only apply for this specific event and does
* not persist on the user.
* Note: this is version is synchronous and blocks the main thread until done.
*
* @param eventType the event type
* @param eventProperties the event properties
* @param groups the groups
*/
public void logEventSync(String eventType, JSONObject eventProperties, JSONObject groups) {
logEventSync(eventType, eventProperties, groups, false);
}
/**
* Log event with the specified event type, event properties, groups, with optional out of
* session flag. If out of session is true, then the sessionId will be -1 for the event,
* indicating that it is not part of the current session. Note: this might be useful when
* logging events for notifications received.
* Note: this is version is synchronous and blocks the main thread until done.
*
* @param eventType the event type
* @param eventProperties the event properties
* @param groups the groups
* @param outOfSession the out of session
* @see
* Setting Event Properties
* @see
* Setting Groups
* @see
* Tracking Sessions
*/
public void logEventSync(String eventType, JSONObject eventProperties, JSONObject groups, boolean outOfSession) {
logEventSync(eventType, eventProperties, groups, getCurrentTimeMillis(), outOfSession);
}
/**
* Log event with the specified event type, event properties, groups, timestamp, with optional
* sout of ession flag. If out of session is true, then the sessionId will be -1 for the event,
* indicating that it is not part of the current session. Note: this might be useful when
* logging events for notifications received.
* Note: this is version is synchronous and blocks the main thread until done.
*
* @param eventType the event type
* @param eventProperties the event properties
* @param groups the groups
* @param timestamp the timestamp in milliseconds since epoch
* @param outOfSession the out of session
*/
public void logEventSync(String eventType, JSONObject eventProperties, JSONObject groups, long timestamp, boolean outOfSession) {
if (validateLogEvent(eventType)) {
logEvent(eventType, eventProperties, null, null, groups, null, timestamp, outOfSession, this.inForeground);
}
}
/**
* Validate the event type being logged. Also verifies that the context and API key
* have been set already with an initialize call.
*
* @param eventType the event type
* @return true if the event type is valid
*/
protected boolean validateLogEvent(String eventType) {
if (Utils.isEmptyString(eventType)) {
logger.e(TAG, "Argument eventType cannot be null or blank in logEvent()");
return false;
}
return contextAndApiKeySet("logEvent()");
}
/**
* Log event async. Internal method to handle the synchronous logging of events.
*
* @param eventType the event type
* @param eventProperties the event properties
* @param apiProperties the api properties
* @param userProperties the user properties
* @param groups the groups
* @param timestamp the timestamp
* @param outOfSession the out of session
*/
protected void logEventAsync(final String eventType, JSONObject eventProperties,
JSONObject apiProperties, JSONObject userProperties, JSONObject groups,
JSONObject groupProperties, final long timestamp, final boolean outOfSession) {
logEventAsync(eventType,eventProperties, apiProperties, userProperties, groups,groupProperties, timestamp, outOfSession, null);
}
protected void logEventAsync(final String eventType, JSONObject eventProperties,
JSONObject apiProperties, JSONObject userProperties, JSONObject groups,
JSONObject groupProperties, final long timestamp, final boolean outOfSession, MiddlewareExtra extra) {
// Clone the incoming eventProperties object before sending over
// to the log thread. Helps avoid ConcurrentModificationException
// if the caller starts mutating the object they passed in.
// Only does a shallow copy, so it's still possible, though unlikely,
// to hit concurrent access if the caller mutates deep in the object.
if (eventProperties != null) {
eventProperties = Utils.cloneJSONObject(eventProperties);
}
if (apiProperties != null) {
apiProperties = Utils.cloneJSONObject(apiProperties);
}
if (userProperties != null) {
userProperties = Utils.cloneJSONObject(userProperties);
}
if (groups != null) {
groups = Utils.cloneJSONObject(groups);
}
if (groupProperties != null) {
groupProperties = Utils.cloneJSONObject(groupProperties);
}
final JSONObject copyEventProperties = eventProperties;
final JSONObject copyApiProperties = apiProperties;
final JSONObject copyUserProperties = userProperties;
final JSONObject copyGroups = groups;
final JSONObject copyGroupProperties = groupProperties;
final boolean isForeground = this.inForeground;
runOnLogThread(new Runnable() {
@Override
public void run() {
if (Utils.isEmptyString(apiKey)) { // in case initialization failed
return;
}
logEvent(
eventType, copyEventProperties, copyApiProperties,
copyUserProperties, copyGroups, copyGroupProperties, timestamp, outOfSession, extra,
isForeground
);
}
});
}
/**
* Log event. Internal method to handle the asynchronous logging of events on background
* thread.
*
* @param eventType the event type
* @param eventProperties the event properties
* @param apiProperties the api properties
* @param userProperties the user properties
* @param groups the groups
* @param timestamp the timestamp
* @param outOfSession the out of session
* @param inForeground in foreground
* @return the event ID if succeeded, else -1.
*/
protected long logEvent(String eventType, JSONObject eventProperties, JSONObject apiProperties,
JSONObject userProperties, JSONObject groups, JSONObject groupProperties,
long timestamp, boolean outOfSession, boolean inForeground) {
return logEvent(eventType, eventProperties, apiProperties, userProperties, groups, groupProperties, timestamp, outOfSession, null, inForeground);
}
protected long logEvent(String eventType, JSONObject eventProperties, JSONObject apiProperties,
JSONObject userProperties, JSONObject groups, JSONObject groupProperties,
long timestamp, boolean outOfSession, MiddlewareExtra extra, boolean inForeground) {
logger.d(TAG, "Logged event to Amplitude: " + eventType);
if (optOut) {
return -1;
}
// skip session check if logging start_session or end_session events
boolean loggingSessionEvent = trackingSessionEvents &&
(eventType.equals(START_SESSION_EVENT) || eventType.equals(END_SESSION_EVENT));
if (!loggingSessionEvent && !outOfSession) {
// default case + corner case when async logEvent between onPause and onResume
if (!inForeground || isEnteringForeground){
isEnteringForeground = false;
startNewSessionIfNeeded(timestamp);
} else {
refreshSessionTime(timestamp);
}
}
long result = -1;
JSONObject event = new JSONObject();
try {
event.put("event_type", replaceWithJSONNull(eventType));
event.put("timestamp", timestamp);
event.put("user_id", replaceWithJSONNull(userId));
event.put("device_id", replaceWithJSONNull(deviceId));
event.put("session_id", outOfSession ? -1 : sessionId);
event.put("uuid", UUID.randomUUID().toString());
event.put("sequence_number", getNextSequenceNumber());
if (appliedTrackingOptions.shouldTrackVersionName()) {
event.put("version_name", replaceWithJSONNull(deviceInfo.getVersionName()));
}
if (appliedTrackingOptions.shouldTrackOsName()) {
event.put("os_name", replaceWithJSONNull(deviceInfo.getOsName()));
}
if (appliedTrackingOptions.shouldTrackOsVersion()) {
event.put("os_version", replaceWithJSONNull(deviceInfo.getOsVersion()));
}
if (appliedTrackingOptions.shouldTrackApiLevel()) {
event.put("api_level", replaceWithJSONNull(Build.VERSION.SDK_INT));
}
if (appliedTrackingOptions.shouldTrackDeviceBrand()) {
event.put("device_brand", replaceWithJSONNull(deviceInfo.getBrand()));
}
if (appliedTrackingOptions.shouldTrackDeviceManufacturer()) {
event.put("device_manufacturer", replaceWithJSONNull(deviceInfo.getManufacturer()));
}
if (appliedTrackingOptions.shouldTrackDeviceModel()) {
event.put("device_model", replaceWithJSONNull(deviceInfo.getModel()));
}
if (appliedTrackingOptions.shouldTrackCarrier()) {
event.put("carrier", replaceWithJSONNull(deviceInfo.getCarrier()));
}
if (appliedTrackingOptions.shouldTrackCountry()) {
event.put("country", replaceWithJSONNull(deviceInfo.getCountry()));
}
if (appliedTrackingOptions.shouldTrackLanguage()) {
event.put("language", replaceWithJSONNull(deviceInfo.getLanguage()));
}
if (appliedTrackingOptions.shouldTrackPlatform()) {
event.put("platform", platform);
}
JSONObject library = new JSONObject();
library.put("name", this.libraryName == null ? Constants.LIBRARY_UNKNOWN : this.libraryName);
library.put("version", this.libraryVersion == null ? Constants.VERSION_UNKNOWN : this.libraryVersion);
event.put("library", library);
if (plan != null) {
event.put("plan", plan.toJSONObject());
}
if (ingestionMetadata != null) {
event.put("ingestion_metadata", ingestionMetadata.toJSONObject());
}
apiProperties = (apiProperties == null) ? new JSONObject() : apiProperties;
if (apiPropertiesTrackingOptions != null && apiPropertiesTrackingOptions.length() > 0) {
apiProperties.put("tracking_options", apiPropertiesTrackingOptions);
}
if (appliedTrackingOptions.shouldTrackLatLng()) {
Location location = deviceInfo.getMostRecentLocation();
if (location != null) {
JSONObject locationJSON = new JSONObject();
locationJSON.put("lat", location.getLatitude());
locationJSON.put("lng", location.getLongitude());
apiProperties.put("location", locationJSON);
}
}
if (appliedTrackingOptions.shouldTrackAdid() && deviceInfo.getAdvertisingId() != null) {
apiProperties.put("androidADID", deviceInfo.getAdvertisingId());
}
if (appliedTrackingOptions.shouldTrackAppSetId() && deviceInfo.getAppSetId() != null) {
apiProperties.put("android_app_set_id", deviceInfo.getAppSetId());
}
apiProperties.put("limit_ad_tracking", deviceInfo.isLimitAdTrackingEnabled());
apiProperties.put("gps_enabled", deviceInfo.isGooglePlayServicesEnabled());
event.put("api_properties", apiProperties);
event.put("event_properties", (eventProperties == null) ? new JSONObject()
: truncate(eventProperties));
event.put("user_properties", (userProperties == null) ? new JSONObject()
: truncate(userProperties));
event.put("groups", (groups == null) ? new JSONObject() : truncate(groups));
event.put("group_properties", (groupProperties == null) ? new JSONObject()
: truncate(groupProperties));
result = saveEvent(eventType, event, extra);
// If the the event is an identify, update the user properties to the core identity
// for experiment SDK to consume.
if (eventType.equals(Constants.IDENTIFY_EVENT) && userProperties != null) {
connector.getIdentityStore().editIdentity()
.updateUserProperties(JSONUtil.toUpdateUserPropertiesMap(userProperties))
.commit();
}
} catch (JSONException e) {
logger.e(TAG, String.format(
"JSON Serialization of event type %s failed, skipping: %s", eventType, e.toString()
));
}
return result;
}
/**
* Save event long. Internal method to save an event to the database.
*
* @param eventType the event type
* @param event the event
* @param extra the extra unstructured data for middleware
* @return the event ID if succeeded, else -1
*/
protected long saveEvent(String eventType, JSONObject event, MiddlewareExtra extra) {
if (!middlewareRunner.run(new MiddlewarePayload(event, extra))) return -1;
if (Utils.isEmptyString(event.toString())) {
logger.e(TAG, String.format(
"Detected empty event string for event type %s, skipping", eventType
));
return -1;
}
// Intercept event
event = identifyInterceptor.intercept(eventType, event);
if (event == null) {
return -1;
}
return saveEvent(eventType, event);
}
/**
* Save event. Internal method to save an event.
*
* @param eventType the event type
* @param event the event
* @return the event ID if succeeded, else -1
*/
protected long saveEvent(String eventType, JSONObject event) {
String eventString = event.toString();
if (eventType.equals(Constants.IDENTIFY_EVENT) || eventType.equals(Constants.GROUP_IDENTIFY_EVENT)) {
lastIdentifyId = dbHelper.addIdentify(eventString);
setLastIdentifyId(lastIdentifyId);
} else {
lastEventId = dbHelper.addEvent(eventString);
setLastEventId(lastEventId);
}
int numEventsToRemove = Math.min(
Math.max(1, eventMaxCount/10),
Constants.EVENT_REMOVE_BATCH_SIZE
);
if (dbHelper.getEventCount() > eventMaxCount) {
dbHelper.removeEvents(dbHelper.getNthEventId(numEventsToRemove));
}
if (dbHelper.getIdentifyCount() > eventMaxCount) {
dbHelper.removeIdentifys(dbHelper.getNthIdentifyId(numEventsToRemove));
}
long totalEventCount = dbHelper.getTotalEventCount(); // counts may have changed, refetch
if ((totalEventCount % eventUploadThreshold) == 0 &&
totalEventCount >= eventUploadThreshold) {
updateServer();
} else {
updateServerLater(eventUploadPeriodMillis);
}
return (
eventType.equals(Constants.IDENTIFY_EVENT) ||
eventType.equals(Constants.GROUP_IDENTIFY_EVENT)
) ? lastIdentifyId : lastEventId;
}
// fetches key from dbHelper longValueStore
// if key does not exist, return defaultValue instead
private long getLongvalue(String key, long defaultValue) {
Long value = dbHelper.getLongValue(key);
return value == null ? defaultValue : value;
}
/**
* Internal method to increment and fetch the next event sequence number.
*
* @return the next sequence number
*/
long getNextSequenceNumber() {
sequenceNumber++;
dbHelper.insertOrReplaceKeyLongValue(SEQUENCE_NUMBER_KEY, sequenceNumber);
return sequenceNumber;
}
/**
* Internal method to set the last event time.
*
* @param timestamp the timestamp
*/
void setLastEventTime(long timestamp) {
lastEventTime = timestamp;
dbHelper.insertOrReplaceKeyLongValue(LAST_EVENT_TIME_KEY, timestamp);
}
/**
* Internal method to set the last event id.
*
* @param eventId the event id
*/
void setLastEventId(long eventId) {
lastEventId = eventId;
dbHelper.insertOrReplaceKeyLongValue(LAST_EVENT_ID_KEY, eventId);
}
/**
* Internal method to set the last identify id.
*
* @param identifyId the identify id
*/
void setLastIdentifyId(long identifyId) {
lastIdentifyId = identifyId;
dbHelper.insertOrReplaceKeyLongValue(LAST_IDENTIFY_ID_KEY, identifyId);
}
/**
* Gets the current session id.
*
* @return The current sessionId value.
*/
public long getSessionId() {
return sessionId;
}
/**
* Internal method to set the previous session id.
*
* @param timestamp the timestamp
*/
void setPreviousSessionId(long timestamp) {
previousSessionId = timestamp;
dbHelper.insertOrReplaceKeyLongValue(PREVIOUS_SESSION_ID_KEY, timestamp);
}
/**
* Public method to start a new session if needed.
*
* @param timestamp the timestamp
* @return whether or not a new session was started
*/
public boolean startNewSessionIfNeeded(long timestamp) {
if (inSession()) {
if (isWithinMinTimeBetweenSessions(timestamp)) {
refreshSessionTime(timestamp);
return false;
}
startNewSession(timestamp);
return true;
}
// no current session - check for previous session
if (isWithinMinTimeBetweenSessions(timestamp)) {
if (previousSessionId == -1) {
startNewSession(timestamp);
return true;
}
// extend previous session
setSessionId(previousSessionId);
refreshSessionTime(timestamp);
return false;
}
startNewSession(timestamp);
return true;
}
private void startNewSession(long timestamp) {
// end previous session
if (trackingSessionEvents) {
sendSessionEvent(END_SESSION_EVENT);
}
// start new session
setSessionId(timestamp);
refreshSessionTime(timestamp);
if (trackingSessionEvents) {
sendSessionEvent(START_SESSION_EVENT);
}
}
private boolean inSession() {
return sessionId >= 0;
}
private boolean isWithinMinTimeBetweenSessions(long timestamp) {
long sessionLimit = usingForegroundTracking ?
minTimeBetweenSessionsMillis : sessionTimeoutMillis;
return (timestamp - lastEventTime) < sessionLimit;
}
private void setSessionId(long timestamp) {
sessionId = timestamp;
setPreviousSessionId(timestamp);
}
/**
* Internal method to refresh the current session time.
*
* @param timestamp the timestamp
*/
void refreshSessionTime(long timestamp) {
if (!inSession()) {
return;
}
setLastEventTime(timestamp);
}
private void sendSessionEvent(final String sessionEvent) {
if (!contextAndApiKeySet(String.format("sendSessionEvent('%s')", sessionEvent))) {
return;
}
if (!inSession()) {
return;
}
JSONObject apiProperties = new JSONObject();
try {
apiProperties.put("special", sessionEvent);
} catch (JSONException e) {
return;
}
logEvent(sessionEvent, null, apiProperties, null, null, null, lastEventTime, false, false);
}
/**
* Internal method to handle on app exit foreground behavior.
*
* @param timestamp the timestamp
*/
void onExitForeground(final long timestamp) {
isEnteringForeground = false;
inForeground = false;
runOnLogThread(new Runnable() {
@Override
public void run() {
if (Utils.isEmptyString(apiKey)) {
return;
}
refreshSessionTime(timestamp);
if (flushEventsOnClose) {
identifyInterceptor.transferInterceptedIdentify();
updateServer();
}
// re-persist metadata into database for good measure
dbHelper.insertOrReplaceKeyValue(DEVICE_ID_KEY, deviceId);
dbHelper.insertOrReplaceKeyValue(USER_ID_KEY, userId);
dbHelper.insertOrReplaceKeyLongValue(OPT_OUT_KEY, optOut ? 1L : 0L);
dbHelper.insertOrReplaceKeyLongValue(PREVIOUS_SESSION_ID_KEY, sessionId);
dbHelper.insertOrReplaceKeyLongValue(LAST_EVENT_TIME_KEY, lastEventTime);
}
});
}
/**
* Internal method to handle on app enter foreground behavior.
*
* @param timestamp the timestamp
*/
void onEnterForeground(final long timestamp) {
isEnteringForeground = true;
inForeground = true;
runOnLogThread(new Runnable() {
@Override
public void run() {
if (Utils.isEmptyString(apiKey)) {
return;
}
if (useDynamicConfig) {
ConfigManager.getInstance().refresh(new ConfigManager.RefreshListener() {
@Override
public void onFinished() {
url = ConfigManager.getInstance().getIngestionEndpoint();
}
}, serverZone);
}
// This should be true, unless somehow an event was tracked
// between here and the beginning of this method
// in that case the session is started in logEvent()
if (isEnteringForeground) {
startNewSessionIfNeeded(timestamp);
}
isEnteringForeground = false;
}
});
}
/**
* Log revenue amount via a revenue event.
*
* @param amount the amount
* @deprecated - use {@code logRevenueV2} instead
* @see
* Tracking Revenue
*/
public void logRevenue(double amount) {
// Amount is in dollars
// ex. $3.99 would be pass as logRevenue(3.99)
logRevenue(null, 1, amount);
}
/**
* Log revenue with a productId, quantity, and price.
*
* @param productId the product id
* @param quantity the quantity
* @param price the price
* @deprecated - use {@code logRevenueV2} instead
* @see
* Tracking Revenue
*/
public void logRevenue(String productId, int quantity, double price) {
logRevenue(productId, quantity, price, null, null);
}
public void logRevenue(String productId, int quantity, double price, String receipt,
String receiptSignature) {
logRevenue(productId, quantity, price, receipt, receiptSignature, null);
}
/**
* Log revenue with a productId, quantity, price, and receipt data for revenue verification.
*
* @param productId the product id
* @param quantity the quantity
* @param price the price
* @param receipt the receipt
* @param receiptSignature the receipt signature
* @param extra the extra unstructured data for middleware
* @deprecated - use {@code logRevenueV2} instead
* @see
* Tracking Revenue
*/
public void logRevenue(String productId, int quantity, double price, String receipt,
String receiptSignature, MiddlewareExtra extra) {
if (!contextAndApiKeySet("logRevenue()")) {
return;
}
// Log revenue in events
JSONObject apiProperties = new JSONObject();
try {
apiProperties.put("special", Constants.AMP_REVENUE_EVENT);
apiProperties.put("productId", productId);
apiProperties.put("quantity", quantity);
apiProperties.put("price", price);
apiProperties.put("receipt", receipt);
apiProperties.put("receiptSig", receiptSignature);
} catch (JSONException e) {
}
logEventAsync(
Constants.AMP_REVENUE_EVENT, null, apiProperties, null, null, null, getCurrentTimeMillis(), false, extra
);
}
/**
* Log revenue v2. Create a {@link Revenue} object to hold your revenue data and properties,
* and log it as a revenue event using this method.
*
* @param revenue a {@link Revenue} object
*/
public void logRevenueV2(Revenue revenue) {
logRevenueV2(revenue, null);
}
public void logRevenueV2(Revenue revenue, MiddlewareExtra extra) {
if (!contextAndApiKeySet("logRevenueV2()") || revenue == null || !revenue.isValidRevenue()) {
return;
}
logEvent(Constants.AMP_REVENUE_EVENT, revenue.toJSONObject(), null, null, null, null, getCurrentTimeMillis(), false, extra, this.inForeground);
}
/**
* Sets user properties. This is a convenience wrapper around the
* {@link Identify} API to set multiple user properties with a single
* command. Note: the replace parameter is deprecated and has no effect.
*
* @param userProperties the user properties
* @param replace the replace - has no effect
* @deprecated
*/
public void setUserProperties(final JSONObject userProperties, final boolean replace) {
setUserProperties(userProperties);
}
/**
* Sets user properties. This is a convenience wrapper around the
* {@link Identify} API to set multiple user properties with a single
* command.
*
* @param userProperties the user properties
*/
public void setUserProperties(final JSONObject userProperties) {
setUserProperties(userProperties, null);
}
/**
* Sets user properties. This is a convenience wrapper around the
* {@link Identify} API to set multiple user properties with a single
* command.
*
* @param userProperties the user properties
* @param extra the extra unstructured data for middleware
*/
public void setUserProperties(final JSONObject userProperties, MiddlewareExtra extra) {
if (userProperties == null || userProperties.length() == 0 ||
!contextAndApiKeySet("setUserProperties")) {
return;
}
Identify identify = convertPropertiesToIdentify(userProperties);
if (identify != null) {
identify(identify, false, extra);
}
}
private Identify convertPropertiesToIdentify(final JSONObject properties) {
if (properties == null) {
return null;
}
// sanitize and truncate properties before trying to convert to identify
JSONObject sanitized = truncate(properties);
if (sanitized.length() == 0) {
return null;
}
Identify identify = new Identify();
Iterator> keys = sanitized.keys();
while (keys.hasNext()) {
String key = (String) keys.next();
try {
identify.setUserProperty(key, sanitized.get(key));
} catch (JSONException e) {
logger.e(TAG, e.toString());
}
}
return identify;
}
/**
* Clear user properties. This will clear all user properties at once. Note: the
* result is irreversible!
*/
public void clearUserProperties() {
Identify identify = new Identify().clearAll();
identify(identify);
}
/**
* Identify. Use this to send an {@link Identify} object containing
* user property operations to Amplitude server.
*
* @param identify an {@link Identify} object
*/
public void identify(Identify identify) {
identify(identify, false);
}
public void identify(Identify identify, boolean outOfSession) {
identify(identify, outOfSession, null);
}
/**
* Identify. Use this to send an {@link com.amplitude.api.Identify} object containing
* user property operations to Amplitude server. If outOfSession is true, then the identify
* event is sent with a session id of -1, and does not trigger any session-handling logic.
*
* @param identify an {@link Identify} object
* @param outOfSession whther to log the identify event out of session
* @param extra the extra unstructured data for middleware
*/
public void identify(Identify identify, boolean outOfSession, MiddlewareExtra extra) {
if (
identify == null || identify.userPropertiesOperations.length() == 0 ||
!contextAndApiKeySet("identify()")
) return;
logEventAsync(
Constants.IDENTIFY_EVENT, null, null, identify.userPropertiesOperations,
null, null, getCurrentTimeMillis(), outOfSession, extra
);
}
/**
* Sets the user's group(s).
*
* @param groupType the group type (ex: orgId)
* @param groupName the group name (ex: 15)
*/
public void setGroup(String groupType, Object groupName) {
setGroup(groupType, groupName, null);
}
/**
* Sets the user's group(s).
*
* @param groupType the group type (ex: orgId)
* @param groupName the group name (ex: 15)
* @param extra the extra unstructured data for middleware
*/
public void setGroup(String groupType, Object groupName, MiddlewareExtra extra) {
if (!contextAndApiKeySet("setGroup()") || Utils.isEmptyString(groupType)) {
return;
}
JSONObject group = null;
try {
group = new JSONObject().put(groupType, groupName);
} catch (JSONException e) {
logger.e(TAG, e.toString());
}
Identify identify = new Identify().setUserProperty(groupType, groupName);
logEventAsync(Constants.IDENTIFY_EVENT, null, null, identify.userPropertiesOperations,
group, null, getCurrentTimeMillis(), false, extra);
}
public void groupIdentify(String groupType, Object groupName, Identify groupIdentify) {
groupIdentify(groupType, groupName, groupIdentify, false);
}
public void groupIdentify(String groupType, Object groupName, Identify groupIdentify, boolean outOfSession) {
groupIdentify(groupType, groupName, groupIdentify, outOfSession, null);
}
public void groupIdentify(String groupType, Object groupName, JSONObject groupProperties, boolean outOfSession, MiddlewareExtra extra) {
Identify identify = convertPropertiesToIdentify(groupProperties);
if (identify != null) {
groupIdentify(groupType, groupName, identify, outOfSession, extra);
}
}
public void groupIdentify(String groupType, Object groupName, Identify groupIdentify, boolean outOfSession, MiddlewareExtra extra) {
if (groupIdentify == null || groupIdentify.userPropertiesOperations.length() == 0 ||
!contextAndApiKeySet("groupIdentify()") || Utils.isEmptyString(groupType)) {
return;
}
JSONObject group = null;
try {
group = new JSONObject().put(groupType, groupName);
} catch (JSONException e) {
logger.e(TAG, e.toString());
}
logEventAsync(
Constants.GROUP_IDENTIFY_EVENT, null, null, null, group,
groupIdentify.userPropertiesOperations, getCurrentTimeMillis(), outOfSession, extra
);
}
/**
* Truncate values in a JSON object. Any string values longer than 1024 characters will be
* truncated to 1024 characters.
* Any dictionary with more than 1000 items will be ignored.
*
* @param object the object
* @return the truncated JSON object
*/
public JSONObject truncate(JSONObject object) {
if (object == null) {
return new JSONObject();
}
if (object.length() > Constants.MAX_PROPERTY_KEYS) {
logger.w(TAG, "Warning: too many properties (more than 1000), ignoring");
return new JSONObject();
}
Iterator> keys = object.keys();
while (keys.hasNext()) {
String key = (String) keys.next();
try {
Object value = object.get(key);
// do not truncate revenue receipt and receipt sig fields
if (key.equals(Constants.AMP_REVENUE_RECEIPT) ||
key.equals(Constants.AMP_REVENUE_RECEIPT_SIG)) {
object.put(key, value);
} else if (value.getClass().equals(String.class)) {
object.put(key, truncate((String) value));
} else if (value.getClass().equals(JSONObject.class)) {
object.put(key, truncate((JSONObject) value));
} else if (value.getClass().equals(JSONArray.class)) {
object.put(key, truncate((JSONArray) value));
}
} catch (JSONException e) {
logger.e(TAG, e.toString());
}
}
return object;
}
/**
* Truncate values in a JSON array. Any string values longer than 1024 characters will be
* truncated to 1024 characters.
*
* @param array the array
* @return the truncated JSON array
* @throws JSONException the json exception
*/
public JSONArray truncate(JSONArray array) throws JSONException {
if (array == null) {
return new JSONArray();
}
for (int i = 0; i < array.length(); i++) {
Object value = array.get(i);
if (value.getClass().equals(String.class)) {
array.put(i, truncate((String) value));
} else if (value.getClass().equals(JSONObject.class)) {
array.put(i, truncate((JSONObject) value));
} else if (value.getClass().equals(JSONArray.class)) {
array.put(i, truncate((JSONArray) value));
}
}
return array;
}
/**
* Truncate a string to 1024 characters.
*
* @param value the value
* @return the truncated string
*/
public static String truncate(String value) {
return value.length() <= Constants.MAX_STRING_LENGTH ? value :
value.substring(0, Constants.MAX_STRING_LENGTH);
}
/**
* Gets the user's id. Can be null.
*
* @return The developer specified identifier for tracking within the analytics system.
*/
public String getUserId() {
return userId;
}
/**
* Sets the user id (can be null).
*
* @param userId the user id
* @return the AmplitudeClient
*/
public AmplitudeClient setUserId(final String userId) {
return setUserId(userId, false);
}
/**
* Sets the user id (can be null).
* If startNewSession is true, ends the session for the previous user and starts a new
* session for the new user id.
*
* @param userId the user id
* @return the AmplitudeClient
*/
public AmplitudeClient setUserId(final String userId, final boolean startNewSession) {
if (!contextAndApiKeySet("setUserId()")) {
return this;
}
final AmplitudeClient client = this;
runOnLogThread(new Runnable() {
@Override
public void run() {
if (Utils.isEmptyString(client.apiKey)) { // in case initialization failed
return;
}
// end previous session
if (startNewSession && trackingSessionEvents) {
sendSessionEvent(END_SESSION_EVENT);
}
client.userId = userId;
dbHelper.insertOrReplaceKeyValue(USER_ID_KEY, userId);
// start new session
if (startNewSession) {
long timestamp = getCurrentTimeMillis();
setSessionId(timestamp);
refreshSessionTime(timestamp);
if (trackingSessionEvents) {
sendSessionEvent(START_SESSION_EVENT);
}
}
// update the user in the core identity store to notify
// experiment to re-fetch variants with the new identity
client.connector.getIdentityStore().editIdentity().setUserId(userId).commit();
}
});
return this;
}
/**
* Sets a custom device id. Note: only do this if you know what you are doing!
*
* @param deviceId the device id
* @return the AmplitudeClient
*/
public AmplitudeClient setDeviceId(final String deviceId) {
Set invalidDeviceIds = getInvalidDeviceIds();
if (!contextAndApiKeySet("setDeviceId()") || Utils.isEmptyString(deviceId) ||
invalidDeviceIds.contains(deviceId)) {
return this;
}
final AmplitudeClient client = this;
runOnLogThread(new Runnable() {
@Override
public void run() {
if (Utils.isEmptyString(client.apiKey)) { // in case initialization failed
return;
}
client.deviceId = deviceId;
saveDeviceId(deviceId);
// update the user in the core identity store to notify
// experiment to re-fetch variants with the new identity
client.connector.getIdentityStore().editIdentity().setDeviceId(deviceId).commit();
}
});
return this;
}
/**
* Regenerates a new random deviceId for current user. Note: this is not recommended unless you
* know what you are doing. This can be used in conjunction with setUserId(null) to anonymize
* users after they log out. With a null userId and a completely new deviceId, the current user
* would appear as a brand new user in dashboard.
*/
public AmplitudeClient regenerateDeviceId() {
if (!contextAndApiKeySet("regenerateDeviceId()")) {
return this;
}
final AmplitudeClient client = this;
runOnLogThread(new Runnable() {
@Override
public void run() {
if (Utils.isEmptyString(client.apiKey)) { // in case initialization failed
return;
}
String randomId = DeviceInfo.generateUUID() + "R";
setDeviceId(randomId);
}
});
return this;
}
/**
* Force SDK to upload any unsent events.
*/
public void uploadEvents() {
if (!contextAndApiKeySet("uploadEvents()")) {
return;
}
logThread.post(new Runnable() {
@Override
public void run() {
if (Utils.isEmptyString(apiKey)) { // in case initialization failed
return;
}
identifyInterceptor.transferInterceptedIdentify();
updateServer();
}
});
}
private void updateServerLater(long delayMillis) {
if (updateScheduled.getAndSet(true)) {
return;
}
logThread.postDelayed(new Runnable() {
@Override
public void run() {
updateScheduled.set(false);
updateServer();
}
}, delayMillis);
}
/**
* Internal method to upload unsent events.
*/
protected void updateServer() {
updateServer(false);
}
/**
* Internal method to upload unsent events. Limit controls whether to use event upload max
* batch size or backoff upload batch size. Note: always call this on logThread
*
* @param limit the limit
*/
protected void updateServer(boolean limit) {
if (optOut || offline) {
return;
}
// Flush middleware
middlewareRunner.flush();
// if returning out of this block, always be sure to set uploadingCurrently to false!!
if (!uploadingCurrently.getAndSet(true)) {
long totalEventCount = dbHelper.getTotalEventCount();
long batchSize = Math.min(
limit ? backoffUploadBatchSize : eventUploadMaxBatchSize,
totalEventCount
);
if (batchSize <= 0) {
uploadingCurrently.set(false);
return;
}
try {
List events = dbHelper.getEvents(lastEventId, batchSize);
List identifys = dbHelper.getIdentifys(lastIdentifyId, batchSize);
final Pair, JSONArray> merged = mergeEventsAndIdentifys(
events, identifys, batchSize);
final JSONArray mergedEvents = merged.second;
if (mergedEvents.length() == 0) {
uploadingCurrently.set(false);
return;
}
final long maxEventId = merged.first.first;
final long maxIdentifyId = merged.first.second;
final String mergedEventsString = merged.second.toString();
httpThread.post(new Runnable() {
@Override
public void run() {
makeEventUploadPostRequest(callFactory, mergedEventsString, maxEventId, maxIdentifyId);
}
});
} catch (JSONException e) {
uploadingCurrently.set(false);
logger.e(TAG, e.toString());
} catch (CursorWindowAllocationException e) {
// handle CursorWindowAllocationException when fetching events, defer upload
uploadingCurrently.set(false);
logger.e(TAG, String.format(
"Caught Cursor window exception during event upload, deferring upload: %s",
e.getMessage()
));
}
}
}
/**
* Internal method to merge unsent events and identifies into a single array by sequence number.
*
* @param events the events
* @param identifys the identifys
* @param numEvents the num events
* @return the merged array, max event id, and max identify id
* @throws JSONException the json exception
*/
protected Pair, JSONArray> mergeEventsAndIdentifys(List events,
List identifys, long numEvents) throws JSONException {
JSONArray merged = new JSONArray();
long maxEventId = -1;
long maxIdentifyId = -1;
while (merged.length() < numEvents) {
boolean noEvents = events.isEmpty();
boolean noIdentifys = identifys.isEmpty();
// case 0: no events or identifys, nothing to grab
// this case should never happen, as it means there are less identifys and events
// than expected
if (noEvents && noIdentifys) {
logger.w(TAG, String.format(
"mergeEventsAndIdentifys: number of events and identifys " +
"less than expected by %d", numEvents - merged.length())
);
break;
// case 1: no identifys, grab from events
} else if (noIdentifys) {
JSONObject event = events.remove(0);
maxEventId = event.getLong("event_id");
merged.put(event);
// case 2: no events, grab from identifys
} else if (noEvents) {
JSONObject identify = identifys.remove(0);
maxIdentifyId = identify.getLong("event_id");
merged.put(identify);
// case 3: need to compare sequence numbers
} else {
// events logged before v2.1.0 won't have a sequence number, put those first
if (!events.get(0).has("sequence_number") ||
events.get(0).getLong("sequence_number") <
identifys.get(0).getLong("sequence_number")) {
JSONObject event = events.remove(0);
maxEventId = event.getLong("event_id");
merged.put(event);
} else {
JSONObject identify = identifys.remove(0);
maxIdentifyId = identify.getLong("event_id");
merged.put(identify);
}
}
}
return new Pair, JSONArray>(new Pair(maxEventId, maxIdentifyId), merged);
}
/**
* Internal method to generate the event upload post request.
*
* @param client the client
* @param events the events
* @param maxEventId the max event id
* @param maxIdentifyId the max identify id
*/
protected void makeEventUploadPostRequest(Call.Factory client, String events, final long maxEventId, final long maxIdentifyId) {
String apiVersionString = "" + Constants.API_VERSION;
String timestampString = "" + getCurrentTimeMillis();
FormBody body = new FormBody.Builder()
.add("v", apiVersionString)
.add("client", apiKey)
.add("e", events)
.add("upload_time", timestampString)
.build();
Request request;
try {
Request.Builder builder = new Request.Builder()
.url(url)
.post(body);
if (!Utils.isEmptyString(bearerToken)) {
builder.addHeader("Authorization", "Bearer " + bearerToken);
}
request = builder.build();
} catch (IllegalArgumentException e) {
logger.e(TAG, e.toString());
uploadingCurrently.set(false);
return;
}
boolean uploadSuccess = false;
try {
Response response = client.newCall(request).execute();
String stringResponse = response.body().string();
if (response.code() == 200) {
uploadSuccess = true;
logThread.post(new Runnable() {
@Override
public void run() {
if (maxEventId >= 0) dbHelper.removeEvents(maxEventId);
if (maxIdentifyId >= 0) dbHelper.removeIdentifys(maxIdentifyId);
uploadingCurrently.set(false);
if (dbHelper.getTotalEventCount() > eventUploadThreshold) {
logThread.post(new Runnable() {
@Override
public void run() {
updateServer(backoffUpload);
}
});
}
else {
backoffUpload = false;
backoffUploadBatchSize = eventUploadMaxBatchSize;
}
}
});
} else if (response.code() == 400 && stringResponse.equals("invalid_api_key")) {
logger.e(TAG, "Invalid API key, make sure your API key is correct in initialize()");
} else if (response.code() == 400 && stringResponse.equals("bad_checksum")) {
logger.w(TAG,
"Bad checksum, post request was mangled in transit, will attempt to reupload later");
} else if (response.code() == 413) {
// If blocked by one massive event, drop it
if (backoffUpload && backoffUploadBatchSize == 1) {
if (maxEventId >= 0) dbHelper.removeEvent(maxEventId);
if (maxIdentifyId >= 0) dbHelper.removeIdentify(maxIdentifyId);
// maybe we want to reset backoffUploadBatchSize after dropping massive event
}
// Server complained about length of request, backoff and try again
backoffUpload = true;
int numEvents = Math.min((int)dbHelper.getEventCount(), backoffUploadBatchSize);
backoffUploadBatchSize = (int)Math.ceil(numEvents / 2.0);
logger.w(TAG, "Request too large, will decrease size and attempt to reupload");
logThread.post(new Runnable() {
@Override
public void run() {
uploadingCurrently.set(false);
updateServer(true);
}
});
} else {
logger.w(TAG, "Upload failed, " + stringResponse
+ ", will attempt to reupload later");
}
} catch (java.net.ConnectException e) {
// logger.w(TAG,
// "No internet connection found, unable to upload events");
lastError = e;
} catch (java.net.UnknownHostException e) {
// logger.w(TAG,
// "No internet connection found, unable to upload events");
lastError = e;
} catch (IOException e) {
logger.e(TAG, e.toString());
lastError = e;
} catch (AssertionError e) {
// This can be caused by a NoSuchAlgorithmException thrown by DefaultHttpClient
logger.e(TAG, "Exception:", e);
lastError = e;
} catch (Exception e) {
// Just log any other exception so things don't crash on upload
logger.e(TAG, "Exception:", e);
lastError = e;
}
if (!uploadSuccess) {
uploadingCurrently.set(false);
}
}
protected DeviceInfo initializeDeviceInfo() {
return new DeviceInfo(context, this.locationListening, appliedTrackingOptions.shouldTrackAdid());
}
/**
* Get the current device id. Can be null if deviceId hasn't been initialized yet.
*
* @return A unique identifier for tracking within the analytics system.
*/
public String getDeviceId() {
return deviceId;
}
// don't need to keep this in memory, if only using it at most 1 or 2 times
private Set getInvalidDeviceIds() {
Set invalidDeviceIds = new HashSet();
invalidDeviceIds.add("");
invalidDeviceIds.add("9774d56d682e549c");
invalidDeviceIds.add("unknown");
invalidDeviceIds.add("000000000000000"); // Common Serial Number
invalidDeviceIds.add("Android");
invalidDeviceIds.add("DEFACE");
invalidDeviceIds.add("00000000-0000-0000-0000-000000000000");
return invalidDeviceIds;
}
private String initializeDeviceId() {
Set invalidIds = getInvalidDeviceIds();
// see if device id already stored in db
String deviceId = dbHelper.getValue(DEVICE_ID_KEY);
if (!(Utils.isEmptyString(deviceId) || invalidIds.contains(deviceId) || deviceId.endsWith("S"))) {
return deviceId;
}
if (!newDeviceIdPerInstall && useAdvertisingIdForDeviceId && !deviceInfo.isLimitAdTrackingEnabled()) {
// Android ID is deprecated by Google.
// We are required to use Advertising ID, and respect the advertising ID preference
String advertisingId = deviceInfo.getAdvertisingId();
if (!(Utils.isEmptyString(advertisingId) || invalidIds.contains(advertisingId))) {
saveDeviceId(advertisingId);
return advertisingId;
}
}
if (useAppSetIdForDeviceId) {
String appSetId = deviceInfo.getAppSetId();
if (!(Utils.isEmptyString(appSetId) || invalidIds.contains(appSetId))) {
// Suffix with S for app set id so in future we can tell if device id is from app set id
String appSetDeviceId = appSetId + "S";
saveDeviceId(appSetDeviceId);
return appSetDeviceId;
}
}
// If this still fails, generate random identifier that does not persist
// across installations. Append R to distinguish as randomly generated
String randomId = deviceInfo.generateUUID() + "R";
saveDeviceId(randomId);
return randomId;
}
private void saveDeviceId(String deviceId) {
dbHelper.insertOrReplaceKeyValue(DEVICE_ID_KEY, deviceId);
}
public AmplitudeClient setDeviceIdCallback(AmplitudeDeviceIdCallback callback) {
this.deviceIdCallback = callback;
return this;
}
protected void runOnLogThread(Runnable r) {
if (Thread.currentThread() != logThread) {
logThread.post(r);
} else {
r.run();
}
}
/**
* Internal method to replace null event fields with JSON null object.
*
* @param obj the obj
* @return the object
*/
protected Object replaceWithJSONNull(Object obj) {
return obj == null ? JSONObject.NULL : obj;
}
/**
* Internal method to check whether application context and api key are set
*
* @param methodName the parent method name to print in error message
* @return whether application context and api key are set
*/
protected synchronized boolean contextAndApiKeySet(String methodName) {
if (context == null) {
logger.e(TAG, "context cannot be null, set context with initialize() before calling "
+ methodName);
return false;
}
if (Utils.isEmptyString(apiKey)) {
logger.e(TAG,
"apiKey cannot be null or empty, set apiKey with initialize() before calling "
+ methodName);
return false;
}
return true;
}
/**
* Internal method to convert bytes to hex string
*
* @param bytes the bytes
* @return the string
*/
protected String bytesToHexString(byte[] bytes) {
final char[] hexArray = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b',
'c', 'd', 'e', 'f' };
char[] hexChars = new char[bytes.length * 2];
int v;
for (int j = 0; j < bytes.length; j++) {
v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
/**
* Internal method to fetch the current time millis. Used for testing.
*
* @return the current time millis
*/
protected long getCurrentTimeMillis() { return System.currentTimeMillis(); }
/**
* Set tracking plan information.
* @param plan Plan object
* @return the AmplitudeClient
*/
public AmplitudeClient setPlan(Plan plan) {
this.plan = plan;
return this;
}
/**
* Set ingestion metadata information.
* @param ingestionMetadata IngestionMetadata object
* @return the AmplitudeClient
*/
public AmplitudeClient setIngestionMetadata(IngestionMetadata ingestionMetadata) {
this.ingestionMetadata = ingestionMetadata;
return this;
}
/**
* Set Amplitude Server Zone, switch to zone related configuration,
* including dynamic configuration and server url.
*
* To send data to Amplitude's EU servers, you need to configure the serverZone to EU like
* client.setServerZone(AmplitudeServerZone.EU);
*
* @param serverZone AmplitudeServerZone, US or EU, default is US
* @return the AmplitudeClient
*/
public AmplitudeClient setServerZone(AmplitudeServerZone serverZone) {
return setServerZone(serverZone, true);
}
/**
* Set Amplitude Server Zone, switch to zone related configuration,
* including dynamic configuration. If updateServerUrl is true, including server url as well.
* Recommend to keep updateServerUrl to be true for alignment.
*
* @param serverZone AmplitudeServerZone, US or EU, default is US
* @param updateServerUrl if update server url when update server zone, recommend setting true
* @return
*/
public AmplitudeClient setServerZone(AmplitudeServerZone serverZone, boolean updateServerUrl) {
if (serverZone == null) {
return null;
}
this.serverZone = serverZone;
if (updateServerUrl) {
setServerUrl(AmplitudeServerZone.getEventLogApiForZone(serverZone));
}
return this;
}
/**
* Get Amplitude Server Zone
*
* @return the current Amplitude Server Zone
*/
public AmplitudeServerZone getServerZone() {
return this.serverZone;
}
}
================================================
FILE: src/main/java/com/amplitude/api/AmplitudeDeviceIdCallback.java
================================================
package com.amplitude.api;
public interface AmplitudeDeviceIdCallback {
void onDeviceIdReady(String deviceId);
}
================================================
FILE: src/main/java/com/amplitude/api/AmplitudeLog.java
================================================
package com.amplitude.api;
import android.util.Log;
public class AmplitudeLog {
private volatile boolean enableLogging = true;
private volatile int logLevel = Log.INFO; // default log level
private AmplitudeLogCallback amplitudeLogCallback = null;
protected static AmplitudeLog instance = new AmplitudeLog();
public static AmplitudeLog getLogger() {
return instance;
}
private AmplitudeLog() {} // prevent instantiation
AmplitudeLog setEnableLogging(boolean enableLogging) {
this.enableLogging = enableLogging;
return instance;
}
AmplitudeLog setLogLevel(int logLevel) {
this.logLevel = logLevel;
return instance;
}
int d(String tag, String msg) {
if (enableLogging && logLevel <= Log.DEBUG) return Log.d(tag, msg);
return 0;
}
int d(String tag, String msg, Throwable tr) {
if (enableLogging && logLevel <= Log.DEBUG) return Log.d(tag, msg, tr);
return 0;
}
int e(String tag, String msg) {
if (enableLogging && logLevel <= Log.ERROR) {
if (this.amplitudeLogCallback != null) {
this.amplitudeLogCallback.onError(tag, msg);
}
return Log.e(tag, msg);
}
return 0;
}
int e(String tag, String msg, Throwable tr) {
if (enableLogging && logLevel <= Log.ERROR) {
if (this.amplitudeLogCallback != null) {
this.amplitudeLogCallback.onError(tag, msg);
}
return Log.e(tag, msg, tr);
}
return 0;
}
String getStackTraceString(Throwable tr) {
return Log.getStackTraceString(tr);
}
int i(String tag, String msg) {
if (enableLogging && logLevel <= Log.INFO) return Log.i(tag, msg);
return 0;
}
int i(String tag, String msg, Throwable tr) {
if (enableLogging && logLevel <= Log.INFO) return Log.i(tag, msg, tr);
return 0;
}
boolean isLoggable(String tag, int level) {
return Log.isLoggable(tag, level);
}
int println(int priority, String tag, String msg) {
return Log.println(priority, tag, msg);
}
int v(String tag, String msg) {
if (enableLogging && logLevel <= Log.VERBOSE) return Log.v(tag, msg);
return 0;
}
int v(String tag, String msg, Throwable tr) {
if (enableLogging && logLevel <= Log.VERBOSE) return Log.v(tag, msg, tr);
return 0;
}
int w(String tag, String msg) {
if (enableLogging && logLevel <= Log.WARN) return Log.w(tag, msg);
return 0;
}
int w(String tag, Throwable tr) {
if (enableLogging && logLevel <= Log.WARN) return Log.w(tag, tr);
return 0;
}
int w(String tag, String msg, Throwable tr) {
if (enableLogging && logLevel <= Log.WARN) return Log.w(tag, msg, tr);
return 0;
}
// wtf = What a Terrible Failure, logged at level ASSERT
int wtf(String tag, String msg) {
if (enableLogging && logLevel <= Log.ASSERT) return Log.wtf(tag, msg);
return 0;
}
int wtf(String tag, Throwable tr) {
if (enableLogging && logLevel <= Log.ASSERT) return Log.wtf(tag, tr);
return 0;
}
int wtf(String tag, String msg, Throwable tr) {
if (enableLogging && logLevel <= Log.ASSERT) return Log.wtf(tag, msg, tr);
return 0;
}
void setAmplitudeLogCallback(AmplitudeLogCallback callback) {
this.amplitudeLogCallback = callback;
}
}
================================================
FILE: src/main/java/com/amplitude/api/AmplitudeLogCallback.java
================================================
package com.amplitude.api;
public interface AmplitudeLogCallback {
void onError(String tag, String message);
}
================================================
FILE: src/main/java/com/amplitude/api/AmplitudeServerZone.java
================================================
package com.amplitude.api;
import java.util.HashMap;
import java.util.Map;
/**
* AmplitudeServerZone is for Data Residency and handling server zone related properties.
* The server zones now are US and EU.
*
* For usage like sending data to Amplitude's EU servers, you need to configure the serverZone
* property after initializing the client with setServerZone method.
*/
public enum AmplitudeServerZone {
US, EU;
private static Map amplitudeServerZoneEventLogApiMap =
new HashMap() {{
put(AmplitudeServerZone.US, Constants.EVENT_LOG_URL);
put(AmplitudeServerZone.EU, Constants.EVENT_LOG_EU_URL);
}};
private static Map amplitudeServerZoneDynamicConfigMap =
new HashMap() {{
put(AmplitudeServerZone.US, Constants.DYNAMIC_CONFIG_URL);
put(AmplitudeServerZone.EU, Constants.DYNAMIC_CONFIG_EU_URL);
}};
protected static String getEventLogApiForZone(AmplitudeServerZone serverZone) {
if (amplitudeServerZoneEventLogApiMap.containsKey(serverZone)) {
return amplitudeServerZoneEventLogApiMap.get(serverZone);
}
return Constants.EVENT_LOG_URL;
}
protected static String getDynamicConfigApi(AmplitudeServerZone serverZone) {
if (amplitudeServerZoneDynamicConfigMap.containsKey(serverZone)) {
return amplitudeServerZoneDynamicConfigMap.get(serverZone);
}
return Constants.DYNAMIC_CONFIG_URL;
}
public static AmplitudeServerZone getServerZone(String serverZone) {
AmplitudeServerZone amplitudeServerZone = AmplitudeServerZone.US;
switch (serverZone) {
case "EU":
amplitudeServerZone = AmplitudeServerZone.EU;
break;
case "US":
amplitudeServerZone = AmplitudeServerZone.US;
break;
default:
break;
}
return amplitudeServerZone;
}
}
================================================
FILE: src/main/java/com/amplitude/api/ConfigManager.java
================================================
package com.amplitude.api;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
public class ConfigManager {
private static String KEY_INGESTION_ENDPOINT = "ingestionEndpoint";
private static ConfigManager instance = null;
private String ingestionEndpoint = Constants.EVENT_LOG_URL;
public String getIngestionEndpoint() {
return ingestionEndpoint;
}
private ConfigManager() {
}
public void refresh(RefreshListener listener, AmplitudeServerZone serverZone) {
try {
String dynamicConfigUrl = AmplitudeServerZone.getDynamicConfigApi(serverZone);
URL obj = new URL(dynamicConfigUrl);
HttpURLConnection con = (HttpURLConnection) obj.openConnection();
int responseCode = con.getResponseCode();
if (responseCode == 200) {
BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
JSONObject json = new JSONObject(response.toString());
if (json.has(KEY_INGESTION_ENDPOINT)) {
this.ingestionEndpoint = "https://" + json.getString(KEY_INGESTION_ENDPOINT);
}
}
} catch (MalformedURLException e) {
} catch (IOException e) {
} catch (JSONException e) {
} catch (Exception e) {
}
listener.onFinished();
}
public static ConfigManager getInstance() {
if (instance == null) {
instance = new ConfigManager();
}
return instance;
}
interface RefreshListener {
void onFinished();
}
}
================================================
FILE: src/main/java/com/amplitude/api/Constants.java
================================================
package com.amplitude.api;
import com.amplitude.BuildConfig;
public class Constants {
public static final String LIBRARY = "amplitude-android";
public static final String VERSION = BuildConfig.AMPLITUDE_VERSION;
public static final String LIBRARY_UNKNOWN = "unknown-library";
public static final String VERSION_UNKNOWN = "unknown-version";
public static final String PLATFORM = "Android";
public static final String EVENT_LOG_URL = "https://api2.amplitude.com/";
public static final String EVENT_LOG_EU_URL = "https://api.eu.amplitude.com/";
public static final String DYNAMIC_CONFIG_URL = "https://regionconfig.amplitude.com/";
public static final String DYNAMIC_CONFIG_EU_URL = "https://regionconfig.eu.amplitude.com/";
public static final String PACKAGE_NAME = "com.amplitude.api";
public static final int API_VERSION = 2;
public static final String DATABASE_NAME = PACKAGE_NAME;
public static final int DATABASE_VERSION = 4;
public static final String DEFAULT_INSTANCE = "$default_instance";
public static final int EVENT_UPLOAD_THRESHOLD = 30;
public static final int EVENT_UPLOAD_MAX_BATCH_SIZE = 50;
public static final int EVENT_MAX_COUNT = 1000;
public static final int EVENT_REMOVE_BATCH_SIZE = 20;
public static final long EVENT_UPLOAD_PERIOD_MILLIS = 30 * 1000; // 30s
public static final long MIN_TIME_BETWEEN_SESSIONS_MILLIS = 5 * 60 * 1000; // 5m
public static final long SESSION_TIMEOUT_MILLIS = 30 * 60 * 1000; // 30m
public static final long IDENTIFY_BATCH_INTERVAL_MILLIS = 30 * 1000; // 30s
public static final int MAX_STRING_LENGTH = 1024;
public static final int MAX_PROPERTY_KEYS = 1000;
public static final String SHARED_PREFERENCES_NAME_PREFIX = PACKAGE_NAME;
public static final String PREFKEY_LAST_EVENT_ID = PACKAGE_NAME + ".lastEventId";
public static final String PREFKEY_LAST_EVENT_TIME = PACKAGE_NAME + ".lastEventTime";
public static final String PREFKEY_LAST_IDENTIFY_ID = PACKAGE_NAME + ".lastIdentifyId";
public static final String PREFKEY_PREVIOUS_SESSION_ID = PACKAGE_NAME + ".previousSessionId";
public static final String PREFKEY_DEVICE_ID = PACKAGE_NAME + ".deviceId";
public static final String PREFKEY_USER_ID = PACKAGE_NAME + ".userId";
public static final String PREFKEY_OPT_OUT = PACKAGE_NAME + ".optOut";
public static final String IDENTIFY_EVENT = "$identify";
public static final String GROUP_IDENTIFY_EVENT = "$groupidentify";
public static final String AMP_OP_ADD = "$add";
public static final String AMP_OP_APPEND = "$append";
public static final String AMP_OP_CLEAR_ALL = "$clearAll";
public static final String AMP_OP_PREPEND = "$prepend";
public static final String AMP_OP_SET = "$set";
public static final String AMP_OP_SET_ONCE = "$setOnce";
public static final String AMP_OP_UNSET = "$unset";
public static final String AMP_OP_PREINSERT = "$preInsert";
public static final String AMP_OP_POSTINSERT = "$postInsert";
public static final String AMP_OP_REMOVE = "$remove";
public static final String AMP_REVENUE_EVENT = "revenue_amount";
public static final String AMP_REVENUE_PRODUCT_ID = "$productId";
public static final String AMP_REVENUE_QUANTITY = "$quantity";
public static final String AMP_REVENUE_PRICE = "$price";
public static final String AMP_REVENUE_REVENUE_TYPE = "$revenueType";
public static final String AMP_REVENUE_RECEIPT = "$receipt";
public static final String AMP_REVENUE_RECEIPT_SIG = "$receiptSig";
public static final String AMP_TRACKING_OPTION_ADID = "adid";
public static final String AMP_TRACKING_OPTION_APP_SET_ID = "app_set_id";
public static final String AMP_TRACKING_OPTION_CARRIER = "carrier";
public static final String AMP_TRACKING_OPTION_CITY = "city";
public static final String AMP_TRACKING_OPTION_COUNTRY = "country";
public static final String AMP_TRACKING_OPTION_DEVICE_BRAND = "device_brand";
public static final String AMP_TRACKING_OPTION_DEVICE_MANUFACTURER = "device_manufacturer";
public static final String AMP_TRACKING_OPTION_DEVICE_MODEL = "device_model";
public static final String AMP_TRACKING_OPTION_DMA = "dma";
public static final String AMP_TRACKING_OPTION_IP_ADDRESS = "ip_address";
public static final String AMP_TRACKING_OPTION_LANGUAGE = "language";
public static final String AMP_TRACKING_OPTION_LAT_LNG = "lat_lng";
public static final String AMP_TRACKING_OPTION_OS_NAME = "os_name";
public static final String AMP_TRACKING_OPTION_OS_VERSION = "os_version";
public static final String AMP_TRACKING_OPTION_API_LEVEL = "api_level";
public static final String AMP_TRACKING_OPTION_PLATFORM = "platform";
public static final String AMP_TRACKING_OPTION_REGION = "region";
public static final String AMP_TRACKING_OPTION_VERSION_NAME = "version_name";
public static final String AMP_PLAN_BRANCH = "branch";
public static final String AMP_PLAN_SOURCE = "source";
public static final String AMP_PLAN_VERSION = "version";
public static final String AMP_PLAN_VERSION_ID = "versionId";
public static final String AMP_INGESTION_METADATA_SOURCE_NAME = "source_name";
public static final String AMP_INGESTION_METADATA_SOURCE_VERSION = "source_version";
}
================================================
FILE: src/main/java/com/amplitude/api/CursorWindowAllocationException.java
================================================
package com.amplitude.api;
/**
* This is Amplitude's substitute for android.database.CursorWindowAllocationException.
* Android's CursorWindow will throw that exception, but Android does not allow you to import
* the exception class directly to catch it. This is Amplitude's stand-in for that class.
*
* @hide
*/
public class CursorWindowAllocationException extends RuntimeException {
public CursorWindowAllocationException(String description) {
super(description);
}
}
================================================
FILE: src/main/java/com/amplitude/api/DatabaseHelper.java
================================================
package com.amplitude.api;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDoneException;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteStatement;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
class DatabaseHelper extends SQLiteOpenHelper {
private static final String TAG = DatabaseHelper.class.getName();
static final Map instances = new HashMap();
protected static final String STORE_TABLE_NAME = "store";
protected static final String LONG_STORE_TABLE_NAME = "long_store";
private static final String KEY_FIELD = "key";
private static final String VALUE_FIELD = "value";
protected static final String EVENT_TABLE_NAME = "events";
protected static final String IDENTIFY_TABLE_NAME = "identifys";
protected static final String IDENTIFY_INTERCEPTOR_TABLE_NAME = "identify_interceptor";
private static final String ID_FIELD = "id";
private static final String EVENT_FIELD = "event";
private static final String CREATE_STORE_TABLE = "CREATE TABLE IF NOT EXISTS "
+ STORE_TABLE_NAME + " (" + KEY_FIELD + " TEXT PRIMARY KEY NOT NULL, "
+ VALUE_FIELD + " TEXT);";
private static final String CREATE_LONG_STORE_TABLE = "CREATE TABLE IF NOT EXISTS "
+ LONG_STORE_TABLE_NAME + " (" + KEY_FIELD + " TEXT PRIMARY KEY NOT NULL, "
+ VALUE_FIELD + " INTEGER);";
private static final String CREATE_EVENTS_TABLE = "CREATE TABLE IF NOT EXISTS "
+ EVENT_TABLE_NAME + " (" + ID_FIELD + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ EVENT_FIELD + " TEXT);";
private static final String CREATE_IDENTIFYS_TABLE = "CREATE TABLE IF NOT EXISTS "
+ IDENTIFY_TABLE_NAME + " (" + ID_FIELD + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ EVENT_FIELD + " TEXT);";
private static final String CREATE_IDENTIFY_INTERCEPTOR_TABLE = "CREATE TABLE IF NOT EXISTS "
+ IDENTIFY_INTERCEPTOR_TABLE_NAME + " (" + ID_FIELD + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ EVENT_FIELD + " TEXT);";
File file;
private String instanceName;
private boolean callResetListenerOnDatabaseReset = true;
private DatabaseResetListener databaseResetListener;
private static final AmplitudeLog logger = AmplitudeLog.getLogger();
@Deprecated
static DatabaseHelper getDatabaseHelper(Context context) {
return getDatabaseHelper(context, null);
}
static synchronized DatabaseHelper getDatabaseHelper(Context context, String instance) {
instance = Utils.normalizeInstanceName(instance);
DatabaseHelper dbHelper = instances.get(instance);
if (dbHelper == null) {
dbHelper = new DatabaseHelper(context.getApplicationContext(), instance);
instances.put(instance, dbHelper);
}
return dbHelper;
}
private static String getDatabaseName(String instance) {
return (Utils.isEmptyString(instance) || instance.equals(Constants.DEFAULT_INSTANCE)) ? Constants.DATABASE_NAME : Constants.DATABASE_NAME + "_" + instance;
}
protected DatabaseHelper(Context context) {
this(context, null);
}
protected DatabaseHelper(Context context, String instance) {
super(context, getDatabaseName(instance), null, Constants.DATABASE_VERSION);
file = context.getDatabasePath(getDatabaseName(instance));
instanceName = Utils.normalizeInstanceName(instance);
}
void setDatabaseResetListener(DatabaseResetListener databaseResetListener) {
this.databaseResetListener = databaseResetListener;
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_STORE_TABLE);
db.execSQL(CREATE_LONG_STORE_TABLE);
// INTEGER PRIMARY KEY AUTOINCREMENT guarantees that all generated values
// for the field will be monotonically increasing and unique over the
// lifetime of the table, even if rows get removed
db.execSQL(CREATE_EVENTS_TABLE);
db.execSQL(CREATE_IDENTIFYS_TABLE);
db.execSQL(CREATE_IDENTIFY_INTERCEPTOR_TABLE);
// NOTE: the database file can become corrupted between interactions
// getWriteableDatabase and getReadableDatabase will test for corruption
// and actually delete the database file and call onCreate again if it's corrupted
// Our normal catch exception and delete database does not get triggered in this scenario
// Therefore we are also calling the reset callback inside onCreate
if (databaseResetListener != null && callResetListenerOnDatabaseReset) {
try {
callResetListenerOnDatabaseReset = false; // guards against stack overflow
databaseResetListener.onDatabaseReset(db);
} catch (SQLiteException e) {
logger.e(TAG, String.format("databaseReset callback failed during onCreate"), e);
} finally {
callResetListenerOnDatabaseReset = true;
}
}
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion > newVersion) {
logger.e(TAG, "onUpgrade() with invalid oldVersion and newVersion");
resetDatabase(db);
return;
}
if (newVersion <= 1) {
return;
}
switch (oldVersion) {
case 1:
db.execSQL(CREATE_STORE_TABLE);
if (newVersion <= 2) break;
case 2:
db.execSQL(CREATE_IDENTIFYS_TABLE);
db.execSQL(CREATE_LONG_STORE_TABLE);
if (newVersion <= 3) break;
case 3:
db.execSQL(CREATE_IDENTIFY_INTERCEPTOR_TABLE);
if (newVersion <= 4) break;
case 4:
break;
default:
logger.e(TAG, "onUpgrade() with unknown oldVersion " + oldVersion);
resetDatabase(db);
}
}
private void resetDatabase(SQLiteDatabase db) {
db.execSQL("DROP TABLE IF EXISTS " + STORE_TABLE_NAME);
db.execSQL("DROP TABLE IF EXISTS " + LONG_STORE_TABLE_NAME);
db.execSQL("DROP TABLE IF EXISTS " + EVENT_TABLE_NAME);
db.execSQL("DROP TABLE IF EXISTS " + IDENTIFY_TABLE_NAME);
db.execSQL("DROP TABLE IF EXISTS " + IDENTIFY_INTERCEPTOR_TABLE_NAME);
onCreate(db);
}
synchronized long insertOrReplaceKeyValue(String key, String value) {
return value == null ? deleteKeyFromTable(STORE_TABLE_NAME, key) :
insertOrReplaceKeyValueToTable(STORE_TABLE_NAME, key, value);
}
synchronized long insertOrReplaceKeyLongValue(String key, Long value) {
return value == null ? deleteKeyFromTable(LONG_STORE_TABLE_NAME, key) :
insertOrReplaceKeyValueToTable(LONG_STORE_TABLE_NAME, key, value);
}
synchronized long insertOrReplaceKeyValueToTable(String table, String key, Object value) {
long result = -1;
SQLiteDatabase db = null;
try {
db = getWritableDatabase();
result = insertOrReplaceKeyValueToTable(db, table, key, value);
} catch (SQLiteException e) {
logger.e(TAG, String.format("insertOrReplaceKeyValue in %s failed", table), e);
// Hard to recover from SQLiteExceptions, just start fresh
delete();
} catch (StackOverflowError e) {
logger.e(TAG, String.format("insertOrReplaceKeyValue in %s failed", table), e);
// potential stack overflow error when getting database on custom Android versions
delete();
} finally {
if (db != null && db.isOpen()) {
close();
}
}
return result;
}
synchronized long insertOrReplaceKeyValueToTable(SQLiteDatabase db, String table, String key, Object value) throws SQLiteException, StackOverflowError {
long result = -1;
ContentValues contentValues = new ContentValues();
contentValues.put(KEY_FIELD, key);
if (value instanceof Long) {
contentValues.put(VALUE_FIELD, (Long) value);
} else {
contentValues.put(VALUE_FIELD, (String) value);
}
result = insertKeyValueContentValuesIntoTable(db, table, contentValues);
if (result == -1) {
logger.w(TAG, "Insert failed");
}
return result;
}
synchronized long insertKeyValueContentValuesIntoTable(SQLiteDatabase db, String table, ContentValues contentValues) throws SQLiteException, StackOverflowError {
return db.insertWithOnConflict(
table,
null,
contentValues,
SQLiteDatabase.CONFLICT_REPLACE
);
}
synchronized long deleteKeyFromTable(String table, String key) {
long result = -1;
try {
SQLiteDatabase db = getWritableDatabase();
result = db.delete(table, KEY_FIELD + "=?", new String[]{key});
} catch (SQLiteException e) {
logger.e(TAG, String.format("deleteKey from %s failed", table), e);
// Hard to recover from SQLiteExceptions, just start fresh
delete();
} catch (StackOverflowError e) {
logger.e(TAG, String.format("deleteKey from %s failed", table), e);
// potential stack overflow error when getting database on custom Android versions
delete();
} finally {
close();
}
return result;
}
synchronized long addEvent(String event) {
return addEventToTable(EVENT_TABLE_NAME, event);
}
synchronized long addIdentify(String identifyEvent) {
return addEventToTable(IDENTIFY_TABLE_NAME, identifyEvent);
}
synchronized long addIdentifyInterceptor(String identifyEvent) {
return addEventToTable(IDENTIFY_INTERCEPTOR_TABLE_NAME, identifyEvent);
}
private synchronized long addEventToTable(String table, String event) {
long result = -1;
try {
SQLiteDatabase db = getWritableDatabase();
ContentValues contentValues = new ContentValues();
contentValues.put(EVENT_FIELD, event);
result = insertEventContentValuesIntoTable(db, table, contentValues);
if (result == -1) {
logger.w(TAG, String.format("Insert into %s failed", table));
}
} catch (SQLiteException e) {
logger.e(TAG, String.format("addEvent to %s failed", table), e);
// Hard to recover from SQLiteExceptions, just start fresh
delete();
} catch (StackOverflowError e) {
logger.e(TAG, String.format("addEvent to %s failed", table), e);
// potential stack overflow error when getting database on custom Android versions
delete();
} finally {
close();
}
return result;
}
synchronized long insertEventContentValuesIntoTable(SQLiteDatabase db, String table, ContentValues contentValues) throws SQLiteException, StackOverflowError {
return db.insert(table, null, contentValues);
}
synchronized String getValue(String key) {
return (String) getValueFromTable(STORE_TABLE_NAME, key);
}
synchronized Long getLongValue(String key) {
return (Long) getValueFromTable(LONG_STORE_TABLE_NAME, key);
}
protected synchronized Object getValueFromTable(String table, String key) {
Object value = null;
Cursor cursor = null;
try {
SQLiteDatabase db = getReadableDatabase();
cursor = queryDb(
db, table, new String[]{KEY_FIELD, VALUE_FIELD}, KEY_FIELD + " = ?",
new String[]{key}, null, null, null, null
);
if (cursor.moveToFirst()) {
value = table.equals(STORE_TABLE_NAME) ? cursor.getString(1) : cursor.getLong(1);
}
} catch (SQLiteException e) {
logger.e(TAG, String.format("getValue from %s failed", table), e);
// Hard to recover from SQLiteExceptions, just start fresh
delete();
} catch (StackOverflowError e) {
logger.e(TAG, String.format("getValue from %s failed", table), e);
// potential stack overflow error when getting database on custom Android versions
delete();
} catch (IllegalStateException e) { // put before Runtime since IllegalState extends
// cursor window row too big exception
handleIfCursorRowTooLargeException(e);
} catch (RuntimeException e) {
// cursor window allocation exception
convertIfCursorWindowException(e);
} finally {
if (cursor != null) {
cursor.close();
}
close();
}
return value;
}
synchronized List getEvents(long upToId, long limit) throws JSONException {
return getEventsFromTable(EVENT_TABLE_NAME, upToId, limit);
}
synchronized List getIdentifys(
long upToId, long limit) throws JSONException {
return getEventsFromTable(IDENTIFY_TABLE_NAME, upToId, limit);
}
synchronized List getIdentifyInterceptors(
long upToId,
long limit
) throws JSONException {
return getEventsFromTable(IDENTIFY_INTERCEPTOR_TABLE_NAME, upToId, limit);
}
protected synchronized List getEventsFromTable(
String table, long upToId, long limit) throws JSONException {
try {
return getEventsBatchFromTable(table, upToId, limit);
} catch (CursorWindowAllocationException e) {
return getEventsRowByRowFromTable(table, upToId, limit);
}
}
private List getEventsBatchFromTable(
String table, long upToId, long limit) throws JSONException {
List events = new LinkedList();
Cursor cursor = null;
try {
SQLiteDatabase db = getReadableDatabase();
cursor = queryDb(
db, table, new String[] { ID_FIELD, EVENT_FIELD },
upToId >= 0 ? ID_FIELD + " <= " + upToId : null, null, null, null,
ID_FIELD + " ASC", limit >= 0 ? "" + limit : null
);
while (cursor.moveToNext()) {
long eventId = cursor.getLong(0);
String event = cursor.getString(1);
if (Utils.isEmptyString(event)) {
continue;
}
JSONObject obj = new JSONObject(event);
obj.put("event_id", eventId);
events.add(obj);
}
} catch (SQLiteException e) {
logger.e(TAG, String.format("getEvents from %s failed", table), e);
delete();
} catch (StackOverflowError e) {
logger.e(TAG, String.format("getEvents from %s failed", table), e);
delete();
} catch (IllegalStateException e) { // put before Runtime since IllegalState extends
handleIfCursorRowTooLargeException(e);
} catch (RuntimeException e) {
convertIfCursorWindowException(e);
} finally {
if (cursor != null) {
cursor.close();
}
close();
}
return events;
}
private List getEventsRowByRowFromTable(
String table, long upToId, long limit) throws JSONException {
List eventIds = new LinkedList();
Cursor cursor = null;
try {
SQLiteDatabase db = getReadableDatabase();
cursor = queryDb(
db, table, new String[] { ID_FIELD },
upToId >= 0 ? ID_FIELD + " <= " + upToId : null, null, null, null,
ID_FIELD + " ASC", limit >= 0 ? "" + limit : null
);
while (cursor.moveToNext()) {
long eventId = cursor.getLong(0);
eventIds.add(eventId);
}
} catch (SQLiteException e) {
logger.e(TAG, String.format("getEvents from %s failed", table), e);
delete();
} catch (StackOverflowError e) {
logger.e(TAG, String.format("getEvents from %s failed", table), e);
delete();
} catch (IllegalStateException e) { // put before Runtime since IllegalState extends
handleIfCursorRowTooLargeException(e);
} catch (RuntimeException e) {
convertIfCursorWindowException(e);
} finally {
if (cursor != null) {
cursor.close();
}
close();
}
try {
List events = new LinkedList();
for (Long eventId : eventIds) {
JSONObject event = getEventFromTable(table, eventId);
if (event != null) {
events.add(event);
}
}
return events;
} finally {
close();
}
}
protected synchronized JSONObject getEventFromTable(String table, long eventId) throws JSONException {
JSONObject event = null;
Cursor cursor = null;
try {
SQLiteDatabase db = getReadableDatabase();
cursor = queryDb(
db, table, new String[] { EVENT_FIELD },
ID_FIELD + " = " + eventId,
null, null, null, null, null
);
if (cursor.moveToFirst()) {
String eventData = cursor.getString(0);
if (!Utils.isEmptyString(eventData)) {
event = new JSONObject(eventData);
event.put("event_id", eventId);
}
}
} catch (SQLiteException e) {
logger.e(TAG, String.format("getEvent from %s failed", table), e);
delete();
} catch (StackOverflowError e) {
logger.e(TAG, String.format("getEvent from %s failed", table), e);
delete();
} catch (IllegalStateException e) { // put before Runtime since IllegalState extends
handleIfCursorRowTooLargeException(e);
} catch (RuntimeException e) {
convertIfCursorWindowException(e);
} finally {
if (cursor != null) {
cursor.close();
}
}
return event;
}
synchronized long getEventCount() {
return getEventCountFromTable(EVENT_TABLE_NAME);
}
synchronized long getIdentifyCount() {
return getEventCountFromTable(IDENTIFY_TABLE_NAME);
}
synchronized long getTotalEventCount() {
return getEventCount() + getIdentifyCount();
}
synchronized long getIdentifyInterceptorCount() {
return getEventCountFromTable(IDENTIFY_INTERCEPTOR_TABLE_NAME);
}
private synchronized long getEventCountFromTable(String table) {
long numberRows = 0;
SQLiteStatement statement = null;
try {
SQLiteDatabase db = getReadableDatabase();
String query = "SELECT COUNT(*) FROM " + table;
statement = db.compileStatement(query);
numberRows = statement.simpleQueryForLong();
} catch (SQLiteException e) {
logger.e(TAG, String.format("getNumberRows for %s failed", table), e);
// Hard to recover from SQLiteExceptions, just start fresh
delete();
} catch (StackOverflowError e) {
logger.e(TAG, String.format("getNumberRows for %s failed", table), e);
// potential stack overflow error when getting database on custom Android versions
delete();
} finally {
if (statement != null) {
statement.close();
}
close();
}
return numberRows;
}
synchronized long getNthEventId(long n) {
return getNthEventIdFromTable(EVENT_TABLE_NAME, n);
}
synchronized long getNthIdentifyId(long n) {
return getNthEventIdFromTable(IDENTIFY_TABLE_NAME, n);
}
synchronized long getLastIdentifyInterceptorId() {
return getNthEventIdFromTable(IDENTIFY_INTERCEPTOR_TABLE_NAME, 1, "DESC");
}
private synchronized long getNthEventIdFromTable(String table, long n) {
return getNthEventIdFromTable(table, n, "ASC");
}
private synchronized long getNthEventIdFromTable(String table, long n, String orderBy) {
long nthEventId = -1;
SQLiteStatement statement = null;
try {
SQLiteDatabase db = getReadableDatabase();
String query = "SELECT " + ID_FIELD + " FROM " + table + " ORDER BY " + ID_FIELD +
" " + orderBy + " LIMIT 1 OFFSET " + (n - 1);
statement = db.compileStatement(query);
nthEventId = -1;
try {
nthEventId = statement.simpleQueryForLong();
} catch (SQLiteDoneException e) {
logger.w(TAG, e);
}
} catch (SQLiteException e) {
logger.e(TAG, String.format("getNthEventId from %s failed", table), e);
// Hard to recover from SQLiteExceptions, just start fresh
delete();
} catch (StackOverflowError e) {
logger.e(TAG, String.format("getNthEventId from %s failed", table), e);
// potential stack overflow error when getting database on custom Android versions
delete();
} finally {
if (statement != null) {
statement.close();
}
close();
}
return nthEventId;
}
synchronized void removeEvents(long maxId) {
removeEventsFromTable(EVENT_TABLE_NAME, maxId);
}
synchronized void removeIdentifys(long maxId) {
removeEventsFromTable(IDENTIFY_TABLE_NAME, maxId);
}
synchronized void removeIdentifyInterceptors(long maxId) {
removeEventsFromTable(IDENTIFY_INTERCEPTOR_TABLE_NAME, maxId);
}
private synchronized void removeEventsFromTable(String table, long maxId) {
try {
SQLiteDatabase db = getWritableDatabase();
db.delete(table, ID_FIELD + " <= " + maxId, null);
} catch (SQLiteException e) {
logger.e(TAG, String.format("removeEvents from %s failed", table), e);
delete();
} catch (StackOverflowError e) {
logger.e(TAG, String.format("removeEvents from %s failed", table), e);
delete();
} finally {
close();
}
}
synchronized void removeEvent(long id) {
removeEventFromTable(EVENT_TABLE_NAME, id);
}
synchronized void removeIdentify(long id) {
removeEventFromTable(IDENTIFY_TABLE_NAME, id);
}
synchronized void removeIdentifyIntercept(long id) {
removeEventFromTable(IDENTIFY_INTERCEPTOR_TABLE_NAME, id);
}
private synchronized void removeEventFromTable(String table, long id) {
try {
SQLiteDatabase db = getWritableDatabase();
db.delete(table, ID_FIELD + " = " + id, null);
} catch (SQLiteException e) {
logger.e(TAG, String.format("removeEvent from %s failed", table), e);
delete();
} catch (StackOverflowError e) {
logger.e(TAG, String.format("removeEvent from %s failed", table), e);
delete();
} finally {
close();
}
}
private void delete() {
// This only gets called if the database somehow gets corrupted AFTER being fetched
// ie after the call to getWriteableDatabase / getReadableDatabase
// or if a SQL exception occurs during the interaction
try {
close();
file.delete();
} catch (SecurityException e) {
logger.e(TAG, "delete failed", e);
} finally {
if (databaseResetListener != null && callResetListenerOnDatabaseReset) {
callResetListenerOnDatabaseReset = false; // guards against stack overflow
SQLiteDatabase db = null;
try {
db = getWritableDatabase();
databaseResetListener.onDatabaseReset(db);
} catch (SQLiteException e) {
logger.e(TAG, String.format("databaseReset callback failed during delete"), e);
}
finally {
callResetListenerOnDatabaseReset = true;
if (db != null && db.isOpen()) {
close();
}
}
}
}
}
boolean dbFileExists() {
return file.exists();
}
// add level of indirection to facilitate mocking during unit tests
Cursor queryDb(
SQLiteDatabase db, String table, String[] columns, String selection,
String[] selectionArgs, String groupBy, String having, String orderBy, String limit
) {
return db.query(table, columns, selection, selectionArgs, groupBy, having, orderBy, limit);
}
/*
Checks if the IllegalStateException is caused by CursorWindow row too big exception
If it is, then we want to reset the database to clear the bad data
*/
private void handleIfCursorRowTooLargeException(IllegalStateException e) {
String message = e.getMessage();
if (!Utils.isEmptyString(message) && message.contains("Couldn't read") && message.contains("CursorWindow")) {
delete();
} else {
throw e;
}
}
/*
Checks if the RuntimeException is an android.database.CursorWindowAllocationException.
If it is, then wrap the message in Amplitude's CursorWindowAllocationException so the
AmplitudeClient can handle it. If not then rethrow.
*/
private static void convertIfCursorWindowException(RuntimeException e) {
String message = e.getMessage();
if (!Utils.isEmptyString(message) && (message.startsWith("Cursor window allocation of") || message.startsWith("Could not allocate CursorWindow"))) {
throw new CursorWindowAllocationException(message);
} else {
throw e;
}
}
}
================================================
FILE: src/main/java/com/amplitude/api/DatabaseResetListener.java
================================================
package com.amplitude.api;
import android.database.sqlite.SQLiteDatabase;
public interface DatabaseResetListener {
public void onDatabaseReset(SQLiteDatabase db);
}
================================================
FILE: src/main/java/com/amplitude/api/DeviceInfo.java
================================================
package com.amplitude.api;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
import android.location.LocationManager;
import android.os.Build;
import android.os.LocaleList;
import android.provider.Settings.Secure;
import android.telephony.TelephonyManager;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
@SuppressWarnings("MissingPermission")
public class DeviceInfo {
private static final String TAG = DeviceInfo.class.getName();
public static final String OS_NAME = "android";
private static final String SETTING_LIMIT_AD_TRACKING = "limit_ad_tracking";
private static final String SETTING_ADVERTISING_ID = "advertising_id";
private boolean locationListening;
private boolean shouldTrackAdid;
private Context context;
private CachedInfo cachedInfo;
/**
* Internal class serves as a cache
*/
private class CachedInfo {
private String advertisingId;
private String country;
private String versionName;
private String osName;
private String osVersion;
private String brand;
private String manufacturer;
private String model;
private String carrier;
private String language;
private boolean limitAdTrackingEnabled;
private boolean gpsEnabled; // google play services
private String appSetId;
private CachedInfo() {
advertisingId = getAdvertisingId();
versionName = getVersionName();
osName = getOsName();
osVersion = getOsVersion();
brand = getBrand();
manufacturer = getManufacturer();
model = getModel();
carrier = getCarrier();
country = getCountry();
language = getLanguage();
gpsEnabled = checkGPSEnabled();
appSetId = getAppSetId();
}
/**
* Internal methods for getting raw information
*/
private String getVersionName() {
PackageInfo packageInfo;
try {
packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
return packageInfo.versionName;
} catch (NameNotFoundException e) {
} catch (Exception e) {
}
return null;
}
private String getOsName() {
return OS_NAME;
}
private String getOsVersion() {
return Build.VERSION.RELEASE;
}
private String getBrand() {
return Build.BRAND;
}
private String getManufacturer() {
return Build.MANUFACTURER;
}
private String getModel() {
return Build.MODEL;
}
private String getCarrier() {
try {
TelephonyManager manager = (TelephonyManager) context
.getSystemService(Context.TELEPHONY_SERVICE);
return manager.getNetworkOperatorName();
} catch (Exception e) {
// Failed to get network operator name from network
}
return null;
}
private String getCountry() {
// This should not be called on the main thread.
// Prioritize reverse geocode, but until we have a result from that,
// we try to grab the country from the network, and finally the locale
String country = getCountryFromLocation();
if (!Utils.isEmptyString(country)) {
return country;
}
country = getCountryFromNetwork();
if (!Utils.isEmptyString(country)) {
return country;
}
return getCountryFromLocale();
}
private String getCountryFromLocation() {
if (!isLocationListening()) {
return null;
}
Location recent = getMostRecentLocation();
if (recent != null) {
try {
if (Geocoder.isPresent()) {
Geocoder geocoder = getGeocoder();
List addresses = geocoder.getFromLocation(recent.getLatitude(),
recent.getLongitude(), 1);
if (addresses != null) {
for (Address address : addresses) {
if (address != null) {
return address.getCountryCode();
}
}
}
}
} catch (IOException e) {
// Failed to reverse geocode location
} catch (NullPointerException e) {
// Failed to reverse geocode location
} catch (NoSuchMethodError e) {
// failed to fetch geocoder
} catch (IllegalArgumentException e) {
// Bad lat / lon values can cause Geocoder to throw IllegalArgumentExceptions
} catch (IllegalStateException e) {
// sometimes the location manager is unavailable
} catch (SecurityException e) {
// Customized Android System without Google Play Service Installed
}
}
return null;
}
private String getCountryFromNetwork() {
try {
TelephonyManager manager = (TelephonyManager) context
.getSystemService(Context.TELEPHONY_SERVICE);
if (manager.getPhoneType() != TelephonyManager.PHONE_TYPE_CDMA) {
String country = manager.getNetworkCountryIso();
if (country != null) {
return country.toUpperCase(Locale.US);
}
}
} catch (Exception e) {
// Failed to get country from network
}
return null;
}
private Locale getLocale() {
final Configuration configuration = Resources.getSystem().getConfiguration();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
final LocaleList localeList = configuration.getLocales();
if (localeList.isEmpty()) {
return Locale.getDefault();
} else {
return localeList.get(0);
}
} else {
return configuration.locale;
}
}
private String getCountryFromLocale() {
return getLocale().getCountry();
}
private String getLanguage() {
return getLocale().getLanguage();
}
private String getAdvertisingId() {
if (!shouldTrackAdid) {
return null;
}
// This should not be called on the main thread.
if ("Amazon".equals(getManufacturer())) {
return getAndCacheAmazonAdvertisingId();
} else {
return getAndCacheGoogleAdvertisingId();
}
}
private String getAppSetId() {
try {
Class AppSet = Class
.forName("com.google.android.gms.appset.AppSet");
Method getClient = AppSet.getMethod("getClient", Context.class);
Object appSetIdClient = getClient.invoke(null, context);
Method getAppSetIdInfo = appSetIdClient.getClass().getMethod("getAppSetIdInfo");
Object taskWithAppSetInfo = getAppSetIdInfo.invoke(appSetIdClient);
Class Tasks = Class.forName("com.google.android.gms.tasks.Tasks");
Method await = Tasks.getMethod("await", Class.forName("com.google.android.gms.tasks.Task"));
Object appSetInfo = await.invoke(null, taskWithAppSetInfo);
Method getId = appSetInfo.getClass().getMethod("getId");
appSetId = (String) getId.invoke(appSetInfo);
} catch (ClassNotFoundException e) {
AmplitudeLog.getLogger().w(TAG, "Google Play Services SDK not found for app set id!");
} catch (InvocationTargetException e) {
AmplitudeLog.getLogger().w(TAG, "Google Play Services not available for app set id");
} catch (Exception e) {
AmplitudeLog.getLogger().e(TAG, "Encountered an error connecting to Google Play Services for app set id", e);
}
return appSetId;
}
private String getAndCacheAmazonAdvertisingId() {
ContentResolver cr = context.getContentResolver();
limitAdTrackingEnabled = Secure.getInt(cr, SETTING_LIMIT_AD_TRACKING, 0) == 1;
advertisingId = Secure.getString(cr, SETTING_ADVERTISING_ID);
return advertisingId;
}
private String getAndCacheGoogleAdvertisingId() {
try {
Class AdvertisingIdClient = Class
.forName("com.google.android.gms.ads.identifier.AdvertisingIdClient");
Method getAdvertisingInfo = AdvertisingIdClient.getMethod("getAdvertisingIdInfo",
Context.class);
Object advertisingInfo = getAdvertisingInfo.invoke(null, context);
Method isLimitAdTrackingEnabled = advertisingInfo.getClass().getMethod(
"isLimitAdTrackingEnabled");
Boolean limitAdTrackingEnabled = (Boolean) isLimitAdTrackingEnabled
.invoke(advertisingInfo);
this.limitAdTrackingEnabled =
limitAdTrackingEnabled != null && limitAdTrackingEnabled;
Method getId = advertisingInfo.getClass().getMethod("getId");
advertisingId = (String) getId.invoke(advertisingInfo);
} catch (ClassNotFoundException e) {
AmplitudeLog.getLogger().w(TAG, "Google Play Services SDK not found for advertising id!");
} catch (InvocationTargetException e) {
AmplitudeLog.getLogger().w(TAG, "Google Play Services not available for advertising id");
} catch (Exception e) {
AmplitudeLog.getLogger().e(TAG, "Encountered an error connecting to Google Play Services for advertising id", e);
}
return advertisingId;
}
private boolean checkGPSEnabled() {
// This should not be called on the main thread.
try {
Class GPSUtil = Class
.forName("com.google.android.gms.common.GooglePlayServicesUtil");
Method getGPSAvailable = GPSUtil.getMethod("isGooglePlayServicesAvailable",
Context.class);
Integer status = (Integer) getGPSAvailable.invoke(null, context);
// status 0 corresponds to com.google.android.gms.common.ConnectionResult.SUCCESS;
return status != null && status.intValue() == 0;
} catch (NoClassDefFoundError e) {
AmplitudeLog.getLogger().w(TAG, "Google Play Services Util not found!");
} catch (ClassNotFoundException e) {
AmplitudeLog.getLogger().w(TAG, "Google Play Services Util not found!");
} catch (NoSuchMethodException e) {
AmplitudeLog.getLogger().w(TAG, "Google Play Services not available");
} catch (InvocationTargetException e) {
AmplitudeLog.getLogger().w(TAG, "Google Play Services not available");
} catch (IllegalAccessException e) {
AmplitudeLog.getLogger().w(TAG, "Google Play Services not available");
} catch (Exception e) {
AmplitudeLog.getLogger().w(TAG,
"Error when checking for Google Play Services: " + e);
}
return false;
}
}
public DeviceInfo(Context context, boolean locationListening, boolean shouldTrackAdid) {
this.context = context;
this.locationListening = locationListening;
this.shouldTrackAdid = shouldTrackAdid;
}
private CachedInfo getCachedInfo() {
if (cachedInfo == null) {
cachedInfo = new CachedInfo();
}
return cachedInfo;
}
public void prefetch() {
getCachedInfo();
}
public static String generateUUID() {
return UUID.randomUUID().toString();
}
public String getVersionName() {
return getCachedInfo().versionName;
}
public String getOsName() {
return getCachedInfo().osName;
}
public String getOsVersion() {
return getCachedInfo().osVersion;
}
public String getBrand() {
return getCachedInfo().brand;
}
public String getManufacturer() {
return getCachedInfo().manufacturer;
}
public String getModel() {
return getCachedInfo().model;
}
public String getCarrier() {
return getCachedInfo().carrier;
}
public String getCountry() {
return getCachedInfo().country;
}
public String getLanguage() {
return getCachedInfo().language;
}
public String getAdvertisingId() {
return getCachedInfo().advertisingId;
}
public boolean isLimitAdTrackingEnabled() {
return getCachedInfo().limitAdTrackingEnabled;
}
public String getAppSetId() {
return getCachedInfo().appSetId;
}
public boolean isGooglePlayServicesEnabled() { return getCachedInfo().gpsEnabled; }
public Location getMostRecentLocation() {
if (!isLocationListening()) {
return null;
}
if (!Utils.checkLocationPermissionAllowed(context)) {
return null;
}
LocationManager locationManager = (LocationManager) context
.getSystemService(Context.LOCATION_SERVICE);
// Don't crash if the device does not have location services.
if (locationManager == null) {
return null;
}
// It's possible that the location service is running out of process
// and the remote getProviders call fails. Handle null provider lists.
List providers = null;
try {
providers = locationManager.getProviders(true);
} catch (SecurityException e) {
// failed to get providers list
} catch (Exception e) {
// other causes
}
if (providers == null) {
return null;
}
List locations = new ArrayList();
for (String provider : providers) {
Location location = null;
try {
location = locationManager.getLastKnownLocation(provider);
} catch (SecurityException e) {
AmplitudeLog.getLogger().w(TAG, "Failed to get most recent location");
} catch (Exception e) {
AmplitudeLog.getLogger().w(TAG, "Failed to get most recent location");
}
if (location != null) {
locations.add(location);
}
}
long maximumTimestamp = -1;
Location bestLocation = null;
for (Location location : locations) {
if (location.getTime() > maximumTimestamp) {
maximumTimestamp = location.getTime();
bestLocation = location;
}
}
return bestLocation;
}
public boolean isLocationListening() {
return locationListening;
}
public void setLocationListening(boolean locationListening) {
this.locationListening = locationListening;
}
// @VisibleForTesting
protected Geocoder getGeocoder() {
return new Geocoder(context, Locale.ENGLISH);
}
}
================================================
FILE: src/main/java/com/amplitude/api/Identify.java
================================================
package com.amplitude.api;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashSet;
import java.util.Set;
/**
*
Identify
* Identify objects are a wrapper for user property operations. Each method adds a user
* property operation to the Identify object, and returns the same Identify object, allowing
* you to chain multiple method calls together, for example:
* {@code Identify identify = new Identify().set("color", "green").add("karma", 1);}
*
* Note: if the same user property is used
* in multiple operations on a single Identify object, only the first operation on that
* property will be saved, and the rest will be ignored.
*
* After creating an Identify object and setting the desired operations, send it to Amplitude
* servers by calling {@code Amplitude.getInstance().identify(identify);} and pass in the object.
*
* @see
* Android SDK README for more information on the Identify API and user property operations.
*/
public class Identify {
/**
* The class identifier tag used in logging. TAG = {@code "com.amplitude.api.Identify";}
*/
private static final String TAG = Identify.class.getName();
/**
* Internal {@code JSONObject} to hold all of the user property operations.
*/
protected JSONObject userPropertiesOperations = new JSONObject();
/**
* Internal set to keep track of user property keys and test for duplicates.
*/
protected Set userProperties = new HashSet();
/**
* Sets a user property value only once. Subsequent @{code setOnce} operations on that user
* property will be ignored.
*
* @param property the user property to setOnce
* @param value the value of the user property
* @return the same Identify object
*/
public Identify setOnce(String property, boolean value) {
addToUserProperties(Constants.AMP_OP_SET_ONCE, property, value);
return this;
}
/**
* Sets a user property value only once. Subsequent @{code setOnce} operations on that user
* property will be ignored.
*
* @param property the user property to setOnce
* @param value the value of the user property
* @return the same Identify object
*/
public Identify setOnce(String property, double value) {
addToUserProperties(Constants.AMP_OP_SET_ONCE, property, value);
return this;
}
/**
* Sets a user property value only once. Subsequent @{code setOnce} operations on that user
* property will be ignored.
*
* @param property the user property to setOnce
* @param value the value of the user property
* @return the same Identify object
*/
public Identify setOnce(String property, float value) {
addToUserProperties(Constants.AMP_OP_SET_ONCE, property, value);
return this;
}
/**
* Sets a user property value only once. Subsequent @{code setOnce} operations on that user
* property will be ignored.
*
* @param property the user property to setOnce
* @param value the value of the user property
* @return the same Identify object
*/
public Identify setOnce(String property, int value) {
addToUserProperties(Constants.AMP_OP_SET_ONCE, property, value);
return this;
}
/**
* Sets a user property value only once. Subsequent @{code setOnce} operations on that user
* property will be ignored.
*
* @param property the user property to setOnce
* @param value the value of the user property
* @return the same Identify object
*/
public Identify setOnce(String property, long value) {
addToUserProperties(Constants.AMP_OP_SET_ONCE, property, value);
return this;
}
/**
* Sets a user property value only once. Subsequent @{code setOnce} operations on that user
* property will be ignored.
*
* @param property the user property to setOnce
* @param value the value of the user property
* @return the same Identify object
*/
public Identify setOnce(String property, String value) {
addToUserProperties(Constants.AMP_OP_SET_ONCE, property, value);
return this;
}
/**
* Sets a user property value only once. Subsequent @{code setOnce} operations on that user
* property will be ignored.
*
* @param property the user property to setOnce
* @param values the value of the user property
* @return the same Identify object
*/
public Identify setOnce(String property, JSONArray values) {
addToUserProperties(Constants.AMP_OP_SET_ONCE, property, values);
return this;
}
/**
* Sets a user property value only once. Subsequent @{code setOnce} operations on that user
* property will be ignored.
*
* @param property the user property to setOnce
* @param values the value of the user property
* @return the same Identify object
*/
public Identify setOnce(String property, JSONObject values) {
addToUserProperties(Constants.AMP_OP_SET_ONCE, property, values);
return this;
}
/**
* Sets a user property value only once. Subsequent @{code setOnce} operations on that user
* property will be ignored.
*
* @param property the user property to setOnce
* @param values the value of the user property
* @return the same Identify object
*/
public Identify setOnce(String property, boolean[] values) {
addToUserProperties(Constants.AMP_OP_SET_ONCE, property, booleanArrayToJSONArray(values));
return this;
}
/**
* Sets a user property value only once. Subsequent @{code setOnce} operations on that user
* property will be ignored.
*
* @param property the user property to setOnce
* @param values the value of the user property
* @return the same Identify object
*/
public Identify setOnce(String property, double[] values) {
addToUserProperties(Constants.AMP_OP_SET_ONCE, property, doubleArrayToJSONArray(values));
return this;
}
/**
* Sets a user property value only once. Subsequent @{code setOnce} operations on that user
* property will be ignored.
*
* @param property the user property to setOnce
* @param values the value of the user property
* @return the same Identify object
*/
public Identify setOnce(String property, float[] values) {
addToUserProperties(Constants.AMP_OP_SET_ONCE, property, floatArrayToJSONArray(values));
return this;
}
/**
* Sets a user property value only once. Subsequent @{code setOnce} operations on that user
* property will be ignored.
*
* @param property the user property to setOnce
* @param values the value of the user property
* @return the same Identify object
*/
public Identify setOnce(String property, int[] values) {
addToUserProperties(Constants.AMP_OP_SET_ONCE, property, intArrayToJSONArray(values));
return this;
}
/**
* Sets a user property value only once. Subsequent @{code setOnce} operations on that user
* property will be ignored.
*
* @param property the user property to setOnce
* @param values the value of the user property
* @return the same Identify object
*/
public Identify setOnce(String property, long[] values) {
addToUserProperties(Constants.AMP_OP_SET_ONCE, property, longArrayToJSONArray(values));
return this;
}
/**
* Sets a user property value only once. Subsequent @{code setOnce} operations on that user
* property will be ignored.
*
* @param property the user property to setOnce
* @param values the value of the user property
* @return the same Identify object
*/
public Identify setOnce(String property, String[] values) {
addToUserProperties(Constants.AMP_OP_SET_ONCE, property, stringArrayToJSONArray(values));
return this;
}
/**
* Sets a user property value. Existing values for that user property will be overwritten.
*
* @param property the user property to set
* @param value the value of the user property
* @return the same Identify object
*/
public Identify set(String property, boolean value) {
addToUserProperties(Constants.AMP_OP_SET, property, value);
return this;
}
/**
* Sets a user property value. Existing values for that user property will be overwritten.
*
* @param property the user property to set
* @param value the value of the user property
* @return the same Identify object
*/
public Identify set(String property, double value) {
addToUserProperties(Constants.AMP_OP_SET, property, value);
return this;
}
/**
* Sets a user property value. Existing values for that user property will be overwritten.
*
* @param property the user property to set
* @param value the value of the user property
* @return the same Identify object
*/
public Identify set(String property, float value) {
addToUserProperties(Constants.AMP_OP_SET, property, value);
return this;
}
/**
* Sets a user property value. Existing values for that user property will be overwritten.
*
* @param property the user property to set
* @param value the value of the user property
* @return the same Identify object
*/
public Identify set(String property, int value) {
addToUserProperties(Constants.AMP_OP_SET, property, value);
return this;
}
/**
* Sets a user property value. Existing values for that user property will be overwritten.
*
* @param property the user property to set
* @param value the value of the user property
* @return the same Identify object
*/
public Identify set(String property, long value) {
addToUserProperties(Constants.AMP_OP_SET, property, value);
return this;
}
/**
* Sets a user property value. Existing values for that user property will be overwritten.
*
* @param property the user property to set
* @param value the value of the user property
* @return the same Identify object
*/
public Identify set(String property, String value) {
addToUserProperties(Constants.AMP_OP_SET, property, value);
return this;
}
/**
* Sets a user property value. Existing values for that user property will be overwritten.
*
* @param property the user property to set
* @param values the value of the user property
* @return the same Identify object
*/
public Identify set(String property, JSONObject values) {
addToUserProperties(Constants.AMP_OP_SET, property, values);
return this;
}
/**
* Sets a user property value. Existing values for that user property will be overwritten.
*
* @param property the user property to set
* @param values the value of the user property
* @return the same Identify object
*/
public Identify set(String property, JSONArray values) {
addToUserProperties(Constants.AMP_OP_SET, property, values);
return this;
}
/**
* Sets a user property value. Existing values for that user property will be overwritten.
*
* @param property the user property to set
* @param values the value of the user property
* @return the same Identify object
*/
public Identify set(String property, boolean[] values) {
addToUserProperties(Constants.AMP_OP_SET, property, booleanArrayToJSONArray(values));
return this;
}
/**
* Sets a user property value. Existing values for that user property will be overwritten.
*
* @param property the user property to set
* @param values the value of the user property
* @return the same Identify object
*/
public Identify set(String property, double[] values) {
addToUserProperties(Constants.AMP_OP_SET, property, doubleArrayToJSONArray(values));
return this;
}
/**
* Sets a user property value. Existing values for that user property will be overwritten.
*
* @param property the user property to set
* @param values the value of the user property
* @return the same Identify object
*/
public Identify set(String property, float[] values) {
addToUserProperties(Constants.AMP_OP_SET, property, floatArrayToJSONArray(values));
return this;
}
/**
* Sets a user property value. Existing values for that user property will be overwritten.
*
* @param property the user property to set
* @param values the value of the user property
* @return the same Identify object
*/
public Identify set(String property, int[] values) {
addToUserProperties(Constants.AMP_OP_SET, property, intArrayToJSONArray(values));
return this;
}
/**
* Sets a user property value. Existing values for that user property will be overwritten.
*
* @param property the user property to set
* @param values the value of the user property
* @return the same Identify object
*/
public Identify set(String property, long[] values) {
addToUserProperties(Constants.AMP_OP_SET, property, longArrayToJSONArray(values));
return this;
}
/**
* Sets a user property value. Existing values for that user property will be overwritten.
*
* @param property the user property to set
* @param values the value of the user property
* @return the same Identify object
*/
public Identify set(String property, String[] values) {
addToUserProperties(Constants.AMP_OP_SET, property, stringArrayToJSONArray(values));
return this;
}
/**
* Increment a user property by some numerical value. If the user property does not have
* a value set, it will be initialized to 0 before being incremented. Value can be
* negative to decrement a user property value.
*
* @param property the user property to increment
* @param value the value (can be negative) to increment
* @return the same Identify object
*/
public Identify add(String property, double value) {
addToUserProperties(Constants.AMP_OP_ADD, property, value);
return this;
}
/**
* Increment a user property by some numerical value. If the user property does not have
* a value set, it will be initialized to 0 before being incremented. Value can be
* negative to decrement a user property value.
*
* @param property the user property to increment
* @param value the value (can be negative) to increment
* @return the same Identify object
*/
public Identify add(String property, float value) {
addToUserProperties(Constants.AMP_OP_ADD, property, value);
return this;
}
/**
* Increment a user property by some numerical value. If the user property does not have
* a value set, it will be initialized to 0 before being incremented. Value can be
* negative to decrement a user property value.
*
* @param property the user property to increment
* @param value the value (can be negative) to increment
* @return the same Identify object
*/
public Identify add(String property, int value) {
addToUserProperties(Constants.AMP_OP_ADD, property, value);
return this;
}
/**
* Increment a user property by some numerical value. If the user property does not have
* a value set, it will be initialized to 0 before being incremented. Value can be
* negative to decrement a user property value.
*
* @param property the user property to increment
* @param value the value (can be negative) to increment
* @return the same Identify object
*/
public Identify add(String property, long value) {
addToUserProperties(Constants.AMP_OP_ADD, property, value);
return this;
}
/**
* Increment a user property by some numerical value. If the user property does not have
* a value set, it will be initialized to 0 before being incremented. Value can be
* negative to decrement a user property value.
*
* @param property the user property to increment
* @param value the value (can be negative) to increment. Server-side we convert
* the string into a number if possible.
* @return the same Identify object
*/
public Identify add(String property, String value) {
addToUserProperties(Constants.AMP_OP_ADD, property, value);
return this;
}
/**
* Increment a user property by some numerical value. If the user property does not have
* a value set, it will be initialized to 0 before being incremented. Value can be
* negative to decrement a user property value.
*
* @param property the user property to increment
* @param values the value (can be negative) to increment. Server-side we flatten
* dictionaries and apply add to each flattened property value.
* @return the same Identify object
*/
public Identify add(String property, JSONObject values) {
addToUserProperties(Constants.AMP_OP_ADD, property, values);
return this;
}
/**
* Append a value or values to a user property. If the user property does not have a value
* set, it will be initialized to an empty list before the new values are appended. If
* the user property has an existing value and it is not a list, it will be converted into
* a list with the new value(s) appended.
*
* @param property the user property property to which to append
* @param value the value being appended
* @return the same Identify object
*/
public Identify append(String property, boolean value) {
addToUserProperties(Constants.AMP_OP_APPEND, property, value);
return this;
}
/**
* Append a value or values to a user property. If the user property does not have a value
* set, it will be initialized to an empty list before the new values are appended. If
* the user property has an existing value and it is not a list, it will be converted into
* a list with the new value(s) appended.
*
* @param property the user property property to which to append
* @param value the value being appended
* @return the same Identify object
*/
public Identify append(String property, double value) {
addToUserProperties(Constants.AMP_OP_APPEND, property, value);
return this;
}
/**
* Append a value or values to a user property. If the user property does not have a value
* set, it will be initialized to an empty list before the new values are appended. If
* the user property has an existing value and it is not a list, it will be converted into
* a list with the new value(s) appended.
*
* @param property the user property property to which to append
* @param value the value being appended
* @return the same Identify object
*/
public Identify append(String property, float value) {
addToUserProperties(Constants.AMP_OP_APPEND, property, value);
return this;
}
/**
* Append a value or values to a user property. If the user property does not have a value
* set, it will be initialized to an empty list before the new values are appended. If
* the user property has an existing value and it is not a list, it will be converted into
* a list with the new value(s) appended.
*
* @param property the user property property to which to append
* @param value the value being appended
* @return the same Identify object
*/
public Identify append(String property, int value) {
addToUserProperties(Constants.AMP_OP_APPEND, property, value);
return this;
}
/**
* Append a value or values to a user property. If the user property does not have a value
* set, it will be initialized to an empty list before the new values are appended. If
* the user property has an existing value and it is not a list, it will be converted into
* a list with the new value(s) appended.
*
* @param property the user property property to which to append
* @param value the value being appended
* @return the same Identify object
*/
public Identify append(String property, long value) {
addToUserProperties(Constants.AMP_OP_APPEND, property, value);
return this;
}
/**
* Append a value or values to a user property. If the user property does not have a value
* set, it will be initialized to an empty list before the new values are appended. If
* the user property has an existing value and it is not a list, it will be converted into
* a list with the new value(s) appended.
*
* @param property the user property property to which to append
* @param value the value being appended
* @return the same Identify object
*/
public Identify append(String property, String value) {
addToUserProperties(Constants.AMP_OP_APPEND, property, value);
return this;
}
/**
* Append a value or values to a user property. If the user property does not have a value
* set, it will be initialized to an empty list before the new values are appended. If
* the user property has an existing value and it is not a list, it will be converted into
* a list with the new value(s) appended.
*
* @param property the user property property to which to append
* @param values the values being appended
* @return the same Identify object
*/
public Identify append(String property, JSONArray values) {
addToUserProperties(Constants.AMP_OP_APPEND, property, values);
return this;
}
/**
* Append a value or values to a user property. If the user property does not have a value
* set, it will be initialized to an empty list before the new values are appended. If
* the user property has an existing value and it is not a list, it will be converted into
* a list with the new value(s) appended.
*
* @param property the user property property to which to append
* @param values the values being appended. Server-side we flatten dictionaries and apply
* append to each flattened property.
* @return the same Identify object
*/
public Identify append(String property, JSONObject values) {
addToUserProperties(Constants.AMP_OP_APPEND, property, values);
return this;
}
/**
* Append a value or values to a user property. If the user property does not have a value
* set, it will be initialized to an empty list before the new values are appended. If
* the user property has an existing value and it is not a list, it will be converted into
* a list with the new value(s) appended.
*
* @param property the user property property to which to append
* @param values the values being appended
* @return the same Identify object
*/
public Identify append(String property, boolean[] values) {
addToUserProperties(Constants.AMP_OP_APPEND, property, booleanArrayToJSONArray(values));
return this;
}
/**
* Append a value or values to a user property. If the user property does not have a value
* set, it will be initialized to an empty list before the new values are appended. If
* the user property has an existing value and it is not a list, it will be converted into
* a list with the new value(s) appended.
*
* @param property the user property property to which to append
* @param values the values being appended
* @return the same Identify object
*/
public Identify append(String property, double[] values) {
addToUserProperties(Constants.AMP_OP_APPEND, property, doubleArrayToJSONArray(values));
return this;
}
/**
* Append a value or values to a user property. If the user property does not have a value
* set, it will be initialized to an empty list before the new values are appended. If
* the user property has an existing value and it is not a list, it will be converted into
* a list with the new value(s) appended.
*
* @param property the user property property to which to append
* @param values the values being appended
* @return the same Identify object
*/
public Identify append(String property, float[] values) {
addToUserProperties(Constants.AMP_OP_APPEND, property, floatArrayToJSONArray(values));
return this;
}
/**
* Append a value or values to a user property. If the user property does not have a value
* set, it will be initialized to an empty list before the new values are appended. If
* the user property has an existing value and it is not a list, it will be converted into
* a list with the new value(s) appended.
*
* @param property the user property property to which to append
* @param values the values being appended
* @return the same Identify object
*/
public Identify append(String property, int[] values) {
addToUserProperties(Constants.AMP_OP_APPEND, property, intArrayToJSONArray(values));
return this;
}
/**
* Append a value or values to a user property. If the user property does not have a value
* set, it will be initialized to an empty list before the new values are appended. If
* the user property has an existing value and it is not a list, it will be converted into
* a list with the new value(s) appended.
*
* @param property the user property property to which to append
* @param values the values being appended
* @return the same Identify object
*/
public Identify append(String property, long[] values) {
addToUserProperties(Constants.AMP_OP_APPEND, property, longArrayToJSONArray(values));
return this;
}
/**
* Append a value or values to a user property. If the user property does not have a value
* set, it will be initialized to an empty list before the new values are appended. If
* the user property has an existing value and it is not a list, it will be converted into
* a list with the new value(s) appended.
*
* @param property the user property property to which to append
* @param values the values being appended
* @return the same Identify object
*/
public Identify append(String property, String[] values) {
addToUserProperties(Constants.AMP_OP_APPEND, property, stringArrayToJSONArray(values));
return this;
}
/**
* Prepend a value or values to a user property. Prepend means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) prepended.
*
* @param property the user property to which to append
* @param value the value being prepended
* @return the same Identify object
*/
public Identify prepend(String property, boolean value) {
addToUserProperties(Constants.AMP_OP_PREPEND, property, value);
return this;
}
/**
* Prepend a value or values to a user property. Prepend means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) prepended.
*
* @param property the user property to which to append
* @param value the value being prepended
* @return the same Identify object
*/
public Identify prepend(String property, double value) {
addToUserProperties(Constants.AMP_OP_PREPEND, property, value);
return this;
}
/**
* Prepend a value or values to a user property. Prepend means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) prepended.
*
* @param property the user property to which to append
* @param value the value being prepended
* @return the same Identify object
*/
public Identify prepend(String property, float value) {
addToUserProperties(Constants.AMP_OP_PREPEND, property, value);
return this;
}
/**
* Prepend a value or values to a user property. Prepend means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) prepended.
*
* @param property the user property to which to append
* @param value the value being prepended
* @return the same Identify object
*/
public Identify prepend(String property, int value) {
addToUserProperties(Constants.AMP_OP_PREPEND, property, value);
return this;
}
/**
* Prepend a value or values to a user property. Prepend means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) prepended.
*
* @param property the user property to which to append
* @param value the value being prepended
* @return the same Identify object
*/
public Identify prepend(String property, long value) {
addToUserProperties(Constants.AMP_OP_PREPEND, property, value);
return this;
}
/**
* Prepend a value or values to a user property. Prepend means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) prepended.
*
* @param property the user property to which to append
* @param value the value being prepended
* @return the same Identify object
*/
public Identify prepend(String property, String value) {
addToUserProperties(Constants.AMP_OP_PREPEND, property, value);
return this;
}
/**
* Prepend a value or values to a user property. Prepend means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) prepended.
*
* @param property the user property to which to append
* @param values the value being prepended
* @return the same Identify object
*/
public Identify prepend(String property, JSONArray values) {
addToUserProperties(Constants.AMP_OP_PREPEND, property, values);
return this;
}
/**
* Prepend a value or values to a user property. Prepend means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) prepended.
*
* @param property the user property to which to append
* @param values the values being prepended. Server-side we flatten dictionaries and apply
* prepend to each flattened property.
* @return the same Identify object
*/
public Identify prepend(String property, JSONObject values) {
addToUserProperties(Constants.AMP_OP_PREPEND, property, values);
return this;
}
/**
* Prepend a value or values to a user property. Prepend means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) prepended.
*
* @param property the user property to which to append
* @param values the values being prepended
* @return the same Identify object
*/
public Identify prepend(String property, boolean[] values) {
addToUserProperties(Constants.AMP_OP_PREPEND, property, booleanArrayToJSONArray(values));
return this;
}
/**
* Prepend a value or values to a user property. Prepend means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) prepended.
*
* @param property the user property to which to append
* @param values the values being prepended
* @return the same Identify object
*/
public Identify prepend(String property, double[] values) {
addToUserProperties(Constants.AMP_OP_PREPEND, property, doubleArrayToJSONArray(values));
return this;
}
/**
* Prepend a value or values to a user property. Prepend means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) prepended.
*
* @param property the user property to which to append
* @param values the values being prepended
* @return the same Identify object
*/
public Identify prepend(String property, float[] values) {
addToUserProperties(Constants.AMP_OP_PREPEND, property, floatArrayToJSONArray(values));
return this;
}
/**
* Prepend a value or values to a user property. Prepend means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) prepended.
*
* @param property the user property to which to append
* @param values the values being prepended
* @return the same Identify object
*/
public Identify prepend(String property, int[] values) {
addToUserProperties(Constants.AMP_OP_PREPEND, property, intArrayToJSONArray(values));
return this;
}
/**
* Prepend a value or values to a user property. Prepend means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) prepended.
*
* @param property the user property to which to append
* @param values the values being prepended
* @return the same Identify object
*/
public Identify prepend(String property, long[] values) {
addToUserProperties(Constants.AMP_OP_PREPEND, property, longArrayToJSONArray(values));
return this;
}
/**
* Prepend a value or values to a user property. Prepend means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) prepended.
*
* @param property the user property to which to append
* @param values the values being prepended
* @return the same Identify object
*/
public Identify prepend(String property, String[] values) {
addToUserProperties(Constants.AMP_OP_PREPEND, property, stringArrayToJSONArray(values));
return this;
}
/**
* Unset and remove a user property.
*
* @param property the user property to unset and remove.
* @return the same Identify object
*/
public Identify unset(String property) {
addToUserProperties(Constants.AMP_OP_UNSET, property, "-");
return this;
}
/**
* Clear all user properties. Note: the result is irreversible! Also Note:
* clearAll needs to be be sent on its own Identify object without any other operations.
*
* @return the same Identify object.
*/
public Identify clearAll() {
if (userPropertiesOperations.length() > 0) {
if (!userProperties.contains(Constants.AMP_OP_CLEAR_ALL)) {
AmplitudeLog.getLogger().w(TAG, String.format(
"Need to send $clearAll on its own Identify object without any other " +
"operations, ignoring $clearAll"
));
}
return this;
}
try {
userPropertiesOperations.put(Constants.AMP_OP_CLEAR_ALL, "-");
} catch (JSONException e) {
AmplitudeLog.getLogger().e(TAG, e.toString());
}
return this;
}
/**
* Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) pre-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to preInsert
* @param value the values being preInsert
* @return the same Identify object
*/
public Identify preInsert(String property, boolean value) {
addToUserProperties(Constants.AMP_OP_PREINSERT, property, value);
return this;
}
/**
* Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) pre-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to preInsert
* @param value the values being preInsert
* @return the same Identify object
*/
public Identify preInsert(String property, double value) {
addToUserProperties(Constants.AMP_OP_PREINSERT, property, value);
return this;
}
/**
* Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) pre-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to preInsert
* @param value the values being preInsert
* @return the same Identify object
*/
public Identify preInsert(String property, float value) {
addToUserProperties(Constants.AMP_OP_PREINSERT, property, value);
return this;
}
/**
* Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) pre-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to preInsert
* @param value the values being preInsert
* @return the same Identify object
*/
public Identify preInsert(String property, int value) {
addToUserProperties(Constants.AMP_OP_PREINSERT, property, value);
return this;
}
/**
* Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) pre-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to preInsert
* @param value the values being preInsert
* @return the same Identify object
*/
public Identify preInsert(String property, long value) {
addToUserProperties(Constants.AMP_OP_PREINSERT, property, value);
return this;
}
/**
* Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) pre-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to preInsert
* @param value the values being preInsert
* @return the same Identify object
*/
public Identify preInsert(String property, String value) {
addToUserProperties(Constants.AMP_OP_PREINSERT, property, value);
return this;
}
/**
* Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) pre-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to preInsert
* @param values the values being preInsert
* @return the same Identify object
*/
public Identify preInsert(String property, JSONArray values) {
addToUserProperties(Constants.AMP_OP_PREINSERT, property, values);
return this;
}
/**
* Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) pre-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to preInsert
* @param values the values being preInsert
* @return the same Identify object
*/
public Identify preInsert(String property, JSONObject values) {
addToUserProperties(Constants.AMP_OP_PREINSERT, property, values);
return this;
}
/**
* Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) pre-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to preInsert
* @param values the values being preInsert
* @return the same Identify object
*/
public Identify preInsert(String property, boolean[] values) {
addToUserProperties(Constants.AMP_OP_PREINSERT, property, booleanArrayToJSONArray(values));
return this;
}
/**
* Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) pre-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to preInsert
* @param values the values being preInsert
* @return the same Identify object
*/
public Identify preInsert(String property, double[] values) {
addToUserProperties(Constants.AMP_OP_PREINSERT, property, doubleArrayToJSONArray(values));
return this;
}
/**
* Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) pre-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to preInsert
* @param values the values being preInsert
* @return the same Identify object
*/
public Identify preInsert(String property, float[] values){
addToUserProperties(Constants.AMP_OP_PREINSERT, property, floatArrayToJSONArray(values));
return this;
}
/**
* Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) pre-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to preInsert
* @param values the values being preInsert
* @return the same Identify object
*/
public Identify preInsert(String property, int[] values){
addToUserProperties(Constants.AMP_OP_PREINSERT, property, intArrayToJSONArray(values));
return this;
}
/**
* Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) pre-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to preInsert
* @param values the values being preInsert
* @return the same Identify object
*/
public Identify preInsert(String property, long[] values) {
addToUserProperties(Constants.AMP_OP_PREINSERT, property, longArrayToJSONArray(values));
return this;
}
/**
* Pre-insert a value or values to a user property. Pre-insert means inserting the value(s) at the
* front of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) pre-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to preInsert
* @param values the values being preInsert
* @return the same Identify object
*/
public Identify preInsert(String property, String[] values) {
addToUserProperties(Constants.AMP_OP_PREINSERT, property, stringArrayToJSONArray(values));
return this;
}
/**
* Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
* end of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) post-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to postInsert
* @param value the values being preInsert
* @return the same Identify object
*/
public Identify postInsert(String property, boolean value) {
addToUserProperties(Constants.AMP_OP_POSTINSERT, property, value);
return this;
}
/**
* Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
* end of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) post-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to postInsert
* @param value the values being preInsert
* @return the same Identify object
*/
public Identify postInsert(String property, double value) {
addToUserProperties(Constants.AMP_OP_POSTINSERT, property, value);
return this;
}
/**
* Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
* end of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) post-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to postInsert
* @param value the values being preInsert
* @return the same Identify object
*/
public Identify postInsert(String property, float value) {
addToUserProperties(Constants.AMP_OP_POSTINSERT, property, value);
return this;
}
/**
* Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
* end of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) post-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to postInsert
* @param value the values being preInsert
* @return the same Identify object
*/
public Identify postInsert(String property, int value) {
addToUserProperties(Constants.AMP_OP_POSTINSERT, property, value);
return this;
}
/**
* Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
* end of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) post-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to postInsert
* @param value the values being preInsert
* @return the same Identify object
*/
public Identify postInsert(String property, long value) {
addToUserProperties(Constants.AMP_OP_POSTINSERT, property, value);
return this;
}
/**
* Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
* end of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) post-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to postInsert
* @param value the values being preInsert
* @return the same Identify object
*/
public Identify postInsert(String property, String value) {
addToUserProperties(Constants.AMP_OP_POSTINSERT, property, value);
return this;
}
/**
* Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
* end of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) post-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to postInsert
* @param values the values being preInsert
* @return the same Identify object
*/
public Identify postInsert(String property, JSONArray values) {
addToUserProperties(Constants.AMP_OP_POSTINSERT, property, values);
return this;
}
/**
* Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
* end of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) post-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to postInsert
* @param values the values being preInsert
* @return the same Identify object
*/
public Identify postInsert(String property, JSONObject values) {
addToUserProperties(Constants.AMP_OP_POSTINSERT, property, values);
return this;
}
/**
* Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
* end of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) post-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to postInsert
* @param values the values being preInsert
* @return the same Identify object
*/
public Identify postInsert(String property, boolean[] values) {
addToUserProperties(Constants.AMP_OP_POSTINSERT, property, booleanArrayToJSONArray(values));
return this;
}
/**
* Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
* end of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) post-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to postInsert
* @param values the values being preInsert
* @return the same Identify object
*/
public Identify postInsert(String property, double[] values) {
addToUserProperties(Constants.AMP_OP_POSTINSERT, property, doubleArrayToJSONArray(values));
return this;
}
/**
* Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
* end of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) post-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to postInsert
* @param values the values being preInsert
* @return the same Identify object
*/
public Identify postInsert(String property, float[] values){
addToUserProperties(Constants.AMP_OP_POSTINSERT, property, floatArrayToJSONArray(values));
return this;
}
/**
* Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
* end of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) post-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to postInsert
* @param values the values being preInsert
* @return the same Identify object
*/
public Identify postInsert(String property, int[] values){
addToUserProperties(Constants.AMP_OP_POSTINSERT, property, intArrayToJSONArray(values));
return this;
}
/**
* Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
* end of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) post-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to postInsert
* @param values the values being preInsert
* @return the same Identify object
*/
public Identify postInsert(String property, long[] values) {
addToUserProperties(Constants.AMP_OP_POSTINSERT, property, longArrayToJSONArray(values));
return this;
}
/**
* Post-insert a value or values to a user property. Post-insert means inserting the value(s) at the
* end of a given list. if the user property does not have a value set, it will be
* initialized to an empty list before the new values are prepended. If the user property
* has an existing value and it is not a list, it will be converted into a list with the
* new value(s) post-insert. If the user property has an existing value, it will do no operation.
*
* @param property the user property to which to postInsert
* @param values the values being preInsert
* @return the same Identify object
*/
public Identify postInsert(String property, String[] values) {
addToUserProperties(Constants.AMP_OP_POSTINSERT, property, stringArrayToJSONArray(values));
return this;
}
/**
* Remove a value or values to a user property. Remove means remove the value(s) from a given list.
* If the user property has the matching value, it will remove that value from the given list.
* If the user property does not have that value set, it will do no operation.
*
* @param property the user property to which to remove
* @param value the values being remove
* @return the same Identify object
*/
public Identify remove(String property, boolean value) {
addToUserProperties(Constants.AMP_OP_REMOVE, property, value);
return this;
}
/**
* Remove a value or values to a user property. Remove means remove the value(s) from a given list.
* If the user property has the matching value, it will remove that value from the given list.
* If the user property does not have that value set, it will do no operation.
*
* @param property the user property to which to remove
* @param value the values being remove
* @return the same Identify object
*/
public Identify remove(String property, double value) {
addToUserProperties(Constants.AMP_OP_REMOVE, property, value);
return this;
}
/**
* Remove a value or values to a user property. Remove means remove the value(s) from a given list.
* If the user property has the matching value, it will remove that value from the given list.
* If the user property does not have that value set, it will do no operation.
*
* @param property the user property to which to remove
* @param value the values being remove
* @return the same Identify object
*/
public Identify remove(String property, float value) {
addToUserProperties(Constants.AMP_OP_REMOVE, property, value);
return this;
}
/**
* Remove a value or values to a user property. Remove means remove the value(s) from a given list.
* If the user property has the matching value, it will remove that value from the given list.
* If the user property does not have that value set, it will do no operation.
*
* @param property the user property to which to remove
* @param value the values being remove
* @return the same Identify object
*/
public Identify remove(String property, int value) {
addToUserProperties(Constants.AMP_OP_REMOVE, property, value);
return this;
}
/**
* Remove a value or values to a user property. Remove means remove the value(s) from a given list.
* If the user property has the matching value, it will remove that value from the given list.
* If the user property does not have that value set, it will do no operation.
*
* @param property the user property to which to remove
* @param value the values being remove
* @return the same Identify object
*/
public Identify remove(String property, long value) {
addToUserProperties(Constants.AMP_OP_REMOVE, property, value);
return this;
}
/**
* Remove a value or values to a user property. Remove means remove the value(s) from a given list.
* If the user property has the matching value, it will remove that value from the given list.
* If the user property does not have that value set, it will do no operation.
*
* @param property the user property to which to remove
* @param value the values being remove
* @return the same Identify object
*/
public Identify remove(String property, String value) {
addToUserProperties(Constants.AMP_OP_REMOVE, property, value);
return this;
}
/**
* Remove a value or values to a user property. Remove means remove the value(s) from a given list.
* If the user property has the matching value, it will remove that value from the given list.
* If the user property does not have that value set, it will do no operation.
*
* @param property the user property to which to remove
* @param values the values being remove
* @return the same Identify object
*/
public Identify remove(String property, JSONArray values) {
addToUserProperties(Constants.AMP_OP_REMOVE, property, values);
return this;
}
/**
* Remove a value or values to a user property. Remove means remove the value(s) from a given list.
* If the user property has the matching value, it will remove that value from the given list.
* If the user property does not have that value set, it will do no operation.
*
* @param property the user property to which to remove
* @param values the values being remove
* @return the same Identify object
*/
public Identify remove(String property, JSONObject values) {
addToUserProperties(Constants.AMP_OP_REMOVE, property, values);
return this;
}
/**
* Remove a value or values to a user property. Remove means remove the value(s) from a given list.
* If the user property has the matching value, it will remove that value from the given list.
* If the user property does not have that value set, it will do no operation.
*
* @param property the user property to which to remove
* @param values the values being remove
* @return the same Identify object
*/
public Identify remove(String property, boolean[] values) {
addToUserProperties(Constants.AMP_OP_REMOVE, property, booleanArrayToJSONArray(values));
return this;
}
/**
* Remove a value or values to a user property. Remove means remove the value(s) from a given list.
* If the user property has the matching value, it will remove that value from the given list.
* If the user property does not have that value set, it will do no operation.
*
* @param property the user property to which to remove
* @param values the values being remove
* @return the same Identify object
*/
public Identify remove(String property, double[] values) {
addToUserProperties(Constants.AMP_OP_REMOVE, property, doubleArrayToJSONArray(values));
return this;
}
/**
* Remove a value or values to a user property. Remove means remove the value(s) from a given list.
* If the user property has the matching value, it will remove that value from the given list.
* If the user property does not have that value set, it will do no operation.
*
* @param property the user property to which to remove
* @param values the values being remove
* @return the same Identify object
*/
public Identify remove(String property, float[] values){
addToUserProperties(Constants.AMP_OP_REMOVE, property, floatArrayToJSONArray(values));
return this;
}
/**
* Remove a value or values to a user property. Remove means remove the value(s) from a given list.
* If the user property has the matching value, it will remove that value from the given list.
* If the user property does not have that value set, it will do no operation.
*
* @param property the user property to which to remove
* @param values the values being remove
* @return the same Identify object
*/
public Identify remove(String property, int[] values){
addToUserProperties(Constants.AMP_OP_REMOVE, property, intArrayToJSONArray(values));
return this;
}
/**
* Remove a value or values to a user property. Remove means remove the value(s) from a given list.
* If the user property has the matching value, it will remove that value from the given list.
* If the user property does not have that value set, it will do no operation.
*
* @param property the user property to which to remove
* @param values the values being remove
* @return the same Identify object
*/
public Identify remove(String property, long[] values) {
addToUserProperties(Constants.AMP_OP_REMOVE, property, longArrayToJSONArray(values));
return this;
}
/**
* Remove a value or values to a user property. Remove means remove the value(s) from a given list.
* If the user property has the matching value, it will remove that value from the given list.
* If the user property does not have that value set, it will do no operation.
*
* @param property the user property to which to remove
* @param values the values being remove
* @return the same Identify object
*/
public Identify remove(String property, String[] values) {
addToUserProperties(Constants.AMP_OP_REMOVE, property, stringArrayToJSONArray(values));
return this;
}
private void addToUserProperties(String operation, String property, Object value) {
if (Utils.isEmptyString(property)) {
AmplitudeLog.getLogger().w(TAG, String.format(
"Attempting to perform operation %s with a null or empty string property, ignoring",
operation
));
return;
}
if (value == null) {
AmplitudeLog.getLogger().w(TAG, String.format(
"Attempting to perform operation %s with null value for property %s, ignoring",
operation, property
));
return;
}
// check that clearAll wasn't already used in this Identify
if (userPropertiesOperations.has(Constants.AMP_OP_CLEAR_ALL)) {
AmplitudeLog.getLogger().w(TAG, String.format(
"This Identify already contains a $clearAll operation, ignoring operation %s",
operation
));
return;
}
// check if property already used in previous operation
if (userProperties.contains(property)) {
AmplitudeLog.getLogger().w(TAG, String.format(
"Already used property %s in previous operation, ignoring operation %s",
property, operation
));
return;
}
try {
if (!userPropertiesOperations.has(operation)) {
userPropertiesOperations.put(operation, new JSONObject());
}
userPropertiesOperations.getJSONObject(operation).put(property, value);
userProperties.add(property);
} catch (JSONException e) {
AmplitudeLog.getLogger().e(TAG, e.toString());
}
}
private JSONArray booleanArrayToJSONArray(boolean[] values) {
JSONArray array = new JSONArray();
for (boolean value : values) array.put(value);
return array;
}
private JSONArray floatArrayToJSONArray(float[] values) {
JSONArray array = new JSONArray();
for (float value : values) {
try {
array.put(value);
} catch (JSONException e) {
AmplitudeLog.getLogger().e(TAG, String.format(
"Error converting float %f to JSON: %s", value, e.toString()
));
}
}
return array;
}
private JSONArray doubleArrayToJSONArray(double[] values) {
JSONArray array = new JSONArray();
for (double value : values) {
try {
array.put(value);
} catch (JSONException e) {
AmplitudeLog.getLogger().e(TAG, String.format(
"Error converting double %d to JSON: %s", value, e.toString()
));
}
}
return array;
}
private JSONArray intArrayToJSONArray(int[] values) {
JSONArray array = new JSONArray();
for (int value : values) array.put(value);
return array;
}
private JSONArray longArrayToJSONArray(long[] values) {
JSONArray array = new JSONArray();
for (long value : values) array.put(value);
return array;
}
private JSONArray stringArrayToJSONArray(String[] values) {
JSONArray array = new JSONArray();
for (String value : values) array.put(value);
return array;
}
/**
* Sets user property.
*
* @param property the property
* @param value the value
* @return the user property
*/
Identify setUserProperty(String property, Object value) {
addToUserProperties(Constants.AMP_OP_SET, property, value);
return this;
}
/**
* Sets a user property value only once. Subsequent @{code setOnce} operations on that user
* property will be ignored. Note: this method has been deprecated. Please use one
* with a different signature.
*
* @param property the user property to setOnce
* @param value the value of the user property
* @return the same Identify object
* @deprecated
*/
public Identify setOnce(String property, Object value) {
AmplitudeLog.getLogger().w(
TAG,
"This version of setOnce is deprecated. Please use one with a different signature."
);
return this;
}
/**
* Sets a user property value. Existing values for that user property will be overwritten.
* Note: this method has been deprecated. Please use one with a different signature.
*
* @param property the user property to set
* @param value the value of the user property
* @return the same Identify object
* @deprecated
*/
public Identify set(String property, Object value) {
AmplitudeLog.getLogger().w(
TAG,
"This version of set is deprecated. Please use one with a different signature."
);
return this;
}
/**
* Public method that exposes the user property operations JSON blob.
* @return a copy of the User Property Operations JSONObject. If copying fails, returns
* an empty JSONObject
*/
public JSONObject getUserPropertiesOperations() {
try {
return new JSONObject(userPropertiesOperations.toString());
} catch (JSONException e) {
AmplitudeLog.getLogger().e(TAG, e.toString());
}
return new JSONObject();
}
}
================================================
FILE: src/main/java/com/amplitude/api/IdentifyInterceptor.java
================================================
package com.amplitude.api;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* IdentifyInterceptor
* This is the internal class for handling identify events intercept and optimize identify volumes.
*/
class IdentifyInterceptor {
private static final String TAG = IdentifyInterceptor.class.getName();
private final DatabaseHelper dbHelper;
private final WorkerThread logThread;
private long identifyBatchIntervalMillis;
private final AtomicBoolean transferScheduled = new AtomicBoolean(false);
private long lastIdentifyInterceptorId = -1;
private final AmplitudeClient client;
private String userId;
private String deviceId;
private final AtomicBoolean identitySet = new AtomicBoolean(false);
public IdentifyInterceptor (
DatabaseHelper dbHelper,
WorkerThread logThread,
long identifyBatchIntervalMillis,
AmplitudeClient client
) {
this.dbHelper = dbHelper;
this.logThread = logThread;
this.identifyBatchIntervalMillis = identifyBatchIntervalMillis;
if (dbHelper.getIdentifyInterceptorCount() > 0) {
lastIdentifyInterceptorId = dbHelper.getLastIdentifyInterceptorId();
}
this.client = client;
}
/**
* Intercept the event if it is identify with set action.
*
* @param eventType the event type
* @param event full event data after middleware run
* @return event with potentially more information or null if intercepted
*/
public JSONObject intercept(String eventType, JSONObject event) {
if (isIdentityUpdated(event)) {
// if userId or deviceId is updated, send out the identify for older identity
transferInterceptedIdentify();
}
if (eventType.equals(Constants.IDENTIFY_EVENT)) {
if (isSetOnly(event) && !isSetGroups(event)) {
// intercept and save user properties
lastIdentifyInterceptorId = saveIdentifyProperties(event);
scheduleTransfer();
return null;
} else if(isClearAll(event)){
// clear existing and return event
dbHelper.removeIdentifyInterceptors(lastIdentifyInterceptorId);
return event;
} else {
// send out the identify for older identity and event
transferInterceptedIdentify();
return event;
}
} else if (eventType.equals(Constants.GROUP_IDENTIFY_EVENT)) {
// no op
return event;
} else {
// send out the identify for older identity and event
transferInterceptedIdentify();
return event;
}
}
/**
* Sets min time for identify batch millis.
*
* @param identifyBatchIntervalMillis the time interval for identify batch interval
*/
public void setIdentifyBatchIntervalMillis(long identifyBatchIntervalMillis) {
this.identifyBatchIntervalMillis = identifyBatchIntervalMillis;
}
private JSONObject getTransferIdentifyEvent() {
try {
List identifys = dbHelper.getIdentifyInterceptors(lastIdentifyInterceptorId, -1);
if (identifys.isEmpty()) {
return null;
}
JSONObject identifyEvent = identifys.get(0);
JSONObject identifyEventUserProperties = identifyEvent.getJSONObject("user_properties").getJSONObject(Constants.AMP_OP_SET);
JSONObject userProperties = mergeIdentifyInterceptList(identifys.subList(1, identifys.size()));
mergeUserProperties(identifyEventUserProperties, userProperties);
identifyEvent.getJSONObject("user_properties").put(Constants.AMP_OP_SET, identifyEventUserProperties);
dbHelper.removeIdentifyInterceptors(lastIdentifyInterceptorId);
return identifyEvent;
} catch (Throwable e) {
AmplitudeLog.getLogger().w(TAG, "Identify Merge error: " + e.getMessage());
}
return null;
}
private void scheduleTransfer() {
if (transferScheduled.getAndSet(true)) {
return;
}
logThread.postDelayed(new Runnable() {
@Override
public void run() {
transferScheduled.set(false);
transferInterceptedIdentify();
}
}, identifyBatchIntervalMillis);
}
public void transferInterceptedIdentify() {
JSONObject identifyEvent = getTransferIdentifyEvent();
if (identifyEvent == null) {
return;
}
client.saveEvent(Constants.IDENTIFY_EVENT, identifyEvent);
}
private JSONObject mergeIdentifyInterceptList(List identifys) throws JSONException {
JSONObject userProperties = new JSONObject();
for (JSONObject identify : identifys) {
JSONObject setUserProperties = identify.getJSONObject("user_properties")
.getJSONObject(Constants.AMP_OP_SET);
mergeUserProperties(userProperties, setUserProperties);
}
return userProperties;
}
private void mergeUserProperties(JSONObject userProperties, JSONObject userPropertiesToMerge) throws JSONException {
Iterator> keys = userPropertiesToMerge.keys();
while (keys.hasNext()) {
String key = (String) keys.next();
if (userPropertiesToMerge.get(key) != null && userPropertiesToMerge.get(key) != JSONObject.NULL) {
userProperties.put(key, userPropertiesToMerge.get(key));
}
}
}
private boolean isSetOnly(JSONObject event) {
return isActionOnly(event, Constants.AMP_OP_SET);
}
private boolean isClearAll(JSONObject event) {
return isActionOnly(event, Constants.AMP_OP_CLEAR_ALL);
}
private boolean isSetGroups(JSONObject event) {
try {
return event.getJSONObject("groups").length() > 0;
} catch (JSONException e) {
return false;
}
}
private boolean isActionOnly(JSONObject event, String action) {
try {
JSONObject userProperties = event.getJSONObject("user_properties");
return userProperties.length() == 1 && userProperties.has(action);
} catch (JSONException e) {
return false;
}
}
private long saveIdentifyProperties(JSONObject event) {
return dbHelper.addIdentifyInterceptor(event.toString());
}
private boolean isIdentityUpdated(JSONObject event) {
try {
if (!identitySet.getAndSet(true)) {
userId = event.getString("user_id");
deviceId = event.getString("device_id");
return true;
}
boolean isUpdated = false;
if (isIdUpdated(userId, event.getString("user_id"))) {
userId = event.getString("user_id");
isUpdated = true;
}
if (isIdUpdated(deviceId, event.getString("device_id"))) {
deviceId = event.getString("device_id");
isUpdated = true;
}
return isUpdated;
} catch (JSONException e) {
return true;
}
}
private boolean isIdUpdated(String id, String updateId) {
if (id == null && updateId == null) {
return false;
}
if (id == null || updateId == null) {
return true;
}
return !id.equals(updateId);
}
}
================================================
FILE: src/main/java/com/amplitude/api/IngestionMetadata.java
================================================
package com.amplitude.api;
import org.json.JSONException;
import org.json.JSONObject;
public class IngestionMetadata {
private static final String TAG = IngestionMetadata.class.getName();
/**
* The source name, e.g. "ampli"
*/
private String sourceName;
/**
* The source version, e.g. "2.0.0"
*/
private String sourceVersion;
/**
* Set the ingestion metadata source name information.
* @param sourceName source name for ingestion metadata
* @return the same IngestionMetadata object
*/
public IngestionMetadata setSourceName(String sourceName) {
this.sourceName = sourceName;
return this;
}
/**
* Set the ingestion metadata source version information.
* @param sourceVersion source version for ingestion metadata
* @return the same IngestionMetadata object
*/
public IngestionMetadata setSourceVersion(String sourceVersion) {
this.sourceVersion = sourceVersion;
return this;
}
/**
* Get JSONObject of current ingestion metadata
* @return JSONObject including ingestion metadata information
*/
protected JSONObject toJSONObject() {
JSONObject jsonObject = new JSONObject();
try {
if (!Utils.isEmptyString(sourceName)) {
jsonObject.put(Constants.AMP_INGESTION_METADATA_SOURCE_NAME, sourceName);
}
if (!Utils.isEmptyString(sourceVersion)) {
jsonObject.put(Constants.AMP_INGESTION_METADATA_SOURCE_VERSION, sourceVersion);
}
} catch (JSONException e) {
AmplitudeLog.getLogger().e(TAG, "JSON Serialization of ingestion metadata object failed");
}
return jsonObject;
}
}
================================================
FILE: src/main/java/com/amplitude/api/Middleware.java
================================================
package com.amplitude.api;
public interface Middleware {
void run(MiddlewarePayload payload, MiddlewareNext next);
}
================================================
FILE: src/main/java/com/amplitude/api/MiddlewareExtended.java
================================================
package com.amplitude.api;
interface MiddlewareExtended extends Middleware {
void flush();
}
================================================
FILE: src/main/java/com/amplitude/api/MiddlewareExtra.java
================================================
package com.amplitude.api;
import java.util.Map;
import java.util.HashMap;
public class MiddlewareExtra extends HashMap {
public MiddlewareExtra() {
super();
}
public MiddlewareExtra(Map map) {
super(map);
}
}
================================================
FILE: src/main/java/com/amplitude/api/MiddlewareNext.java
================================================
package com.amplitude.api;
public interface MiddlewareNext {
public void run(MiddlewarePayload curPayload);
}
================================================
FILE: src/main/java/com/amplitude/api/MiddlewarePayload.java
================================================
package com.amplitude.api;
import org.json.JSONObject;
public class MiddlewarePayload {
public JSONObject event;
public MiddlewareExtra extra;
public MiddlewarePayload(JSONObject event, MiddlewareExtra extra) {
this.event = event;
this.extra = extra;
}
public MiddlewarePayload(JSONObject event) {
this(event, null);
}
}
================================================
FILE: src/main/java/com/amplitude/api/MiddlewareRunner.java
================================================
package com.amplitude.api;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
public class MiddlewareRunner {
private final ConcurrentLinkedQueue middlewares;
public MiddlewareRunner() {
middlewares = new ConcurrentLinkedQueue<>();
}
public void add(Middleware middleware) {
this.middlewares.add(middleware);
}
private void runMiddlewares(List middlewares, MiddlewarePayload payload, MiddlewareNext next) {
if (middlewares.size() == 0 ){
next.run(payload);
return;
}
middlewares.get(0).run(payload, new MiddlewareNext() {
@Override
public void run(MiddlewarePayload curPayload) {
runMiddlewares((middlewares.subList(1, middlewares.size())), curPayload, next);
}
});
}
public boolean run(MiddlewarePayload payload) {
AtomicBoolean middlewareCompleted = new AtomicBoolean(false);
this.run(payload, new MiddlewareNext() {
@Override
public void run(MiddlewarePayload curPayload) {
middlewareCompleted.set(true);
}
});
return middlewareCompleted.get();
}
public void run(MiddlewarePayload payload, MiddlewareNext next) {
List middlewareList = new ArrayList<>(this.middlewares);
runMiddlewares(middlewareList, payload, next);
}
void flush() {
for (Middleware middleware : middlewares) {
if (middleware instanceof MiddlewareExtended) {
((MiddlewareExtended) middleware).flush();
}
}
}
}
================================================
FILE: src/main/java/com/amplitude/api/PinnedAmplitudeClient.java
================================================
package com.amplitude.api;
import android.content.Context;
import com.amplitude.util.DoubleCheck;
import com.amplitude.util.Provider;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import okhttp3.OkHttpClient;
import okio.Buffer;
import okio.ByteString;
/**
*
PinnedAmplitudeClient
* This is a version of the AmplitudeClient that supports SSL pinning for encrypted requests.
* Please contact Amplitude Support before you ship any
* products with SSL pinning enabled so that we are aware and can provide documentation
* and implementation help.
*/
public class PinnedAmplitudeClient extends AmplitudeClient {
/**
* The class identifier tag used in logging. TAG = {@code "com.amplitude.api.PinnedAmplitudeClient";}
*/
private static final String TAG = PinnedAmplitudeClient.class.getName();
// CN=COMODO RSA Domain Validation Secure Server CA, O=COMODO CA Limited,
// L=Salford, ST=Greater Manchester, C=GB
private static final String CERTIFICATE_US = ""
+ "MIIGCDCCA/CgAwIBAgIQKy5u6tl1NmwUim7bo3yMBzANBgkqhkiG9w0BAQwFADCBhT"
+ "ELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE"
+ "BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIk"
+ "NPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTQwMjEyMDAwMDAw"
+ "WhcNMjkwMjExMjM1OTU5WjCBkDELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZX"
+ "IgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENB"
+ "IExpbWl0ZWQxNjA0BgNVBAMTLUNPTU9ETyBSU0EgRG9tYWluIFZhbGlkYXRpb24gU2"
+ "VjdXJlIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAI7C"
+ "AhnhoFmk6zg1jSz9AdDTScBkxwtiBUUWOqigwAwCfx3M28ShbXcDow+G+eMGnD4LgY"
+ "qbSRutA776S9uMIO3Vzl5ljj4Nr0zCsLdFXlIvNN5IJGS0Qa4Al/e+Z96e0HqnU4A7"
+ "fK31llVvl0cKfIWLIpeNs4TgllfQcBhglo/uLQeTnaG6ytHNe+nEKpooIZFNb5JPJa"
+ "XyejXdJtxGpdCsWTWM/06RQ1A/WZMebFEh7lgUq/51UHg+TLAchhP6a5i84DuUHoVS"
+ "3AOTJBhuyydRReZw3iVDpA3hSqXttn7IzW3uLh0nc13cRTCAquOyQQuvvUSH2rnlG5"
+ "1/ruWFgqUCAwEAAaOCAWUwggFhMB8GA1UdIwQYMBaAFLuvfgI9+qbxPISOre44mOzZ"
+ "MjLUMB0GA1UdDgQWBBSQr2o6lFoL2JDqElZz30O0Oija5zAOBgNVHQ8BAf8EBAMCAY"
+ "YwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUH"
+ "AwIwGwYDVR0gBBQwEjAGBgRVHSAAMAgGBmeBDAECATBMBgNVHR8ERTBDMEGgP6A9hj"
+ "todHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9SU0FDZXJ0aWZpY2F0aW9uQXV0"
+ "aG9yaXR5LmNybDBxBggrBgEFBQcBAQRlMGMwOwYIKwYBBQUHMAKGL2h0dHA6Ly9jcn"
+ "QuY29tb2RvY2EuY29tL0NPTU9ET1JTQUFkZFRydXN0Q0EuY3J0MCQGCCsGAQUFBzAB"
+ "hhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZIhvcNAQEMBQADggIBAE4rdk"
+ "+SHGI2ibp3wScF9BzWRJ2pmj6q1WZmAT7qSeaiNbz69t2Vjpk1mA42GHWx3d1Qcnyu"
+ "3HeIzg/3kCDKo2cuH1Z/e+FE6kKVxF0NAVBGFfKBiVlsit2M8RKhjTpCipj4SzR7Jz"
+ "sItG8kO3KdY3RYPBpsP0/HEZrIqPW1N+8QRcZs2eBelSaz662jue5/DJpmNXMyYE7l"
+ "3YphLG5SEXdoltMYdVEVABt0iN3hxzgEQyjpFv3ZBdRdRydg1vs4O2xyopT4Qhrf7W"
+ "8GjEXCBgCq5Ojc2bXhc3js9iPc0d1sjhqPpepUfJa3w/5Vjo1JXvxku88+vZbrac2/"
+ "4EjxYoIQ5QxGV/Iz2tDIY+3GH5QFlkoakdH368+PUq4NCNk+qKBR6cGHdNXJ93SrLl"
+ "P7u3r7l+L4HyaPs9Kg4DdbKDsx5Q5XLVq4rXmsXiBmGqW5prU5wfWYQ//u+aen/e7K"
+ "JD2AFsQXj4rBYKEMrltDR5FL1ZoXX/nUh8HCjLfn4g8wGTeGrODcQgPmlKidrv0PJF"
+ "GUzpII0fxQ8ANAe4hZ7Q7drNJ3gjTcBpUC2JD5Leo31Rpg0Gcg19hCC0Wvgmje3WYk"
+ "N5AplBlGGSW4gNfL1IYoakRwJiNiqZ+Gb7+6kHDSVneFeO/qJakXzlByjAA6quPbYz"
+ "Sf+AZxAeKCINT+b72x";
private static final String CERTIFICATE_EU = ""
+ "MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF\n"
+ "ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6\n"
+ "b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL\n"
+ "MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv\n"
+ "b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj\n"
+ "ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM\n"
+ "9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw\n"
+ "IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6\n"
+ "VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L\n"
+ "93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm\n"
+ "jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\n"
+ "AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA\n"
+ "A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI\n"
+ "U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs\n"
+ "N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv\n"
+ "o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU\n"
+ "5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy\n"
+ "rqXRfboQnoZsG4q5WTP468SQvvG5";
private static final AmplitudeLog logger = AmplitudeLog.getLogger();
protected static String getCertificate(AmplitudeServerZone serverZone) {
return (serverZone == AmplitudeServerZone.EU) ? CERTIFICATE_EU : CERTIFICATE_US;
}
/**
* Pinned certificate chain for api.amplitude.com.
*/
protected static SSLContextBuilder getPinnedCertificateChain(AmplitudeServerZone serverZone) {
String CERTIFICATE = getCertificate(serverZone);
return new SSLContextBuilder(serverZone).addCertificate(CERTIFICATE);
}
/**
* SSl context builder, used to generate the SSL context.
*/
protected static class SSLContextBuilder {
private final List certificateBase64s = new ArrayList();
protected AmplitudeServerZone serverZone;
public SSLContextBuilder() {
this.serverZone = AmplitudeServerZone.US;
}
public SSLContextBuilder(AmplitudeServerZone serverZone) {
this.serverZone = serverZone;
}
/**
* Add certificate ssl context builder.
*
* @param certificateBase64 the certificate base 64
* @return the ssl context builder
*/
public SSLContextBuilder addCertificate(String certificateBase64) {
certificateBase64s.add(certificateBase64);
return this;
}
/**
* Build ssl context.
*
* @return the ssl context
*/
public SSLContext build() {
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null); // Use a null input stream + password to create an empty key store.
// Decode the certificates and add 'em to the key store.
int nextName = 1;
for (String certificateBase64 : certificateBase64s) {
Buffer certificateBuffer = new Buffer().write(ByteString.decodeBase64(certificateBase64));
X509Certificate certificate = (X509Certificate) certificateFactory.generateCertificate(
certificateBuffer.inputStream());
keyStore.setCertificateEntry(Integer.toString(nextName++), certificate);
}
// Create an SSL context that uses these certificates as its trust store.
trustManagerFactory.init(keyStore);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
return sslContext;
} catch (GeneralSecurityException e) {
logger.e(TAG, e.getMessage(), e);
} catch (IOException e) {
logger.e(TAG, e.getMessage(), e);
}
return null;
}
}
static Map instances = new HashMap();
/**
* Gets the default instance.
*
* @return the default instance
*/
public static PinnedAmplitudeClient getInstance() {
return getInstance(null);
}
/**
* Gets the specified instance. If instance is null or empty string, fetches the default
* instance instead.
*
* @param instance name to get "ex app 1"
* @return the specified instance
*/
public static synchronized PinnedAmplitudeClient getInstance(String instance) {
instance = Utils.normalizeInstanceName(instance);
PinnedAmplitudeClient client = instances.get(instance);
if (client == null) {
client = new PinnedAmplitudeClient(instance);
instances.put(instance, client);
}
return client;
}
/**
* The SSl socket factory.
*/
protected SSLSocketFactory sslSocketFactory;
/**
* Instantiates a new Pinned amplitude client.
*/
public PinnedAmplitudeClient(String instance) {
super(instance);
}
/**
* The Initialized ssl socket factory.
*/
protected boolean initializedSSLSocketFactory = false;
public synchronized AmplitudeClient initializeInternal(
Context context,
String apiKey,
String userId,
Provider clientProvider
) {
super.initialize(context, apiKey, userId);
final PinnedAmplitudeClient client = this;
runOnLogThread(new Runnable() {
@Override
public void run() {
if (!client.initializedSSLSocketFactory) {
SSLSocketFactory factory = getPinnedCertSslSocketFactory(client.getServerZone());
if (factory != null) {
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null); // Use a null input stream + password to create an empty key store.
List certificateBase64s = new ArrayList();
String CERTIFICATE = getCertificate(client.getServerZone());
certificateBase64s.add(CERTIFICATE);
// Decode the certificates and add 'em to the key store.
int nextName = 1;
for (String certificateBase64 : certificateBase64s) {
Buffer certificateBuffer = new Buffer().write(ByteString.decodeBase64(certificateBase64));
X509Certificate certificate = (X509Certificate) certificateFactory.generateCertificate(
certificateBuffer.inputStream());
keyStore.setCertificateEntry(Integer.toString(nextName++), certificate);
}
// Create an SSL context that uses these certificates as its trust store.
trustManagerFactory.init(keyStore);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
throw new IllegalStateException("Unexpected default trust managers:"
+ Arrays.toString(trustManagers));
}
X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
final Provider finalClientProvider = DoubleCheck.provider(() -> {
final OkHttpClient.Builder builder;
if (clientProvider != null) {
builder = clientProvider.get().newBuilder();
} else {
builder = new OkHttpClient.Builder();
}
return builder.sslSocketFactory(factory, trustManager).build();
});
client.callFactory = request -> finalClientProvider.get().newCall(request);
} catch (GeneralSecurityException e) {
logger.e(TAG, e.getMessage(), e);
} catch (IOException e) {
logger.e(TAG, e.getMessage(), e);
}
} else {
logger.e(TAG, "Unable to pin SSL as requested. Will send data without SSL pinning.");
}
client.initializedSSLSocketFactory = true;
}
}
});
return this;
}
// why not override base method?
@Override
public synchronized AmplitudeClient initialize(Context context, String apiKey, String userId) {
return initializeInternal(context, apiKey, userId, null);
}
public synchronized AmplitudeClient initialize(
Context context,
String apiKey,
String userId,
Provider clientProvider) {
return initializeInternal(context, apiKey, userId, clientProvider);
}
/**
* Gets pinned cert ssl socket factory.
*
* @param serverZone the current server zone
* @return the pinned cert ssl socket factory
*/
protected SSLSocketFactory getPinnedCertSslSocketFactory(AmplitudeServerZone serverZone) {
return getPinnedCertSslSocketFactory(getPinnedCertificateChain(serverZone));
}
/**
* Gets pinned cert ssl socket factory.
*
* @param context the context
* @return the pinned cert ssl socket factory
*/
protected SSLSocketFactory getPinnedCertSslSocketFactory(SSLContextBuilder context) {
if (context == null) {
return null;
}
if (sslSocketFactory == null) {
try {
sslSocketFactory = context.build().getSocketFactory();
if (context.serverZone == AmplitudeServerZone.EU) {
logger.i(TAG, "Pinning SSL session using AWS Root CA Cert");
} else {
logger.i(TAG, "Pinning SSL session using Comodo CA Cert");
}
} catch (Exception e) {
logger.e(TAG, e.getMessage(), e);
}
}
return sslSocketFactory;
}
@Override
public AmplitudeClient setServerZone(AmplitudeServerZone serverZone) {
super.setServerZone(serverZone);
this.initializedSSLSocketFactory = false;
this.sslSocketFactory = null;
this.initialize(this.context, this.apiKey, this.userId);
return this;
}
}
================================================
FILE: src/main/java/com/amplitude/api/Plan.java
================================================
package com.amplitude.api;
import org.json.JSONException;
import org.json.JSONObject;
public class Plan {
private static final String TAG = Plan.class.getName();
/**
* The tracking plan branch name e.g. "main"
*/
private String branch;
/**
* The tracking plan source e.g. "web", "mobile"
*/
private String source;
/**
* The tracking plan version e.g. "1", "15"
*/
private String version;
/**
* The tracking plan version Id e.g. "9ec23ba0-275f-468f-80d1-66b88bff9529"
*/
private String versionId;
/**
* Set the tracking plan branch information.
* @param branch The tracking plan branch name e.g. "main"
* @return the same Plan object
*/
public Plan setBranch(String branch) {
this.branch = branch;
return this;
}
/**
* Set the tracking plan source information.
* @param source The tracking plan source e.g. "web", "mobile"
* @return the same Plan object
*/
public Plan setSource(String source) {
this.source = source;
return this;
}
/**
* Set the tracking plan version information.
* @param version The tracking plan version e.g. "1", "15"
* @return the same Plan object
*/
public Plan setVersion(String version) {
this.version = version;
return this;
}
/**
* Set the tracking plan version Id.
* @param version The tracking plan version e.g. "9ec23ba0-275f-468f-80d1-66b88bff9529"
* @return the same Plan object
*/
public Plan setVersionId(String versionId) {
this.versionId = versionId;
return this;
}
/**
* Get JSONObject of current tacking plan
* @return JSONObject including plan information
*/
protected JSONObject toJSONObject() {
JSONObject plan = new JSONObject();
try {
if (!Utils.isEmptyString(branch)) {
plan.put(Constants.AMP_PLAN_BRANCH, branch);
}
if (!Utils.isEmptyString(source)) {
plan.put(Constants.AMP_PLAN_SOURCE, source);
}
if (!Utils.isEmptyString(version)) {
plan.put(Constants.AMP_PLAN_VERSION, version);
}
if (!Utils.isEmptyString(versionId)) {
plan.put(Constants.AMP_PLAN_VERSION_ID, versionId);
}
} catch (JSONException e) {
AmplitudeLog.getLogger().e(TAG, "JSON Serialization of tacking plan object failed");
}
return plan;
}
}
================================================
FILE: src/main/java/com/amplitude/api/Revenue.java
================================================
package com.amplitude.api;
import org.json.JSONException;
import org.json.JSONObject;
/**
*
Revenue
* Revenue objects are a wrapper for revenue events and revenue properties. This should be used
* in conjunction with {@code AmplitudeClient.logRevenueV2()} to record in-app transactions.
* Each set method returns the same Revenue object, allowing
* you to chain multiple set calls together, for example:
* {@code Revenue revenue = new Revenue().setProductId("com.product.id").setPrice(3.99);}
*
* Note: {@code price} is a required field. If {@code quantity} is not
* specified, it will default to 1. {@code productId}, {@code receipt} and {@code receiptSignature}
* re required if you want to verify the revenue event.
*
* Note: the total revenue amount is calculated as price * quantity.
*
* After creating a Revenue object and setting the desired transaction properties, send it to
* Amplitude servers by calling {@code Amplitude.getInstance().logRevenueV2(revenue);} and pass in
* the object.
*
* @see
* Android SDK README for more information on logging revenue.
*/
public class Revenue {
/**
* The class identifier tag used in logging. TAG = {@code "com.amplitude.api.Revenue"}
*/
private static final String TAG = Revenue.class.getName();
private static AmplitudeLog logger = AmplitudeLog.getLogger();
/**
* The Product ID field.
*/
protected String productId = null;
/**
* The Quantity field (defaults to 1).
*/
protected int quantity = 1;
/**
* The Price field (required).
*/
protected Double price = null;
/**
* The Revenue Type field (optional).
*/
protected String revenueType = null;
/**
* The Receipt field (required if you want to verify the revenue event).
*/
protected String receipt = null;
/**
* The Receipt Signature field (required if you want to verify the revenue event).
*/
protected String receiptSig = null;
/**
* The Revenue Event Properties field (optional).
*/
protected JSONObject properties = null;
/**
* Verifies that revenue object is valid and contains the required fields
*
* @return true if revenue object is valid, else false
*/
protected boolean isValidRevenue() {
if (price == null) {
logger.w(TAG, "Invalid revenue, need to set price");
return false;
}
return true;
}
/**
* Set a value for the product identifier. Empty and invalid strings are ignored.
*
* @param productId the product id
* @return the same Revenue object
*/
public Revenue setProductId(String productId) {
if (Utils.isEmptyString(productId)) {
logger.w(TAG, "Invalid empty productId");
return this;
}
this.productId = productId;
return this;
}
/**
* Set a value for the quantity. Note: revenue amount is calculated as price * quantity.
*
* @param quantity the quantity
* @return the same Revenue object
*/
public Revenue setQuantity(int quantity) {
this.quantity = quantity;
return this;
}
/**
* Set a value for the price. Note: revenue amount is calculated as price * quantity.
*
* @param price the price
* @return the same Revenue object
*/
public Revenue setPrice(double price) {
this.price = price;
return this;
}
/**
* Set a value for the revenue type.
*
* @param revenueType the revenue type
* @return the same Revenue object
*/
public Revenue setRevenueType(String revenueType) {
this.revenueType = revenueType; // no input validation for optional field
return this;
}
/**
* Set the receipt and receipt signature. Both fields are required to verify the revenue event.
*
* @param receipt the receipt
* @param receiptSignature the receipt signature
* @return the same Revenue object
*/
public Revenue setReceipt(String receipt, String receiptSignature) {
this.receipt = receipt;
this.receiptSig = receiptSignature;
return this;
}
/**
* This is deprecated. RevenueProperties is a confusing name, should be EventProperties
*
* @param revenueProperties the revenue properties
* @return the same Revenue object
* @deprecated - use {@code Revenue.setEventProperties()} instead
*/
public Revenue setRevenueProperties(JSONObject revenueProperties) {
logger.w(TAG, "setRevenueProperties is deprecated, please use setEventProperties instead");
return setEventProperties(revenueProperties);
}
/**
* Set event properties for the revenue event, like you would for an event during logEvent.
*
* @param eventProperties the event properties
* @return the same Revenue object
* @see
* Event Properties for more information about logging event properties.
*/
public Revenue setEventProperties(JSONObject eventProperties) {
this.properties = Utils.cloneJSONObject(eventProperties);
return this;
}
/**
* Converts Revenue object into a JSONObject to send to Amplitude servers
*
* @return the JSON representation of this Revenue object
*/
protected JSONObject toJSONObject() {
JSONObject obj = properties == null ? new JSONObject() : properties;
try {
obj.put(Constants.AMP_REVENUE_PRODUCT_ID, productId);
obj.put(Constants.AMP_REVENUE_QUANTITY, quantity);
obj.put(Constants.AMP_REVENUE_PRICE, price);
obj.put(Constants.AMP_REVENUE_REVENUE_TYPE, revenueType);
obj.put(Constants.AMP_REVENUE_RECEIPT, receipt);
obj.put(Constants.AMP_REVENUE_RECEIPT_SIG, receiptSig);
} catch (JSONException e) {
logger.e(
TAG, String.format("Failed to convert revenue object to JSON: %s", e.toString())
);
}
return obj;
}
/**
* Custom equals function to compare 2 revenue objects. 2 Revenue objects are equal if
* all of their fields are equal. Generated by Android Studio
*
* @param o the other object to compare to
* @return true if the two Revenue objects are equal, false otherwise
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Revenue revenue = (Revenue) o;
if (quantity != revenue.quantity) return false;
if (productId != null ? !productId.equals(revenue.productId) : revenue.productId != null)
return false;
if (price != null ? !price.equals(revenue.price) : revenue.price != null) return false;
if (revenueType != null ? !revenueType.equals(revenue.revenueType) : revenue.revenueType != null)
return false;
if (receipt != null ? !receipt.equals(revenue.receipt) : revenue.receipt != null)
return false;
if (receiptSig != null ? !receiptSig.equals(revenue.receiptSig) : revenue.receiptSig != null)
return false;
return !(properties != null ? !Utils.compareJSONObjects(properties, revenue.properties): revenue.properties != null);
}
/**
* Custom hashcode generator function for Revenue object. Generated by Android Studio.
*
* @return the hashcode for this Revenue instance~
*/
@Override
public int hashCode() {
int result = productId != null ? productId.hashCode() : 0;
result = 31 * result + quantity;
result = 31 * result + (price != null ? price.hashCode() : 0);
result = 31 * result + (revenueType != null ? revenueType.hashCode() : 0);
result = 31 * result + (receipt != null ? receipt.hashCode() : 0);
result = 31 * result + (receiptSig != null ? receiptSig.hashCode() : 0);
result = 31 * result + (properties != null ? properties.hashCode() : 0);
return result;
}
}
================================================
FILE: src/main/java/com/amplitude/api/TrackingOptions.java
================================================
package com.amplitude.api;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashSet;
import java.util.Set;
public class TrackingOptions {
private static final String TAG = TrackingOptions.class.getName();
private static String[] SERVER_SIDE_PROPERTIES = {
Constants.AMP_TRACKING_OPTION_CITY,
Constants.AMP_TRACKING_OPTION_COUNTRY,
Constants.AMP_TRACKING_OPTION_DMA,
Constants.AMP_TRACKING_OPTION_IP_ADDRESS,
Constants.AMP_TRACKING_OPTION_LAT_LNG,
Constants.AMP_TRACKING_OPTION_REGION,
};
private static String[] COPPA_CONTROL_PROPERTIES = {
Constants.AMP_TRACKING_OPTION_ADID,
Constants.AMP_TRACKING_OPTION_CITY,
Constants.AMP_TRACKING_OPTION_IP_ADDRESS,
Constants.AMP_TRACKING_OPTION_LAT_LNG,
};
Set disabledFields = new HashSet();
public TrackingOptions disableAdid() {
disableTrackingField(Constants.AMP_TRACKING_OPTION_ADID);
return this;
}
boolean shouldTrackAdid() {
return shouldTrackField(Constants.AMP_TRACKING_OPTION_ADID);
}
public TrackingOptions disableAppSetId() {
disableTrackingField(Constants.AMP_TRACKING_OPTION_APP_SET_ID);
return this;
}
boolean shouldTrackAppSetId() {
return shouldTrackField(Constants.AMP_TRACKING_OPTION_APP_SET_ID);
}
public TrackingOptions disableCarrier() {
disableTrackingField(Constants.AMP_TRACKING_OPTION_CARRIER);
return this;
}
boolean shouldTrackCarrier() {
return shouldTrackField(Constants.AMP_TRACKING_OPTION_CARRIER);
}
public TrackingOptions disableCity() {
disableTrackingField(Constants.AMP_TRACKING_OPTION_CITY);
return this;
}
boolean shouldTrackCity() {
return shouldTrackField(Constants.AMP_TRACKING_OPTION_CITY);
}
public TrackingOptions disableCountry() {
disableTrackingField(Constants.AMP_TRACKING_OPTION_COUNTRY);
return this;
}
boolean shouldTrackCountry() {
return shouldTrackField(Constants.AMP_TRACKING_OPTION_COUNTRY);
}
public TrackingOptions disableDeviceBrand() {
disableTrackingField(Constants.AMP_TRACKING_OPTION_DEVICE_BRAND);
return this;
}
boolean shouldTrackDeviceBrand() {
return shouldTrackField(Constants.AMP_TRACKING_OPTION_DEVICE_BRAND);
}
public TrackingOptions disableDeviceManufacturer() {
disableTrackingField(Constants.AMP_TRACKING_OPTION_DEVICE_MANUFACTURER);
return this;
}
boolean shouldTrackDeviceManufacturer() {
return shouldTrackField(Constants.AMP_TRACKING_OPTION_DEVICE_MANUFACTURER);
}
public TrackingOptions disableDeviceModel() {
disableTrackingField(Constants.AMP_TRACKING_OPTION_DEVICE_MODEL);
return this;
}
boolean shouldTrackDeviceModel() {
return shouldTrackField(Constants.AMP_TRACKING_OPTION_DEVICE_MODEL);
}
public TrackingOptions disableDma() {
disableTrackingField(Constants.AMP_TRACKING_OPTION_DMA);
return this;
}
boolean shouldTrackDma() {
return shouldTrackField(Constants.AMP_TRACKING_OPTION_DMA);
}
public TrackingOptions disableIpAddress() {
disableTrackingField(Constants.AMP_TRACKING_OPTION_IP_ADDRESS);
return this;
}
boolean shouldTrackIpAddress() {
return shouldTrackField(Constants.AMP_TRACKING_OPTION_IP_ADDRESS);
}
public TrackingOptions disableLanguage() {
disableTrackingField(Constants.AMP_TRACKING_OPTION_LANGUAGE);
return this;
}
boolean shouldTrackLanguage() {
return shouldTrackField(Constants.AMP_TRACKING_OPTION_LANGUAGE);
}
public TrackingOptions disableLatLng() {
disableTrackingField(Constants.AMP_TRACKING_OPTION_LAT_LNG);
return this;
}
boolean shouldTrackLatLng() {
return shouldTrackField(Constants.AMP_TRACKING_OPTION_LAT_LNG);
}
public TrackingOptions disableOsName() {
disableTrackingField(Constants.AMP_TRACKING_OPTION_OS_NAME);
return this;
}
boolean shouldTrackOsName() {
return shouldTrackField(Constants.AMP_TRACKING_OPTION_OS_NAME);
}
public TrackingOptions disableOsVersion() {
disableTrackingField(Constants.AMP_TRACKING_OPTION_OS_VERSION);
return this;
}
boolean shouldTrackOsVersion() {
return shouldTrackField(Constants.AMP_TRACKING_OPTION_OS_VERSION);
}
public TrackingOptions disableApiLevel() {
disableTrackingField(Constants.AMP_TRACKING_OPTION_API_LEVEL);
return this;
}
boolean shouldTrackApiLevel() {
return shouldTrackField(Constants.AMP_TRACKING_OPTION_API_LEVEL);
}
public TrackingOptions disablePlatform() {
disableTrackingField(Constants.AMP_TRACKING_OPTION_PLATFORM);
return this;
}
boolean shouldTrackPlatform() {
return shouldTrackField(Constants.AMP_TRACKING_OPTION_PLATFORM);
}
public TrackingOptions disableRegion() {
disableTrackingField(Constants.AMP_TRACKING_OPTION_REGION);
return this;
}
boolean shouldTrackRegion() {
return shouldTrackField(Constants.AMP_TRACKING_OPTION_REGION);
}
public TrackingOptions disableVersionName() {
disableTrackingField(Constants.AMP_TRACKING_OPTION_VERSION_NAME);
return this;
}
boolean shouldTrackVersionName() {
return shouldTrackField(Constants.AMP_TRACKING_OPTION_VERSION_NAME);
}
private void disableTrackingField(String field) {
disabledFields.add(field);
}
protected JSONObject getApiPropertiesTrackingOptions() {
JSONObject apiPropertiesTrackingOptions = new JSONObject();
if (disabledFields.isEmpty()) {
return apiPropertiesTrackingOptions;
}
for (String key : SERVER_SIDE_PROPERTIES) {
if (disabledFields.contains(key)) {
try {
apiPropertiesTrackingOptions.put(key, false);
} catch (JSONException e) {
AmplitudeLog.getLogger().e(TAG, e.toString());
}
}
}
return apiPropertiesTrackingOptions;
}
private boolean shouldTrackField(String field) {
return !disabledFields.contains(field);
}
TrackingOptions mergeIn(TrackingOptions other) {
for (String key : other.disabledFields) {
disableTrackingField(key);
}
return this;
}
static TrackingOptions copyOf(TrackingOptions other) {
TrackingOptions trackingOptions = new TrackingOptions();
for (String key : other.disabledFields) {
trackingOptions.disableTrackingField(key);
}
return trackingOptions;
}
static TrackingOptions forCoppaControl() {
TrackingOptions trackingOptions = new TrackingOptions();
for (String key : COPPA_CONTROL_PROPERTIES) {
trackingOptions.disableTrackingField(key);
}
return trackingOptions;
}
public boolean equals(Object other) {
if (this == other) {
return true; // self check
}
if (other == null) {
return false; // null check
}
if (getClass() != other.getClass()) {
return false; // type check and cast
}
TrackingOptions options = (TrackingOptions) other;
return options.disabledFields.equals(this.disabledFields);
}
}
================================================
FILE: src/main/java/com/amplitude/api/Utils.java
================================================
package com.amplitude.api;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Iterator;
public class Utils {
private static final String TAG = Utils.class.getName();
private static AmplitudeLog logger = AmplitudeLog.getLogger();
/**
* Do a shallow copy of a JSONObject. Takes a bit of code to avoid
* stringify and reparse given the API.
*/
static JSONObject cloneJSONObject(final JSONObject obj) {
if (obj == null) {
return null;
}
if (obj.length() == 0) {
return new JSONObject();
}
// obj.names returns null if the json obj is empty.
JSONArray nameArray = null;
try {
nameArray = obj.names();
} catch (ArrayIndexOutOfBoundsException e) {
logger.e(TAG, e.toString());
}
int len = (nameArray != null ? nameArray.length() : 0);
String[] names = new String[len];
for (int i = 0; i < len; i++) {
names[i] = nameArray.optString(i);
}
try {
return new JSONObject(obj, names);
} catch (JSONException e) {
logger.e(TAG, e.toString());
return null;
}
}
static boolean compareJSONObjects(JSONObject o1, JSONObject o2) {
try {
if (o1 == o2) {
return true;
}
if ((o1 != null && o2 == null) || (o1 == null && o2 != null)) {
return false;
}
if (o1.length() != o2.length()) {
return false;
}
Iterator> keys = o1.keys();
while (keys.hasNext()) {
String key = (String) keys.next();
if (!o2.has(key)) {
return false;
}
Object value1 = o1.get(key);
Object value2 = o2.get(key);
if (!value1.getClass().equals(value2.getClass())) {
return false;
}
if (value1.getClass() == JSONObject.class) {
if (!compareJSONObjects((JSONObject) value1, (JSONObject) value2)) {
return false;
}
} else if (!value1.equals(value2)) {
return false;
}
}
return true;
} catch (JSONException e) {}
return false;
}
public static boolean isEmptyString(String s) {
return (s == null || s.length() == 0);
}
static String normalizeInstanceName(String instance) {
if (isEmptyString(instance)) {
instance = Constants.DEFAULT_INSTANCE;
}
return instance.toLowerCase();
}
static boolean checkLocationPermissionAllowed(Context context) {
return checkPermissionAllowed(context, Manifest.permission.ACCESS_COARSE_LOCATION) ||
checkPermissionAllowed(context, Manifest.permission.ACCESS_FINE_LOCATION);
}
static boolean checkPermissionAllowed(Context context, String permission) {
// ANDROID 6.0 AND UP!
if (android.os.Build.VERSION.SDK_INT >= 23) {
boolean hasPermission = false;
try {
// Invoke checkSelfPermission method from Android 6 (API 23 and UP)
java.lang.reflect.Method methodCheckPermission = Activity.class.getMethod("checkSelfPermission", java.lang.String.class);
Object resultObj = methodCheckPermission.invoke(context, permission);
int result = Integer.parseInt(resultObj.toString());
hasPermission = (result == PackageManager.PERMISSION_GRANTED);
} catch (Exception ex) {
}
return hasPermission;
} else {
return true;
}
}
}
================================================
FILE: src/main/java/com/amplitude/api/WorkerThread.java
================================================
package com.amplitude.api;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Process;
public class WorkerThread extends HandlerThread {
public WorkerThread(String name) {
super(name, Process.THREAD_PRIORITY_BACKGROUND);
}
private Handler handler;
Handler getHandler() {
return handler;
}
void post(Runnable r) {
waitForInitialization();
handler.post(r);
}
void postDelayed(Runnable r, long delayMillis) {
waitForInitialization();
handler.postDelayed(r, delayMillis);
}
void removeCallbacks(Runnable r) {
waitForInitialization();
handler.removeCallbacks(r);
}
private synchronized void waitForInitialization() {
if (handler == null) {
handler = new Handler(getLooper());
}
}
}
================================================
FILE: src/main/java/com/amplitude/eventexplorer/EventExplorer.java
================================================
package com.amplitude.eventexplorer;
import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.PixelFormat;
import android.os.Handler;
import android.os.Looper;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.View;
import android.view.WindowManager;
import com.amplitude.R;
public class EventExplorer {
private String instanceName;
private View bubbleView;
public EventExplorer(String instanceName) {
this.instanceName = instanceName;
}
public void show(final Activity rootActivity) {
if (this.bubbleView == null) {
new Handler(Looper.getMainLooper()).post(() -> {
final WindowManager windowManager = rootActivity.getWindowManager();
final DisplayMetrics displayMetrics = new DisplayMetrics();
if (windowManager.getDefaultDisplay() != null) {
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
}
final WindowManager.LayoutParams layoutParams
= prepareWindowManagerLayoutParams(rootActivity, displayMetrics);
this.bubbleView = rootActivity.getLayoutInflater().inflate(R.layout.amp_bubble_view, null);
windowManager.addView(this.bubbleView, layoutParams);
this.bubbleView.setOnTouchListener(new EventExplorerTouchHandler(windowManager, layoutParams, this.instanceName));
});
}
}
private WindowManager.LayoutParams prepareWindowManagerLayoutParams(Context context,
DisplayMetrics displayMetrics) {
int navbarHeight = 0;
Resources resources = context.getResources();
int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android");
if (resourceId > 0) {
navbarHeight = resources.getDimensionPixelSize(resourceId);
}
final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION;
layoutParams.format = PixelFormat.TRANSLUCENT;
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
layoutParams.y = (displayMetrics.heightPixels - navbarHeight) / 2;
layoutParams.x = (displayMetrics.widthPixels) / 2;
layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
return layoutParams;
}
}
================================================
FILE: src/main/java/com/amplitude/eventexplorer/EventExplorerInfoActivity.java
================================================
package com.amplitude.eventexplorer;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.amplitude.R;
import com.amplitude.api.Amplitude;
public class EventExplorerInfoActivity extends Activity {
private ImageView closeImageView;
private Button deviceIdCopyButton;
private Button userIdCopyButton;
private TextView deviceIdTextView;
private TextView userIdTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.amp_activity_eventexplorer_info);
this.closeImageView = findViewById(R.id.amp_eeInfo_iv_close);
this.closeImageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
onBackPressed();
}
});
this.deviceIdTextView = findViewById(R.id.amp_eeInfo_tv_deviceId);
this.userIdTextView = findViewById(R.id.amp_eeInfo_tv_userId);
Intent intent = getIntent();
String instanceName = intent.getExtras().getString("instanceName");
String deviceId = Amplitude.getInstance(instanceName).getDeviceId();
String userId = Amplitude.getInstance(instanceName).getUserId();
this.deviceIdTextView.setText(deviceId != null ? deviceId : getString(R.string.amp_label_not_avail));
this.userIdTextView.setText(userId != null ? userId : getString(R.string.amp_label_not_avail));
this.deviceIdCopyButton = findViewById(R.id.amp_eeInfo_btn_copyDeviceId);
this.deviceIdCopyButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
copyText(view.getContext(), deviceId);
}
});
this.userIdCopyButton = findViewById(R.id.amp_eeInfo_btn_copyUserId);
this.userIdCopyButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
copyText(view.getContext(), userId);
}
});
}
private void copyText(Context context, String text) {
if (text != null) {
ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("copied text", text);
clipboard.setPrimaryClip(clip);
Toast toast = Toast.makeText(context, getString(R.string.amp_label_copied), Toast.LENGTH_SHORT);
toast.show();
}
}
}
================================================
FILE: src/main/java/com/amplitude/eventexplorer/EventExplorerTouchHandler.java
================================================
package com.amplitude.eventexplorer;
import android.content.Intent;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
public class EventExplorerTouchHandler implements View.OnTouchListener {
private int initialX;
private float initialTouchX;
private int initialY;
private float initialTouchY;
private WindowManager.LayoutParams layoutParams;
private WindowManager windowManager;
private String instanceName;
EventExplorerTouchHandler(WindowManager windowManager,
WindowManager.LayoutParams layoutParams,
String instanceName) {
this.layoutParams = layoutParams;
this.windowManager = windowManager;
this.instanceName = instanceName;
}
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
initialY = layoutParams.y;
initialX = layoutParams.x;
initialTouchX = event.getRawX();
initialTouchY = event.getRawY();
return true;
case MotionEvent.ACTION_MOVE:
layoutParams.y = initialY + (int) (event.getRawY() - initialTouchY);
layoutParams.x = initialX + (int) (event.getRawX() - initialTouchX);
windowManager.updateViewLayout(v, layoutParams);
return true;
case MotionEvent.ACTION_UP:
float endX = event.getRawX();
float endY = event.getRawY();
if (isAClick(initialTouchX, endX, initialTouchY, endY)) {
v.performClick();
Intent intent = new Intent(v.getContext(), EventExplorerInfoActivity.class);
intent.putExtra("instanceName", this.instanceName);
v.getContext().startActivity(intent);
}
return true;
}
return false;
}
private boolean isAClick(float startX, float endX, float startY, float endY) {
float differenceX = Math.abs(startX - endX);
float differenceY = Math.abs(startY - endY);
return !(differenceX > 5 || differenceY > 5);
}
}
================================================
FILE: src/main/java/com/amplitude/unity/plugins/AmplitudePlugin.java
================================================
package com.amplitude.unity.plugins;
import android.app.Application;
import android.content.Context;
import com.amplitude.api.Amplitude;
import com.amplitude.api.AmplitudeServerZone;
import com.amplitude.api.Identify;
import com.amplitude.api.Revenue;
import com.amplitude.api.TrackingOptions;
import com.amplitude.api.Utils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
public class AmplitudePlugin {
public static JSONObject ToJSONObject(String jsonString) {
JSONObject properties = null;
try {
properties = new JSONObject(jsonString);
} catch (JSONException e) {
e.printStackTrace();
}
return properties;
}
public static JSONArray ToJSONArray(String[] values) {
JSONArray result = new JSONArray();
for (String value : values) {
result.put(value);
}
return result;
}
public static void init(String instanceName, Context context, String apiKey) {
Amplitude.getInstance(instanceName).initialize(context, apiKey);
}
public static void init(String instanceName, Context context, String apiKey, String userId) {
Amplitude.getInstance(instanceName).initialize(context, apiKey, userId);
}
public static void setTrackingOptions(String instanceName, String trackingOptionsJson) {
JSONObject trackingOptionsDict = ToJSONObject(trackingOptionsJson);
TrackingOptions trackingOptions = new TrackingOptions();
if (trackingOptionsDict.optBoolean("disableADID", false)) {
trackingOptions.disableAdid();
}
if (trackingOptionsDict.optBoolean("disableAppSetId", false)) {
trackingOptions.disableAppSetId();
}
if (trackingOptionsDict.optBoolean("disableCarrier", false)) {
trackingOptions.disableCarrier();
}
if (trackingOptionsDict.optBoolean("disableCity", false)) {
trackingOptions.disableCity();
}
if (trackingOptionsDict.optBoolean("disableCountry", false)) {
trackingOptions.disableCountry();
}
if (trackingOptionsDict.optBoolean("disableDeviceBrand", false)) {
trackingOptions.disableDeviceBrand();
}
if (trackingOptionsDict.optBoolean("disableDeviceManufacturer", false)) {
trackingOptions.disableDeviceManufacturer();
}
if (trackingOptionsDict.optBoolean("disableDeviceModel", false)) {
trackingOptions.disableDeviceModel();
}
if (trackingOptionsDict.optBoolean("disableDMA", false)) {
trackingOptions.disableDma();
}
if (trackingOptionsDict.optBoolean("disableIPAddress", false)) {
trackingOptions.disableIpAddress();
}
if (trackingOptionsDict.optBoolean("disableLanguage", false)) {
trackingOptions.disableLanguage();
}
if (trackingOptionsDict.optBoolean("disableLatLng", false)) {
trackingOptions.disableLatLng();
}
if (trackingOptionsDict.optBoolean("disableOSName", false)) {
trackingOptions.disableOsName();
}
if (trackingOptionsDict.optBoolean("disableOSVersion", false)) {
trackingOptions.disableOsVersion();
}
if (trackingOptionsDict.optBoolean("disableApiLevel", false)) {
trackingOptions.disableApiLevel();
}
if (trackingOptionsDict.optBoolean("disablePlatform", false)) {
trackingOptions.disablePlatform();
}
if (trackingOptionsDict.optBoolean("disableRegion", false)) {
trackingOptions.disableRegion();
}
if (trackingOptionsDict.optBoolean("disableVersionName", false)) {
trackingOptions.disableVersionName();
}
Amplitude.getInstance(instanceName).setTrackingOptions(trackingOptions);
}
public static void enableForegroundTracking(String instanceName, Application app) {
Amplitude.getInstance(instanceName).enableForegroundTracking(app);
}
public static void enableCoppaControl(String instanceName) {
Amplitude.getInstance(instanceName).enableCoppaControl();
}
public static void disableCoppaControl(String instanceName) {
Amplitude.getInstance(instanceName).disableCoppaControl();
}
public static void setLibraryName(String instanceName, String libraryName) {
Amplitude.getInstance(instanceName).setLibraryName(libraryName);
}
public static void setLibraryVersion(String instanceName, String libraryVersion) {
Amplitude.getInstance(instanceName).setLibraryVersion(libraryVersion);
}
public static void setServerUrl(String instanceName, String serverUrl) {
Amplitude.getInstance(instanceName).setServerUrl(serverUrl);
}
public static void setServerZone(String instanceName, String serverZone, boolean updateServerUrl) {
AmplitudeServerZone amplitudeServerZone = AmplitudeServerZone.getServerZone(serverZone);
Amplitude.getInstance(instanceName).setServerZone(amplitudeServerZone, updateServerUrl);
}
public static void setUseDynamicConfig(String instanceName, boolean useDynamicConfig) {
Amplitude.getInstance(instanceName).setUseDynamicConfig(useDynamicConfig);
}
@Deprecated
public static void startSession() { return; }
@Deprecated
public static void endSession() { return; }
public static void logEvent(String instanceName, String event) {
Amplitude.getInstance(instanceName).logEvent(event);
}
public static void logEvent(String instanceName, String event, String jsonProperties) {
Amplitude.getInstance(instanceName).logEvent(event, ToJSONObject(jsonProperties));
}
public static void logEvent(String instanceName, String event, String jsonProperties, boolean outOfSession) {
Amplitude.getInstance(instanceName).logEvent(event, ToJSONObject(jsonProperties), outOfSession);
}
public static void uploadEvents(String instanceName) {
Amplitude.getInstance(instanceName).uploadEvents();
}
public static void useAdvertisingIdForDeviceId(String instanceName) {
Amplitude.getInstance(instanceName).useAdvertisingIdForDeviceId();
}
public static void useAppSetIdForDeviceId(String instanceName) {
Amplitude.getInstance(instanceName).useAppSetIdForDeviceId();
}
public static void setOffline(String instanceName, boolean offline) {
Amplitude.getInstance(instanceName).setOffline(offline);
}
public static void setUserId(String instanceName, String userId) {
Amplitude.getInstance(instanceName).setUserId(userId);
}
public static void setOptOut(String instanceName, boolean enabled) {
Amplitude.getInstance(instanceName).setOptOut(enabled);
}
public static void setMinTimeBetweenSessionsMillis(String instanceName, long minTimeBetweenSessionsMillis) {
Amplitude.getInstance(instanceName).setMinTimeBetweenSessionsMillis(minTimeBetweenSessionsMillis);
}
public static void setEventUploadPeriodMillis(String instanceName, int eventUploadPeriodMillis) {
Amplitude.getInstance(instanceName).setEventUploadPeriodMillis(eventUploadPeriodMillis);
}
public static void setUserProperties(String instanceName, String jsonProperties) {
Amplitude.getInstance(instanceName).setUserProperties(ToJSONObject(jsonProperties));
}
public static void setGroup(String instanceName, String groupType, String groupName) {
Amplitude.getInstance(instanceName).setGroup(groupType, groupName);
}
public static void setGroup(String instanceName, String groupType, String[] groupName) {
Amplitude.getInstance(instanceName).setGroup(groupType, ToJSONArray(groupName));
}
public static void logRevenue(String instanceName, double amount) {
Amplitude.getInstance(instanceName).logRevenue(amount);
}
public static void logRevenue(String instanceName, String productId, int quantity, double price) {
Amplitude.getInstance(instanceName).logRevenue(productId, quantity, price);
}
public static void logRevenue(String instanceName, String productId, int quantity, double price, String receipt, String receiptSignature) {
Amplitude.getInstance(instanceName).logRevenue(productId, quantity, price, receipt, receiptSignature);
}
public static void logRevenue(String instanceName, String productId, int quantity, double price, String receipt, String receiptSignature, String revenueType, String jsonProperties) {
Revenue revenue = new Revenue().setQuantity(quantity).setPrice(price);
if (!Utils.isEmptyString(productId)) {
revenue.setProductId(productId);
}
if (!Utils.isEmptyString(receipt) && !Utils.isEmptyString(receiptSignature)) {
revenue.setReceipt(receipt, receiptSignature);
}
if (!Utils.isEmptyString(revenueType)) {
revenue.setRevenueType(revenueType);
}
if (!Utils.isEmptyString(jsonProperties)) {
revenue.setEventProperties(ToJSONObject(jsonProperties));
}
Amplitude.getInstance(instanceName).logRevenueV2(revenue);
}
public static String getDeviceId(String instanceName) {
return Amplitude.getInstance(instanceName).getDeviceId();
}
public static void setDeviceId(String instanceName, String deviceId) {
Amplitude.getInstance(instanceName).setDeviceId(deviceId);
}
public static void regenerateDeviceId(String instanceName) { Amplitude.getInstance(instanceName).regenerateDeviceId(); }
public static void trackSessionEvents(String instanceName, boolean enabled) {
Amplitude.getInstance(instanceName).trackSessionEvents(enabled);
}
public static long getSessionId(String instanceName) { return Amplitude.getInstance(instanceName).getSessionId(); }
// User Property Operations
// clear user properties
public static void clearUserProperties(String instanceName) {
Amplitude.getInstance(instanceName).clearUserProperties();
}
// unset user property
public static void unsetUserProperty(String instanceName, String property) {
Amplitude.getInstance(instanceName).identify(new Identify().unset(property));
}
// setOnce user property
public static void setOnceUserProperty(String instanceName, String property, boolean value) {
Amplitude.getInstance(instanceName).identify(new Identify().setOnce(property, value));
}
public static void setOnceUserProperty(String instanceName, String property, double value) {
Amplitude.getInstance(instanceName).identify(new Identify().setOnce(property, value));
}
public static void setOnceUserProperty(String instanceName, String property, float value) {
Amplitude.getInstance(instanceName).identify(new Identify().setOnce(property, value));
}
public static void setOnceUserProperty(String instanceName, String property, int value) {
Amplitude.getInstance(instanceName).identify(new Identify().setOnce(property, value));
}
public static void setOnceUserProperty(String instanceName, String property, long value) {
Amplitude.getInstance(instanceName).identify(new Identify().setOnce(property, value));
}
public static void setOnceUserProperty(String instanceName, String property, String value) {
Amplitude.getInstance(instanceName).identify(new Identify().setOnce(property, value));
}
public static void setOnceUserPropertyDict(String instanceName, String property, String values) {
Amplitude.getInstance(instanceName).identify(new Identify().setOnce(property, ToJSONObject(values)));
}
public static void setOnceUserPropertyList(String instanceName, String property, String values) {
JSONObject properties = ToJSONObject(values);
if (properties == null) {
return;
}
Amplitude.getInstance(instanceName).identify(new Identify().setOnce(
property, properties.optJSONArray("list")
));
}
public static void setOnceUserProperty(String instanceName, String property, boolean[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().setOnce(property, values));
}
public static void setOnceUserProperty(String instanceName, String property, double[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().setOnce(property, values));
}
public static void setOnceUserProperty(String instanceName, String property, float[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().setOnce(property, values));
}
public static void setOnceUserProperty(String instanceName, String property, int[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().setOnce(property, values));
}
public static void setOnceUserProperty(String instanceName, String property, long[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().setOnce(property, values));
}
public static void setOnceUserProperty(String instanceName, String property, String[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().setOnce(property, values));
}
// set user property
public static void setUserProperty(String instanceName, String property, boolean value) {
Amplitude.getInstance(instanceName).identify(new Identify().set(property, value));
}
public static void setUserProperty(String instanceName, String property, double value) {
Amplitude.getInstance(instanceName).identify(new Identify().set(property, value));
}
public static void setUserProperty(String instanceName, String property, float value) {
Amplitude.getInstance(instanceName).identify(new Identify().set(property, value));
}
public static void setUserProperty(String instanceName, String property, int value) {
Amplitude.getInstance(instanceName).identify(new Identify().set(property, value));
}
public static void setUserProperty(String instanceName, String property, long value) {
Amplitude.getInstance(instanceName).identify(new Identify().set(property, value));
}
public static void setUserProperty(String instanceName, String property, String value) {
Amplitude.getInstance(instanceName).identify(new Identify().set(property, value));
}
public static void setUserPropertyDict(String instanceName, String property, String values) {
Amplitude.getInstance(instanceName).identify(new Identify().set(property, ToJSONObject(values)));
}
public static void setUserPropertyList(String instanceName, String property, String values) {
JSONObject properties = ToJSONObject(values);
if (properties == null) {
return;
}
Amplitude.getInstance(instanceName).identify(new Identify().set(
property, properties.optJSONArray("list")
));
}
public static void setUserProperty(String instanceName, String property, boolean[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().set(property, values));
}
public static void setUserProperty(String instanceName, String property, double[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().set(property, values));
}
public static void setUserProperty(String instanceName, String property, float[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().set(property, values));
}
public static void setUserProperty(String instanceName, String property, int[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().set(property, values));
}
public static void setUserProperty(String instanceName, String property, long[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().set(property, values));
}
public static void setUserProperty(String instanceName, String property, String[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().set(property, values));
}
// add
public static void addUserProperty(String instanceName, String property, double value) {
Amplitude.getInstance(instanceName).identify(new Identify().add(property, value));
}
public static void addUserProperty(String instanceName, String property, float value) {
Amplitude.getInstance(instanceName).identify(new Identify().add(property, value));
}
public static void addUserProperty(String instanceName, String property, int value) {
Amplitude.getInstance(instanceName).identify(new Identify().add(property, value));
}
public static void addUserProperty(String instanceName, String property, long value) {
Amplitude.getInstance(instanceName).identify(new Identify().add(property, value));
}
public static void addUserProperty(String instanceName, String property, String value) {
Amplitude.getInstance(instanceName).identify(new Identify().add(property, value));
}
public static void addUserPropertyDict(String instanceName, String property, String values) {
Amplitude.getInstance(instanceName).identify(new Identify().add(property, ToJSONObject(values)));
}
// append user property
public static void appendUserProperty(String instanceName, String property, boolean value) {
Amplitude.getInstance(instanceName).identify(new Identify().append(property, value));
}
public static void appendUserProperty(String instanceName, String property, double value) {
Amplitude.getInstance(instanceName).identify(new Identify().append(property, value));
}
public static void appendUserProperty(String instanceName, String property, float value) {
Amplitude.getInstance(instanceName).identify(new Identify().append(property, value));
}
public static void appendUserProperty(String instanceName, String property, int value) {
Amplitude.getInstance(instanceName).identify(new Identify().append(property, value));
}
public static void appendUserProperty(String instanceName, String property, long value) {
Amplitude.getInstance(instanceName).identify(new Identify().append(property, value));
}
public static void appendUserProperty(String instanceName, String property, String value) {
Amplitude.getInstance(instanceName).identify(new Identify().append(property, value));
}
public static void appendUserPropertyDict(String instanceName, String property, String values) {
Amplitude.getInstance(instanceName).identify(new Identify().append(property, ToJSONObject(values)));
}
public static void appendUserPropertyList(String instanceName, String property, String values) {
JSONObject properties = ToJSONObject(values);
if (properties == null) {
return;
}
Amplitude.getInstance(instanceName).identify(new Identify().append(
property, properties.optJSONArray("list")
));
}
public static void appendUserProperty(String instanceName, String property, boolean[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().append(property, values));
}
public static void appendUserProperty(String instanceName, String property, double[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().append(property, values));
}
public static void appendUserProperty(String instanceName, String property, float[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().append(property, values));
}
public static void appendUserProperty(String instanceName, String property, int[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().append(property, values));
}
public static void appendUserProperty(String instanceName, String property, long[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().append(property, values));
}
public static void appendUserProperty(String instanceName, String property, String[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().append(property, values));
}
//prepend user property
public static void prependUserProperty(String instanceName, String property, boolean value) {
Amplitude.getInstance(instanceName).identify(new Identify().prepend(property, value));
}
public static void prependUserProperty(String instanceName, String property, double value) {
Amplitude.getInstance(instanceName).identify(new Identify().prepend(property, value));
}
public static void prependUserProperty(String instanceName, String property, float value) {
Amplitude.getInstance(instanceName).identify(new Identify().prepend(property, value));
}
public static void prependUserProperty(String instanceName, String property, int value) {
Amplitude.getInstance(instanceName).identify(new Identify().prepend(property, value));
}
public static void prependUserProperty(String instanceName, String property, long value) {
Amplitude.getInstance(instanceName).identify(new Identify().prepend(property, value));
}
public static void prependUserProperty(String instanceName, String property, String value) {
Amplitude.getInstance(instanceName).identify(new Identify().prepend(property, value));
}
public static void prependUserPropertyDict(String instanceName, String property, String values) {
Amplitude.getInstance(instanceName).identify(new Identify().prepend(property, ToJSONObject(values)));
}
public static void prependUserPropertyList(String instanceName, String property, String values) {
JSONObject properties = ToJSONObject(values);
if (properties == null) {
return;
}
Amplitude.getInstance(instanceName).identify(new Identify().prepend(
property, properties.optJSONArray("list")
));
}
public static void prependUserProperty(String instanceName, String property, boolean[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().prepend(property, values));
}
public static void prependUserProperty(String instanceName, String property, double[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().prepend(property, values));
}
public static void prependUserProperty(String instanceName, String property, float[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().prepend(property, values));
}
public static void prependUserProperty(String instanceName, String property, int[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().prepend(property, values));
}
public static void prependUserProperty(String instanceName, String property, long[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().prepend(property, values));
}
public static void prependUserProperty(String instanceName, String property, String[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().prepend(property, values));
}
//preInsert user property
public static void preInsertUserProperty(String instanceName, String property, boolean value) {
Amplitude.getInstance(instanceName).identify(new Identify().preInsert(property, value));
}
public static void preInsertUserProperty(String instanceName, String property, double value) {
Amplitude.getInstance(instanceName).identify(new Identify().preInsert(property, value));
}
public static void preInsertUserProperty(String instanceName, String property, float value) {
Amplitude.getInstance(instanceName).identify(new Identify().preInsert(property, value));
}
public static void preInsertUserProperty(String instanceName, String property, int value) {
Amplitude.getInstance(instanceName).identify(new Identify().preInsert(property, value));
}
public static void preInsertUserProperty(String instanceName, String property, long value) {
Amplitude.getInstance(instanceName).identify(new Identify().preInsert(property, value));
}
public static void preInsertUserProperty(String instanceName, String property, String value) {
Amplitude.getInstance(instanceName).identify(new Identify().preInsert(property, value));
}
public static void preInsertUserPropertyDict(String instanceName, String property, String values) {
Amplitude.getInstance(instanceName).identify(new Identify().preInsert(property, ToJSONObject(values)));
}
public static void preInsertUserPropertyList(String instanceName, String property, String values) {
JSONObject properties = ToJSONObject(values);
if (properties == null) {
return;
}
Amplitude.getInstance(instanceName).identify(new Identify().preInsert(
property, properties.optJSONArray("list")
));
}
public static void preInsertUserProperty(String instanceName, String property, boolean[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().preInsert(property, values));
}
public static void preInsertUserProperty(String instanceName, String property, double[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().preInsert(property, values));
}
public static void preInsertUserProperty(String instanceName, String property, float[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().preInsert(property, values));
}
public static void preInsertUserProperty(String instanceName, String property, int[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().preInsert(property, values));
}
public static void preInsertUserProperty(String instanceName, String property, long[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().preInsert(property, values));
}
public static void preInsertUserProperty(String instanceName, String property, String[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().preInsert(property, values));
}
//postInsert user property
public static void postInsertUserProperty(String instanceName, String property, boolean value) {
Amplitude.getInstance(instanceName).identify(new Identify().postInsert(property, value));
}
public static void postInsertUserProperty(String instanceName, String property, double value) {
Amplitude.getInstance(instanceName).identify(new Identify().postInsert(property, value));
}
public static void postInsertUserProperty(String instanceName, String property, float value) {
Amplitude.getInstance(instanceName).identify(new Identify().postInsert(property, value));
}
public static void postInsertUserProperty(String instanceName, String property, int value) {
Amplitude.getInstance(instanceName).identify(new Identify().postInsert(property, value));
}
public static void postInsertUserProperty(String instanceName, String property, long value) {
Amplitude.getInstance(instanceName).identify(new Identify().postInsert(property, value));
}
public static void postInsertUserProperty(String instanceName, String property, String value) {
Amplitude.getInstance(instanceName).identify(new Identify().postInsert(property, value));
}
public static void postInsertUserPropertyDict(String instanceName, String property, String values) {
Amplitude.getInstance(instanceName).identify(new Identify().postInsert(property, ToJSONObject(values)));
}
public static void postInsertUserPropertyList(String instanceName, String property, String values) {
JSONObject properties = ToJSONObject(values);
if (properties == null) {
return;
}
Amplitude.getInstance(instanceName).identify(new Identify().postInsert(
property, properties.optJSONArray("list")
));
}
public static void postInsertUserProperty(String instanceName, String property, boolean[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().postInsert(property, values));
}
public static void postInsertUserProperty(String instanceName, String property, double[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().postInsert(property, values));
}
public static void postInsertUserProperty(String instanceName, String property, float[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().postInsert(property, values));
}
public static void postInsertUserProperty(String instanceName, String property, int[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().postInsert(property, values));
}
public static void postInsertUserProperty(String instanceName, String property, long[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().postInsert(property, values));
}
public static void postInsertUserProperty(String instanceName, String property, String[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().postInsert(property, values));
}
//remove user property
public static void removeUserProperty(String instanceName, String property, boolean value) {
Amplitude.getInstance(instanceName).identify(new Identify().remove(property, value));
}
public static void removeUserProperty(String instanceName, String property, double value) {
Amplitude.getInstance(instanceName).identify(new Identify().remove(property, value));
}
public static void removeUserProperty(String instanceName, String property, float value) {
Amplitude.getInstance(instanceName).identify(new Identify().remove(property, value));
}
public static void removeUserProperty(String instanceName, String property, int value) {
Amplitude.getInstance(instanceName).identify(new Identify().remove(property, value));
}
public static void removeUserProperty(String instanceName, String property, long value) {
Amplitude.getInstance(instanceName).identify(new Identify().remove(property, value));
}
public static void removeUserProperty(String instanceName, String property, String value) {
Amplitude.getInstance(instanceName).identify(new Identify().remove(property, value));
}
public static void removeUserPropertyDict(String instanceName, String property, String values) {
Amplitude.getInstance(instanceName).identify(new Identify().remove(property, ToJSONObject(values)));
}
public static void removeUserPropertyList(String instanceName, String property, String values) {
JSONObject properties = ToJSONObject(values);
if (properties == null) {
return;
}
Amplitude.getInstance(instanceName).identify(new Identify().remove(
property, properties.optJSONArray("list")
));
}
public static void removeUserProperty(String instanceName, String property, boolean[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().remove(property, values));
}
public static void removeUserProperty(String instanceName, String property, double[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().remove(property, values));
}
public static void removeUserProperty(String instanceName, String property, float[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().remove(property, values));
}
public static void removeUserProperty(String instanceName, String property, int[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().remove(property, values));
}
public static void removeUserProperty(String instanceName, String property, long[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().remove(property, values));
}
public static void removeUserProperty(String instanceName, String property, String[] values) {
Amplitude.getInstance(instanceName).identify(new Identify().remove(property, values));
}
}
================================================
FILE: src/main/java/com/amplitude/util/DoubleCheck.java
================================================
package com.amplitude.util;
/**
* Copy from https://github.com/google/dagger/blob/master/java/dagger/internal/DoubleCheck.java
*
* Apache v2.0
*/
public class DoubleCheck implements Provider {
private static final Object UNINITIALIZED = new Object();
private volatile Provider provider;
private volatile Object instance = UNINITIALIZED;
private DoubleCheck(Provider provider) {
assert provider != null;
this.provider = provider;
}
@SuppressWarnings("unchecked") // cast only happens when result comes from the provider
@Override
public T get() {
Object result = instance;
if (result == UNINITIALIZED) {
synchronized (this) {
result = instance;
if (result == UNINITIALIZED) {
result = provider.get();
instance = reentrantCheck(instance, result);
/* Null out the reference to the provider. We are never going to need it again, so we
* can make it eligible for GC. */
provider = null;
}
}
}
return (T) result;
}
/**
* Checks to see if creating the new instance has resulted in a recursive call. If it has, and the
* new instance is the same as the current instance, return the instance. However, if the new
* instance differs from the current instance, an {@link IllegalStateException} is thrown.
*/
public static Object reentrantCheck(Object currentInstance, Object newInstance) {
boolean isReentrant = !(currentInstance == UNINITIALIZED);
if (isReentrant && currentInstance != newInstance) {
throw new IllegalStateException("Scoped provider was invoked recursively returning "
+ "different results: " + currentInstance + " & " + newInstance + ". This is likely "
+ "due to a circular dependency.");
}
return newInstance;
}
/** Returns a {@link Provider} that caches the value from the given delegate provider. */
public static
, T> Provider provider(P delegate) {
if (delegate == null) {
throw new IllegalArgumentException("delegate cannot be null");
}
if (delegate instanceof DoubleCheck) {
/* This should be a rare case, but if we have a scoped @Binds that delegates to a scoped
* binding, we shouldn't cache the value again. */
return delegate;
}
return new DoubleCheck(delegate);
}
}
================================================
FILE: src/main/java/com/amplitude/util/Provider.java
================================================
package com.amplitude.util;
public interface Provider {
T get();
}
================================================
FILE: src/main/res/drawable/amp_button_bg.xml
================================================
================================================
FILE: src/main/res/layout/amp_activity_eventexplorer_info.xml
================================================
================================================
FILE: src/main/res/layout/amp_bubble_view.xml
================================================
================================================
FILE: src/main/res/values/colors.xml
================================================
#00000000#E5E5E5#C6D0D9#60758B#0C2A4B#005488
================================================
FILE: src/main/res/values/strings.xml
================================================
CopyCopied To ClipboardUser InformationDEVICE IDUSER IDN/A
================================================
FILE: src/test/java/com/amplitude/api/AmplitudeClientTest.java
================================================
package com.amplitude.api;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Matchers;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.robolectric.Robolectric;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLooper;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.RecordedRequest;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.robolectric.Shadows.shadowOf;
@RunWith(AndroidJUnit4.class)
@Config(manifest = Config.NONE)
public class AmplitudeClientTest extends BaseTest {
private String generateStringWithLength(int length, char c) {
if (length < 0) return "";
char [] array = new char[length];
Arrays.fill(array, c);
return new String(array);
}
@Before
public void setUp() throws Exception {
super.setUp();
amplitude.initialize(context, apiKey);
Shadows.shadowOf(amplitude.logThread.getLooper()).runOneTask();
}
@After
public void tearDown() throws Exception {
super.tearDown();
}
@Test
public void testConstructor() {
// verify that the constructor lowercases the instance name
AmplitudeClient a = new AmplitudeClient("APP1");
AmplitudeClient b = new AmplitudeClient("New_App_2");
assertEquals(a.instanceName, "app1");
assertEquals(b.instanceName, "new_app_2");
}
@Test
public void testSetUserId() {
DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
String userId = "user_id";
amplitude.setUserId(userId);
looper.runToEndOfTasks();
assertEquals(userId, dbHelper.getValue(AmplitudeClient.USER_ID_KEY));
assertEquals(userId, amplitude.getUserId());
// try setting to null
amplitude.setUserId(null);
looper.runToEndOfTasks();
assertNull(dbHelper.getValue(AmplitudeClient.USER_ID_KEY));
assertNull(amplitude.getUserId());
}
@Test
public void testSetUserIdTwice() {
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
String userId1 = "user_id1";
String userId2 = "user_id2";
amplitude.setUserId(userId1);
looper.runToEndOfTasks();
assertEquals(amplitude.getUserId(), userId1);
amplitude.logEvent("event1");
looper.runToEndOfTasks();
JSONObject event1 = getLastUnsentEvent();
assertEquals(event1.optString("event_type"), "event1");
assertEquals(event1.optString("user_id"), userId1);
amplitude.setUserId(userId2);
looper.runToEndOfTasks();
assertEquals(amplitude.getUserId(), userId2);
amplitude.logEvent("event2");
looper.runToEndOfTasks();
JSONObject event2 = getLastUnsentEvent();
assertEquals(event2.optString("event_type"), "event2");
assertEquals(event2.optString("user_id"), userId2);
}
@Test
public void testSetDeviceId() {
DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
String deviceId = amplitude.getDeviceId(); // Randomly generated device ID
assertNotNull(deviceId);
assertEquals(deviceId.length(), 36 + 1); // 36 for UUID, + 1 for appended R
assertEquals(deviceId.charAt(36), 'R');
assertEquals(dbHelper.getValue(amplitude.DEVICE_ID_KEY), deviceId);
// test setting invalid device ids
amplitude.setDeviceId(null);
looper.runToEndOfTasks();
assertEquals(amplitude.getDeviceId(), deviceId);
assertEquals(dbHelper.getValue(amplitude.DEVICE_ID_KEY), deviceId);
amplitude.setDeviceId("");
looper.runToEndOfTasks();
assertEquals(amplitude.getDeviceId(), deviceId);
assertEquals(dbHelper.getValue(amplitude.DEVICE_ID_KEY), deviceId);
amplitude.setDeviceId("9774d56d682e549c");
looper.runToEndOfTasks();
assertEquals(amplitude.getDeviceId(), deviceId);
assertEquals(dbHelper.getValue(amplitude.DEVICE_ID_KEY), deviceId);
amplitude.setDeviceId("unknown");
looper.runToEndOfTasks();
assertEquals(amplitude.getDeviceId(), deviceId);
assertEquals(dbHelper.getValue(amplitude.DEVICE_ID_KEY), deviceId);
amplitude.setDeviceId("000000000000000");
looper.runToEndOfTasks();
assertEquals(amplitude.getDeviceId(), deviceId);
assertEquals(dbHelper.getValue(amplitude.DEVICE_ID_KEY), deviceId);
amplitude.setDeviceId("Android");
looper.runToEndOfTasks();
assertEquals(amplitude.getDeviceId(), deviceId);
assertEquals(dbHelper.getValue(amplitude.DEVICE_ID_KEY), deviceId);
amplitude.setDeviceId("DEFACE");
looper.runToEndOfTasks();
assertEquals(amplitude.getDeviceId(), deviceId);
assertEquals(dbHelper.getValue(amplitude.DEVICE_ID_KEY), deviceId);
amplitude.setDeviceId("00000000-0000-0000-0000-000000000000");
assertEquals(amplitude.getDeviceId(), deviceId);
assertEquals(dbHelper.getValue(amplitude.DEVICE_ID_KEY), deviceId);
// set valid device id
String newDeviceId = UUID.randomUUID().toString();
amplitude.setDeviceId(newDeviceId);
looper.runToEndOfTasks();
assertEquals(amplitude.getDeviceId(), newDeviceId);
assertEquals(dbHelper.getValue(amplitude.DEVICE_ID_KEY), newDeviceId);
amplitude.logEvent("test");
looper.runToEndOfTasks();
JSONObject event = getLastUnsentEvent();
assertEquals(event.optString("event_type"), "test");
assertEquals(event.optString("device_id"), newDeviceId);
}
@Test
public void testSetUserProperties() throws JSONException {
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
// setting null or empty user properties does nothing
amplitude.setUserProperties(null);
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 0);
assertEquals(getUnsentIdentifyCount(), 0);
amplitude.setUserProperties(new JSONObject());
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 0);
assertEquals(getUnsentIdentifyCount(), 0);
JSONObject userProperties = new JSONObject().put("key1", "value1").put("key2", "value2");
amplitude.setUserProperties(userProperties);
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 0);
assertEquals(getUnsentIdentifyCount(), 1);
JSONObject event = getLastUnsentIdentify();
assertEquals(Constants.IDENTIFY_EVENT, event.optString("event_type"));
assertTrue(Utils.compareJSONObjects(
event.optJSONObject("event_properties"), new JSONObject()
));
JSONObject userPropertiesOperations = event.optJSONObject("user_properties");
assertEquals(userPropertiesOperations.length(), 1);
assertTrue(userPropertiesOperations.has(Constants.AMP_OP_SET));
JSONObject setOperations = userPropertiesOperations.optJSONObject(Constants.AMP_OP_SET);
assertTrue(Utils.compareJSONObjects(userProperties, setOperations));
}
@Test
public void testSetCustomLibrary() {
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
amplitude.setLibraryName("amplitude-unity");
amplitude.setLibraryVersion("1.0.0");
amplitude.logEvent("test");
looper.runToEndOfTasks();
JSONObject event = getLastEvent();
assertNotNull(event);
try {
JSONObject library = event.getJSONObject("library");
String libName = library.getString("name");
String libVersion = library.getString("version");
assertEquals(libName, "amplitude-unity");
assertEquals(libVersion, "1.0.0");
} catch (Exception e) {
Assert.fail();
}
}
@Test
public void testSetCustomLibraryWithNullValues() {
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
amplitude.setLibraryName(null);
amplitude.setLibraryVersion(null);
amplitude.logEvent("test");
looper.runToEndOfTasks();
JSONObject event = getLastEvent();
assertNotNull(event);
try {
JSONObject library = event.getJSONObject("library");
String libName = library.getString("name");
String libVersion = library.getString("version");
assertEquals(libName, "unknown-library");
assertEquals(libVersion, "unknown-version");
} catch (Exception e) {
Assert.fail();
}
}
@Test
public void testIdentifyMultipleOperations() throws JSONException {
String property1 = "string value";
String value1 = "testValue";
String property2 = "double value";
double value2 = 0.123;
String property3 = "boolean value";
boolean value3 = true;
String property4 = "json value";
Identify identify = new Identify().setOnce(property1, value1).add(property2, value2);
identify.set(property3, value3).unset(property4);
// identify should ignore this since duplicate key
identify.set(property4, value3);
amplitude.identify(identify);
Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();
assertEquals(getUnsentIdentifyCount(), 1);
assertEquals(getUnsentEventCount(), 0);
JSONObject event = getLastUnsentIdentify();
assertEquals(Constants.IDENTIFY_EVENT, event.optString("event_type"));
JSONObject userProperties = event.optJSONObject("user_properties");
JSONObject expected = new JSONObject();
expected.put(Constants.AMP_OP_SET_ONCE, new JSONObject().put(property1, value1));
expected.put(Constants.AMP_OP_ADD, new JSONObject().put(property2, value2));
expected.put(Constants.AMP_OP_SET, new JSONObject().put(property3, value3));
expected.put(Constants.AMP_OP_UNSET, new JSONObject().put(property4, "-"));
assertTrue(Utils.compareJSONObjects(userProperties, expected));
}
@Test
public void testOptOut() {
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
ShadowLooper httplooper = Shadows.shadowOf(amplitude.httpThread.getLooper());
DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);
assertFalse(amplitude.isOptedOut());
assertNull(dbHelper.getLongValue(AmplitudeClient.OPT_OUT_KEY));
amplitude.setOptOut(true);
looper.runToEndOfTasks();
assertTrue(amplitude.isOptedOut());
assertEquals((long) dbHelper.getLongValue(AmplitudeClient.OPT_OUT_KEY), 1L);
RecordedRequest request = sendEvent(amplitude, "test_opt_out", null);
assertNull(request);
// Event shouldn't be sent event once opt out is turned off.
amplitude.setOptOut(false);
looper.runToEndOfTasks();
assertFalse(amplitude.isOptedOut());
assertEquals((long) dbHelper.getLongValue(AmplitudeClient.OPT_OUT_KEY), 0L);
looper.runToEndOfTasks();
looper.runToEndOfTasks();
httplooper.runToEndOfTasks();
assertNull(request);
request = sendEvent(amplitude, "test_opt_out", null);
assertNotNull(request);
}
@Test
public void testOffline() {
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
ShadowLooper httplooper = Shadows.shadowOf(amplitude.httpThread.getLooper());
amplitude.setOffline(true);
RecordedRequest request = sendEvent(amplitude, "test_offline", null);
assertNull(request);
// Events should be sent after offline is turned off.
amplitude.setOffline(false);
looper.runToEndOfTasks();
looper.runToEndOfTasks();
httplooper.runToEndOfTasks();
try {
request = server.takeRequest(1, SECONDS);
} catch (InterruptedException e) {
}
assertNotNull(request);
}
@Test
public void testLogEvent() {
RecordedRequest request = sendEvent(amplitude, "test_event", null);
assertNotNull(request);
}
@Test
public void testIdentify() throws JSONException {
long [] timestamps = {1000, 1001};
clock.setTimestamps(timestamps);
shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();
amplitude.identify(new Identify().set("key", "value"));
shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();
JSONObject identifyIntercepted = getLastIdentifyInterceptor();
JSONObject expected = new JSONObject();
expected.put("key", "value");
assertTrue(Utils.compareJSONObjects(identifyIntercepted.getJSONObject("user_properties")
.getJSONObject(Constants.AMP_OP_SET), expected));
// verify db state
DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);
assertNull(dbHelper.getValue(AmplitudeClient.USER_ID_KEY));
assertNull(dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY));
assertNull(dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_ID_KEY));
assertEquals((long) dbHelper.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY), 1L);
assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY), timestamps[0]);
shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();
shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();
RecordedRequest request = runRequest(amplitude);
JSONArray events = getEventsFromRequest(request);
assertEquals(events.length(), 1);
JSONObject identify = events.getJSONObject(0);
assertEquals(identify.getString("event_type"), Constants.IDENTIFY_EVENT);
assertEquals(identify.getLong("event_id"), 1);
assertEquals(identify.getLong("timestamp"), timestamps[0]);
assertEquals(identify.getLong("sequence_number"), 1);
JSONObject userProperties = identify.getJSONObject("user_properties");
assertEquals(userProperties.length(), 1);
assertTrue(userProperties.has(Constants.AMP_OP_SET));
assertTrue(Utils.compareJSONObjects(userProperties.getJSONObject(Constants.AMP_OP_SET), expected));
// verify db state
assertNull(dbHelper.getValue(AmplitudeClient.USER_ID_KEY));
assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY), 1L);
assertNull(dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_ID_KEY));
assertEquals((long) dbHelper.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY), 1L);
assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY), timestamps[0]);
assertEquals((long)dbHelper.getLastIdentifyInterceptorId(), -1L);
}
@Test
public void testNullIdentify() {
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 0);
assertEquals(getUnsentIdentifyCount(), 0);
amplitude.identify(null);
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 0);
assertEquals(getUnsentIdentifyCount(), 0);
}
@Test
public void testLog3Events() throws InterruptedException {
long [] timestamps = {1, 2, 3, 4, 5, 6, 7};
clock.setTimestamps(timestamps);
Robolectric.getForegroundThreadScheduler().advanceTo(1);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
amplitude.logEvent("test_event1");
amplitude.logEvent("test_event2");
amplitude.logEvent("test_event3");
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 3);
assertEquals(getUnsentIdentifyCount(), 0);
JSONArray events = getUnsentEvents(3);
for (int i = 0; i < 3; i++) {
assertEquals(events.optJSONObject(i).optString("event_type"), "test_event" + (i+1));
assertEquals(events.optJSONObject(i).optLong("timestamp"), timestamps[i]);
assertEquals(events.optJSONObject(i).optLong("sequence_number"), i+1);
}
// send response and check that remove events works properly
runRequest(amplitude);
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 0);
assertEquals(getUnsentIdentifyCount(), 0);
}
@Test
public void testLog3Identifys() throws JSONException {
long [] timestamps = {1, 2, 3, 4, 5, 6, 7};
clock.setTimestamps(timestamps);
Robolectric.getForegroundThreadScheduler().advanceTo(1);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
Robolectric.getForegroundThreadScheduler().advanceTo(1);
amplitude.identify(new Identify().set("photo_count", 1));
amplitude.identify(new Identify().add("karma", 2));
amplitude.identify(new Identify().unset("gender"));
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 0);
assertEquals(getUnsentIdentifyCount(), 3);
JSONArray events = getUnsentIdentifys(3);
JSONObject expectedIdentify1 = new JSONObject();
expectedIdentify1.put(Constants.AMP_OP_SET, new JSONObject().put("photo_count", 1));
JSONObject expectedIdentify2 = new JSONObject();
expectedIdentify2.put(Constants.AMP_OP_ADD, new JSONObject().put("karma", 2));
JSONObject expectedIdentify3 = new JSONObject();
expectedIdentify3.put(Constants.AMP_OP_UNSET, new JSONObject().put("gender", "-"));
assertEquals(events.optJSONObject(0).optString("event_type"), Constants.IDENTIFY_EVENT);
assertEquals(events.optJSONObject(0).optLong("timestamp"), timestamps[0]);
assertEquals(events.optJSONObject(0).optLong("sequence_number"), 1);
assertTrue(Utils.compareJSONObjects(
events.optJSONObject(0).optJSONObject("user_properties"), expectedIdentify1
));
assertEquals(events.optJSONObject(1).optString("event_type"), Constants.IDENTIFY_EVENT);
assertEquals(events.optJSONObject(1).optLong("timestamp"), timestamps[1]);
assertEquals(events.optJSONObject(1).optLong("sequence_number"), 2);
assertTrue(Utils.compareJSONObjects(
events.optJSONObject(1).optJSONObject("user_properties"), expectedIdentify2
));
assertEquals(events.optJSONObject(2).optString("event_type"), Constants.IDENTIFY_EVENT);
assertEquals(events.optJSONObject(2).optLong("timestamp"), timestamps[2]);
assertEquals(events.optJSONObject(2).optLong("sequence_number"), 3);
assertTrue(Utils.compareJSONObjects(
events.optJSONObject(2).optJSONObject("user_properties"), expectedIdentify3
));
// send response and check that remove events works properly
runRequest(amplitude);
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 0);
assertEquals(getUnsentIdentifyCount(), 0);
}
@Test
public void testLogEventAndIdentify() throws JSONException {
long [] timestamps = {1, 1, 2};
clock.setTimestamps(timestamps);
Robolectric.getForegroundThreadScheduler().advanceTo(1);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
amplitude.logEvent("test_event");
amplitude.identify(new Identify().add("photo_count", 1));
looper.runToEndOfTasks();
looper.runToEndOfTasks();
// verify some internal counters
assertEquals(getUnsentEventCount(), 1);
assertEquals(amplitude.lastEventId, 1);
assertEquals(getUnsentIdentifyCount(), 1);
assertEquals(amplitude.lastIdentifyId, 1);
JSONArray unsentEvents = getUnsentEvents(1);
assertEquals(unsentEvents.optJSONObject(0).optString("event_type"), "test_event");
assertEquals(unsentEvents.optJSONObject(0).optLong("sequence_number"), 1);
JSONObject expectedIdentify = new JSONObject();
expectedIdentify.put(Constants.AMP_OP_ADD, new JSONObject().put("photo_count", 1));
JSONArray unsentIdentifys = getUnsentIdentifys(1);
assertEquals(unsentIdentifys.optJSONObject(0).optString("event_type"), Constants.IDENTIFY_EVENT);
assertEquals(unsentIdentifys.optJSONObject(0).optLong("sequence_number"), 2);
assertTrue(Utils.compareJSONObjects(
unsentIdentifys.optJSONObject(0).optJSONObject("user_properties"), expectedIdentify
));
// send response and check that remove events works properly
RecordedRequest request = runRequest(amplitude);
JSONArray events = getEventsFromRequest(request);
assertEquals(events.length(), 2);
assertEquals(events.optJSONObject(0).optString("event_type"), "test_event");
assertEquals(events.optJSONObject(1).optString("event_type"), Constants.IDENTIFY_EVENT);
assertTrue(Utils.compareJSONObjects(
events.optJSONObject(1).optJSONObject("user_properties"), expectedIdentify
));
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 0);
assertEquals(getUnsentIdentifyCount(), 0);
}
@Test
public void testMergeEventsAndIdentifys() throws JSONException {
long [] timestamps = {1, 2, 3, 4, 5, 5, 6, 7, 8, 9, 10};
clock.setTimestamps(timestamps);
Robolectric.getForegroundThreadScheduler().advanceTo(1);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
amplitude.logEvent("test_event1");
amplitude.identify(new Identify().add("photo_count", 1));
amplitude.logEvent("test_event2");
amplitude.logEvent("test_event3");
amplitude.logEvent("test_event4");
amplitude.identify(new Identify().set("gender", "male"));
amplitude.identify(new Identify().unset("karma"));
looper.runToEndOfTasks();
looper.runToEndOfTasks();
// verify some internal counters
assertEquals(getUnsentEventCount(), 4);
assertEquals(amplitude.lastEventId, 4);
assertEquals(getUnsentIdentifyCount(), 3);
assertEquals(amplitude.lastIdentifyId, 3);
RecordedRequest request = runRequest(amplitude);
JSONArray events = getEventsFromRequest(request);
assertEquals(events.length(), 7);
JSONObject expectedIdentify1 = new JSONObject();
expectedIdentify1.put(Constants.AMP_OP_ADD, new JSONObject().put("photo_count", 1));
JSONObject expectedIdentify2 = new JSONObject();
expectedIdentify2.put(Constants.AMP_OP_SET, new JSONObject().put("gender", "male"));
JSONObject expectedIdentify3 = new JSONObject();
expectedIdentify3.put(Constants.AMP_OP_UNSET, new JSONObject().put("karma", "-"));
assertEquals(events.getJSONObject(0).getString("event_type"), "test_event1");
assertEquals(events.getJSONObject(0).getLong("event_id"), 1);
assertEquals(events.getJSONObject(0).getLong("timestamp"), timestamps[0]);
assertEquals(events.getJSONObject(0).getLong("sequence_number"), 1);
assertEquals(events.getJSONObject(1).getString("event_type"), Constants.IDENTIFY_EVENT);
assertEquals(events.getJSONObject(1).getLong("event_id"), 1);
assertEquals(events.getJSONObject(1).getLong("timestamp"), timestamps[1]);
assertEquals(events.getJSONObject(1).getLong("sequence_number"), 2);
assertTrue(Utils.compareJSONObjects(
events.getJSONObject(1).getJSONObject("user_properties"), expectedIdentify1
));
assertEquals(events.getJSONObject(2).getString("event_type"), "test_event2");
assertEquals(events.getJSONObject(2).getLong("event_id"), 2);
assertEquals(events.getJSONObject(2).getLong("timestamp"), timestamps[2]);
assertEquals(events.getJSONObject(2).getLong("sequence_number"), 3);
assertEquals(events.getJSONObject(3).getString("event_type"), "test_event3");
assertEquals(events.getJSONObject(3).getLong("event_id"), 3);
assertEquals(events.getJSONObject(3).getLong("timestamp"), timestamps[3]);
assertEquals(events.getJSONObject(3).getLong("sequence_number"), 4);
// sequence number guarantees strict ordering regardless of timestamp
assertEquals(events.getJSONObject(4).getString("event_type"), "test_event4");
assertEquals(events.getJSONObject(4).getLong("event_id"), 4);
assertEquals(events.getJSONObject(4).getLong("timestamp"), timestamps[4]);
assertEquals(events.getJSONObject(4).getLong("sequence_number"), 5);
assertEquals(events.getJSONObject(5).getString("event_type"), Constants.IDENTIFY_EVENT);
assertEquals(events.getJSONObject(5).getLong("event_id"), 2);
assertEquals(events.getJSONObject(5).getLong("timestamp"), timestamps[5]);
assertEquals(events.getJSONObject(5).getLong("sequence_number"), 6);
assertTrue(Utils.compareJSONObjects(
events.getJSONObject(5).getJSONObject("user_properties"), expectedIdentify2
));
assertEquals(events.getJSONObject(6).getString("event_type"), Constants.IDENTIFY_EVENT);
assertEquals(events.getJSONObject(6).getLong("event_id"), 3);
assertEquals(events.getJSONObject(6).getLong("timestamp"), timestamps[6]);
assertEquals(events.getJSONObject(6).getLong("sequence_number"), 7);
assertTrue(Utils.compareJSONObjects(
events.getJSONObject(6).getJSONObject("user_properties"), expectedIdentify3
));
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 0);
assertEquals(getUnsentIdentifyCount(), 0);
// verify db state
DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);
assertNull(dbHelper.getValue(AmplitudeClient.USER_ID_KEY));
assertEquals((long) dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY), 3L);
assertEquals((long) dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_ID_KEY), 4L);
assertEquals((long) dbHelper.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY), 7L);
assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY), timestamps[6]);
}
@Test
public void testMergeEventBackwardsCompatible() throws JSONException {
amplitude.setEventUploadThreshold(4);
// eventst logged before v2.1.0 won't have a sequence number, should get priority
long [] timestamps = {1, 1, 2, 3};
clock.setTimestamps(timestamps);
Robolectric.getForegroundThreadScheduler().advanceTo(1);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
amplitude.uploadingCurrently.set(true);
amplitude.identify(new Identify().add("photo_count", 1));
amplitude.logEvent("test_event1");
amplitude.identify(new Identify().add("photo_count", 2));
looper.runToEndOfTasks();
looper.runToEndOfTasks();
// need to delete sequence number from test event
JSONObject event = getUnsentEvents(1).getJSONObject(0);
assertEquals(event.getLong("event_id"), 1);
event.remove("sequence_number");
event.remove("event_id");
// delete event from db and reinsert modified event
DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);
dbHelper.removeEvent(1);
dbHelper.addEvent(event.toString());
amplitude.uploadingCurrently.set(false);
// log another event to trigger upload
amplitude.logEvent("test_event2");
looper.runToEndOfTasks();
looper.runToEndOfTasks();
// verify some internal counters
assertEquals(getUnsentEventCount(), 2);
assertEquals(amplitude.lastEventId, 3);
assertEquals(getUnsentIdentifyCount(), 2);
assertEquals(amplitude.lastIdentifyId, 2);
JSONObject expectedIdentify1 = new JSONObject();
expectedIdentify1.put(Constants.AMP_OP_ADD, new JSONObject().put("photo_count", 1));
JSONObject expectedIdentify2 = new JSONObject();
expectedIdentify2.put(Constants.AMP_OP_ADD, new JSONObject().put("photo_count", 2));
// send response and check that merging events correctly ordered events
RecordedRequest request = runRequest(amplitude);
JSONArray events = getEventsFromRequest(request);
assertEquals(events.length(), 4);
assertEquals(events.optJSONObject(0).optString("event_type"), "test_event1");
assertFalse(events.optJSONObject(0).has("sequence_number"));
assertEquals(events.optJSONObject(1).optString("event_type"), Constants.IDENTIFY_EVENT);
assertEquals(events.optJSONObject(1).optLong("sequence_number"), 1);
assertTrue(Utils.compareJSONObjects(
events.optJSONObject(1).optJSONObject("user_properties"), expectedIdentify1
));
assertEquals(events.optJSONObject(2).optString("event_type"), Constants.IDENTIFY_EVENT);
assertEquals(events.optJSONObject(2).optLong("sequence_number"), 3);
assertTrue(Utils.compareJSONObjects(
events.optJSONObject(2).optJSONObject("user_properties"), expectedIdentify2
));
assertEquals(events.optJSONObject(3).optString("event_type"), "test_event2");
assertEquals(events.optJSONObject(3).optLong("sequence_number"), 4);
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 0);
assertEquals(getUnsentIdentifyCount(), 0);
}
@Test
public void testRemoveAfterSuccessfulUpload() throws JSONException {
long [] timestamps = new long[Constants.EVENT_UPLOAD_MAX_BATCH_SIZE + 4];
for (int i = 0; i < timestamps.length; i++) timestamps[i] = i;
clock.setTimestamps(timestamps);
Robolectric.getForegroundThreadScheduler().advanceTo(1);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
for (int i = 0; i < Constants.EVENT_UPLOAD_THRESHOLD; i++) {
amplitude.logEvent("test_event" + i);
}
amplitude.identify(new Identify().add("photo_count", 1));
amplitude.identify(new Identify().add("photo_count", 2));
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), Constants.EVENT_UPLOAD_THRESHOLD);
assertEquals(getUnsentIdentifyCount(), 2);
RecordedRequest request = runRequest(amplitude);
JSONArray events = getEventsFromRequest(request);
for (int i = 0; i < events.length(); i++) {
assertEquals(events.optJSONObject(i).optString("event_type"), "test_event" + i);
}
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 0);
assertEquals(getUnsentIdentifyCount(), 2); // should have 2 identifys left
}
@Test
public void testLogEventHasUUID() {
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
looper.runToEndOfTasks();
amplitude.logEvent("test_event");
looper.runToEndOfTasks();
looper.runToEndOfTasks();
JSONObject event = getLastUnsentEvent();
assertTrue(event.has("uuid"));
assertNotNull(event.optString("uuid"));
assertTrue(event.optString("uuid").length() > 0);
}
@Test
public void testLogRevenue() {
Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();
JSONObject event, apiProps;
amplitude.logRevenue(10.99);
Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();
Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();
event = getLastUnsentEvent();
apiProps = event.optJSONObject("api_properties");
assertEquals(Constants.AMP_REVENUE_EVENT, event.optString("event_type"));
assertEquals(Constants.AMP_REVENUE_EVENT, apiProps.optString("special"));
assertEquals(1, apiProps.optInt("quantity"));
assertNull(apiProps.optString("productId", null));
assertEquals(10.99, apiProps.optDouble("price"), .01);
assertNull(apiProps.optString("receipt", null));
assertNull(apiProps.optString("receiptSig", null));
amplitude.logRevenue("ID1", 2, 9.99);
Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();
Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();
event = getLastUnsentEvent();
apiProps = event.optJSONObject("api_properties");;
assertEquals(Constants.AMP_REVENUE_EVENT, event.optString("event_type"));
assertEquals(Constants.AMP_REVENUE_EVENT, apiProps.optString("special"));
assertEquals(2, apiProps.optInt("quantity"));
assertEquals("ID1", apiProps.optString("productId"));
assertEquals(9.99, apiProps.optDouble("price"), .01);
assertNull(apiProps.optString("receipt", null));
assertNull(apiProps.optString("receiptSig", null));
amplitude.logRevenue("ID2", 3, 8.99, "RECEIPT", "SIG");
Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();
Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();
event = getLastUnsentEvent();
apiProps = event.optJSONObject("api_properties");
assertEquals(Constants.AMP_REVENUE_EVENT, event.optString("event_type"));
assertEquals(Constants.AMP_REVENUE_EVENT, apiProps.optString("special"));
assertEquals(3, apiProps.optInt("quantity"));
assertEquals("ID2", apiProps.optString("productId"));
assertEquals(8.99, apiProps.optDouble("price"), .01);
assertEquals("RECEIPT", apiProps.optString("receipt"));
assertEquals("SIG", apiProps.optString("receiptSig"));
assertNotNull(runRequest(amplitude));
}
@Test
public void testLogRevenueV2() throws JSONException {
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
// ignore invalid revenue objects
amplitude.logRevenueV2(null);
looper.runToEndOfTasks();
amplitude.logRevenueV2(new Revenue());
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 0);
// log valid revenue object
double price = 10.99;
int quantity = 15;
String productId = "testProductId";
String receipt = "testReceipt";
String receiptSig = "testReceiptSig";
String revenueType = "testRevenueType";
JSONObject props = new JSONObject().put("city", "Boston");
Revenue revenue = new Revenue().setProductId(productId).setPrice(price);
revenue.setQuantity(quantity).setReceipt(receipt, receiptSig);
revenue.setRevenueType(revenueType).setRevenueProperties(props);
amplitude.logRevenueV2(revenue);
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 1);
JSONObject event = getLastUnsentEvent();
assertEquals(event.optString("event_type"), "revenue_amount");
JSONObject obj = event.optJSONObject("event_properties");
assertEquals(obj.optDouble("$price"), price, 0);
assertEquals(obj.optInt("$quantity"), 15);
assertEquals(obj.optString("$productId"), productId);
assertEquals(obj.optString("$receipt"), receipt);
assertEquals(obj.optString("$receiptSig"), receiptSig);
assertEquals(obj.optString("$revenueType"), revenueType);
assertEquals(obj.optString("city"), "Boston");
// user properties should be empty
assertTrue(Utils.compareJSONObjects(
event.optJSONObject("user_properties"), new JSONObject()
));
// api properties should not have any revenue info
JSONObject apiProps = event.optJSONObject("api_properties");
assertTrue(apiProps.length() > 0);
assertFalse(apiProps.has("special"));
assertFalse(apiProps.has("productId"));
assertFalse(apiProps.has("quantity"));
assertFalse(apiProps.has("price"));
assertFalse(apiProps.has("receipt"));
assertFalse(apiProps.has("receiptSig"));
}
@Test
public void testLogEventSync() {
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
amplitude.logEventSync("test_event_sync", null);
// Event should be in the database synchronously.
JSONObject event = getLastEvent();
assertEquals("test_event_sync", event.optString("event_type"));
looper.runToEndOfTasks();
server.enqueue(new MockResponse().setBody("success"));
ShadowLooper httplooper = Shadows.shadowOf(amplitude.httpThread.getLooper());
httplooper.runToEndOfTasks();
try {
assertNotNull(server.takeRequest(1, SECONDS));
} catch (InterruptedException e) {
fail(e.toString());
}
}
/**
* Test for not excepting on empty event properties.
* See https://github.com/amplitude/Amplitude-Android/issues/35
*/
@Test
public void testEmptyEventProps() {
RecordedRequest request = sendEvent(amplitude, "test_event", new JSONObject());
assertNotNull(request);
}
/**
* Test that resend failed events only occurs every 30 events.
*/
@Test
public void testSaveEventLogic() {
amplitude.trackSessionEvents(true);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 0);
for (int i = 0; i < Constants.EVENT_UPLOAD_THRESHOLD; i++) {
amplitude.logEvent("test");
}
looper.runToEndOfTasks();
// unsent events will be threshold (+1 for start session)
assertEquals(getUnsentEventCount(), Constants.EVENT_UPLOAD_THRESHOLD + 1);
server.enqueue(new MockResponse().setResponseCode(400).setBody("invalid_api_key"));
server.enqueue(new MockResponse().setResponseCode(400).setBody("bad_checksum"));
ShadowLooper httpLooper = Shadows.shadowOf(amplitude.httpThread.getLooper());
httpLooper.runToEndOfTasks();
// no events sent, queue should be same size
assertEquals(getUnsentEventCount(), Constants.EVENT_UPLOAD_THRESHOLD + 1);
for (int i = 0; i < Constants.EVENT_UPLOAD_THRESHOLD; i++) {
amplitude.logEvent("test");
}
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), Constants.EVENT_UPLOAD_THRESHOLD * 2 + 1);
httpLooper.runToEndOfTasks();
// sent 61 events, should have only made 2 requests
assertEquals(server.getRequestCount(), 2);
}
@Test
public void testRequestTooLargeBackoffLogic() {
amplitude.trackSessionEvents(true);
Robolectric.getForegroundThreadScheduler().advanceTo(1);
// verify event queue empty
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 0);
// 413 error force backoff with 2 events --> new upload limit will be 1
amplitude.logEvent("test");
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 2); // 2 events: start session + test
server.enqueue(new MockResponse().setResponseCode(413));
ShadowLooper httpLooper = Shadows.shadowOf(amplitude.httpThread.getLooper());
httpLooper.runToEndOfTasks();
// 413 error with upload limit 1 will remove the top (start session) event
amplitude.logEvent("test");
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 3);
server.enqueue(new MockResponse().setResponseCode(413));
httpLooper.runToEndOfTasks();
// verify only start session event removed
assertEquals(getUnsentEventCount(), 2);
JSONArray events = getUnsentEvents(2);
assertEquals(events.optJSONObject(0).optString("event_type"), "test");
assertEquals(events.optJSONObject(1).optString("event_type"), "test");
// upload limit persists until event count below threshold
server.enqueue(new MockResponse().setBody("success"));
looper.runToEndOfTasks(); // retry uploading after removing large event
httpLooper.runToEndOfTasks(); // send success --> 1 event sent
looper.runToEndOfTasks(); // event count below threshold --> disable backoff
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 1);
// verify backoff disabled - queue 2 more events, see that all get uploaded
amplitude.logEvent("test");
amplitude.logEvent("test");
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 3);
server.enqueue(new MockResponse().setBody("success"));
httpLooper.runToEndOfTasks();
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 0);
}
@Test
public void testUploadRemainingEvents() {
long [] timestamps = {1, 2, 3, 4, 5, 6, 7};
clock.setTimestamps(timestamps);
Robolectric.getForegroundThreadScheduler().advanceTo(1);
DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 0);
assertEquals(getUnsentIdentifyCount(), 0);
amplitude.setEventUploadMaxBatchSize(2);
amplitude.setEventUploadThreshold(2);
amplitude.uploadingCurrently.set(true); // block uploading until we queue up enough events
for (int i = 0; i < 6; i++) {
amplitude.logEvent(String.format("test%d", i));
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(dbHelper.getTotalEventCount(), i+1);
}
amplitude.uploadingCurrently.set(false);
// allow event uploads
// 7 events in queue, should upload 2, and then 2, and then 2, and then 2
amplitude.logEvent("test7");
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(dbHelper.getEventCount(), 7);
assertEquals(dbHelper.getIdentifyCount(), 0);
assertEquals(dbHelper.getTotalEventCount(), 7);
// server response
server.enqueue(new MockResponse().setBody("success"));
ShadowLooper httpLooper = Shadows.shadowOf(amplitude.httpThread.getLooper());
httpLooper.runToEndOfTasks();
// when receive success response, continue uploading
looper.runToEndOfTasks();
looper.runToEndOfTasks(); // remove uploaded events
assertEquals(dbHelper.getEventCount(), 5);
assertEquals(dbHelper.getIdentifyCount(), 0);
assertEquals(dbHelper.getTotalEventCount(), 5);
// 2nd server response
server.enqueue(new MockResponse().setBody("success"));
httpLooper.runToEndOfTasks();
looper.runToEndOfTasks(); // remove uploaded events
assertEquals(dbHelper.getEventCount(), 3);
assertEquals(dbHelper.getIdentifyCount(), 0);
assertEquals(dbHelper.getTotalEventCount(), 3);
// 3rd server response
server.enqueue(new MockResponse().setBody("success"));
httpLooper.runToEndOfTasks();
looper.runToEndOfTasks(); // remove uploaded events
looper.runToEndOfTasks();
assertEquals(dbHelper.getEventCount(), 1);
assertEquals(dbHelper.getIdentifyCount(), 0);
assertEquals(dbHelper.getTotalEventCount(), 1);
}
@Test
public void testBackoffRemoveIdentify() {
long [] timestamps = {1, 1, 2, 3, 4, 5};
clock.setTimestamps(timestamps);
Robolectric.getForegroundThreadScheduler().advanceTo(1);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 0);
assertEquals(getUnsentIdentifyCount(), 0);
// 413 error force backoff with 2 events --> new upload limit will be 1
amplitude.identify(new Identify().add("photo_count", 1));
amplitude.logEvent("test1");
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(getUnsentIdentifyCount(), 1);
assertEquals(getUnsentEventCount(), 1);
server.enqueue(new MockResponse().setResponseCode(413));
ShadowLooper httpLooper = Shadows.shadowOf(amplitude.httpThread.getLooper());
httpLooper.runToEndOfTasks();
// 413 error with upload limit 1 will remove the top identify
amplitude.logEvent("test2");
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 2);
assertEquals(getUnsentIdentifyCount(), 1);
server.enqueue(new MockResponse().setResponseCode(413));
httpLooper.runToEndOfTasks();
// verify only identify removed
assertEquals(getUnsentEventCount(), 2);
assertEquals(getUnsentIdentifyCount(), 0);
JSONArray events = getUnsentEvents(2);
assertEquals(events.optJSONObject(0).optString("event_type"), "test1");
assertEquals(events.optJSONObject(1).optString("event_type"), "test2");
}
@Test
public void testLimitTrackingEnabled() {
amplitude.logEvent("test");
Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();
JSONObject apiProperties = getLastUnsentEvent().optJSONObject("api_properties");
assertTrue(apiProperties.has("limit_ad_tracking"));
assertFalse(apiProperties.optBoolean("limit_ad_tracking"));
assertFalse(apiProperties.has("androidADID"));
}
@Test
public void testTruncateString() {
String longString = generateStringWithLength(Constants.MAX_STRING_LENGTH * 2, 'c');
assertEquals(longString.length(), Constants.MAX_STRING_LENGTH * 2);
String truncatedString = amplitude.truncate(longString);
assertEquals(truncatedString.length(), Constants.MAX_STRING_LENGTH);
assertEquals(truncatedString, generateStringWithLength(Constants.MAX_STRING_LENGTH, 'c'));
}
@Test
public void testTruncateJSONObject() throws JSONException {
String longString = generateStringWithLength(Constants.MAX_STRING_LENGTH * 2, 'c');
String truncString = generateStringWithLength(Constants.MAX_STRING_LENGTH, 'c');
JSONObject object = new JSONObject();
object.put("int value", 10);
object.put("bool value", false);
object.put("long string", longString);
object.put("array", new JSONArray().put(longString).put(10));
object.put("jsonobject", new JSONObject().put("long string", longString));
object.put(Constants.AMP_REVENUE_RECEIPT, longString);
object.put(Constants.AMP_REVENUE_RECEIPT_SIG, longString);
object = amplitude.truncate(object);
assertEquals(object.optInt("int value"), 10);
assertEquals(object.optBoolean("bool value"), false);
assertEquals(object.optString("long string"), truncString);
assertEquals(object.optJSONArray("array").length(), 2);
assertEquals(object.optJSONArray("array").getString(0), truncString);
assertEquals(object.optJSONArray("array").getInt(1), 10);
assertEquals(object.optJSONObject("jsonobject").length(), 1);
assertEquals(object.optJSONObject("jsonobject").optString("long string"), truncString);
// receipt and receipt sig should not be truncated
assertEquals(object.optString(Constants.AMP_REVENUE_RECEIPT), longString);
assertEquals(object.optString(Constants.AMP_REVENUE_RECEIPT_SIG), longString);
}
@Test
public void testTruncateNullJSONObject() throws JSONException {
assertTrue(Utils.compareJSONObjects(
amplitude.truncate((JSONObject) null), new JSONObject()
));
assertEquals(amplitude.truncate((JSONArray) null).length(), 0);
}
@Test
public void testTruncateEventAndIdentify() throws JSONException {
String longString = generateStringWithLength(Constants.MAX_STRING_LENGTH * 2, 'c');
String truncString = generateStringWithLength(Constants.MAX_STRING_LENGTH, 'c');
long [] timestamps = {1, 1, 2, 3};
clock.setTimestamps(timestamps);
Robolectric.getForegroundThreadScheduler().advanceTo(1);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
amplitude.identify(new Identify().set("long_string", longString));
amplitude.logEvent("test", new JSONObject().put("long_string", longString));
looper.runToEndOfTasks();
looper.runToEndOfTasks();
RecordedRequest request = runRequest(amplitude);
JSONArray events = getEventsFromRequest(request);
assertEquals(events.optJSONObject(0).optString("event_type"), Constants.IDENTIFY_EVENT);
assertTrue(Utils.compareJSONObjects(
events.optJSONObject(0).optJSONObject("user_properties").optJSONObject(Constants.AMP_OP_SET),
new JSONObject().put("long_string", truncString)
));
assertEquals(events.optJSONObject(1).optString("event_type"), "test");
assertTrue(Utils.compareJSONObjects(
events.optJSONObject(1).optJSONObject("event_properties"),
new JSONObject().put("long_string", truncString)
));
}
@Test
public void testAutoIncrementSequenceNumber() {
DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);
int limit = 10;
for (int i = 0; i < limit; i++) {
assertEquals(amplitude.getNextSequenceNumber(), i+1);
assertEquals(dbHelper.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY), Long.valueOf(i+1));
}
}
@Test
public void testSetOffline() throws JSONException {
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
amplitude.setOffline(true);
amplitude.logEvent("test1");
amplitude.logEvent("test2");
amplitude.identify(new Identify().unset("key1"));
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 2);
assertEquals(getUnsentIdentifyCount(), 1);
amplitude.setOffline(false);
looper.runToEndOfTasks();
RecordedRequest request = runRequest(amplitude);
JSONArray events = getEventsFromRequest(request);
looper.runToEndOfTasks();
assertEquals(events.length(), 3);
assertEquals(getUnsentEventCount(), 0);
assertEquals(getUnsentIdentifyCount(), 0);
}
@Test
public void testSetOfflineTruncate() throws JSONException {
long [] timestamps = {1, 2, 3, 4, 5, 6, 7, 8, 9};
clock.setTimestamps(timestamps);
Robolectric.getForegroundThreadScheduler().advanceTo(1);
DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);
int eventMaxCount = 3;
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
amplitude.setEventMaxCount(eventMaxCount).setOffline(true);
amplitude.logEvent("test1");
amplitude.logEvent("test2");
amplitude.logEvent("test3");
amplitude.identify(new Identify().unset("key1"));
amplitude.identify(new Identify().unset("key2"));
amplitude.identify(new Identify().unset("key3"));
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), eventMaxCount);
assertEquals(getUnsentIdentifyCount(), eventMaxCount);
amplitude.logEvent("test4");
amplitude.identify(new Identify().unset("key4"));
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), eventMaxCount);
assertEquals(getUnsentIdentifyCount(), eventMaxCount);
List events = dbHelper.getEvents(-1, -1);
assertEquals(events.size(), eventMaxCount);
assertEquals(events.get(0).optString("event_type"), "test2");
assertEquals(events.get(1).optString("event_type"), "test3");
assertEquals(events.get(2).optString("event_type"), "test4");
List identifys = dbHelper.getIdentifys(-1, -1);
assertEquals(identifys.size(), eventMaxCount);
assertEquals(identifys.get(0).optJSONObject("user_properties").optJSONObject("$unset").optString("key2"), "-");
assertEquals(identifys.get(1).optJSONObject("user_properties").optJSONObject("$unset").optString("key3"), "-");
assertEquals(identifys.get(2).optJSONObject("user_properties").optJSONObject("$unset").optString("key4"), "-");
}
@Test
public void testTruncateEventsQueues() {
DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);
int eventMaxCount = 50;
assertTrue(eventMaxCount > Constants.EVENT_REMOVE_BATCH_SIZE);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
amplitude.setEventMaxCount(eventMaxCount).setOffline(true);
for (int i = 0; i < eventMaxCount; i++) {
amplitude.logEvent("test");
}
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), eventMaxCount);
amplitude.logEvent("test");
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), eventMaxCount - (eventMaxCount/10) + 1);
}
@Test
public void testTruncateEventsQueuesWithOneEvent() {
DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);
int eventMaxCount = 1;
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
amplitude.setEventMaxCount(eventMaxCount).setOffline(true);
amplitude.logEvent("test1");
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), eventMaxCount);
amplitude.logEvent("test2");
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), eventMaxCount);
JSONObject event = getLastUnsentEvent();
assertEquals(event.optString("event_type"), "test2");
}
@Test
public void testClearUserProperties() throws JSONException {
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
amplitude.clearUserProperties();
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 0);
assertEquals(getUnsentIdentifyCount(), 1);
JSONObject event = getLastUnsentIdentify();
assertEquals(Constants.IDENTIFY_EVENT, event.optString("event_type"));
assertTrue(Utils.compareJSONObjects(
event.optJSONObject("event_properties"), new JSONObject()
));
JSONObject userPropertiesOperations = event.optJSONObject("user_properties");
assertEquals(userPropertiesOperations.length(), 1);
assertTrue(userPropertiesOperations.has(Constants.AMP_OP_CLEAR_ALL));
assertEquals(
"-", userPropertiesOperations.optString(Constants.AMP_OP_CLEAR_ALL)
);
}
@Test
public void testSetGroup() throws JSONException {
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
amplitude.setGroup("orgId", new JSONArray().put(10).put(15));
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 0);
assertEquals(getUnsentIdentifyCount(), 1);
JSONObject event = getLastUnsentIdentify();
assertEquals(Constants.IDENTIFY_EVENT, event.optString("event_type"));
assertTrue(Utils.compareJSONObjects(
event.optJSONObject("event_properties"), new JSONObject()
));
JSONObject userPropertiesOperations = event.optJSONObject("user_properties");
assertEquals(userPropertiesOperations.length(), 1);
assertTrue(userPropertiesOperations.has(Constants.AMP_OP_SET));
JSONObject groups = event.optJSONObject("groups");
assertEquals(groups.length(), 1);
assertEquals(groups.optJSONArray("orgId"), new JSONArray().put(10).put(15));
JSONObject setOperations = userPropertiesOperations.optJSONObject(Constants.AMP_OP_SET);
assertEquals(setOperations.length(), 1);
assertEquals(setOperations.optJSONArray("orgId"), new JSONArray().put(10).put(15));
}
@Test
public void testLogEventWithGroups() throws JSONException {
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
JSONObject groups = new JSONObject().put("orgId", 10).put("sport", "tennis");
amplitude.logEvent("test", null, groups);
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 1);
assertEquals(getUnsentIdentifyCount(), 0);
JSONObject event = getLastUnsentEvent();
assertEquals(event.optString("event_type"), "test");
assertTrue(Utils.compareJSONObjects(
event.optJSONObject("event_properties"), new JSONObject()
));
assertTrue(Utils.compareJSONObjects(
event.optJSONObject("user_properties"), new JSONObject()
));
JSONObject eventGroups = event.optJSONObject("groups");
assertEquals(eventGroups.length(), 2);
assertEquals(eventGroups.optInt("orgId"), 10);
assertEquals(eventGroups.optString("sport"), "tennis");
}
@Test
public void testMergeEventsArrayIndexOutOfBounds() throws JSONException {
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
amplitude.setOffline(true);
amplitude.logEvent("testEvent1");
looper.runToEndOfTasks();
looper.runToEndOfTasks();
// force failure case
amplitude.setLastEventId(0);
amplitude.setOffline(false);
looper.runToEndOfTasks();
// make sure next upload succeeds
amplitude.setLastEventId(1);
amplitude.logEvent("testEvent2");
looper.runToEndOfTasks();
looper.runToEndOfTasks();
RecordedRequest request = runRequest(amplitude);
JSONArray events = getEventsFromRequest(request);
assertEquals(events.length(), 2);
assertEquals(events.getJSONObject(0).optString("event_type"), "testEvent1");
assertEquals(events.getJSONObject(0).optLong("event_id"), 1);
assertEquals(events.getJSONObject(1).optString("event_type"), "testEvent2");
assertEquals(events.getJSONObject(1).optLong("event_id"), 2);
}
@Test
public void testCursorWindowAllocationException() {
Robolectric.getForegroundThreadScheduler().advanceTo(1);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
// log an event successfully
amplitude.logEvent("testEvent1");
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 1);
assertEquals(getUnsentIdentifyCount(), 0);
// mock out database helper to force CursorWindowAllocationExceptions
DatabaseHelper.instances.put(Constants.DEFAULT_INSTANCE, new MockDatabaseHelper(context));
// force an upload and verify no request sent
// make sure we catch it during sending of events and defer sending
RecordedRequest request = runRequest(amplitude);
assertNull(request);
assertEquals(getUnsentEventCount(), 1);
assertEquals(getUnsentIdentifyCount(), 0);
// make sure we catch it during initialization and treat as uninitialized
amplitude.initialized = false;
amplitude.initialize(context, apiKey);
looper.runToEndOfTasks();
assertNull(amplitude.apiKey);
// since event meta data is loaded during initialize, in theory we should
// be able to log an event even if we can't query from it
amplitude.context = context;
amplitude.apiKey = apiKey;
Identify identify = new Identify().set("car", "blue");
amplitude.identify(identify);
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 1);
assertEquals(getUnsentIdentifyCount(), 1);
}
@Test
public void testBlockTooManyEventUserProperties() throws JSONException {
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
JSONObject eventProperties = new JSONObject();
JSONObject userProperties = new JSONObject();
Identify identify = new Identify();
for (int i = 0; i < Constants.MAX_PROPERTY_KEYS + 1; i++) {
eventProperties.put(String.valueOf(i), i);
userProperties.put(String.valueOf(i*2), i*2);
identify.setOnce(String.valueOf(i), i);
}
// verify user properties is filtered out
amplitude.setUserProperties(userProperties);
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(getUnsentIdentifyCount(), 0);
// verify scrubbed from events
amplitude.logEvent("test event", eventProperties);
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 1);
JSONObject event = getLastUnsentEvent();
assertEquals(event.optString("event_type"), "test event");
assertTrue(Utils.compareJSONObjects(
event.optJSONObject("event_properties"), new JSONObject()
));
// verify scrubbed from identifys - but leaves an empty JSONObject
amplitude.identify(identify);
looper.runToEndOfTasks();
assertEquals(getUnsentIdentifyCount(), 1);
JSONObject identifyEvent = getLastUnsentIdentify();
assertEquals(identifyEvent.optString("event_type"), "$identify");
assertTrue(Utils.compareJSONObjects(
identifyEvent.optJSONObject("user_properties"),
new JSONObject().put("$setOnce", new JSONObject())
));
}
@Test
public void testLogEventWithTimestamp() throws JSONException {
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
amplitude.logEvent("test", null, null, 1000, false);
looper.runToEndOfTasks();
JSONObject event = getLastUnsentEvent();
assertEquals(event.optLong("timestamp"), 1000);
amplitude.logEventSync("test", null, null, 2000, false);
looper.runToEndOfTasks();
event = getLastUnsentEvent();
assertEquals(event.optLong("timestamp"), 2000);
}
@Test
public void testRegenerateDeviceId() {
DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);
String oldDeviceId = amplitude.getDeviceId();
assertEquals(oldDeviceId, dbHelper.getValue("device_id"));
amplitude.regenerateDeviceId();
Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();
String newDeviceId = amplitude.getDeviceId();
assertNotEquals(oldDeviceId, newDeviceId);
assertEquals(newDeviceId, dbHelper.getValue("device_id"));
assertTrue(newDeviceId.endsWith("R"));
}
@Test
public void testSendNullEvents() throws JSONException {
DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
dbHelper.addEvent(null);
amplitude.setLastEventId(1);
amplitude.getNextSequenceNumber();
assertEquals(getUnsentEventCount(), 1);
amplitude.logEvent("test event");
looper.runToEndOfTasks();
amplitude.updateServer();
RecordedRequest request = runRequest(amplitude);
JSONArray events = getEventsFromRequest(request);
assertEquals(events.length(), 1);
assertEquals(events.optJSONObject(0).optString("event_type"), "test event");
}
@Test
@PrepareForTest(OkHttpClient.class)
public void testHandleUploadExceptions() throws Exception {
ShadowLooper logLooper = Shadows.shadowOf(amplitude.logThread.getLooper());
ShadowLooper httpLooper = Shadows.shadowOf(amplitude.httpThread.getLooper());
IOException error = new IOException("test IO Exception");
// mock out client
Call.Factory oldClient = amplitude.callFactory;
OkHttpClient mockClient = PowerMockito.mock(OkHttpClient.class);
// need to have mock client return mock call that throws exception
Call mockCall = PowerMockito.mock(Call.class);
PowerMockito.when(mockCall.execute()).thenThrow(error);
PowerMockito.when(mockClient.newCall(Matchers.any(Request.class))).thenReturn(mockCall);
// attach mock client to amplitude
amplitude.callFactory = mockClient;
amplitude.logEvent("test event");
logLooper.runToEndOfTasks();
logLooper.runToEndOfTasks();
httpLooper.runToEndOfTasks();
assertEquals(amplitude.lastError, error);
// restore old client
amplitude.callFactory = oldClient;
}
@Test
public void testDefaultPlatform() throws InterruptedException {
long [] timestamps = {1, 2, 3, 4, 5, 6, 7};
clock.setTimestamps(timestamps);
Robolectric.getForegroundThreadScheduler().advanceTo(1);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
assertEquals(amplitude.platform, Constants.PLATFORM);
amplitude.logEvent("test_event1");
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 1);
assertEquals(getUnsentIdentifyCount(), 0);
JSONArray events = getUnsentEvents(1);
for (int i = 0; i < 1; i++) {
assertEquals(events.optJSONObject(i).optString("event_type"), "test_event" + (i+1));
assertEquals(events.optJSONObject(i).optLong("timestamp"), timestamps[i]);
assertEquals(events.optJSONObject(i).optString("platform"), Constants.PLATFORM);
}
runRequest(amplitude);
}
@Test
public void testOverridePlatform() throws InterruptedException {
long [] timestamps = {1, 2, 3, 4, 5, 6, 7};
clock.setTimestamps(timestamps);
Robolectric.getForegroundThreadScheduler().advanceTo(1);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
String customPlatform = "test_custom_platform";
// force re-initialize to override platform
amplitude.initialized = false;
amplitude.initialize(context, apiKey, null, customPlatform, false);
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(amplitude.platform, customPlatform);
amplitude.logEvent("test_event1");
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 1);
assertEquals(getUnsentIdentifyCount(), 0);
JSONArray events = getUnsentEvents(1);
for (int i = 0; i < 1; i++) {
assertEquals(events.optJSONObject(i).optString("event_type"), "test_event" + (i+1));
assertEquals(events.optJSONObject(i).optLong("timestamp"), timestamps[i]);
assertEquals(events.optJSONObject(i).optString("platform"), customPlatform);
}
runRequest(amplitude);
}
@Test
public void testSetTrackingConfig() throws JSONException {
long [] timestamps = {1, 2, 3, 4, 5, 6, 7};
clock.setTimestamps(timestamps);
Robolectric.getForegroundThreadScheduler().advanceTo(1);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
TrackingOptions options = new TrackingOptions().disableCity().disableCountry().disableIpAddress().disableLanguage().disableLatLng();
amplitude.setTrackingOptions(options);
assertEquals(amplitude.appliedTrackingOptions, options);
assertTrue(Utils.compareJSONObjects(amplitude.apiPropertiesTrackingOptions, options.getApiPropertiesTrackingOptions()));
assertFalse(amplitude.appliedTrackingOptions.shouldTrackCity());
assertFalse(amplitude.appliedTrackingOptions.shouldTrackCountry());
assertFalse(amplitude.appliedTrackingOptions.shouldTrackIpAddress());
assertFalse(amplitude.appliedTrackingOptions.shouldTrackLanguage());
assertFalse(amplitude.appliedTrackingOptions.shouldTrackLatLng());
amplitude.logEvent("test event");
looper.runToEndOfTasks();
looper.runToEndOfTasks();
JSONArray events = getUnsentEvents(1);
assertEquals(events.length(), 1);
JSONObject event = events.getJSONObject(0);
// verify we do have platform and carrier since those were not filtered out
assertTrue(event.has("carrier"));
assertTrue(event.has("platform"));
// verify we do not have any of the filtered out fields
assertFalse(event.has("city"));
assertFalse(event.has("country"));
assertFalse(event.has("language"));
// verify api properties contains tracking options for location filtering
JSONObject apiProperties = event.getJSONObject("api_properties");
assertFalse(apiProperties.getBoolean("limit_ad_tracking"));
assertFalse(apiProperties.getBoolean("gps_enabled"));
assertTrue(apiProperties.has("tracking_options"));
JSONObject trackingOptions = apiProperties.getJSONObject("tracking_options");
assertEquals(trackingOptions.length(), 4);
assertFalse(trackingOptions.getBoolean("city"));
assertFalse(trackingOptions.getBoolean("country"));
assertFalse(trackingOptions.getBoolean("ip_address"));
assertFalse(trackingOptions.getBoolean("lat_lng"));
}
@Test
public void testEnableCoppaControl() throws JSONException {
long [] timestamps = {1, 2, 3, 4, 5, 6, 7};
clock.setTimestamps(timestamps);
Robolectric.getForegroundThreadScheduler().advanceTo(1);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
amplitude.disableCoppaControl(); // this shouldn't do anything
TrackingOptions options = new TrackingOptions();
assertEquals(amplitude.inputTrackingOptions, options);
assertEquals(amplitude.appliedTrackingOptions, options);
assertTrue(Utils.compareJSONObjects(amplitude.apiPropertiesTrackingOptions, options.getApiPropertiesTrackingOptions()));
// haven't merged in the privacy guard settings yet
assertTrue(amplitude.appliedTrackingOptions.shouldTrackLanguage());
assertTrue(amplitude.appliedTrackingOptions.shouldTrackCarrier());
assertTrue(amplitude.appliedTrackingOptions.shouldTrackIpAddress());
assertTrue(amplitude.appliedTrackingOptions.shouldTrackAdid());
assertTrue(amplitude.appliedTrackingOptions.shouldTrackCity());
assertTrue(amplitude.appliedTrackingOptions.shouldTrackLatLng());
amplitude.logEvent("test event");
looper.runToEndOfTasks();
looper.runToEndOfTasks();
JSONArray events = getUnsentEvents(1);
assertEquals(events.length(), 1);
JSONObject event = events.getJSONObject(0);
// verify we do have platform and carrier since those were not filtered out
assertTrue(event.has("country"));
assertTrue(event.has("platform"));
assertTrue(event.has("carrier"));
assertTrue(event.has("language"));
// verify api properties contains tracking options for location filtering
JSONObject apiProperties = event.getJSONObject("api_properties");
assertFalse(apiProperties.getBoolean("limit_ad_tracking"));
assertFalse(apiProperties.getBoolean("gps_enabled"));
assertFalse(apiProperties.has("tracking_options"));
// test enabling privacy guard
amplitude.enableCoppaControl();
assertEquals(amplitude.inputTrackingOptions, options);
assertNotEquals(amplitude.appliedTrackingOptions, options);
// make sure we merge in the privacy guard options
assertFalse(amplitude.appliedTrackingOptions.shouldTrackIpAddress());
assertFalse(amplitude.appliedTrackingOptions.shouldTrackAdid());
assertFalse(amplitude.appliedTrackingOptions.shouldTrackCity());
assertFalse(amplitude.appliedTrackingOptions.shouldTrackLatLng());
amplitude.logEvent("test event 1");
looper.runToEndOfTasks();
looper.runToEndOfTasks();
events = getUnsentEvents(2);
assertEquals(events.length(), 2);
event = events.getJSONObject(1);
// verify we do have platform and carrier since those were not filtered out
assertTrue(event.has("country"));
assertTrue(event.has("platform"));
assertTrue(event.has("carrier"));
assertTrue(event.has("language"));
// verify api properties contains tracking options for location filtering
apiProperties = event.getJSONObject("api_properties");
assertFalse(apiProperties.getBoolean("limit_ad_tracking"));
assertFalse(apiProperties.getBoolean("gps_enabled"));
assertTrue(apiProperties.has("tracking_options"));
JSONObject trackingOptions = apiProperties.getJSONObject("tracking_options");
assertEquals(trackingOptions.length(), 3);
assertFalse(trackingOptions.getBoolean("ip_address"));
assertFalse(trackingOptions.getBoolean("city"));
assertFalse(trackingOptions.getBoolean("lat_lng"));
// test disabling privacy guard
amplitude.disableCoppaControl();
assertEquals(amplitude.inputTrackingOptions, options);
assertEquals(amplitude.appliedTrackingOptions, options);
assertTrue(Utils.compareJSONObjects(amplitude.apiPropertiesTrackingOptions, options.getApiPropertiesTrackingOptions()));
assertTrue(amplitude.appliedTrackingOptions.shouldTrackLanguage());
assertTrue(amplitude.appliedTrackingOptions.shouldTrackCarrier());
assertTrue(amplitude.appliedTrackingOptions.shouldTrackIpAddress());
assertTrue(amplitude.appliedTrackingOptions.shouldTrackAdid());
assertTrue(amplitude.appliedTrackingOptions.shouldTrackCity());
assertTrue(amplitude.appliedTrackingOptions.shouldTrackLatLng());
amplitude.logEvent("test event 2");
looper.runToEndOfTasks();
looper.runToEndOfTasks();
events = getUnsentEvents(3);
assertEquals(events.length(), 3);
event = events.getJSONObject(2);
// verify we do have platform and carrier since those were not filtered out
assertTrue(event.has("country"));
assertTrue(event.has("platform"));
assertTrue(event.has("carrier"));
assertTrue(event.has("language"));
// verify api properties contains tracking options for location filtering
apiProperties = event.getJSONObject("api_properties");
assertFalse(apiProperties.getBoolean("limit_ad_tracking"));
assertFalse(apiProperties.getBoolean("gps_enabled"));
assertFalse(apiProperties.has("tracking_options"));
}
@Test
public void testEnableCoppaControlWithOptions() throws JSONException {
long [] timestamps = {1, 2, 3, 4, 5, 6, 7};
clock.setTimestamps(timestamps);
Robolectric.getForegroundThreadScheduler().advanceTo(1);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
TrackingOptions options = new TrackingOptions().disableLanguage().disableCarrier().disableIpAddress();
amplitude.setTrackingOptions(options);
assertEquals(amplitude.inputTrackingOptions, options);
assertEquals(amplitude.appliedTrackingOptions, options);
assertTrue(Utils.compareJSONObjects(amplitude.apiPropertiesTrackingOptions, options.getApiPropertiesTrackingOptions()));
assertFalse(amplitude.appliedTrackingOptions.shouldTrackLanguage());
assertFalse(amplitude.appliedTrackingOptions.shouldTrackCarrier());
assertFalse(amplitude.appliedTrackingOptions.shouldTrackIpAddress());
// haven't merged in the privacy guard settings yet
assertTrue(amplitude.appliedTrackingOptions.shouldTrackAdid());
assertTrue(amplitude.appliedTrackingOptions.shouldTrackCity());
assertTrue(amplitude.appliedTrackingOptions.shouldTrackLatLng());
amplitude.logEvent("test event");
looper.runToEndOfTasks();
looper.runToEndOfTasks();
JSONArray events = getUnsentEvents(1);
assertEquals(events.length(), 1);
JSONObject event = events.getJSONObject(0);
// verify we do have platform and carrier since those were not filtered out
assertTrue(event.has("country"));
assertTrue(event.has("platform"));
// verify we do not have any of the filtered out fields
assertFalse(event.has("carrier"));
assertFalse(event.has("language"));
// verify api properties contains tracking options for location filtering
JSONObject apiProperties = event.getJSONObject("api_properties");
assertFalse(apiProperties.getBoolean("limit_ad_tracking"));
assertFalse(apiProperties.getBoolean("gps_enabled"));
assertTrue(apiProperties.has("tracking_options"));
JSONObject trackingOptions = apiProperties.getJSONObject("tracking_options");
assertEquals(trackingOptions.length(), 1);
assertFalse(trackingOptions.getBoolean("ip_address"));
// when we enable privacy guard, make sure we maintain original tracking options
amplitude.enableCoppaControl();
assertEquals(amplitude.inputTrackingOptions, options);
assertNotEquals(amplitude.appliedTrackingOptions, options);
// also make sure we merge in the privacy guard options
assertFalse(amplitude.appliedTrackingOptions.shouldTrackLanguage());
assertFalse(amplitude.appliedTrackingOptions.shouldTrackCarrier());
assertFalse(amplitude.appliedTrackingOptions.shouldTrackIpAddress());
assertFalse(amplitude.appliedTrackingOptions.shouldTrackAdid());
assertFalse(amplitude.appliedTrackingOptions.shouldTrackCity());
assertFalse(amplitude.appliedTrackingOptions.shouldTrackLatLng());
amplitude.logEvent("test event 1");
looper.runToEndOfTasks();
looper.runToEndOfTasks();
events = getUnsentEvents(2);
assertEquals(events.length(), 2);
event = events.getJSONObject(1);
// verify we do have platform and carrier since those were not filtered out
assertTrue(event.has("country"));
assertTrue(event.has("platform"));
// verify we do not have any of the filtered out fields
assertFalse(event.has("carrier"));
assertFalse(event.has("language"));
// verify api properties contains tracking options for location filtering
apiProperties = event.getJSONObject("api_properties");
assertFalse(apiProperties.getBoolean("limit_ad_tracking"));
assertFalse(apiProperties.getBoolean("gps_enabled"));
assertTrue(apiProperties.has("tracking_options"));
trackingOptions = apiProperties.getJSONObject("tracking_options");
assertEquals(trackingOptions.length(), 3);
assertFalse(trackingOptions.getBoolean("ip_address"));
assertFalse(trackingOptions.getBoolean("city"));
assertFalse(trackingOptions.getBoolean("lat_lng"));
// disable privacy guard and make sure original user input is maintained
amplitude.disableCoppaControl();
assertEquals(amplitude.inputTrackingOptions, options);
assertEquals(amplitude.appliedTrackingOptions, options);
assertTrue(Utils.compareJSONObjects(amplitude.apiPropertiesTrackingOptions, options.getApiPropertiesTrackingOptions()));
assertFalse(amplitude.appliedTrackingOptions.shouldTrackLanguage());
assertFalse(amplitude.appliedTrackingOptions.shouldTrackCarrier());
assertFalse(amplitude.appliedTrackingOptions.shouldTrackIpAddress());
// haven't merged in the privacy guard settings yet
assertTrue(amplitude.appliedTrackingOptions.shouldTrackAdid());
assertTrue(amplitude.appliedTrackingOptions.shouldTrackCity());
assertTrue(amplitude.appliedTrackingOptions.shouldTrackLatLng());
amplitude.logEvent("test event 2");
looper.runToEndOfTasks();
looper.runToEndOfTasks();
events = getUnsentEvents(3);
assertEquals(events.length(), 3);
event = events.getJSONObject(2);
// verify we do have platform and carrier since those were not filtered out
assertTrue(event.has("country"));
assertTrue(event.has("platform"));
// verify we do not have any of the filtered out fields
assertFalse(event.has("carrier"));
assertFalse(event.has("language"));
// verify api properties contains tracking options for location filtering
apiProperties = event.getJSONObject("api_properties");
assertFalse(apiProperties.getBoolean("limit_ad_tracking"));
assertFalse(apiProperties.getBoolean("gps_enabled"));
assertTrue(apiProperties.has("tracking_options"));
trackingOptions = apiProperties.getJSONObject("tracking_options");
assertEquals(trackingOptions.length(), 1);
assertFalse(trackingOptions.getBoolean("ip_address"));
}
@Test
public void testGroupIdentifyMultipleOperations() throws JSONException {
String groupType = "test group type";
String groupName = "test group name";
String property1 = "string value";
String value1 = "testValue";
String property2 = "double value";
double value2 = 0.123;
String property3 = "boolean value";
boolean value3 = true;
String property4 = "json value";
Identify identify = new Identify().setOnce(property1, value1).add(property2, value2);
identify.set(property3, value3).unset(property4);
// identify should ignore this since duplicate key
identify.set(property4, value3);
amplitude.groupIdentify(groupType, groupName, identify);
Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();
assertEquals(getUnsentIdentifyCount(), 1);
assertEquals(getUnsentEventCount(), 0);
assertEquals(0, getIdentifyInterceptorCount());
JSONObject event = getLastUnsentIdentify();
assertEquals(Constants.GROUP_IDENTIFY_EVENT, event.optString("event_type"));
assertTrue(Utils.compareJSONObjects(event.optJSONObject("event_properties"), new JSONObject()));
assertTrue(Utils.compareJSONObjects(event.optJSONObject("user_properties"), new JSONObject()));
JSONObject groups = event.optJSONObject("groups");
JSONObject expectedGroups = new JSONObject();
expectedGroups.put(groupType, groupName);
assertTrue(Utils.compareJSONObjects(groups, expectedGroups));
JSONObject groupProperties = event.optJSONObject("group_properties");
JSONObject expected = new JSONObject();
expected.put(Constants.AMP_OP_SET_ONCE, new JSONObject().put(property1, value1));
expected.put(Constants.AMP_OP_ADD, new JSONObject().put(property2, value2));
expected.put(Constants.AMP_OP_SET, new JSONObject().put(property3, value3));
expected.put(Constants.AMP_OP_UNSET, new JSONObject().put(property4, "-"));
assertTrue(Utils.compareJSONObjects(groupProperties, expected));
}
@Test
public void testGroupIdentifyPropertiesObject() throws JSONException {
String groupType = "test group type";
String groupName = "test group name";
String property1 = "string value";
String value1 = "testValue";
String property2 = "double value";
double value2 = 0.123;
String property3 = "boolean value";
boolean value3 = true;
String property4 = "json value";
JSONObject properties = new JSONObject()
.put(property1, value1)
.put(property2, value2)
.put(property3, value3)
.put(property4, null);
amplitude.groupIdentify(groupType, groupName, properties, false, null);
Shadows.shadowOf(amplitude.logThread.getLooper()).runToEndOfTasks();
assertEquals(getUnsentIdentifyCount(), 1);
assertEquals(getUnsentEventCount(), 0);
JSONObject event = getLastUnsentIdentify();
assertEquals(Constants.GROUP_IDENTIFY_EVENT, event.optString("event_type"));
assertTrue(Utils.compareJSONObjects(event.optJSONObject("event_properties"), new JSONObject()));
assertTrue(Utils.compareJSONObjects(event.optJSONObject("user_properties"), new JSONObject()));
JSONObject groups = event.optJSONObject("groups");
JSONObject expectedGroups = new JSONObject();
expectedGroups.put(groupType, groupName);
assertTrue(Utils.compareJSONObjects(groups, expectedGroups));
JSONObject groupProperties = event.optJSONObject("group_properties");
JSONObject expected = new JSONObject();
expected.put(Constants.AMP_OP_SET, new JSONObject()
.put(property1, value1)
.put(property2, value2)
.put(property3, value3));
assertTrue(Utils.compareJSONObjects(groupProperties, expected));
}
@Test
public void testSetLogCallback() {
class TestLogCallback implements AmplitudeLogCallback {
String errorMsg = null;
@Override
public void onError(String tag, String message) {
this.errorMsg = message;
}
private String getErrorMsg() {
return this.errorMsg;
}
}
TestLogCallback callback = new TestLogCallback();
amplitude.setLogCallback(callback);
assertNull(callback.getErrorMsg());
amplitude.validateLogEvent("");
assertEquals("Argument eventType cannot be null or blank in logEvent()", callback.getErrorMsg());
}
@Test
public void testSetPlan() {
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
String branch = "main";
String version = "1.0.0";
String versionId = "9ec23ba0-275f-468f-80d1-66b88bff9529";
Plan plan = new Plan().setBranch(branch).setVersion(version).setVersionId(versionId);
amplitude.setPlan(plan);
amplitude.logEvent("test");
looper.runToEndOfTasks();
JSONObject event = getLastEvent();
assertNotNull(event);
try {
JSONObject planJsonObject = event.getJSONObject("plan");
assertEquals(branch, planJsonObject.getString("branch"));
assertEquals(version, planJsonObject.getString("version"));
assertEquals(versionId, planJsonObject.getString("versionId"));
} catch (Exception e) {
Assert.fail();
}
}
@Test
public void testSetIngestionMetadata() {
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
String sourceName = "ampli";
String sourceVersion = "1.0.0";
IngestionMetadata ingestionMetadata = new IngestionMetadata()
.setSourceName(sourceName)
.setSourceVersion(sourceVersion);
amplitude.setIngestionMetadata(ingestionMetadata);
amplitude.logEvent("test");
looper.runToEndOfTasks();
JSONObject event = getLastEvent();
assertNotNull(event);
try {
JSONObject jsonObject = event.getJSONObject("ingestion_metadata");
assertEquals(sourceName, jsonObject.getString("source_name"));
assertEquals(sourceVersion, jsonObject.getString("source_version"));
} catch (Exception e) {
Assert.fail();
}
}
@Test
public void testSetServerZoneWithoutUpdateServerUrl() {
String urlBeforeChange = amplitude.url;
AmplitudeServerZone euZone = AmplitudeServerZone.EU;
amplitude.setServerZone(euZone, false);
assertEquals(euZone, getPrivateFieldValueFromClient(amplitude, "serverZone"));
assertEquals(urlBeforeChange, amplitude.url);
}
@Test
public void testSetServerZoneAndUpdateServerUrl() {
AmplitudeServerZone euZone = AmplitudeServerZone.EU;
amplitude.setServerZone(euZone);
assertEquals(euZone, getPrivateFieldValueFromClient(amplitude, "serverZone"));
assertEquals(Constants.EVENT_LOG_EU_URL, amplitude.url);
}
@Test
public void testMiddlewareSupport() throws JSONException {
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
Map extraMap = new HashMap<>();
extraMap.put("description", "extra description");
MiddlewareExtra extra = new MiddlewareExtra(extraMap);
Middleware middleware = new Middleware() {
@Override
public void run(MiddlewarePayload payload, MiddlewareNext next) {
try {
payload.event.optJSONObject("event_properties").put("description", "extra description");
} catch (JSONException e) {
e.printStackTrace();
}
next.run(payload);
}
};
amplitude.addEventMiddleware(middleware);
amplitude.logEvent("middleware_event_type", new JSONObject().put("user_id", "middleware_user"), extra);
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 1);
JSONArray eventObject = getUnsentEvents(1);;
assertEquals(eventObject.optJSONObject(0).optString("event_type"), "middleware_event_type");
assertEquals(eventObject.optJSONObject(0).optJSONObject("event_properties").getString("description"), "extra description");
assertEquals(eventObject.optJSONObject(0).optJSONObject("event_properties").optString("user_id"), "middleware_user");
}
@Test
public void testWithSwallowMiddleware() throws JSONException {
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
Middleware middleware = new Middleware() {
@Override
public void run(MiddlewarePayload payload, MiddlewareNext next) {
}
};
amplitude.addEventMiddleware(middleware);
amplitude.logEvent("middleware_event_type", new JSONObject().put("user_id", "middleware_user"));
looper.runToEndOfTasks();
looper.runToEndOfTasks();
assertEquals(getUnsentEventCount(), 0);
}
@Test
public void setIdentifyBatchIntervalMillis() {
amplitude.setIdentifyBatchIntervalMillis(10000);
assertEquals(30000L, getPrivateFieldValueFromClient(amplitude, "identifyBatchIntervalMillis"));
amplitude.setIdentifyBatchIntervalMillis(40000);
assertEquals(40000L, getPrivateFieldValueFromClient(amplitude, "identifyBatchIntervalMillis"));
}
@Test
public void testMultipleIdentifyWithSetActionOnlySendOneIdentifyEvent() throws JSONException {
long [] timestamps = {1000, 1001, 1002, 1003};
clock.setTimestamps(timestamps);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
amplitude.identify(new Identify().set("key1", "key1-value1").set("key2", "key2-value1").set("key3", "key3-value1"));
amplitude.identify(new Identify().set("key1", "key1-value2").set("key4", "key4-value1"));
amplitude.identify(new Identify().set("key2", "key2-value2"));
amplitude.identify(new Identify().set("key3", "key3-value2").set("key4", "key4-value2"));
looper.runToEndOfTasks();
DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);
assertEquals(4L, getIdentifyInterceptorCount());
assertEquals(0, getUnsentIdentifyCount());
assertNull(dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY));
assertEquals((long) dbHelper.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY), 4L);
assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY), timestamps[3]);
looper.runToEndOfTasks();
looper.runToEndOfTasks();
RecordedRequest request = runRequest(amplitude);
JSONArray events = getEventsFromRequest(request);
assertEquals(events.length(), 1);
JSONObject identify = events.getJSONObject(0);
assertEquals(identify.getString("event_type"), Constants.IDENTIFY_EVENT);
assertEquals(identify.getLong("event_id"), 1);
assertEquals(identify.getLong("timestamp"), timestamps[0]);
assertEquals(identify.getLong("sequence_number"), 1);
JSONObject userProperties = identify.getJSONObject("user_properties");
assertEquals(userProperties.length(), 1);
assertTrue(userProperties.has(Constants.AMP_OP_SET));
JSONObject expected = new JSONObject();
expected.put("key1", "key1-value2");
expected.put("key2", "key2-value2");
expected.put("key3", "key3-value2");
expected.put("key4", "key4-value2");
assertTrue(Utils.compareJSONObjects(userProperties.getJSONObject(Constants.AMP_OP_SET), expected));
assertEquals(0, getIdentifyInterceptorCount());
assertEquals((long)dbHelper.getLastIdentifyInterceptorId(), -1L);
assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY), 1L);
}
@Test
public void testMultipleIdentifyWithSetActionOnlyAndOneEvent() throws JSONException {
long [] timestamps = {1000, 1001, 1002, 1003, 1004};
clock.setTimestamps(timestamps);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
Middleware middleware = new Middleware() {
@Override
public void run(MiddlewarePayload payload, MiddlewareNext next) {
try {
if (!payload.event.getString("event_type").equals(Constants.IDENTIFY_EVENT)) {
payload.event.optJSONObject("user_properties").put("key1", "key1-value3")
.put("key2", "key2-value3");
}
} catch (JSONException e) {
e.printStackTrace();
}
next.run(payload);
}
};
amplitude.addEventMiddleware(middleware);
amplitude.identify(new Identify().set("key1", "key1-value1").set("key2", "key2-value1").set("key3", "key3-value1"));
amplitude.identify(new Identify().set("key1", "key1-value2").set("key4", "key4-value1"));
amplitude.identify(new Identify().set("key2", "key2-value2"));
amplitude.identify(new Identify().set("key3", "key3-value2").set("key4", "key4-value2"));
amplitude.logEvent("test_event", new JSONObject().put("test_event_prop_key", "test_event_prop_value"));
looper.runToEndOfTasks();
DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);
assertEquals(1L, getUnsentIdentifyCount());
assertEquals(1L, getUnsentEventCount());
assertEquals(1L, (long)dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY));
assertEquals((long) dbHelper.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY), 5L);
assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY), timestamps[4]);
looper.runToEndOfTasks();
looper.runToEndOfTasks();
RecordedRequest request = runRequest(amplitude);
JSONArray events = getEventsFromRequest(request);
assertEquals(events.length(), 2);
JSONObject event = events.getJSONObject(0);
assertEquals(event.getString("event_type"), Constants.IDENTIFY_EVENT);
assertEquals(event.getLong("event_id"), 1);
assertEquals(event.getLong("timestamp"), timestamps[0]);
assertEquals(event.getLong("sequence_number"), 1);
JSONObject userProperties = event.getJSONObject("user_properties").getJSONObject(Constants.AMP_OP_SET);
assertEquals(userProperties.length(), 4);
JSONObject expected = new JSONObject();
expected.put("key1", "key1-value2");
expected.put("key2", "key2-value2");
expected.put("key3", "key3-value2");
expected.put("key4", "key4-value2");
assertTrue(Utils.compareJSONObjects(userProperties, expected));
JSONObject event2 = events.getJSONObject(1);
assertEquals(event2.getString("event_type"), "test_event");
assertEquals(event2.getLong("event_id"), 1);
assertEquals(event2.getLong("timestamp"), timestamps[4]);
assertEquals(event2.getLong("sequence_number"), 5);
JSONObject userProperties2 = event2.getJSONObject("user_properties");
assertEquals(userProperties2.length(), 2);
JSONObject expected2 = new JSONObject();
expected2.put("key1", "key1-value3");
expected2.put("key2", "key2-value3");
assertTrue(Utils.compareJSONObjects(userProperties2, expected2));
assertEquals(0, getIdentifyInterceptorCount());
assertEquals((long)dbHelper.getLastIdentifyInterceptorId(), -1L);
assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY), 1L);
assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_ID_KEY), 1L);
}
@Test
public void testMultipleIdentifyWithSetActionAndOneEventAndIdentify() throws JSONException {
long [] timestamps = {1000, 1001, 1002, 1003, 1004, 1005};
clock.setTimestamps(timestamps);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
Middleware middleware = new Middleware() {
@Override
public void run(MiddlewarePayload payload, MiddlewareNext next) {
try {
if (!payload.event.getString("event_type").equals(Constants.IDENTIFY_EVENT)) {
payload.event.optJSONObject("user_properties").put("key1", "key1-value3")
.put("key2", "key2-value3");
}
} catch (JSONException e) {
e.printStackTrace();
}
next.run(payload);
}
};
amplitude.addEventMiddleware(middleware);
amplitude.identify(new Identify().set("key1", "key1-value1").set("key2", "key2-value1").set("key3", "key3-value1"));
amplitude.identify(new Identify().set("key1", "key1-value2").set("key4", "key4-value1"));
amplitude.identify(new Identify().set("key2", "key2-value2"));
amplitude.identify(new Identify().set("key3", "key3-value2").set("key4", "key4-value2"));
amplitude.logEvent("test_event", new JSONObject().put("test_event_prop_key", "test_event_prop_value"));
amplitude.setUserProperties(new JSONObject().put("key1", "key1-value4"));
looper.runToEndOfTasks();
DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);
assertEquals(1L, getUnsentIdentifyCount());
assertEquals(1L, getUnsentEventCount());
assertEquals(1L, (long)dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY));
assertEquals((long) dbHelper.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY), 6L);
assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY), timestamps[5]);
looper.runToEndOfTasks();
looper.runToEndOfTasks();
RecordedRequest request = runRequest(amplitude);
JSONArray events = getEventsFromRequest(request);
assertEquals(events.length(), 3);
JSONObject event = events.getJSONObject(0);
assertEquals(event.getString("event_type"), Constants.IDENTIFY_EVENT);
assertEquals(event.getLong("event_id"), 1);
assertEquals(event.getLong("timestamp"), timestamps[0]);
assertEquals(event.getLong("sequence_number"), 1);
JSONObject userProperties = event.getJSONObject("user_properties").getJSONObject(Constants.AMP_OP_SET);
assertEquals(userProperties.length(), 4);
JSONObject expected = new JSONObject();
expected.put("key1", "key1-value2");
expected.put("key2", "key2-value2");
expected.put("key3", "key3-value2");
expected.put("key4", "key4-value2");
JSONObject event2 = events.getJSONObject(1);
assertEquals(event2.getString("event_type"), "test_event");
assertEquals(event2.getLong("event_id"), 1);
assertEquals(event2.getLong("timestamp"), timestamps[4]);
assertEquals(event2.getLong("sequence_number"), 5);
JSONObject userProperties2 = event2.getJSONObject("user_properties");
assertEquals(userProperties.length(), 4);
JSONObject expected2 = new JSONObject();
expected2.put("key1", "key1-value3");
expected2.put("key2", "key2-value3");
assertTrue(Utils.compareJSONObjects(userProperties2, expected2));
JSONObject event3 = events.getJSONObject(2);
assertEquals(event3.getString("event_type"), Constants.IDENTIFY_EVENT);
assertEquals(event3.getLong("event_id"), 2);
assertEquals(event3.getLong("timestamp"), timestamps[5]);
assertEquals(event3.getLong("sequence_number"), 6);
JSONObject userProperties3 = event3.getJSONObject("user_properties").getJSONObject(Constants.AMP_OP_SET);
assertEquals(userProperties2.length(), 2);
JSONObject expected3 = new JSONObject();
expected3.put("key1", "key1-value4");
assertTrue(Utils.compareJSONObjects(userProperties3, expected3));
assertEquals(0, getIdentifyInterceptorCount());
assertEquals((long)dbHelper.getLastIdentifyInterceptorId(), -1L);
assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY), 2L);
assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_ID_KEY), 1L);
}
@Test
public void testIdentifyInterceptWithSetAndClearAll() throws JSONException {
long [] timestamps = {1000, 1001, 1002, 1003, 1004};
clock.setTimestamps(timestamps);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
amplitude.identify(new Identify().set("key1", "key1-value1").set("key2", "key2-value1").set("key3", "key3-value1"));
amplitude.identify(new Identify().set("key1", "key1-value2").set("key4", "key4-value1"));
amplitude.identify(new Identify().set("key2", "key2-value2"));
amplitude.identify(new Identify().set("key3", "key3-value2").set("key4", "key4-value2"));
amplitude.identify(new Identify().clearAll());
looper.runToEndOfTasks();
DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);
assertEquals(0, getIdentifyInterceptorCount());
assertEquals(1, getUnsentIdentifyCount());
assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY), 1L);
assertEquals((long) dbHelper.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY), 5L);
assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY), timestamps[4]);
looper.runToEndOfTasks();
looper.runToEndOfTasks();
RecordedRequest request = runRequest(amplitude);
JSONArray events = getEventsFromRequest(request);
assertEquals(events.length(), 1);
JSONObject identify = events.getJSONObject(0);
assertEquals(identify.getString("event_type"), Constants.IDENTIFY_EVENT);
assertEquals(identify.getLong("event_id"), 1);
assertEquals(identify.getLong("timestamp"), timestamps[4]);
assertEquals(identify.getLong("sequence_number"), 5);
JSONObject userProperties = identify.getJSONObject("user_properties");
assertEquals(userProperties.length(), 1);
assertTrue(userProperties.has(Constants.AMP_OP_CLEAR_ALL));
assertFalse(userProperties.has(Constants.AMP_OP_SET));
assertEquals(0, getIdentifyInterceptorCount());
assertEquals((long)dbHelper.getLastIdentifyInterceptorId(), -1L);
}
@Test
public void testMultipleIdentifyWithSetActionAndAnotherIdentify() throws JSONException {
long [] timestamps = {1000, 1001, 1002, 1003, 1004};
clock.setTimestamps(timestamps);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
amplitude.identify(new Identify().set("key1", "key1-value1").set("key2", "key2-value1").set("key3", "key3-value1"));
amplitude.identify(new Identify().set("key1", "key1-value2").set("key4", "key4-value1"));
amplitude.identify(new Identify().set("key2", "key2-value2"));
amplitude.identify(new Identify().set("key3", "key3-value2").set("key4", "key4-value2"));
amplitude.identify(new Identify().add("key5", 2));
looper.runToEndOfTasks();
DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);
assertEquals(2L, getUnsentIdentifyCount());
assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY), 2L);
assertEquals((long) dbHelper.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY), 5L);
assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY), timestamps[4]);
looper.runToEndOfTasks();
looper.runToEndOfTasks();
RecordedRequest request = runRequest(amplitude);
JSONArray events = getEventsFromRequest(request);
assertEquals(events.length(), 2);
JSONObject event = events.getJSONObject(0);
assertEquals(event.getString("event_type"), Constants.IDENTIFY_EVENT);
assertEquals(event.getLong("event_id"), 1);
assertEquals(event.getLong("timestamp"), timestamps[0]);
assertEquals(event.getLong("sequence_number"), 1);
JSONObject userProperties = event.getJSONObject("user_properties");
assertEquals(userProperties.length(), 1);
JSONObject expected = new JSONObject();
expected.put("key1", "key1-value2");
expected.put("key2", "key2-value2");
expected.put("key3", "key3-value2");
expected.put("key4", "key4-value2");
assertTrue(Utils.compareJSONObjects(userProperties.getJSONObject(Constants.AMP_OP_SET), expected));
JSONObject event2 = events.getJSONObject(1);
assertEquals(event2.getString("event_type"), Constants.IDENTIFY_EVENT);
assertEquals(event2.getLong("event_id"), 2);
assertEquals(event2.getLong("timestamp"), timestamps[4]);
assertEquals(event2.getLong("sequence_number"), 5);
JSONObject userProperties2 = event2.getJSONObject("user_properties");
assertEquals(userProperties2.length(), 1);
JSONObject expectedAdd = new JSONObject();
expectedAdd.put("key5", 2);
assertTrue(Utils.compareJSONObjects(userProperties2.getJSONObject(Constants.AMP_OP_ADD), expectedAdd));
assertEquals(0, getIdentifyInterceptorCount());
assertEquals((long)dbHelper.getLastIdentifyInterceptorId(), -1L);
}
@Test
public void testUploadEventsSendInterceptedIdentify() throws JSONException {
long [] timestamps = {1000, 1001, 1002, 1003};
clock.setTimestamps(timestamps);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
amplitude.identify(new Identify().set("key1", "key1-value1").set("key2", "key2-value1").set("key3", "key3-value1"));
amplitude.identify(new Identify().set("key1", "key1-value2").set("key4", "key4-value1"));
amplitude.setUserProperties(new JSONObject().put("key2", "key2-value2"));
amplitude.identify(new Identify().set("key3", "key3-value2").set("key4", "key4-value2"));
looper.runToEndOfTasks();
DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);
assertEquals(4L, getIdentifyInterceptorCount());
assertEquals(0, getUnsentIdentifyCount());
assertNull(dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY));
assertEquals((long) dbHelper.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY), 4L);
assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY), timestamps[3]);
amplitude.uploadEvents();
looper.runToEndOfTasks();
RecordedRequest request = runRequest(amplitude);
JSONArray events = getEventsFromRequest(request);
assertEquals(events.length(), 1);
JSONObject identify = events.getJSONObject(0);
assertEquals(identify.getString("event_type"), Constants.IDENTIFY_EVENT);
assertEquals(identify.getLong("event_id"), 1);
assertEquals(identify.getLong("timestamp"), timestamps[0]);
assertEquals(identify.getLong("sequence_number"), 1);
JSONObject userProperties = identify.getJSONObject("user_properties");
assertEquals(userProperties.length(), 1);
assertTrue(userProperties.has(Constants.AMP_OP_SET));
JSONObject expected = new JSONObject();
expected.put("key1", "key1-value2");
expected.put("key2", "key2-value2");
expected.put("key3", "key3-value2");
expected.put("key4", "key4-value2");
assertTrue(Utils.compareJSONObjects(userProperties.getJSONObject(Constants.AMP_OP_SET), expected));
assertEquals(0, getIdentifyInterceptorCount());
assertEquals((long)dbHelper.getLastIdentifyInterceptorId(), -1L);
assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY), 1L);
}
@Test
public void testMultipleIdentifyWithSetActionAndSetGroup() throws JSONException {
long [] timestamps = {1000, 1001, 1002, 1003, 1004, 1005};
clock.setTimestamps(timestamps);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
amplitude.identify(new Identify().set("key1", "key1-value1").set("key2", "key2-value1").set("key3", "key3-value1"));
amplitude.identify(new Identify().set("key1", "key1-value2").set("key4", "key4-value1"));
amplitude.identify(new Identify().set("key2", "key2-value2"));
amplitude.identify(new Identify().set("key3", "key3-value2").set("key4", "key4-value2"));
amplitude.setGroup("test-group-type", "test-group-value");
amplitude.identify(new Identify().set("key3", "key3-value3").set("key4", "key4-value3"));
looper.runToEndOfTasks();
DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);
assertEquals(2L, getUnsentIdentifyCount());
assertEquals(1L, getIdentifyInterceptorCount());
assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY), 2L);
assertEquals((long) dbHelper.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY), 6L);
assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY), timestamps[5]);
looper.runToEndOfTasks();
looper.runToEndOfTasks();
RecordedRequest request = runRequest(amplitude);
JSONArray events = getEventsFromRequest(request);
assertEquals(events.length(), 3);
JSONObject event = events.getJSONObject(0);
assertEquals(event.getString("event_type"), Constants.IDENTIFY_EVENT);
assertEquals(event.getLong("event_id"), 1);
assertEquals(event.getLong("timestamp"), timestamps[0]);
assertEquals(event.getLong("sequence_number"), 1);
JSONObject userProperties = event.getJSONObject("user_properties");
assertEquals(userProperties.length(), 1);
JSONObject expected = new JSONObject();
expected.put("key1", "key1-value2");
expected.put("key2", "key2-value2");
expected.put("key3", "key3-value2");
expected.put("key4", "key4-value2");
assertTrue(Utils.compareJSONObjects(userProperties.getJSONObject(Constants.AMP_OP_SET), expected));
JSONObject event2 = events.getJSONObject(1);
assertEquals(event2.getString("event_type"), Constants.IDENTIFY_EVENT);
assertEquals(event2.getLong("event_id"), 2);
assertEquals(event2.getLong("timestamp"), timestamps[4]);
assertEquals(event2.getLong("sequence_number"), 5);
JSONObject userProperties2 = event2.getJSONObject("user_properties");
assertEquals(userProperties2.length(), 1);
JSONObject expected2 = new JSONObject();
expected2.put("test-group-type", "test-group-value");
JSONObject expectedGroups = new JSONObject();
expectedGroups.put("test-group-type", "test-group-value");
assertTrue(Utils.compareJSONObjects(userProperties2.getJSONObject(Constants.AMP_OP_SET), expected2));
assertTrue(Utils.compareJSONObjects(event2.getJSONObject("groups"), expectedGroups));
JSONObject event3 = events.getJSONObject(2);
assertEquals(event3.getString("event_type"), Constants.IDENTIFY_EVENT);
assertEquals(event3.getLong("event_id"), 3);
assertEquals(event3.getLong("timestamp"), timestamps[5]);
assertEquals(event3.getLong("sequence_number"), 6);
JSONObject userProperties3 = event3.getJSONObject("user_properties");
assertEquals(userProperties3.length(), 1);
JSONObject expected3 = new JSONObject();
expected3.put("key3", "key3-value3");
expected3.put("key4", "key4-value3");
assertTrue(Utils.compareJSONObjects(userProperties3.getJSONObject(Constants.AMP_OP_SET), expected3));
assertEquals(0, getIdentifyInterceptorCount());
assertEquals((long)dbHelper.getLastIdentifyInterceptorId(), -1L);
}
@Test
public void testMultipleIdentifyWithSetActionAndUserIdUpdated() throws JSONException {
long [] timestamps = {1000, 1001, 1002, 1003, 1004};
clock.setTimestamps(timestamps);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
amplitude.identify(new Identify().set("key1", "key1-value1").set("key2", "key2-value1").set("key3", "key3-value1"));
amplitude.identify(new Identify().set("key1", "key1-value2").set("key4", "key4-value1"));
amplitude.identify(new Identify().set("key2", "key2-value2"));
amplitude.identify(new Identify().set("key3", "key3-value2").set("key4", "key4-value2"));
amplitude.setUserId("identify-user-id");
amplitude.identify(new Identify().set("key3", "key3-value3").set("key4", "key4-value3"));
looper.runToEndOfTasks();
DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);
assertEquals(1L, getUnsentIdentifyCount());
assertEquals(1L, getIdentifyInterceptorCount());
assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY), 1L);
assertEquals((long) dbHelper.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY), 5L);
assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY), timestamps[4]);
looper.runToEndOfTasks();
looper.runToEndOfTasks();
RecordedRequest request = runRequest(amplitude);
JSONArray events = getEventsFromRequest(request);
assertEquals(events.length(), 2);
JSONObject event = events.getJSONObject(0);
assertEquals(event.getString("event_type"), Constants.IDENTIFY_EVENT);
assertEquals(event.getLong("event_id"), 1);
assertEquals(event.getLong("timestamp"), timestamps[0]);
assertEquals(event.getLong("sequence_number"), 1);
assertEquals(event.getString("user_id"), "null");
JSONObject userProperties = event.getJSONObject("user_properties");
assertEquals(userProperties.length(), 1);
JSONObject expected = new JSONObject();
expected.put("key1", "key1-value2");
expected.put("key2", "key2-value2");
expected.put("key3", "key3-value2");
expected.put("key4", "key4-value2");
assertTrue(Utils.compareJSONObjects(userProperties.getJSONObject(Constants.AMP_OP_SET), expected));
JSONObject event2 = events.getJSONObject(1);
assertEquals(event2.getString("event_type"), Constants.IDENTIFY_EVENT);
assertEquals(event2.getLong("event_id"), 2);
assertEquals(event2.getLong("timestamp"), timestamps[4]);
assertEquals(event2.getLong("sequence_number"), 5);
assertEquals(event2.getString("user_id"), "identify-user-id");
JSONObject userProperties2 = event2.getJSONObject("user_properties");
assertEquals(userProperties2.length(), 1);
JSONObject expected2 = new JSONObject();
expected2.put("key3", "key3-value3");
expected2.put("key4", "key4-value3");
assertTrue(Utils.compareJSONObjects(userProperties2.getJSONObject(Constants.AMP_OP_SET), expected2));
assertEquals(0, getIdentifyInterceptorCount());
assertEquals((long)dbHelper.getLastIdentifyInterceptorId(), -1L);
}
@Test
public void testNullUserPropertyFilteredOut() throws JSONException {
long [] timestamps = {1000, 1001, 1002};
clock.setTimestamps(timestamps);
ShadowLooper looper = Shadows.shadowOf(amplitude.logThread.getLooper());
looper.runToEndOfTasks();
amplitude.identify(new Identify().set("key1", "key1-value1").set("key2", "key2-value1").set("key3", "key3-value1"));
amplitude.identify(new Identify().set("key1", "key1-value2").set("key4", "key4-value1"));
amplitude.setUserProperties(new JSONObject().put("key2", null).put("key4", JSONObject.NULL));
looper.runToEndOfTasks();
DatabaseHelper dbHelper = DatabaseHelper.getDatabaseHelper(context);
assertEquals(3L, getIdentifyInterceptorCount());
assertEquals(0, getUnsentIdentifyCount());
assertNull(dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY));
assertEquals((long) dbHelper.getLongValue(AmplitudeClient.SEQUENCE_NUMBER_KEY), 3L);
assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_EVENT_TIME_KEY), timestamps[2]);
amplitude.uploadEvents();
looper.runToEndOfTasks();
RecordedRequest request = runRequest(amplitude);
JSONArray events = getEventsFromRequest(request);
assertEquals(events.length(), 1);
JSONObject identify = events.getJSONObject(0);
assertEquals(identify.getString("event_type"), Constants.IDENTIFY_EVENT);
assertEquals(identify.getLong("event_id"), 1);
assertEquals(identify.getLong("timestamp"), timestamps[0]);
assertEquals(identify.getLong("sequence_number"), 1);
JSONObject userProperties = identify.getJSONObject("user_properties");
assertEquals(userProperties.length(), 1);
assertTrue(userProperties.has(Constants.AMP_OP_SET));
JSONObject expected = new JSONObject();
expected.put("key1", "key1-value2");
expected.put("key2", "key2-value1");
expected.put("key3", "key3-value1");
expected.put("key4", "key4-value1");
System.out.println(userProperties.getJSONObject(Constants.AMP_OP_SET));
assertTrue(Utils.compareJSONObjects(userProperties.getJSONObject(Constants.AMP_OP_SET), expected));
assertEquals(0, getIdentifyInterceptorCount());
assertEquals((long)dbHelper.getLastIdentifyInterceptorId(), -1L);
assertEquals((long)dbHelper.getLongValue(AmplitudeClient.LAST_IDENTIFY_ID_KEY), 1L);
}
}
================================================
FILE: src/test/java/com/amplitude/api/AmplitudeServerZoneTest.java
================================================
package com.amplitude.api;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.robolectric.annotation.Config;
import java.util.Arrays;
import java.util.Collection;
import static org.junit.Assert.assertEquals;
@RunWith(Parameterized.class)
@Config(manifest=Config.NONE)
public class AmplitudeServerZoneTest {
@Parameterized.Parameters
public static Collection