├── .github ├── ISSUE_TEMPLATE │ └── appsflyer-issue-template.md ├── bash_scripts │ ├── pre_release.sh │ └── release.sh └── workflows │ ├── prepare-for-QA-release.yml │ ├── prepare-for-release-workflow.yml │ ├── release-QA-workflow.yml │ ├── release-production-workflow.yml │ └── unit-tests-workflow.yml ├── .gitignore ├── LICENSE ├── RELEASENOTES.md ├── Readme.md ├── app ├── build.gradle ├── proguard-rules.pro ├── publish.gradle └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── segment │ │ └── analytics │ │ └── android │ │ └── integrations │ │ └── appsflyer │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── segment │ │ └── analytics │ │ └── android │ │ └── integrations │ │ └── appsflyer │ │ └── AppsflyerIntegration.java │ └── test │ ├── java │ └── com │ │ └── segment │ │ └── analytics │ │ └── android │ │ └── integrations │ │ └── appsflyer │ │ ├── AppsflyerIntegrationConversionListenerTests.java │ │ ├── AppsflyerIntegrationTests.java │ │ └── TestHelper.java │ └── resources │ └── mockito-extensions │ └── org.mockito.plugins.MockMaker ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── segmenttestapp ├── .gitignore ├── build.gradle ├── google-services.json ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── appsflyer │ │ └── app │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── appsflyer │ │ │ └── segment │ │ │ └── app │ │ │ ├── MainActivity.java │ │ │ └── SampleApplication.java │ └── res │ │ ├── drawable │ │ ├── appsflyer_logo.png │ │ └── segment_logo.png │ │ ├── layout-v11 │ │ └── activity_main.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ └── item1.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ ├── dimens.xml │ │ └── strings.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── appsflyer │ └── app │ └── ExampleUnitTest.java └── settings.gradle /.github/ISSUE_TEMPLATE/appsflyer-issue-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: AppsFlyer Issue Template 3 | about: Open issues to AppsFlyer with this template 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 15 | 16 | # Report 17 | 18 | ## Plugin Version 19 | 20 | ℹ Please replace these two lines with the plugin version. 21 | e.g. via `4.1.1` 22 | 23 | ## On what Platform are you having the issue? 24 | 25 | ℹ Please replace these two lines with the SDK version. 26 | e.g. ios and android 27 | 28 | ## What did you do? 29 | 30 | ℹ Please replace these two lines with what you did. 31 | e.g. Run `pod install` 32 | 33 | ## What did you expect to happen? 34 | 35 | ℹ Please replace these two lines with what you expected to happen. 36 | e.g. Event to be tracked 37 | 38 | ## What happened instead? 39 | 40 | ℹ Please replace these two lines with of what happened instead. 41 | e.g. No uninstalls on my dashboard 42 | 43 | ## Please provide any other relevant information. 44 | 45 | ℹ Please replace these two lines with more information. 46 | e.g. The issue started when we upgraded the plugin to the latest version 47 | -------------------------------------------------------------------------------- /.github/bash_scripts/pre_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | appsflyerversion=$1 4 | rc=$2 5 | 6 | sed -E -i '' "s/(.*af-android-sdk:)([0-9]+\.[0-9]+\.[0-9]+)'/\1$appsflyerversion\'/g" app/build.gradle 7 | 8 | sed -E -i '' "s/(.*af-android-sdk:)([0-9]+\.[0-9]+\.[0-9]+)'/\1$appsflyerversion\'/g" segmenttestapp/build.gradle 9 | 10 | version_code=$(grep -E 'VERSION_CODE=([0-9]+)' gradle.properties | grep -o '[0-9]\+') 11 | version_code=$((version_code+1)) 12 | sed -E -i '' "s/VERSION_CODE=([0-9]+)/VERSION_CODE=$version_code/g" gradle.properties 13 | 14 | sed -E -i '' "s/VERSION_NAME=([0-9]+\.[0-9]+\.[0-9]+)/VERSION_NAME=$appsflyerversion-rc$rc/g" gradle.properties 15 | 16 | sed -E -i '' "s/(POM_ARTIFACT_ID=.*)/\1-beta/g" gradle.properties 17 | 18 | sed -E -i '' "s/(Built with AppsFlyer Android SDK.*)([0-9]+\.[0-9]+\.[0-9]+)(.*)/\1$appsflyerversion\3/g" Readme.md 19 | sed -E -i '' "s/(.*appsflyer:segment-android-integration:)([0-9]+\.[0-9]+\.[0-9]+)(.*)/\1$appsflyerversion\3/g" Readme.md 20 | 21 | sed -E -i '' "s/(.*setPluginInfo.*)([0-9]+\.[0-9]+\.[0-9]+)(.*)/\1$appsflyerversion\3/g" app/src/main/java/com/segment/analytics/android/integrations/appsflyer/AppsflyerIntegration.java 22 | 23 | touch "releasenotes.$appsflyerversion" 24 | -------------------------------------------------------------------------------- /.github/bash_scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | releaseversion=$1 4 | 5 | sed -i '' 's/^/* /' "releasenotes.$releaseversion" 6 | NEW_VERSION_RELEASE_NOTES=$(cat "releasenotes.$releaseversion") 7 | NEW_VERSION_SECTION="### $releaseversion\n$NEW_VERSION_RELEASE_NOTES\n\n" 8 | echo -e "$NEW_VERSION_SECTION$(cat RELEASENOTES.md)" > RELEASENOTES.md 9 | 10 | rm -r "releasenotes.$releaseversion" 11 | 12 | sed -E -i '' "s/VERSION_NAME=([0-9]+\.[0-9]+\.[0-9]+).*/VERSION_NAME=$releaseversion/g" gradle.properties 13 | 14 | sed -E -i '' "s/(POM_ARTIFACT_ID=.*)-beta/\1/g" gradle.properties -------------------------------------------------------------------------------- /.github/workflows/prepare-for-QA-release.yml: -------------------------------------------------------------------------------- 1 | name: pre-release 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | Change-HardCoded-Version: 8 | name: Pre Release 9 | runs-on: 10 | - self-hosted 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | - name: Determine release tag and release branch 16 | run: | 17 | TAG=$(echo "${{github.ref_name}}" | grep -Eo '[0-9]+.[0-9]+.[0-9]+') 18 | RC=$(echo "${{github.ref_name}}" | grep -Eo '[0-9]+$') 19 | echo "PLUGIN_VERSION=$TAG" >> $GITHUB_ENV 20 | echo "RC_NUMBER=$RC" >> $GITHUB_ENV 21 | - name: run script 22 | run: bash .github/bash_scripts/pre_release.sh ${{env.PLUGIN_VERSION}} ${{env.RC_NUMBER}} 23 | - name: Commit changes 24 | uses: EndBug/add-and-commit@v9 25 | with: 26 | author_name: Moris Gateno 27 | author_email: moris.gateno@appsflyer.com 28 | message: 'Commited from github action - prepaing the repo for QA.' 29 | add: '.' 30 | - name: Set up JDK 31 | uses: actions/setup-java@v1 32 | with: 33 | java-version: '11' 34 | - name: Grant execute permission for gradlew 35 | run: | 36 | chmod +x ./gradlew 37 | - name: Publish package to QA (-Beta) 38 | run: | 39 | ./gradlew publish 40 | - name: Notify with Slack 41 | uses: slackapi/slack-github-action@v1.23.0 42 | with: 43 | payload: | 44 | { 45 | "appsflyer_version": "${{env.PLUGIN_VERSION}}", 46 | "environment": "QA" 47 | } 48 | env: 49 | SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK_URL }} 50 | 51 | 52 | -------------------------------------------------------------------------------- /.github/workflows/prepare-for-release-workflow.yml: -------------------------------------------------------------------------------- 1 | name: Prepare plugin for production 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | branches: 8 | - 'master' 9 | # - 'dev/add-release-workflow' 10 | 11 | jobs: 12 | Prepare-Plugin-For-Production: 13 | if: startsWith(github.head_ref, 'releases/') 14 | name: Prepare for production after testing the plugin 15 | runs-on: 16 | - self-hosted 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v3 20 | - name: PR branch check 21 | uses: mdecoleman/pr-branch-name@1.2.0 22 | id: vars 23 | with: 24 | repo-token: ${{ secrets.CI_GITHUB_TOKEN }} 25 | - name: Determine release tag and release branch 26 | run: | 27 | TAG=$(echo "${{ steps.vars.outputs.branch }}" | grep -Eo '[0-9]+.[0-9]+.[0-9]+') 28 | echo "PLUGIN_VERSION=$TAG" >> $GITHUB_ENV 29 | - name: run script 30 | run: bash .github/bash_scripts/release.sh ${{env.PLUGIN_VERSION}} 31 | - name: Commit and Push 32 | run : | 33 | git add . 34 | git commit -m"Commited from github action - prepaing the repo for production." 35 | git push origin HEAD:${{ steps.vars.outputs.branch }} --force -------------------------------------------------------------------------------- /.github/workflows/release-QA-workflow.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Release plugin to QA 3 | 4 | on: 5 | push: 6 | branches: 7 | - releases/[0-9]+.x.x/[0-9]+.[0-9]+.x/[0-9]+.[0-9]+.[0-9]+-rc[0-9]+ 8 | 9 | jobs: 10 | Check-If-ReleaseNotes-Pushed: 11 | runs-on: 12 | - self-hosted 13 | outputs: 14 | answer: ${{ steps.filter.outputs.releasenotesfile }} 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: dorny/paths-filter@v2 18 | id: filter 19 | with: 20 | filters: | 21 | releasenotesfile: 22 | - 'releasenotes.**' 23 | 24 | Run-Unit-Tests: 25 | needs: Check-If-ReleaseNotes-Pushed 26 | if: needs.Check-If-ReleaseNotes-Pushed.outputs.answer == 'false' 27 | uses: ./.github/workflows/unit-tests-workflow.yml 28 | secrets: inherit 29 | 30 | Deploy-Locally-To-QA: 31 | needs: [Run-Unit-Tests,Check-If-ReleaseNotes-Pushed] 32 | if: needs.Check-If-ReleaseNotes-Pushed.outputs.answer == 'false' 33 | uses: ./.github/workflows/prepare-for-QA-release.yml 34 | secrets: inherit 35 | -------------------------------------------------------------------------------- /.github/workflows/release-production-workflow.yml: -------------------------------------------------------------------------------- 1 | name: Release plugin to production 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - closed 7 | branches: 8 | - 'master' 9 | # - 'dev/add-release-workflow' 10 | 11 | jobs: 12 | Deploy-To-Production: 13 | if: github.event.pull_request.merged == true && startsWith(github.head_ref, 'releases/') 14 | runs-on: 15 | - self-hosted 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | - name: PR branch check 20 | uses: mdecoleman/pr-branch-name@1.2.0 21 | id: vars 22 | with: 23 | repo-token: ${{ secrets.CI_GITHUB_TOKEN }} 24 | - name: Determine release tag and release branch 25 | run: | 26 | TAG=$(echo "${{ steps.vars.outputs.branch }}" | grep -Eo '[0-9]+.[0-9]+.[0-9]+') 27 | echo "PLUGIN_VERSION=$TAG" >> $GITHUB_ENV 28 | echo "RELEASE_BRANCH_NAME=${{ steps.vars.outputs.branch }}" >> $GITHUB_ENV 29 | echo "push new release >> $TAG" 30 | - name: Create release and tag 31 | env: 32 | TAG: ${{env.PLUGIN_VERSION}} 33 | uses: "actions/github-script@v5" 34 | with: 35 | script: | 36 | try { 37 | await github.rest.repos.createRelease({ 38 | draft: false, 39 | generate_release_notes: false, 40 | name: process.env.TAG, 41 | owner: context.repo.owner, 42 | prerelease: false, 43 | repo: context.repo.repo, 44 | tag_name: process.env.TAG 45 | }); 46 | } catch (error) { 47 | core.setFailed(error.message); 48 | } 49 | - name: Set up JDK 50 | uses: actions/setup-java@v1 51 | with: 52 | java-version: '11' 53 | - name: Grant execute permission for gradlew 54 | run: | 55 | chmod +x ./gradlew 56 | - name: Publish package 57 | run: | 58 | ./gradlew publish 59 | - name: Notify with Slack 60 | uses: slackapi/slack-github-action@v1.23.0 61 | with: 62 | payload: | 63 | { 64 | "appsflyer_version": "${{env.PLUGIN_VERSION}}", 65 | "environment": "Production" 66 | } 67 | env: 68 | SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK_URL }} -------------------------------------------------------------------------------- /.github/workflows/unit-tests-workflow.yml: -------------------------------------------------------------------------------- 1 | name: CI - Tests 2 | on: 3 | push: 4 | branches-ignore: 5 | - 'master' 6 | - 'releases/**' 7 | workflow_call: 8 | jobs: 9 | Tests: 10 | runs-on: 11 | - self-hosted 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up JDK 15 | uses: actions/setup-java@v1 16 | with: 17 | java-version: '11' 18 | - name: Setup Android SDK 19 | uses: android-actions/setup-android@v2 20 | - name: Make gradlew executable 21 | run: chmod +x ./gradlew 22 | - name: Run Tests 23 | run: ./gradlew test 24 | - name: Test Report 25 | uses: dorny/test-reporter@v1 26 | if: always() 27 | with: 28 | name: Test Results 29 | path: app/build/test-results/testDebugUnitTest/TEST-*.xml # Path to test results 30 | reporter: java-junit # Format of test results 31 | fail-on-error: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | 19 | # Local configuration file (sdk path, etc) 20 | local.properties 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Log Files 26 | *.log 27 | 28 | # Android Studio Navigation editor temp files 29 | .navigation/ 30 | 31 | # Android Studio captures folder 32 | *.iml 33 | segmenttestapp/*.iml 34 | captures/ 35 | 36 | \.idea/ 37 | 38 | \.DS_Store 39 | 40 | .project 41 | 42 | .settings/org.eclipse.buildship.core.prefs 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 AppsFlyer Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /RELEASENOTES.md: -------------------------------------------------------------------------------- 1 | ### 6.15.0 2 | * Update Android SDK to v6.15.0 3 | * Added the logAdRevenue method to send ad revenue data to AppsFlyer. Note: Starting with this version, the AdRevenue Connector should no longer be used. For more details, [see] (https://dev.appsflyer.com/hc/docs/ad-revenue-1) 4 | 5 | ### 6.14.0 6 | * Update Android SDK to v6.14.0 7 | * Updated Huawei Referrer integration. [Learn more](https://dev.appsflyer.com/hc/docs/install-android-sdk#huawei-install-referrer). 8 | 9 | ### 6.13.0 10 | * Update Android SDK to v6.13.0 11 | * Added support for Google's new EU consent policy 12 | 13 | ### 6.12.2 14 | * Update Android SDK to v6.12.2 15 | 16 | ### 6.10.3 17 | * Update Android SDK to v6.10.1 18 | * Added CI-CD pipeline 19 | 20 | ### 6.10.2 21 | * Update Android SDK to v6.10.2 22 | * Added Platform Extension v2 call. 23 | 24 | ### 6.10.1 25 | * Update Android SDK to v6.10.1 26 | * Added unit tests. 27 | * Added github workflows. 28 | 29 | ### 6.8.2 30 | * Update Android SDK to v6.8.2 31 | 32 | ### 6.8.0 33 | * Update Android SDK to v6.8.0 34 | 35 | ### 6.5.2 36 | * Update Android SDK to v6.5.2 37 | 38 | ### 6.4.3 39 | * Update Android SDK to v6.4.3 40 | 41 | ### 6.4.1 42 | * Update Android SDK to v6.4.1 43 | 44 | ### 6.4.0 45 | * Update Android SDK to v6.4.0 46 | 47 | ### 6.3.2 48 | * Update Android SDK to v6.3.2 49 | 50 | ### 6.3.0 51 | * Update Android SDK to v6.3.0 52 | 53 | ### 6.2.3 54 | * Update Android SDK to v6.2.3 55 | * fix OnAppOpenAttribution 56 | 57 | ### 6.1.1 58 | * Update Android SDK to v6.1.4 59 | 60 | ### 6.1.1 61 | * Update Android SDK to v6.1.1 62 | * Support for Unified deep linking 63 | 64 | ### 5.4.4 65 | * Update Android SDK to v5.4.4 66 | * Deprecated AppsflyerIntegration.ConversionListenerDisplay() 67 | * Add ExternalAppsFlyerConversionListener to get the callbacks 68 | 69 | ### 5.2.1 70 | * Updated Android SDK to v5.2.0 71 | 72 | ### 5.1.1 73 | * Fixed issue when first launch was not sent when integration was used by @segment/analytics-react-native-appsflyer 74 | 75 | ### 5.1.0 76 | * Updated Android SDK to v5.1.0 77 | * Updated structure of campaign : 78 | @"campaign" : @{ 79 | @"ad_group" : @"", 80 | @"name" : @"", 81 | @"source" : @"" 82 | } 83 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # AppsFlyer - Segment Integration 5 | [![CI - Tests](https://github.com/AppsFlyerSDK/appsflyer-segment-android-plugin/actions/workflows/unit-tests-workflow.yml/badge.svg)](https://github.com/AppsFlyerSDK/appsflyer-segment-android-plugin/actions/workflows/unit-tests-workflow.yml) 6 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.appsflyer/segment-android-integration/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.appsflyer/segment-android-integration) 7 | 8 | ---------- 9 | In order for us to provide optimal support, we would kindly ask you to submit any issues to support@appsflyer.com 10 | 11 | *When submitting an issue please specify your AppsFlyer sign-up (account) email , your app ID , production steps, logs, code snippets and any additional relevant information.* 12 | 13 | 14 | # Overview 15 | 16 | AppsFlyer SDK provides app installation and event tracking functionality. We have developed an SDK that is highly robust (7+ billion SDK installations to date), secure, lightweight and very simple to embed. 17 | 18 | 19 | 20 | You can track installs, updates and sessions and also track additional in-app events beyond app installs (including in-app purchases, game levels, etc.) to evaluate ROI and user engagement levels. 21 | 22 | --- 23 | 24 | Built with AppsFlyer Android SDK `v6.15.0` 25 | 26 | ## Table of content 27 | 28 | - [Introduction](#whatIsSegment) 29 | - [Getting Started](#quickStart) 30 | - [Manual mode](#manual) 31 | - [SDK Initialization](#sdk_init) 32 | - [Register In-App Events](#adding_events) 33 | - [Get Conversion Data](#conversion_data) 34 | - [Unified Deep Linking](#deep_linking) 35 | - [Send consent for DMA compliance](#dma_support) 36 | - [Sample App](#sample_app) 37 | 38 | 39 | ### 40 | # Introduction 41 | 42 | Segment makes it easy to send your data to AppsFlyer. Once you have tracked your data through Segment's open source libraries, the data is translated and routed to AppsFlyer in the appropriate format. AppsFlyer helps marketers to pinpoint targeting, optimize ad spend and boost ROI. 43 | 44 | 45 | The AppsFlyer integration code is open-source on GitHub if you want to [check it out](https://github.com/segment-integrations/integration-appsflyer). 46 | 47 | Check out the Segment AppsFlyer docs [here](https://segment.com/docs/destinations/appsflyer/). 48 | 49 | 50 | 51 | 52 | 53 | 54 | ### 55 | # Getting Started 56 | 57 | 58 | #### DashBoard Setup 59 | 60 | To enable AppsFlyer in the Segment dashboard, follow these steps: 61 | 62 | 1. Enter your unique AppsFlyer Dev Key, which is accessible from your AppsFlyer account, in Segment’s destination settings. 63 | 2. After you build and release to the app store, your data is translated and sent to AppsFlyer automatically. 64 | 65 | 66 | 67 | The Segment AppsFlyer integration is entirely handled through Segment's servers, so you don’t need to bundle AppsFlyer's iOS or Android SDKs. Your Segment SDK will be enough. 68 | 69 | AppsFlyer supports the `identify` and `track` methods. 70 | 71 | ### 72 | # Manual mode 73 | Starting version 6.8.0, we support a manual mode to seperate the initialization of the AppsFlyer SDK and the start of the SDK. In this case, the AppsFlyer SDK won't start automatically, giving the developper more freedom when to start the AppsFlyer SDK. Please note that in manual mode, the developper is required to implement the API startAppsFlyer(Context context) in order to start the SDK. 74 |
If you are using CMP to collect consent data this feature is needed. See explanation [here](#dma_support). 75 | ### Example: 76 | 77 | ```java 78 | AppsflyerIntegration.setManualMode(true); 79 | ``` 80 | 81 | And to start the AppsFlyer SDK, use `void startAppsFlyer(Context context)` API. 82 | 83 | ### Example: 84 | 85 | ```java 86 | protected void onCreate(Bundle savedInstanceState) { 87 | AppsflyerIntegration.startAppsFlyer(this); 88 | } 89 | ``` 90 | 91 | 92 | ###
93 | 94 | # Setting up the SDK 95 | 96 | #### 2.1) Adding the Plugin to your Project 97 | 98 | Add the AppsFlyer Segment Integration dependency to your app `build.gradle` file. 99 | ```java 100 | implementation 'com.appsflyer:segment-android-integration:6.15.0' 101 | implementation 'com.android.installreferrer:installreferrer:2.1' 102 | ``` 103 | 104 | #### 2.2) Setting the Required Permissions 105 | 106 | The AndroidManifest.xml should include the following permissions: 107 | 108 | ```xml 109 | 110 | 111 | 112 | ``` 113 | 114 | In v6.8.0 of the AppsFlyer SDK, we added the normal permission com.google.android.gms.permission.AD_ID to the SDK's AndroidManifest, 115 | to allow the SDK to collect the Android Advertising ID on apps targeting API 33. 116 | If your app is targeting children, you may need to revoke this permission to comply with Google's Data policy. 117 | You can read more about it [here](https://support.appsflyer.com/hc/en-us/articles/7569900844689). 118 | 119 | Starting from **6.14.0** Huawei Referrer integration was updated. [Learn more](https://dev.appsflyer.com/hc/docs/install-android-sdk#huawei-install-referrer). 120 | 121 | ### 2.2) Init AppsFlyer 122 | 123 | ```java 124 | 125 | static final String SEGMENT_WRITE_KEY = ""; 126 | 127 | @Override 128 | protected void onCreate(Bundle savedInstanceState) { 129 | super.onCreate(savedInstanceState); 130 | setContentView(R.layout.activity_main); 131 | 132 | Analytics.Builder builder = new Analytics.Builder(this , SEGMENT_WRITE_KEY) 133 | .use(AppsflyerIntegration.FACTORY) 134 | 135 | ... 136 | (optional) 137 | 138 | .logLevel(Analytics.LogLevel.VERBOSE) 139 | .recordScreenViews() 140 | .trackApplicationLifecycleEvents() // Application Opened , Application Updated , Application Installed events 141 | .build(); 142 | 143 | Analytics.setSingletonInstance(builder.build()); 144 | 145 | } 146 | ``` 147 | 148 | Adding `.trackApplicationLifecycleEvents()` will send `Application Opened` , `Application Updated` and `Application Installed` events to AppsFlyer. 149 | 150 | 151 | 152 | 153 | ## In-app events 154 | 155 | When you call `track`, Segment translates it automatically and sends the event to AppsFlyer. 156 | 157 | Segment includes all the event properties as callback parameters on the AppsFlyer event, and automatically translates `properties.revenue` to the appropriate AppsFlyer purchase event properties based on Segment's spec’d properties. 158 | 159 | Finally, Segment automatically uses AppsFlyer’s transactionId-based de-duplication when sending an an `orderId`. 160 | 161 | 162 | Purchase Event Example: 163 | ```java 164 | Map eventValue = new HashMap(); 165 | eventValue.put("productId","com.test.id"); 166 | eventValue.put("revenue","1.00"); 167 | eventValue.put("currency","USD"); 168 | 169 | Analytics analytics = Analytics.with(this); 170 | Properties properties = new Properties(); 171 | properties.putAll(eventValue); 172 | 173 | analytics.track("purchase", properties); 174 | ``` 175 | 176 | Note: AppsFlyer will map `revenue -> af_revenue` and `currency -> af_currency`. 177 | 178 | Check out the Segment docs on track [here](https://segment.com/docs/spec/track/). 179 | 180 | 181 | ### 182 | 183 | ## Identify 184 | 185 | 186 | When you `identify` a user, that user’s information is passed to AppsFlyer with `customer user Id` as AppsFlyer’s External User ID. Segment’s special traits recognized as AppsFlyer’s standard user profile fields (in parentheses) are: 187 | 188 | `customerUserId` (`Customer User Id`)
189 | `currencyCode` (`Currency Code`) 190 | 191 | All other traits will be sent to AppsFlyer as custom attributes. 192 | 193 | ```java 194 | Analytics analytics = Analytics.with(this); 195 | 196 | analytics.identify("a user's id", new Traits() 197 | .putName("a user's name") 198 | .putEmail("maxim@appsflyer.com"), 199 | null); 200 | ``` 201 | 202 | Check out the Segment docs on indentify [here](https://segment.com/docs/spec/identify/). 203 | 204 | ###
205 | 206 | ## Get Conversion Data 207 | 208 | For Conversion data your should call the method below. 209 | 210 | ```java 211 | AppsflyerIntegration.conversionListener = new AppsflyerIntegration.ExternalAppsFlyerConversionListener() { 212 | @Override 213 | public void onConversionDataSuccess(Map map) { 214 | // Process Deferred Deep Linking here 215 | for (String attrName : map.keySet()) { 216 | Log.d(TAG, "attribute: " + attrName + " = " + map.get(attrName)); 217 | } 218 | } 219 | 220 | @Override 221 | public void onConversionDataFail(String s) { 222 | 223 | } 224 | 225 | @Override 226 | public void onAppOpenAttribution(Map map) { 227 | // Process Direct Deep Linking here 228 | for (String attrName : map.keySet()) { 229 | Log.d(TAG, "attribute: " + attrName + " = " + map.get(attrName)); 230 | } 231 | } 232 | 233 | @Override 234 | public void onAttributionFailure(String s) { 235 | 236 | } 237 | }; 238 | ``` 239 | 240 | In order for Conversion Data to be sent to Segment, make sure you have enabled "Track Attribution Data" in AppsFlyer destination settings: 241 | 242 | Xnip2019-05-11_19-19-31 243 | 244 | ### 245 | 246 | ## Unified deep linking 247 | In order to implement unified deep linking, call the method below : 248 | 249 | ```java 250 | AppsflyerIntegration.deepLinkListener = new AppsflyerIntegration.ExternalDeepLinkListener() { 251 | @Override 252 | public void onDeepLinking(@NonNull DeepLinkResult deepLinkResult) { 253 | //TODO handle deep link logic 254 | } 255 | }; 256 | ``` 257 | For more information about unified deep linking, check [here](https://dev.appsflyer.com/docs/android-unified-deep-linking) 258 | 259 | ## Send consent for DMA compliance 260 | For a general introduction to DMA consent data, see [here](https://dev.appsflyer.com/hc/docs/send-consent-for-dma-compliance). 261 | The SDK offers two alternative methods for gathering consent data:
262 | - **Through a Consent Management Platform (CMP)**: If the app uses a CMP that complies with the [Transparency and Consent Framework (TCF) v2.2 protocol](https://iabeurope.eu/tcf-supporting-resources/), the SDK can automatically retrieve the consent details.
263 |
OR

264 | - **Through a dedicated SDK API**: Developers can pass Google's required consent data directly to the SDK using a specific API designed for this purpose. 265 | ### Use CMP to collect consent data 266 | A CMP compatible with TCF v2.2 collects DMA consent data and stores it in SharedPreferences. To enable the SDK to access this data and include it with every event, follow these steps:
267 |
    268 |
  1. Call AppsFlyerLib.getInstance().enableTCFDataCollection(true) to instruct the SDK to collect the TCF data from the device. 269 |
  2. Set the the adapter to be manual : AppsflyerIntegration.setManualMode(true).
    This will allow us to delay the Conversion call in order to provide the SDK with the user consent. 270 |
  3. Initialize Segment using AppsflyerIntegration.FACTORY. 271 |
  4. In the Activity class, use the CMP to decide if you need the consent dialog in the current session. 272 |
  5. If needed, show the consent dialog, using the CMP, to capture the user consent decision. Otherwise, go to step 6. 273 |
  6. Get confirmation from the CMP that the user has made their consent decision, and the data is available in SharedPreferences. 274 |
  7. Call AppsflyerIntegration.startAppsFlyer(this) 275 |
276 | 277 | #### Application class 278 | ``` kotlin 279 | @Override public void onCreate() { 280 | super.onCreate(); 281 | AppsFlyerLib.getInstance().enableTCFDataCollection(true); 282 | AppsflyerIntegration.setManualMode(true); 283 | initSegmentAnalytics(); 284 | } 285 | 286 | private void initSegmentAnalytics() { 287 | Analytics.Builder builder = new Analytics.Builder(this, SEGMENT_WRITE_KEY) 288 | .use(AppsflyerIntegration.FACTORY) 289 | .logLevel(Analytics.LogLevel.VERBOSE) 290 | .trackApplicationLifecycleEvents() // Enable this to record certain application events automatically! 291 | .recordScreenViews(); // Enable this to record screen views automatically! 292 | // Set the initialized instance as a globally accessible instance. 293 | Analytics.setSingletonInstance(builder.build()); 294 | } 295 | ``` 296 | #### Activity class 297 | ```kotlin 298 | public class MainActivity extends AppCompatActivity { 299 | 300 | private boolean consentRequired = true; 301 | @Override 302 | protected void onCreate(Bundle savedInstanceState) { 303 | super.onCreate(savedInstanceState); 304 | setContentView(R.layout.activity_main); 305 | if (consentRequired) 306 | initConsentCollection(); 307 | else 308 | AppsflyerIntegration.startAppsFlyer(this); 309 | } 310 | 311 | private void initConsentCollection() { 312 | // Implement here the you CMP flow 313 | // When the flow is completed and consent was collected 314 | // call onConsentCollectionFinished() 315 | } 316 | 317 | private void onConsentCollectionFinished() { 318 | AppsflyerIntegration.startAppsFlyer(this); 319 | } 320 | } 321 | ``` 322 | 323 | 324 | ### Manually collect consent data 325 | If your app does not use a CMP compatible with TCF v2.2, use the SDK API detailed below to provide the consent data directly to the SDK. 326 |
    327 |
  1. Initialize AppsFlyerIntegration using manual mode and also Analytics. This will allow us to delay the Conversion call in order to provide the SDK with the user consent. 328 |
  2. In the Activity class, determine whether the GDPR applies or not to the user.
    329 | - If GDPR applies to the user, perform the following: 330 |
      331 |
    1. Given that GDPR is applicable to the user, determine whether the consent data is already stored for this session. 332 |
        333 |
      1. If there is no consent data stored, show the consent dialog to capture the user consent decision. 334 |
      2. If there is consent data stored continue to the next step. 335 |
      336 |
    2. To transfer the consent data to the SDK create an object called AppsFlyerConsent using the forGDPRUser() method with the following parameters:
      337 | - hasConsentForDataUsage - Indicates whether the user has consented to use their data for advertising purposes.
      338 | - hasConsentForAdsPersonalization - Indicates whether the user has consented to use their data for personalized advertising purposes. 339 |
    3. Call AppsFlyerLib.getInstance().setConsentData() with the AppsFlyerConsent object. 340 |
    4. Call AppsflyerIntegration.startAppsFlyer(this). 341 |

    342 | - If GDPR doesn’t apply to the user perform the following: 343 |
      344 |
    1. Create an AppsFlyerConsent object using the forNonGDPRUser() method. This method doesn’t accept any parameters. 345 |
    2. Call AppsFlyerLib.getInstance().setConsentData() with the AppsFlyerConsent object. 346 |
    3. Call AppsflyerIntegration.startAppsFlyer(this). 347 |
    348 |
349 | 350 | ###
351 | 352 | ## Sample App 353 |

AppsFlyer has created a sample Android application that integrates AppsFlyer via Segment. Check it out at the Github repo.

354 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdk 33 5 | 6 | defaultConfig { 7 | minSdk 19 8 | targetSdk 33 9 | versionCode 1 10 | versionName "1.0" 11 | testApplicationId "com.example.test" 12 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | compileOptions { 21 | sourceCompatibility JavaVersion.VERSION_1_8 22 | targetCompatibility JavaVersion.VERSION_1_8 23 | } 24 | 25 | testOptions { 26 | unitTests { 27 | all { 28 | testLogging { 29 | exceptionFormat = "full" 30 | events "PASSED", "FAILED", "SKIPPED" 31 | } 32 | forkEvery 1 33 | } 34 | includeAndroidResources = true 35 | returnDefaultValues = true 36 | } 37 | } 38 | } 39 | 40 | dependencies { 41 | api 'com.appsflyer:af-android-sdk:6.15.0' 42 | compileOnly 'com.segment.analytics.android:analytics:4.+' 43 | compileOnly 'com.android.installreferrer:installreferrer:2.2' 44 | 45 | testImplementation 'androidx.test:core:1.6.1' 46 | testImplementation 'androidx.test.ext:junit:1.2.1' 47 | testImplementation 'com.android.installreferrer:installreferrer:2.2' 48 | 49 | testImplementation 'junit:junit:4.13.2' 50 | testImplementation 'org.mockito:mockito-core:4.2.0' 51 | testImplementation 'org.robolectric:robolectric:4.9.2' 52 | 53 | testImplementation 'com.segment.analytics.android:analytics:4.+' 54 | testImplementation 'com.segment.analytics.android:analytics-tests:4.+' 55 | } 56 | 57 | tasks.withType(Test) { 58 | testLogging { 59 | exceptionFormat "full" 60 | events "started", "skipped", "passed", "failed" 61 | showStandardStreams true 62 | } 63 | } 64 | 65 | apply from:file("publish.gradle") 66 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/shacharaharon/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/publish.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven-publish' 2 | apply plugin: 'signing' 3 | 4 | def isReleaseBuild() { 5 | return !VERSION_NAME.contains("SNAPSHOT") 6 | } 7 | 8 | def getReleaseRepositoryUrl() { 9 | return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL 10 | : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" 11 | } 12 | 13 | def getSnapshotRepositoryUrl() { 14 | return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL 15 | : "https://oss.sonatype.org/content/repositories/snapshots/" 16 | } 17 | 18 | task androidJavadocs(type: Javadoc) { 19 | exclude "**/*.orig" // exclude files created by source control 20 | source = android.sourceSets.main.java.srcDirs 21 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 22 | failOnError false 23 | } 24 | 25 | task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { 26 | archiveClassifier.set("javadoc") 27 | 28 | from androidJavadocs.destinationDir 29 | } 30 | 31 | task androidSourcesJar(type: Jar) { 32 | archiveClassifier.set("sources") 33 | 34 | from android.sourceSets.main.java.source 35 | } 36 | 37 | def logger(log) { 38 | println log 39 | } 40 | 41 | def configurePom(pom) { 42 | logger("configurePom") 43 | pom.name = POM_NAME 44 | pom.packaging = POM_PACKAGING 45 | pom.description = POM_DESCRIPTION 46 | pom.url = POM_URL 47 | 48 | pom.scm { 49 | url = POM_SCM_URL 50 | connection = POM_SCM_CONNECTION 51 | developerConnection = POM_SCM_DEV_CONNECTION 52 | } 53 | 54 | pom.licenses { 55 | license { 56 | name = POM_LICENCE_NAME 57 | url = POM_LICENCE_URL 58 | distribution = POM_LICENCE_DIST 59 | } 60 | } 61 | 62 | pom.developers { 63 | developer { 64 | id = POM_DEVELOPER_ID 65 | name = POM_DEVELOPER_NAME 66 | } 67 | } 68 | } 69 | 70 | afterEvaluate { 71 | publishing { 72 | publications { 73 | release(MavenPublication) { 74 | logger("release") 75 | // The coordinates of the library, being set from variables that 76 | // we'll set up in a moment 77 | groupId GROUP 78 | artifactId POM_ARTIFACT_ID 79 | version VERSION_NAME 80 | 81 | // Two artifacts, the `aar` and the sources 82 | // artifact("$buildDir/outputs/aar/${project.getName()}-release.aar") 83 | artifact bundleReleaseAar 84 | artifact androidSourcesJar 85 | artifact androidJavadocsJar 86 | 87 | // Self-explanatory metadata for the most part 88 | pom { 89 | configurePom(pom) 90 | // A slight fix so that the generated POM will include any transitive dependencies 91 | // that the library builds upon 92 | withXml { 93 | def dependenciesNode = asNode().appendNode('dependencies') 94 | 95 | project.configurations.implementation.allDependencies.each { 96 | def dependencyNode = dependenciesNode.appendNode('dependency') 97 | dependencyNode.appendNode('groupId', it.group) 98 | dependencyNode.appendNode('artifactId', it.name) 99 | dependencyNode.appendNode('version', it.version) 100 | } 101 | } 102 | } 103 | } 104 | } 105 | repositories { 106 | maven { 107 | name = "sonatype" 108 | 109 | // You only need this if you want to publish snapshots, otherwise just set the URL 110 | // to the release repo directly 111 | url = isReleaseBuild() ? getReleaseRepositoryUrl() : getSnapshotRepositoryUrl() 112 | 113 | credentials(PasswordCredentials) { 114 | username = getSonatypeRepositoryToken() 115 | password = getSonatypeRepositoryTokenPassword() 116 | } 117 | } 118 | } 119 | } 120 | } 121 | 122 | signing { 123 | logger("signing") 124 | sign publishing.publications 125 | } 126 | 127 | publish.dependsOn build 128 | publishToMavenLocal.dependsOn build -------------------------------------------------------------------------------- /app/src/androidTest/java/com/segment/analytics/android/integrations/appsflyer/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | //package com.segment.analytics.android.integration.appsflyer; 2 | // 3 | //import android.app.Application; 4 | //import android.test.ApplicationTestCase; 5 | // 6 | ///** 7 | // * Testing Fundamentals 8 | // */ 9 | //public class ApplicationTest extends ApplicationTestCase { 10 | // public ApplicationTest() { 11 | // super(Application.class); 12 | // } 13 | //} -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/java/com/segment/analytics/android/integrations/appsflyer/AppsflyerIntegration.java: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.android.integrations.appsflyer; 2 | 3 | 4 | import android.app.Activity; 5 | import android.app.Application; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.content.SharedPreferences; 9 | import android.os.Bundle; 10 | import android.util.Log; 11 | 12 | import androidx.annotation.NonNull; 13 | 14 | import com.appsflyer.AFInAppEventParameterName; 15 | import com.appsflyer.AFLogger; 16 | import com.appsflyer.AppsFlyerConversionListener; 17 | import com.appsflyer.AppsFlyerLib; 18 | import com.appsflyer.deeplink.DeepLinkListener; 19 | import com.appsflyer.deeplink.DeepLinkResult; 20 | import com.appsflyer.internal.platform_extension.Plugin; 21 | import com.appsflyer.internal.platform_extension.PluginInfo; 22 | import com.segment.analytics.Analytics; 23 | import com.segment.analytics.Properties; 24 | import com.segment.analytics.ValueMap; 25 | import com.segment.analytics.integrations.IdentifyPayload; 26 | import com.segment.analytics.integrations.Integration; 27 | import com.segment.analytics.integrations.Logger; 28 | import com.segment.analytics.integrations.TrackPayload; 29 | 30 | import java.util.Collections; 31 | import java.util.LinkedHashMap; 32 | import java.util.Map; 33 | 34 | import static com.segment.analytics.internal.Utils.transform; 35 | 36 | /** 37 | * Created by shacharaharon on 12/04/2016. 38 | */ 39 | public class AppsflyerIntegration extends Integration { 40 | 41 | static final String AF_SEGMENT_SHARED_PREF = "appsflyer-segment-data"; 42 | static final String CONV_KEY = "AF_onConversion_Data"; 43 | 44 | static final Map MAPPER; 45 | 46 | private static final String APPSFLYER_KEY = "AppsFlyer"; 47 | private static final String SEGMENT_REVENUE = "revenue"; 48 | private static final String SEGMENT_CURRENCY = "currency"; 49 | public static ExternalAppsFlyerConversionListener conversionListener; 50 | public static ExternalDeepLinkListener deepLinkListener; 51 | 52 | /** 53 | * Responsible to map revenue -> af_revenue , currency -> af_currency 54 | */ 55 | static { 56 | Map mapper = new LinkedHashMap<>(); 57 | mapper.put(SEGMENT_REVENUE, AFInAppEventParameterName.REVENUE); 58 | mapper.put(SEGMENT_CURRENCY, AFInAppEventParameterName.CURRENCY); 59 | MAPPER = Collections.unmodifiableMap(mapper); 60 | } 61 | 62 | final Logger logger; 63 | final AppsFlyerLib appsflyer; 64 | final String appsFlyerDevKey; 65 | final boolean isDebug; 66 | private Context context; 67 | private String customerUserId; 68 | private String currencyCode; 69 | 70 | public static void setManualMode(Boolean manualMode) { 71 | AppsflyerIntegration.manualMode = manualMode; 72 | } 73 | 74 | public static void startAppsFlyer(@NonNull Context context){ 75 | if (context != null){ 76 | AppsFlyerLib.getInstance().start(context); 77 | } 78 | } 79 | 80 | public static Boolean manualMode = false; 81 | public static ConversionListenerDisplay cld; 82 | public static final Factory FACTORY = new Integration.Factory() { 83 | @Override 84 | public Integration create(ValueMap settings, Analytics analytics) { 85 | if(settings == null || analytics == null){ 86 | return null; 87 | } 88 | Logger logger = analytics.logger(APPSFLYER_KEY); 89 | AppsFlyerLib afLib = AppsFlyerLib.getInstance(); 90 | 91 | String devKey = settings.getString("appsFlyerDevKey"); 92 | boolean trackAttributionData = settings.getBoolean("trackAttributionData", false); 93 | Application application = analytics.getApplication(); 94 | 95 | AppsFlyerConversionListener listener = null; 96 | if (trackAttributionData) { 97 | listener = new ConversionListener(analytics); 98 | } 99 | 100 | AppsFlyerLib.getInstance().setPluginInfo(new PluginInfo(Plugin.SEGMENT,"6.15.0")); 101 | afLib.setDebugLog(logger.logLevel != Analytics.LogLevel.NONE); 102 | afLib.init(devKey, listener, application.getApplicationContext()); 103 | if (deepLinkListener != null) 104 | AppsFlyerLib.getInstance().subscribeForDeepLink( deepLinkListener); 105 | logger.verbose("AppsFlyer.getInstance().start(%s, %s)", application, devKey.substring(0, 1) + "*****" + devKey.substring(devKey.length() - 2)); 106 | 107 | 108 | // RD-34040 109 | boolean isReact = true; 110 | // Check if Segment React Native integration with AppsFLyer is used 111 | try { 112 | Class.forName("com.segment.analytics.reactnative.integration.appsflyer.RNAnalyticsIntegration_AppsFlyerModule"); 113 | } catch (ClassNotFoundException e) { 114 | // Segment React Native integration with AppsFLyer is NOT used 115 | isReact = false; 116 | } 117 | // Segment React Native integration with AppsFLyer is used, we need to send first launch manually 118 | if(isReact){ 119 | afLib.start(application, devKey); 120 | logger.verbose("Segment React Native AppsFlye rintegration is used, sending first launch manually"); 121 | } 122 | return new AppsflyerIntegration(application, logger, afLib, devKey); 123 | } 124 | 125 | @Override 126 | public String key() { 127 | return APPSFLYER_KEY; 128 | } 129 | 130 | }; 131 | 132 | public AppsflyerIntegration(Context context, Logger logger, AppsFlyerLib afLib, String devKey) { 133 | this.context = context; 134 | this.logger = logger; 135 | this.appsflyer = afLib; 136 | this.appsFlyerDevKey = devKey; 137 | this.isDebug = (logger.logLevel != Analytics.LogLevel.NONE); 138 | } 139 | 140 | @Override 141 | public void onActivityCreated(Activity activity, Bundle savedInstanceState) { 142 | super.onActivityCreated(activity, savedInstanceState); 143 | Intent intent = activity.getIntent(); 144 | if (!manualMode) { 145 | AppsFlyerLib.getInstance().start(activity); 146 | updateEndUserAttributes(); 147 | } 148 | } 149 | 150 | @Override 151 | public AppsFlyerLib getUnderlyingInstance() { 152 | return appsflyer; 153 | } 154 | 155 | @Override 156 | public void identify(IdentifyPayload identify) { 157 | super.identify(identify); 158 | 159 | customerUserId = identify.userId(); 160 | currencyCode = identify.traits().getString("currencyCode"); 161 | 162 | if (appsflyer != null) { 163 | updateEndUserAttributes(); 164 | } else { 165 | logger.verbose("couldn't update 'Identify' attributes"); 166 | } 167 | } 168 | 169 | private void updateEndUserAttributes() { 170 | appsflyer.setCustomerUserId(customerUserId); 171 | logger.verbose("appsflyer.setCustomerUserId(%s)", customerUserId); 172 | appsflyer.setCurrencyCode(currencyCode); 173 | logger.verbose("appsflyer.setCurrencyCode(%s)", currencyCode); 174 | appsflyer.setDebugLog(isDebug); 175 | logger.verbose("appsflyer.setDebugLog(%s)", isDebug); 176 | } 177 | 178 | @Override 179 | public void track(TrackPayload track) { 180 | String event = track.event(); 181 | Properties properties = track.properties(); 182 | 183 | Map afProperties = transform(properties, MAPPER); 184 | 185 | appsflyer.logEvent(context, event, afProperties); 186 | logger.verbose("appsflyer.logEvent(context, %s, %s)", event, properties); 187 | } 188 | 189 | @Deprecated 190 | public interface ConversionListenerDisplay { 191 | void display(Map attributionData); 192 | } 193 | 194 | public interface ExternalAppsFlyerConversionListener extends AppsFlyerConversionListener {} 195 | public interface ExternalDeepLinkListener extends DeepLinkListener {} 196 | 197 | static class ConversionListener implements AppsFlyerConversionListener { 198 | final Analytics analytics; 199 | 200 | public ConversionListener(Analytics analytics) { 201 | this.analytics = analytics; 202 | } 203 | 204 | @Override 205 | public void onConversionDataSuccess(Map conversionData) { 206 | 207 | if (!getFlag(CONV_KEY)) { 208 | trackInstallAttributed(conversionData); 209 | setFlag(CONV_KEY, true); 210 | } 211 | 212 | 213 | if (cld != null) { 214 | conversionData.put("type", "onInstallConversionData"); 215 | cld.display(conversionData); 216 | } 217 | 218 | if (conversionListener != null) { 219 | conversionListener.onConversionDataSuccess(conversionData); 220 | } 221 | } 222 | 223 | @Override 224 | public void onConversionDataFail(String errorMessage) { 225 | if (conversionListener != null) { 226 | conversionListener.onConversionDataFail(errorMessage); 227 | } 228 | } 229 | 230 | @Override 231 | public void onAppOpenAttribution(Map attributionData) { 232 | 233 | if (cld != null) { 234 | attributionData.put("type", "onAppOpenAttribution"); 235 | cld.display(attributionData); 236 | } 237 | 238 | if (conversionListener != null) { 239 | conversionListener.onAppOpenAttribution(attributionData); 240 | } 241 | } 242 | 243 | @Override 244 | public void onAttributionFailure(String errorMessage) { 245 | if (conversionListener != null) { 246 | conversionListener.onAttributionFailure(errorMessage); 247 | } 248 | } 249 | 250 | private Object getFromAttr(Object value) { 251 | return (value != null) ? value : ""; 252 | } 253 | 254 | void trackInstallAttributed(Map attributionData) { 255 | // See https://segment.com/docs/spec/mobile/#install-attributed. 256 | Map campaign = new ValueMap() // 257 | .putValue("source", getFromAttr(attributionData.get("media_source"))) 258 | .putValue("name", getFromAttr(attributionData.get("campaign"))) 259 | .putValue("ad_group", getFromAttr(attributionData.get("adgroup"))); 260 | 261 | 262 | Properties properties = new Properties().putValue("provider", "AppsFlyer"); 263 | properties.putAll(attributionData); 264 | // Remove properties set in campaign. 265 | properties.remove("media_source"); 266 | properties.remove("adgroup"); 267 | 268 | 269 | // replace original campaign with new created 270 | properties.putValue("campaign", campaign); 271 | 272 | 273 | // If you are working with networks that don't allow passing user level data to 3rd parties, 274 | // you will need to apply code to filter out these networks before calling 275 | // `analytics.track("Install Attributed", properties);` 276 | analytics.track("Install Attributed", properties); 277 | } 278 | 279 | private boolean getFlag(final String key) { 280 | 281 | Context context = getContext(); 282 | 283 | if (context == null) { 284 | return false; 285 | } 286 | 287 | SharedPreferences sharedPreferences = context.getSharedPreferences(AF_SEGMENT_SHARED_PREF, 0); 288 | return sharedPreferences.getBoolean(key, false); 289 | } 290 | 291 | private void setFlag(final String key, final boolean value) { 292 | 293 | Context context = getContext(); 294 | 295 | if (context == null) { 296 | return; 297 | } 298 | 299 | SharedPreferences sharedPreferences = context.getSharedPreferences(AF_SEGMENT_SHARED_PREF, 0); 300 | android.content.SharedPreferences.Editor editor = sharedPreferences.edit(); 301 | editor.putBoolean(key, value); 302 | editorCommit(editor); 303 | } 304 | 305 | private void editorCommit(SharedPreferences.Editor editor) { 306 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.GINGERBREAD) { 307 | editor.apply(); 308 | } else { 309 | editor.commit(); 310 | } 311 | } 312 | 313 | private Context getContext() { 314 | return this.analytics.getApplication().getApplicationContext(); 315 | } 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /app/src/test/java/com/segment/analytics/android/integrations/appsflyer/AppsflyerIntegrationConversionListenerTests.java: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.android.integrations.appsflyer; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.mockito.Mockito; 7 | import android.app.Application; 8 | import android.content.Context; 9 | import android.content.SharedPreferences; 10 | import androidx.test.ext.junit.runners.AndroidJUnit4; 11 | import com.segment.analytics.Analytics; 12 | import com.segment.analytics.Properties; 13 | import com.segment.analytics.ValueMap; 14 | import static org.mockito.Mockito.*; 15 | import java.lang.reflect.Method; 16 | import java.util.HashMap; 17 | import java.util.Map; 18 | 19 | @RunWith(AndroidJUnit4.class) 20 | public class AppsflyerIntegrationConversionListenerTests { 21 | 22 | @Test 23 | public void testAppsflyerIntegration_ConversionListener_ctor_happyFlow() { 24 | Analytics analytics = mock(Analytics.class); 25 | AppsflyerIntegration.ConversionListener conversionListener = new AppsflyerIntegration.ConversionListener(analytics); 26 | 27 | Assert.assertEquals(conversionListener.analytics, analytics); 28 | 29 | reset(analytics); 30 | } 31 | 32 | @Test 33 | public void testAppsflyerIntegration_ConversionListener_ctor_nullFlow() { 34 | Analytics analytics = null; 35 | AppsflyerIntegration.ConversionListener conversionListener = new AppsflyerIntegration.ConversionListener(analytics); 36 | 37 | Assert.assertEquals(conversionListener.analytics, analytics); 38 | } 39 | 40 | @Test 41 | public void testAppsflyerIntegration_ConversionListener_onConversionDataSuccess_happyFlow() { 42 | //I want just to check the conversionListener gets the map. 43 | AppsflyerIntegration.conversionListener = mock(AppsflyerIntegration.ExternalAppsFlyerConversionListener.class); 44 | Analytics analytics = mock(Analytics.class); 45 | Map conversionData = new ValueMap(); 46 | Application app = mock(Application.class); 47 | Context context = mock(Context.class); 48 | SharedPreferences sharedPreferences = mock(SharedPreferences.class); 49 | when(analytics.getApplication()).thenReturn(app); 50 | when(app.getApplicationContext()).thenReturn(context); 51 | when(context.getSharedPreferences("appsflyer-segment-data",0)).thenReturn(sharedPreferences); 52 | when(sharedPreferences.getBoolean("AF_onConversion_Data",false)).thenReturn(true); 53 | AppsflyerIntegration.ConversionListener conversionListener = new AppsflyerIntegration.ConversionListener(analytics); 54 | 55 | conversionListener.onConversionDataSuccess(conversionData); 56 | 57 | verify(AppsflyerIntegration.conversionListener).onConversionDataSuccess(conversionData); 58 | 59 | reset(AppsflyerIntegration.conversionListener,analytics,app,context,sharedPreferences); 60 | } 61 | 62 | @Test 63 | public void testAppsflyerIntegration_ConversionListener_onAttributionFailure_happyFlow() { 64 | AppsflyerIntegration.conversionListener = mock(AppsflyerIntegration.ExternalAppsFlyerConversionListener.class); 65 | Analytics analytics = Mockito.mock(Analytics.class); 66 | AppsflyerIntegration.ConversionListener conversionListener = new AppsflyerIntegration.ConversionListener(analytics); 67 | String errorMsg = "error - test"; 68 | 69 | conversionListener.onAttributionFailure(errorMsg); 70 | 71 | verify(AppsflyerIntegration.conversionListener,times(1)).onAttributionFailure(errorMsg); 72 | 73 | reset(analytics,AppsflyerIntegration.conversionListener); 74 | } 75 | 76 | @Test 77 | public void testAppsflyerIntegration_ConversionListener_onAttributionFailure_nullFlow() { 78 | AppsflyerIntegration.conversionListener = mock(AppsflyerIntegration.ExternalAppsFlyerConversionListener.class); 79 | Analytics analytics = Mockito.mock(Analytics.class); 80 | AppsflyerIntegration.ConversionListener conversionListener = new AppsflyerIntegration.ConversionListener(analytics); 81 | String errorMsg = null; 82 | conversionListener.onAttributionFailure(errorMsg); 83 | verify(AppsflyerIntegration.conversionListener,times(1)).onAttributionFailure(null); 84 | 85 | reset(analytics,AppsflyerIntegration.conversionListener); 86 | } 87 | 88 | @Test 89 | public void testAppsflyerIntegration_ConversionListener_trackInstallAttributed_happyFlow() { 90 | Analytics analytics =mock(Analytics.class); 91 | Map attributionData = new HashMap<>(); 92 | attributionData.put("media_source", "media_source_moris"); 93 | attributionData.put("campaign", "campaign_moris"); 94 | attributionData.put("adgroup", "adgroup_moris"); 95 | 96 | Map campaign = new ValueMap() 97 | .putValue("source", attributionData.get("media_source")) 98 | .putValue("name", attributionData.get("campaign")) 99 | .putValue("ad_group", attributionData.get("adgroup")); 100 | Properties properties = new Properties().putValue("provider", "AppsFlyer"); 101 | properties.putAll(attributionData); 102 | properties.remove("media_source"); 103 | properties.remove("adgroup"); 104 | properties.putValue("campaign", campaign); 105 | AppsflyerIntegration.ConversionListener conversionListener = new AppsflyerIntegration.ConversionListener(analytics); 106 | 107 | conversionListener.trackInstallAttributed(attributionData); 108 | 109 | verify(analytics,times(1)).track("Install Attributed", properties); 110 | 111 | reset(analytics); 112 | } 113 | 114 | @Test 115 | public void testAppsflyerIntegration_ConversionListener_trackInstallAttributed_negativeFlow() { 116 | Analytics analytics =mock(Analytics.class); 117 | Map attributionData = new HashMap(); 118 | Map campaign = new ValueMap() // 119 | .putValue("source", "") 120 | .putValue("name", "") 121 | .putValue("ad_group", ""); 122 | Properties properties = new Properties().putValue("provider", "AppsFlyer"); 123 | properties.putAll(attributionData); 124 | properties.remove("media_source"); 125 | properties.remove("adgroup"); 126 | properties.putValue("campaign", campaign); 127 | AppsflyerIntegration.ConversionListener conversionListener = new AppsflyerIntegration.ConversionListener(analytics); 128 | conversionListener.trackInstallAttributed(attributionData); 129 | 130 | verify(analytics,times(1)).track("Install Attributed", properties); 131 | 132 | reset(analytics); 133 | } 134 | 135 | @Test 136 | public void testAppsflyerIntegration_ConversionListener_getFlag_happyFlow() throws Exception { 137 | String key="key"; 138 | Analytics analytics = mock(Analytics.class); 139 | Application app = mock(Application.class); 140 | Context context = mock(Context.class); 141 | SharedPreferences sharedPreferences = mock(SharedPreferences.class); 142 | when(analytics.getApplication()).thenReturn(app); 143 | when(app.getApplicationContext()).thenReturn(context); 144 | when(context.getSharedPreferences("appsflyer-segment-data",0)).thenReturn(sharedPreferences); 145 | when(sharedPreferences.getBoolean(key,false)).thenReturn(true); 146 | AppsflyerIntegration.ConversionListener conversionListener = new AppsflyerIntegration.ConversionListener(analytics); 147 | 148 | boolean resBoolean = (Boolean) TestHelper.getPrivateMethodForObjectReadyToInvoke("getFlag",String.class).invoke(conversionListener,key); 149 | 150 | Assert.assertTrue(resBoolean); 151 | 152 | reset(analytics,app,context,sharedPreferences); 153 | } 154 | 155 | @Test 156 | public void testAppsflyerIntegration_ConversionListener_setFlag_happyFlow() throws Exception { 157 | String key="key"; 158 | boolean value=true; 159 | Analytics analytics = mock(Analytics.class); 160 | Application app = mock(Application.class); 161 | Context context = mock(Context.class); 162 | SharedPreferences sharedPreferences = mock(SharedPreferences.class); 163 | SharedPreferences.Editor editor = mock(SharedPreferences.Editor.class); 164 | when(analytics.getApplication()).thenReturn(app); 165 | when(app.getApplicationContext()).thenReturn(context); 166 | when(context.getSharedPreferences("appsflyer-segment-data",0)).thenReturn(sharedPreferences); 167 | when(sharedPreferences.edit()).thenReturn(editor); 168 | AppsflyerIntegration.ConversionListener conversionListener = new AppsflyerIntegration.ConversionListener(analytics); 169 | 170 | TestHelper.getPrivateMethodForObjectReadyToInvoke("setFlag",String.class,boolean.class).invoke(conversionListener,key,value); 171 | 172 | if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.GINGERBREAD){ 173 | verify(editor,times(1)).apply(); 174 | } 175 | else{ 176 | verify(editor,times(1)).commit(); 177 | } 178 | 179 | reset(analytics,app,context,sharedPreferences); 180 | } 181 | 182 | @Test 183 | public void testAppsflyerIntegration_ConversionListener_getContext_happyFlow() throws Exception { 184 | Analytics analytics = mock(Analytics.class); 185 | Application app = mock(Application.class); 186 | Context context = mock(Context.class); 187 | when(analytics.getApplication()).thenReturn(app); 188 | when(app.getApplicationContext()).thenReturn(context); 189 | AppsflyerIntegration.ConversionListener conversionListener = new AppsflyerIntegration.ConversionListener(analytics); 190 | 191 | Context resContext = (Context) TestHelper.getPrivateMethodForObjectReadyToInvoke("getContext").invoke(conversionListener); 192 | 193 | Assert.assertEquals(resContext, context); 194 | 195 | reset(analytics,app,context); 196 | } 197 | 198 | @Test 199 | public void testAppsflyerIntegration_ConversionListener_getContext_nullFlow() throws Exception{ 200 | Analytics analytics = mock(Analytics.class); 201 | Application app = mock(Application.class); 202 | Context context = null; 203 | when(analytics.getApplication()).thenReturn(app); 204 | when(app.getApplicationContext()).thenReturn(context); 205 | AppsflyerIntegration.ConversionListener conversionListener = new AppsflyerIntegration.ConversionListener(analytics); 206 | 207 | Context resContext = (Context) TestHelper.getPrivateMethodForObjectReadyToInvoke("getContext").invoke(conversionListener); 208 | 209 | Assert.assertEquals(resContext, context); 210 | 211 | reset(analytics,app); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /app/src/test/java/com/segment/analytics/android/integrations/appsflyer/AppsflyerIntegrationTests.java: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.android.integrations.appsflyer; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.mockito.ArgumentCaptor; 7 | import org.mockito.MockedStatic; 8 | import org.mockito.Mockito; 9 | import android.app.Application; 10 | import android.content.Context; 11 | import androidx.test.ext.junit.runners.AndroidJUnit4; 12 | import com.appsflyer.AFInAppEventParameterName; 13 | import com.appsflyer.AppsFlyerLib; 14 | import com.segment.analytics.Analytics; 15 | import com.segment.analytics.Properties; 16 | import com.segment.analytics.Traits; 17 | import com.segment.analytics.ValueMap; 18 | import com.segment.analytics.integrations.IdentifyPayload; 19 | import com.segment.analytics.integrations.Integration; 20 | import com.segment.analytics.integrations.Logger; 21 | import com.segment.analytics.integrations.TrackPayload; 22 | import static org.mockito.Mockito.*; 23 | import java.lang.reflect.Field; 24 | import java.lang.reflect.Method; 25 | import java.lang.reflect.Type; 26 | import java.util.Map; 27 | 28 | @RunWith(AndroidJUnit4.class) 29 | public class AppsflyerIntegrationTests { 30 | private TestHelper testHelper = new TestHelper(); 31 | 32 | @Test 33 | public void testAppsflyerIntegration_ctor_happyFlow() throws Exception { 34 | Context context = mock(Context.class); 35 | Logger logger = new Logger("test", Analytics.LogLevel.INFO); 36 | AppsFlyerLib appsflyer = mock(AppsFlyerLib.class); 37 | String appsflyerDevKey = "appsflyerDevKey"; 38 | boolean isDebug = logger.logLevel != Analytics.LogLevel.NONE; 39 | 40 | AppsflyerIntegration appsflyerIntegration = new AppsflyerIntegration(context,logger,appsflyer,appsflyerDevKey); 41 | Assert.assertEquals(appsflyerIntegration.isDebug , isDebug); 42 | Assert.assertEquals(appsflyerIntegration.appsFlyerDevKey, appsflyerDevKey); 43 | Assert.assertEquals(appsflyerIntegration.appsflyer, appsflyer); 44 | Assert.assertEquals(appsflyerIntegration.logger, logger); 45 | Context contextInappsflyerIntegration = (Context) TestHelper.getPrivateFieldForObject("context",AppsflyerIntegration.class,appsflyerIntegration); 46 | Assert.assertEquals(contextInappsflyerIntegration, context); 47 | // checking the static clause 48 | Assert.assertEquals(AppsflyerIntegration.MAPPER.get("revenue"), AFInAppEventParameterName.REVENUE); 49 | Assert.assertEquals(AppsflyerIntegration.MAPPER.get("currency"), AFInAppEventParameterName.CURRENCY); 50 | 51 | reset(context,appsflyer); 52 | } 53 | 54 | @Test 55 | public void testAppsflyerIntegration_setManualMode_happyFlow() { 56 | Assert.assertFalse(AppsflyerIntegration.manualMode); 57 | AppsflyerIntegration.setManualMode(true); 58 | Assert.assertTrue(AppsflyerIntegration.manualMode); 59 | AppsflyerIntegration.setManualMode(false); 60 | Assert.assertFalse(AppsflyerIntegration.manualMode); 61 | } 62 | 63 | @Test 64 | public void testAppsflyerIntegration_startAppsFlyer_happyFlow() { 65 | AppsFlyerLib appsFlyerLib = testHelper.mockAppsflyerLib(); 66 | Context context = mock(Context.class); 67 | 68 | AppsflyerIntegration.startAppsFlyer(context); 69 | 70 | verify(appsFlyerLib).start(context); 71 | 72 | reset(appsFlyerLib,context); 73 | testHelper.closeMockAppsflyerLib(); 74 | } 75 | 76 | @Test 77 | public void testAppsflyerIntegration_startAppsFlyer_nilFlow() { 78 | AppsFlyerLib appsFlyerLib = testHelper.mockAppsflyerLib(); 79 | 80 | AppsflyerIntegration.startAppsFlyer(null); 81 | 82 | verify(appsFlyerLib,never()).start(any()); 83 | 84 | reset(appsFlyerLib); 85 | testHelper.closeMockAppsflyerLib(); 86 | } 87 | 88 | @Test 89 | public void testAppsflyerIntegration_FACTORYCreate_happyFlow() { 90 | AppsFlyerLib appsFlyerLib = testHelper.mockAppsflyerLib(); 91 | Analytics analytics = mock(Analytics.class); 92 | ValueMap settings = new ValueMap(); 93 | settings.put("appsFlyerDevKey" , "devKey"); 94 | settings.put("trackAttributionData" , true); 95 | Logger logger = new Logger("test", Analytics.LogLevel.INFO); 96 | Mockito.when(analytics.logger("AppsFlyer")).thenReturn(logger); 97 | Application app = mock(Application.class); 98 | Mockito.when(analytics.getApplication()).thenReturn(app); 99 | AppsflyerIntegration.deepLinkListener = mock(AppsflyerIntegration.ExternalDeepLinkListener.class); 100 | 101 | Integration integration= (Integration) AppsflyerIntegration.FACTORY.create(settings,analytics); 102 | 103 | verify(appsFlyerLib).setDebugLog(logger.logLevel!=Analytics.LogLevel.NONE); 104 | ArgumentCaptor captorListener = ArgumentCaptor.forClass(AppsflyerIntegration.ConversionListener.class); 105 | ArgumentCaptor captorDevKey = ArgumentCaptor.forClass(String.class); 106 | ArgumentCaptor captorContext = ArgumentCaptor.forClass(Context.class); 107 | verify(appsFlyerLib).init(captorDevKey.capture(), captorListener.capture() , captorContext.capture()); 108 | Assert.assertNotNull(captorListener.getValue()); 109 | Assert.assertEquals(captorDevKey.getValue(), settings.getString("appsFlyerDevKey")); 110 | Assert.assertEquals(captorContext.getValue(), app.getApplicationContext()); 111 | verify(appsFlyerLib).subscribeForDeepLink(AppsflyerIntegration.deepLinkListener); 112 | 113 | reset(appsFlyerLib,analytics,app,AppsflyerIntegration.deepLinkListener); 114 | testHelper.closeMockAppsflyerLib(); 115 | } 116 | 117 | @Test 118 | public void testAppsflyerIntegration_FACTORYCreate_nilFlow() { 119 | Analytics analytics = null; 120 | ValueMap settings = null; 121 | 122 | Integration integration= (Integration) AppsflyerIntegration.FACTORY.create(settings,analytics); 123 | 124 | Assert.assertNull(integration); 125 | } 126 | 127 | @Test 128 | public void testAppsflyerIntegration_FACTORYKEY_happyFlow() { 129 | Assert.assertEquals(AppsflyerIntegration.FACTORY.key(),"AppsFlyer"); 130 | } 131 | 132 | @Test 133 | public void testAppsflyerIntegration_getUnderlyingInstance_happyFlow() { 134 | AppsFlyerLib appsFlyerLib = mock(AppsFlyerLib.class); 135 | Logger logger = new Logger("test", Analytics.LogLevel.INFO); 136 | AppsflyerIntegration appsflyerIntegration = new AppsflyerIntegration(null,logger,appsFlyerLib,null); 137 | 138 | Assert.assertEquals(appsflyerIntegration.getUnderlyingInstance(),appsFlyerLib); 139 | 140 | reset(appsFlyerLib); 141 | } 142 | 143 | @Test 144 | public void testAppsflyerIntegration_identify_happyFlow() throws Exception { 145 | AppsFlyerLib appsFlyerLib = mock(AppsFlyerLib.class); 146 | Logger logger = spy(new Logger("test", Analytics.LogLevel.INFO)); 147 | AppsflyerIntegration appsflyerIntegration = new AppsflyerIntegration(null,logger,appsFlyerLib,null); 148 | IdentifyPayload identifyPayload = mock(IdentifyPayload.class); 149 | Traits traits = mock(Traits.class); 150 | when(identifyPayload.userId()).thenReturn("moris"); 151 | when(identifyPayload.traits()).thenReturn(traits); 152 | when(traits.getString("currencyCode")).thenReturn("ILS"); 153 | 154 | appsflyerIntegration.identify(identifyPayload); 155 | 156 | verify(logger, never()).verbose(any()); 157 | String customerUserIdInappsflyerIntegration = (String) TestHelper.getPrivateFieldForObject("customerUserId",AppsflyerIntegration.class,appsflyerIntegration); 158 | Assert.assertEquals(customerUserIdInappsflyerIntegration, "moris"); 159 | String currencyCodeInappsflyerIntegration = (String) TestHelper.getPrivateFieldForObject("currencyCode",AppsflyerIntegration.class,appsflyerIntegration); 160 | Assert.assertEquals(currencyCodeInappsflyerIntegration, "ILS"); 161 | 162 | reset(appsFlyerLib,identifyPayload,traits); 163 | } 164 | 165 | @Test 166 | public void testAppsflyerIntegration_identify_nilflow() { 167 | Logger logger = spy(new Logger("test", Analytics.LogLevel.INFO)); 168 | AppsflyerIntegration appsflyerIntegration = new AppsflyerIntegration(null,logger,null,null); 169 | IdentifyPayload identifyPayload = mock(IdentifyPayload.class); 170 | Traits traits = mock(Traits.class); 171 | when(identifyPayload.traits()).thenReturn(traits); 172 | 173 | appsflyerIntegration.identify(identifyPayload); 174 | 175 | verify(logger, times(1)).verbose("couldn't update 'Identify' attributes"); 176 | 177 | reset(identifyPayload,traits); 178 | } 179 | 180 | @Test 181 | public void testAppsflyerIntegration_updateEndUserAttributes_happyflow() throws Exception { 182 | AppsFlyerLib appsFlyerLib = mock(AppsFlyerLib.class); 183 | Logger logger = spy(new Logger("test", Analytics.LogLevel.INFO)); 184 | AppsflyerIntegration appsflyerIntegration = new AppsflyerIntegration(null,logger,appsFlyerLib,null); 185 | Method updateEndUserAttributes = AppsflyerIntegration.class.getDeclaredMethod("updateEndUserAttributes"); 186 | updateEndUserAttributes.setAccessible(true); 187 | TestHelper.setPrivateFieldForObject("customerUserId",AppsflyerIntegration.class,appsflyerIntegration,String.class,"Moris"); 188 | TestHelper.setPrivateFieldForObject("currencyCode",AppsflyerIntegration.class,appsflyerIntegration,String.class,"ILS"); 189 | updateEndUserAttributes.invoke(appsflyerIntegration); 190 | 191 | verify(logger, times(1)).verbose("appsflyer.setCustomerUserId(%s)", "Moris"); 192 | verify(logger, times(1)).verbose("appsflyer.setCurrencyCode(%s)", "ILS"); 193 | verify(logger, times(1)).verbose("appsflyer.setDebugLog(%s)", true); 194 | 195 | reset(appsFlyerLib); 196 | } 197 | 198 | @Test 199 | public void testAppsflyerIntegration_track_happyflow() throws Exception { 200 | AppsFlyerLib appsFlyerLib = mock(AppsFlyerLib.class); 201 | Logger logger = spy(new Logger("test", Analytics.LogLevel.INFO)); 202 | AppsflyerIntegration appsflyerIntegration = new AppsflyerIntegration(null,logger,appsFlyerLib,null); 203 | TrackPayload trackPayload = mock(TrackPayload.class); 204 | String event = "event"; 205 | Properties properties= mock(Properties.class); 206 | Map afProperties = mock(Map.class); 207 | MockedStatic staticUtils = mockStatic(com.segment.analytics.internal.Utils.class); 208 | when(trackPayload.event()).thenReturn(event); 209 | when(trackPayload.properties()).thenReturn(properties); 210 | staticUtils.when(()->com.segment.analytics.internal.Utils.transform(any(),any())).thenReturn(afProperties); 211 | 212 | appsflyerIntegration.track(trackPayload); 213 | 214 | Context contextInAppsflyerIntegration = (Context) TestHelper.getPrivateFieldForObject("context",AppsflyerIntegration.class,appsflyerIntegration); 215 | verify(appsFlyerLib, times(1)).logEvent(contextInAppsflyerIntegration,event,afProperties); 216 | verify(logger, times(1)).verbose("appsflyer.logEvent(context, %s, %s)", event, properties); 217 | 218 | reset(appsFlyerLib,trackPayload,properties,afProperties); 219 | staticUtils.close(); 220 | } 221 | } -------------------------------------------------------------------------------- /app/src/test/java/com/segment/analytics/android/integrations/appsflyer/TestHelper.java: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.android.integrations.appsflyer; 2 | 3 | import static org.mockito.Mockito.mock; 4 | import static org.mockito.Mockito.mockStatic; 5 | 6 | import com.appsflyer.AppsFlyerLib; 7 | 8 | import org.mockito.MockedStatic; 9 | 10 | import java.lang.reflect.Field; 11 | import java.lang.reflect.Method; 12 | 13 | public class TestHelper { 14 | MockedStatic staticAppsFlyerLib; 15 | public AppsFlyerLib mockAppsflyerLib(){ 16 | this.staticAppsFlyerLib = mockStatic(AppsFlyerLib.class); 17 | AppsFlyerLib appsFlyerLib = mock(AppsFlyerLib.class); 18 | this.staticAppsFlyerLib.when(AppsFlyerLib::getInstance).thenReturn(appsFlyerLib); 19 | return appsFlyerLib; 20 | } 21 | public void closeMockAppsflyerLib(){ 22 | this.staticAppsFlyerLib.close(); 23 | } 24 | 25 | public static Object getPrivateFieldForObject(String fieldName, Class classObject, Object objToGetValueFrom) throws Exception{ 26 | Field field = classObject.getDeclaredField(fieldName); 27 | field.setAccessible(true); 28 | return field.get(classObject.cast(objToGetValueFrom)); 29 | } 30 | 31 | public static void setPrivateFieldForObject(String fieldName, Class classObject, Object objToGetValueFrom, Class valueClass, Object value) throws Exception{ 32 | Field field = classObject.getDeclaredField(fieldName); 33 | field.setAccessible(true); 34 | field.set(objToGetValueFrom,valueClass.cast(value)); 35 | } 36 | 37 | public static Method getPrivateMethodForObjectReadyToInvoke(String funcName,Class... parameterTypesForMethod) throws Exception{ 38 | Method getFlagMethod = AppsflyerIntegration.ConversionListener.class.getDeclaredMethod(funcName,parameterTypesForMethod); 39 | getFlagMethod.setAccessible(true); 40 | return getFlagMethod; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline 2 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | jcenter() 5 | google() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:7.2.2' 9 | classpath "io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.21.2" 10 | } 11 | } 12 | apply plugin: 'io.codearte.nexus-staging' 13 | 14 | def getSonatypeRepositoryToken() { return hasProperty('ossrhToken') ? ossrhToken : "" } 15 | 16 | def getSonatypeRepositoryTokenPassword() { return hasProperty('ossrhTokenPassword') ? ossrhTokenPassword : "" } 17 | 18 | nexusStaging { 19 | packageGroup = GROUP // optional if packageGroup == project.getGroup() 20 | username = getSonatypeRepositoryToken() 21 | password = getSonatypeRepositoryTokenPassword() 22 | delayBetweenRetriesInMillis = 30000 23 | numberOfRetries = 120 24 | } 25 | allprojects { 26 | repositories { 27 | mavenCentral() 28 | google() 29 | } 30 | } 31 | task clean(type: Delete) { delete rootProject.buildDir } 32 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | GROUP=com.appsflyer 21 | 22 | VERSION_CODE=19 23 | VERSION_NAME=6.15.0 24 | POM_ARTIFACT_ID=segment-android-integration 25 | POM_PACKAGING=aar 26 | 27 | POM_NAME=AppsFlyer Integration 28 | POM_DESCRIPTION=AppsFlyer Integration for Segment Android Analytics 29 | 30 | POM_URL=http://bitbucket.org/appsflyer/appsflyer-android-sdk 31 | POM_SCM_URL=http://bitbucket.org/appsflyer/appsflyer-android-sdk 32 | POM_SCM_CONNECTION=scm:hg:http://bitbucket.org/appsflyer/appsflyer-android-sdk 33 | POM_SCM_DEV_CONNECTION=scm:hg:https://bitbucket.org/appsflyer/appsflyer-android-sdk 34 | 35 | POM_LICENCE_NAME=The MIT License (MIT) 36 | POM_LICENCE_URL=http://opensource.org/licenses/MIT 37 | POM_LICENCE_DIST=repo 38 | 39 | POM_DEVELOPER_ID=appsflyer 40 | POM_DEVELOPER_NAME=AppsFlyer, Inc. 41 | 42 | android.useAndroidX=true 43 | android.enableJetifier=true 44 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppsFlyerSDK/appsflyer-segment-android-plugin/0908b7cd8384beb54f4cd3d1d9422f18c985881b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jan 10 15:43:55 IST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /segmenttestapp/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /segmenttestapp/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdk 34 5 | 6 | defaultConfig { 7 | applicationId "com.appsflyer.segment.app" 8 | minSdk 19 9 | targetSdk 34 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | compileOptions { 20 | sourceCompatibility JavaVersion.VERSION_1_8 21 | targetCompatibility JavaVersion.VERSION_1_8 22 | } 23 | } 24 | 25 | dependencies { 26 | implementation project(path: ':app') 27 | implementation 'com.appsflyer:af-android-sdk:6.15.0' 28 | implementation 'com.android.support:appcompat-v7:28.0.0' 29 | implementation 'com.segment.analytics.android:analytics:4.+' 30 | implementation 'com.android.installreferrer:installreferrer:2.2' 31 | 32 | testImplementation 'junit:junit:4.13.2' 33 | } 34 | -------------------------------------------------------------------------------- /segmenttestapp/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "120680937670", 4 | "firebase_url": "https://segment-test-app.firebaseio.com", 5 | "project_id": "segment-test-app", 6 | "storage_bucket": "segment-test-app.appspot.com" 7 | }, 8 | "client": [ 9 | { 10 | "client_info": { 11 | "mobilesdk_app_id": "1:120680937670:android:65a6d394bf3bcd02", 12 | "android_client_info": { 13 | "package_name": "com.appsflyer.segmenttestapp" 14 | } 15 | }, 16 | "oauth_client": [ 17 | { 18 | "client_id": "120680937670-0dluiq1o8rrosif894ik85fns32ebmgh.apps.googleusercontent.com", 19 | "client_type": 3 20 | } 21 | ], 22 | "api_key": [ 23 | { 24 | "current_key": "" 25 | } 26 | ], 27 | "services": { 28 | "analytics_service": { 29 | "status": 1 30 | }, 31 | "appinvite_service": { 32 | "status": 1, 33 | "other_platform_oauth_client": [] 34 | }, 35 | "ads_service": { 36 | "status": 2 37 | } 38 | } 39 | }, 40 | { 41 | "client_info": { 42 | "mobilesdk_app_id": "1:120680937670:android:6c184fc9ff32cd55", 43 | "android_client_info": { 44 | "package_name": "com.appsflyer.segment.app" 45 | } 46 | }, 47 | "oauth_client": [ 48 | { 49 | "client_id": "120680937670-0dluiq1o8rrosif894ik85fns32ebmgh.apps.googleusercontent.com", 50 | "client_type": 3 51 | } 52 | ], 53 | "api_key": [ 54 | { 55 | "current_key": "" 56 | } 57 | ], 58 | "services": { 59 | "analytics_service": { 60 | "status": 1 61 | }, 62 | "appinvite_service": { 63 | "status": 1, 64 | "other_platform_oauth_client": [] 65 | }, 66 | "ads_service": { 67 | "status": 2 68 | } 69 | } 70 | } 71 | ], 72 | "configuration_version": "1" 73 | } -------------------------------------------------------------------------------- /segmenttestapp/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/shacharaharon/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /segmenttestapp/src/androidTest/java/com/appsflyer/app/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.appsflyer.segment.app; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /segmenttestapp/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /segmenttestapp/src/main/java/com/appsflyer/segment/app/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.appsflyer.segment.app; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import androidx.appcompat.app.AppCompatActivity; 7 | import android.util.Log; 8 | import android.view.Gravity; 9 | import android.view.KeyEvent; 10 | import android.view.LayoutInflater; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | import android.view.inputmethod.EditorInfo; 14 | import android.widget.BaseAdapter; 15 | import android.widget.Button; 16 | import android.widget.EditText; 17 | import android.widget.LinearLayout; 18 | import android.widget.ListView; 19 | import android.widget.RelativeLayout; 20 | import android.widget.TextView; 21 | 22 | import com.segment.analytics.Analytics; 23 | import com.segment.analytics.Properties; 24 | import com.segment.analytics.android.integrations.appsflyer.AppsflyerIntegration; 25 | 26 | 27 | import java.util.ArrayList; 28 | import java.util.LinkedHashMap; 29 | import java.util.List; 30 | import java.util.Map; 31 | 32 | public class MainActivity extends AppCompatActivity implements View.OnClickListener { 33 | 34 | private static final String TAG = "AppsFlyer-Segment"; 35 | private KeyValueAdapter adapter; 36 | private EditText eventNameET; 37 | private EditText keyET; 38 | private EditText valueET; 39 | 40 | @Override 41 | protected void onCreate(Bundle savedInstanceState) { 42 | super.onCreate(savedInstanceState); 43 | setContentView(R.layout.activity_main); 44 | initListView(); 45 | findViewById(R.id.button_add).setOnClickListener(this); 46 | findViewById(R.id.track_button).setOnClickListener(this); 47 | eventNameET = (EditText) findViewById(R.id.event_name_editText); 48 | keyET = (EditText) findViewById(R.id.key_text_editText); 49 | valueET = (EditText) findViewById(R.id.value_text_editText); 50 | valueET.setOnEditorActionListener(new TextView.OnEditorActionListener() { 51 | @Override 52 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 53 | if (actionId == EditorInfo.IME_ACTION_DONE) { 54 | adapter.addItem(keyET.getText().toString(), valueET.getText().toString()); 55 | keyET.requestFocus(); 56 | } 57 | return false; 58 | } 59 | }); 60 | 61 | Log.d(TAG, "AppsFlyer's Segment Integration TestApp is now initializing.."); 62 | 63 | initConversionListener(); 64 | Log.d(TAG, "Done!"); 65 | } 66 | 67 | 68 | 69 | private void initConversionListener() { 70 | AppsflyerIntegration.conversionListener = new AppsflyerIntegration.ExternalAppsFlyerConversionListener() { 71 | @Override 72 | public void onConversionDataSuccess(Map map) { 73 | for (String attrName : map.keySet()) { 74 | Log.d(TAG, "attribute: " + attrName + " = " + map.get(attrName)); 75 | //SCREEN VALUES// 76 | //noinspection StringBufferReplaceableByString 77 | StringBuilder sb = new StringBuilder(); 78 | sb.append("callbackType: ").append("Conversion Data\n"); 79 | sb.append("Install Type: ").append(map.get("af_status")).append("\n"); 80 | sb.append("Media Source: ").append(map.get("media_source")).append("\n"); 81 | sb.append("Click Time(GMT): ").append(map.get("click_time")).append("\n"); 82 | sb.append("Install Time(GMT): ").append(map.get("install_time")).append("\n"); 83 | final String conversionDataString = sb.toString(); 84 | runOnUiThread(new Runnable() { 85 | public void run() { 86 | TextView conversionTextView = (TextView) findViewById(R.id.conversionDataTextView); 87 | if (conversionTextView != null) { 88 | conversionTextView.setGravity(Gravity.CENTER_VERTICAL); 89 | conversionTextView.setText(conversionDataString); 90 | } else { 91 | Log.d(TAG,"Could not load conversion data"); 92 | } 93 | } 94 | }); 95 | } 96 | } 97 | 98 | @Override 99 | public void onConversionDataFail(String s) { 100 | 101 | } 102 | 103 | @Override 104 | public void onAppOpenAttribution(Map map) { 105 | for (String attrName : map.keySet()) { 106 | Log.d(TAG, "attribute: " + attrName + " = " + map.get(attrName)); 107 | } 108 | } 109 | 110 | @Override 111 | public void onAttributionFailure(String s) { 112 | 113 | } 114 | }; 115 | 116 | 117 | 118 | } 119 | 120 | 121 | 122 | private void initListView() { 123 | adapter = new KeyValueAdapter(); 124 | ((ListView) findViewById(R.id.listView_items)).setAdapter(adapter); 125 | } 126 | 127 | @Override 128 | public void onClick(View view) { 129 | if (view.getId() == R.id.track_button) { 130 | String eventName = eventNameET.getText().toString(); 131 | 132 | Analytics analytics = Analytics.with(this); 133 | 134 | if (!eventName.equals("")) { 135 | Properties properties = new Properties(); 136 | properties.putAll(adapter.mData); 137 | analytics.track(eventName, properties); 138 | } else { 139 | analytics.track("Testing attribution!"); 140 | } 141 | } else if (view.getId() == R.id.button_add) { 142 | adapter.addItem(keyET.getText().toString(), valueET.getText().toString()); 143 | } else { 144 | Log.w(TAG,"Unknown button ID. Check you're implementation."); 145 | 146 | } 147 | } 148 | 149 | public void deleteClickHandler(View v) { 150 | RelativeLayout vwParentRow = (RelativeLayout) v.getParent(); 151 | LinearLayout keyValContainer = (LinearLayout) vwParentRow.getChildAt(0); 152 | String key = ((TextView) keyValContainer.getChildAt(0)).getText().toString(); 153 | adapter.mData.remove(key); 154 | adapter.mKeys.remove(key); 155 | adapter.notifyDataSetChanged(); 156 | } 157 | 158 | public static class ViewHolder { 159 | TextView keyTextView; 160 | TextView valueTextView; 161 | Button deleteButton; 162 | } 163 | 164 | private class KeyValueAdapter extends BaseAdapter { 165 | 166 | Map mData = new LinkedHashMap<>(); 167 | private List mKeys; 168 | private LayoutInflater mInflater; 169 | 170 | @SuppressWarnings("unused") 171 | public KeyValueAdapter(LinkedHashMap data) { 172 | mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); 173 | mData = data; 174 | mKeys = new ArrayList<>(mData.keySet()); 175 | } 176 | 177 | KeyValueAdapter() { 178 | mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); 179 | mKeys = new ArrayList<>(mData.keySet()); 180 | } 181 | 182 | void addItem(String key, String value) { 183 | mData.put(key, value); 184 | if (!mKeys.contains(key)) { 185 | mKeys.add(key); 186 | } 187 | notifyDataSetChanged(); 188 | keyET.setText(""); 189 | valueET.setText(""); 190 | } 191 | 192 | @Override 193 | public int getCount() { 194 | return mData.size(); 195 | } 196 | 197 | @Override 198 | public String getItem(int position) { 199 | return mKeys.get(position); 200 | } 201 | 202 | @Override 203 | public long getItemId(int position) { 204 | return position; 205 | } 206 | 207 | @Override 208 | public View getView(int position, View convertView, ViewGroup parent) { 209 | System.out.println("getView " + position + " " + convertView); 210 | ViewHolder holder; 211 | if (convertView == null) { 212 | convertView = mInflater.inflate(R.layout.item1, parent, false); //http://stackoverflow.com/questions/14978296/unable-to-start-activityunsupportedoperationexception-addviewview-layoutpara 213 | holder = new ViewHolder(); 214 | holder.keyTextView = (TextView) convertView.findViewById(R.id.key_text_item); 215 | holder.valueTextView = (TextView) convertView.findViewById(R.id.value_text_item); 216 | holder.deleteButton = (Button) convertView.findViewById(R.id.remove_button); 217 | convertView.setTag(holder); 218 | } else { 219 | holder = (ViewHolder) convertView.getTag(); 220 | } 221 | holder.keyTextView.setText(mKeys.get(position)); 222 | holder.valueTextView.setText(mData.get(mKeys.get(position))); 223 | return convertView; 224 | } 225 | 226 | } 227 | 228 | } 229 | -------------------------------------------------------------------------------- /segmenttestapp/src/main/java/com/appsflyer/segment/app/SampleApplication.java: -------------------------------------------------------------------------------- 1 | package com.appsflyer.segment.app; 2 | 3 | import android.app.Application; 4 | import android.util.Log; 5 | 6 | import com.segment.analytics.Analytics; 7 | import com.segment.analytics.Traits; 8 | import com.segment.analytics.android.integrations.appsflyer.AppsflyerIntegration; 9 | 10 | 11 | import java.util.Map; 12 | 13 | //https://segment.com/docs/spec/identify/ 14 | //https://segment.com/docs/sources/mobile/android/ 15 | public class SampleApplication extends Application { 16 | 17 | 18 | static final String SEGMENT_WRITE_KEY = ""; 19 | static final String TAG = "SEG_AF"; 20 | 21 | @Override public void onCreate() { 22 | super.onCreate(); 23 | 24 | initSegmentAnalytics(); 25 | 26 | Analytics analytics = Analytics.with(this); 27 | 28 | analytics.onIntegrationReady("Segment.io", new Analytics.Callback() { 29 | @Override public void onReady(Object instance) { 30 | Log.d(TAG, "Segment integration ready."); 31 | } 32 | }); 33 | 34 | analytics.identify("a user's id", new Traits() 35 | .putName("a user's name") 36 | .putEmail("maxim@appsflyer.com"), 37 | null); 38 | 39 | 40 | } 41 | 42 | private void initSegmentAnalytics() { 43 | Analytics.Builder builder = new Analytics.Builder(this, SEGMENT_WRITE_KEY) 44 | .use(AppsflyerIntegration.FACTORY) 45 | .logLevel(Analytics.LogLevel.VERBOSE) 46 | .trackApplicationLifecycleEvents() // Enable this to record certain application events automatically! 47 | .recordScreenViews(); // Enable this to record screen views automatically! 48 | 49 | 50 | // Set the initialized instance as a globally accessible instance. 51 | Analytics.setSingletonInstance(builder.build()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /segmenttestapp/src/main/res/drawable/appsflyer_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppsFlyerSDK/appsflyer-segment-android-plugin/0908b7cd8384beb54f4cd3d1d9422f18c985881b/segmenttestapp/src/main/res/drawable/appsflyer_logo.png -------------------------------------------------------------------------------- /segmenttestapp/src/main/res/drawable/segment_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppsFlyerSDK/appsflyer-segment-android-plugin/0908b7cd8384beb54f4cd3d1d9422f18c985881b/segmenttestapp/src/main/res/drawable/segment_logo.png -------------------------------------------------------------------------------- /segmenttestapp/src/main/res/layout-v11/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 25 | 26 | 35 | 36 | 46 | 47 | 53 | 54 | 58 | 59 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 88 | 89 | 98 | 99 | 100 | 101 | 102 |