├── .github └── workflows │ ├── uitests.yaml │ └── uitests_saucelabs.yaml ├── .gitignore ├── README.md ├── analytics ├── .gitignore ├── build.gradle ├── google-services.json ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── appodealstack │ │ └── demo │ │ └── analytics │ │ ├── AnalyticsActivity.kt │ │ ├── AnalyticsViewModel.kt │ │ └── BillingUseCase.kt │ └── res │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ └── activity_analytics.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ └── network_security_config.xml ├── banner ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── appodealstack │ │ └── demo │ │ └── banner │ │ └── BannerActivity.kt │ └── res │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ └── activity_banner.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── values │ ├── strings.xml │ └── themes.xml │ └── xml │ └── network_security_config.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── interstitial ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── appodealstack │ │ └── demo │ │ └── interstitial │ │ └── InterstitialActivity.kt │ └── res │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ └── activity_interstitial.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── values │ ├── strings.xml │ └── themes.xml │ └── xml │ └── network_security_config.xml ├── mrec ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── appodealstack │ │ └── demo │ │ └── mrec │ │ └── MrecActivity.kt │ └── res │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ └── activity_mrec.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── values │ ├── strings.xml │ └── themes.xml │ └── xml │ └── network_security_config.xml ├── native ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── appodealstack │ │ └── demo │ │ └── nativead │ │ ├── NativeActivity.kt │ │ ├── NativeListFragment.kt │ │ └── adapter │ │ ├── DiffUtils.kt │ │ ├── ListItem.kt │ │ └── NativeListAdapter.kt │ └── res │ ├── drawable │ ├── ic_launcher_background.xml │ └── native_custom_round_outline.xml │ ├── layout │ ├── activity_native.xml │ ├── native_ad_view_custom.xml │ ├── native_list_fragment.xml │ └── your_data_item.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ └── network_security_config.xml ├── rewarded ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── appodealstack │ │ └── demo │ │ └── rewarded │ │ └── RewardedActivity.kt │ └── res │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ └── activity_rewarded.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── values │ ├── strings.xml │ └── themes.xml │ └── xml │ └── network_security_config.xml └── settings.gradle /.github/workflows/uitests.yaml: -------------------------------------------------------------------------------- 1 | name: UITest 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | device: 7 | required: true 8 | default: 'Pixel8_API33' 9 | type: choice 10 | options: 11 | - 'Pixel8_API33' 12 | - 'Pixel8_API35' 13 | runner: 14 | required: true 15 | default: 'autotestdebug' 16 | type: choice 17 | options: 18 | - 'autotestdebug' 19 | - 'ubuntu-latest' 20 | 21 | jobs: 22 | build: 23 | name: build android application for ui tests 24 | runs-on: ${{ github.event.inputs.runner || 'autotestdebug' }} 25 | timeout-minutes: 30 26 | steps: 27 | - name: Print Env Variables 28 | run: env 29 | working-directory: ${{ github.workspace }} 30 | 31 | - name: checkout source code of application 32 | uses: actions/checkout@v4 33 | with: 34 | clean: true 35 | path: 'appodeal-android-sdk' 36 | 37 | - name: Set up JDK 17 38 | uses: actions/setup-java@v4 39 | with: 40 | java-version: '17' 41 | distribution: 'temurin' 42 | 43 | - name: Build with Gradle 44 | working-directory: ${{ github.workspace }}/appodeal-android-sdk 45 | run: ./gradlew :banner:assembleDebug 46 | 47 | - name: save debug build for aws 48 | uses: actions/upload-artifact@v4 49 | with: 50 | name: banner-debug.apk 51 | path: appodeal-android-sdk/banner/build/outputs/apk/debug/banner-debug.apk 52 | retention-days: 14 53 | 54 | tests: 55 | name: run ui tests on aws with appium 56 | needs: build 57 | runs-on: ${{ github.event.inputs.runner || 'autotestdebug' }} 58 | timeout-minutes: 30 59 | steps: 60 | - name: Print Env Variables 61 | run: env 62 | working-directory: ${{ github.workspace }} 63 | 64 | - name: checkout source code of application 65 | uses: actions/checkout@v4 66 | with: 67 | path: 'SDK-Auto-Test' 68 | clean: true 69 | repository: 'appodeal/SDK-Auto-Test' 70 | ref: 'aws' 71 | token: ${{ secrets.UITESTREPOACCESS }} 72 | 73 | - name: Set up JDK 11 74 | uses: actions/setup-java@v4 75 | with: 76 | java-version: '11' 77 | distribution: 'temurin' 78 | 79 | - name: download debug build 80 | uses: actions/download-artifact@v4 81 | with: 82 | name: banner-debug.apk 83 | path: ./SDK-Auto-Test/apk 84 | 85 | - name: prepare build 86 | working-directory: ${{ github.workspace }}/SDK-Auto-Test 87 | run: | 88 | mvn clean 89 | sleep 10 90 | mvn jar:jar 91 | sleep 10 92 | mvn jar:test-jar 93 | sleep 10 94 | mvn assembly:assembly -DskipTests -Ddescriptor=src/main/assembly/zip.xml 95 | 96 | - name: Configure AWS credentials 97 | uses: aws-actions/configure-aws-credentials@v4 98 | with: 99 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 100 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 101 | aws-region: us-west-2 102 | 103 | - name: Upload APK to AWS Device Farm 104 | id: upload-apk 105 | run: | 106 | APP_UPLOAD_RESPONSE=$(aws devicefarm create-upload --project-arn arn:aws:devicefarm:us-west-2:381491970378:project:4c28c1e5-8344-4d34-919c-a1e9377d3b2f --name banner-debug.apk --type ANDROID_APP) 107 | APP_ARN=$(echo $APP_UPLOAD_RESPONSE | jq -r .upload.arn) 108 | echo "APP_ARN=$APP_ARN" >> $GITHUB_ENV 109 | APP_URL=$(echo $APP_UPLOAD_RESPONSE | jq -r .upload.url) 110 | curl -T ./SDK-Auto-Test/apk/banner-debug.apk "$APP_URL" 111 | # Wait until the upload is processed 112 | while [[ "$(aws devicefarm get-upload --arn $APP_ARN | jq -r '.upload.status')" != "SUCCEEDED" ]]; do 113 | echo "Waiting for APK upload to complete..." 114 | sleep 10 115 | done 116 | 117 | - name: Upload Test Package to AWS Device Farm 118 | id: upload-tests 119 | run: | 120 | TEST_PACKAGE_UPLOAD_RESPONSE=$(aws devicefarm create-upload --project-arn arn:aws:devicefarm:us-west-2:381491970378:project:4c28c1e5-8344-4d34-919c-a1e9377d3b2f --name zip-with-dependencies.zip --type APPIUM_JAVA_TESTNG_TEST_PACKAGE) 121 | TEST_PACKAGE_ARN=$(echo $TEST_PACKAGE_UPLOAD_RESPONSE | jq -r .upload.arn) 122 | echo "TEST_PACKAGE_ARN=$TEST_PACKAGE_ARN" >> $GITHUB_ENV 123 | TEST_PACKAGE_URL=$(echo $TEST_PACKAGE_UPLOAD_RESPONSE | jq -r .upload.url) 124 | curl -T ./SDK-Auto-Test/target/zip-with-dependencies.zip $TEST_PACKAGE_URL 125 | # Wait until the upload is processed 126 | while [[ "$(aws devicefarm get-upload --arn $TEST_PACKAGE_ARN | jq -r '.upload.status')" != "SUCCEEDED" ]]; do 127 | echo "Waiting for Test Package upload to complete..." 128 | sleep 10 129 | done 130 | 131 | - name: Schedule Device Farm Automated Test 132 | id: run-test 133 | uses: aws-actions/aws-devicefarm-mobile-device-testing@v2.3 134 | with: 135 | run-settings-json: | 136 | { 137 | "name": "GitHubAction-${{ github.workflow }}_${{ github.run_id }}_${{ github.run_attempt }}", 138 | "projectArn": "arn:aws:devicefarm:us-west-2:381491970378:project:4c28c1e5-8344-4d34-919c-a1e9377d3b2f", 139 | "appArn": "${{ env.APP_ARN }}", 140 | "devicePoolArn": "arn:aws:devicefarm:us-west-2:381491970378:devicepool:4c28c1e5-8344-4d34-919c-a1e9377d3b2f/86ebd86a-2150-4997-b71f-2e3d72510e0d", 141 | "test": { 142 | "type": "APPIUM_JAVA_TESTNG", 143 | "testPackageArn": "${{ env.TEST_PACKAGE_ARN }}", 144 | "testSpecArn": "default.yml" 145 | } 146 | } 147 | artifact-types: ALL 148 | 149 | - uses: actions/upload-artifact@v4 150 | if: always() 151 | with: 152 | name: AutomatedTestOutputFiles 153 | path: ${{ steps.run-test.outputs.artifact-folder }} 154 | 155 | - name: Adding summary 156 | if: ${{ always() }} 157 | run: | 158 | echo "### Results of test execution :fire:" >> $GITHUB_STEP_SUMMARY 159 | echo "Launch: ${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY 160 | echo "Device: ${{ github.event.inputs.device }}" >> $GITHUB_STEP_SUMMARY 161 | echo "Build agent: ${{ github.event.inputs.runner }}" >> $GITHUB_STEP_SUMMARY 162 | 163 | report: 164 | name: send test report to slack 165 | needs: tests 166 | runs-on: ${{ github.event.inputs.runner || 'autotestdebug' }} 167 | timeout-minutes: 10 168 | steps: 169 | - name: Print Env Variables 170 | run: env 171 | working-directory: ${{ github.workspace }} 172 | 173 | - name: checkout source code of application 174 | uses: actions/checkout@v4 175 | with: 176 | path: 'SDK-Auto-Test' 177 | clean: true 178 | repository: 'appodeal/SDK-Auto-Test' 179 | ref: 'aws' 180 | token: ${{ secrets.UITESTREPOACCESS }} 181 | 182 | - name: Set up JDK 11 183 | uses: actions/setup-java@v4 184 | with: 185 | java-version: '11' 186 | distribution: 'temurin' 187 | 188 | - name: download debug build 189 | uses: actions/download-artifact@v4 190 | with: 191 | name: AutomatedTestOutputFiles 192 | path: ./SDK-Auto-Test/ 193 | 194 | - name: Copy Junit Reports 195 | if: always() 196 | working-directory: ${{ github.workspace }}/SDK-Auto-Test 197 | env: 198 | DEVICEFARM_LOG_DIR: \$DEVICEFARM_LOG_DIR 199 | run: | 200 | rm -rf saved_reports && mkdir saved_reports 201 | find . -type f -name "00003-Customer Artifacts.zip" | while read -r file; do 202 | unzip "$file" -d "$(dirname "$file")" 203 | test_file_path=$(find "$(dirname "$file")" -type f -name "TEST-tests.example_APD.ApdBannerTest.xml") 204 | if [ -n "$test_file_path" ]; then 205 | echo "Файл найден: $test_file_path" 206 | removed_spaces=$(echo "$test_file_path" | tr -d ' ') 207 | first_directory=$(echo "$removed_spaces" | cut -d'/' -f2) 208 | cp "$test_file_path" ./saved_reports/$first_directory.xml 209 | fi 210 | done 211 | 212 | - name: Run JUnitReportParser for all XML files 213 | if: always() 214 | working-directory: ${{ github.workspace }}/SDK-Auto-Test 215 | run: | 216 | chmod +x slack.sh 217 | if [ -d "./saved_reports" ]; then 218 | find ./saved_reports -type f -name "*.xml" | while read -r test_file_path; do 219 | if [ -n "$test_file_path" ]; then 220 | echo "Файл найден: $test_file_path" 221 | ./slack.sh "$test_file_path" ${{ secrets.SLACK_WEBHOOK_URL }} 222 | fi 223 | done 224 | else 225 | echo "Directory ./saved_reports does not exist." 226 | fi 227 | -------------------------------------------------------------------------------- /.github/workflows/uitests_saucelabs.yaml: -------------------------------------------------------------------------------- 1 | name: UITest on SauceLabs 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | device: 7 | required: true 8 | default: 'Pixel8_API33' 9 | type: choice 10 | options: 11 | - 'Pixel8_API33' 12 | - 'Pixel8_API35' 13 | runner: 14 | required: true 15 | default: 'autotestdebug' 16 | type: choice 17 | options: 18 | - 'autotestdebug' 19 | - 'ubuntu-latest' 20 | 21 | jobs: 22 | build: 23 | name: build android application for ui tests 24 | runs-on: ${{ github.event.inputs.runner || 'autotestdebug' }} 25 | timeout-minutes: 30 26 | steps: 27 | - name: Print Env Variables 28 | run: env 29 | working-directory: ${{ github.workspace }} 30 | 31 | - name: checkout source code of application 32 | uses: actions/checkout@v4 33 | with: 34 | clean: true 35 | path: 'appodeal-android-sdk' 36 | 37 | - name: Set up JDK 17 38 | uses: actions/setup-java@v4 39 | with: 40 | java-version: '17' 41 | distribution: 'temurin' 42 | 43 | - name: Build with Gradle 44 | working-directory: ${{ github.workspace }}/appodeal-android-sdk 45 | run: ./gradlew :banner:assembleDebug 46 | 47 | - name: save debug build for aws 48 | uses: actions/upload-artifact@v4 49 | with: 50 | name: banner-debug.apk 51 | path: appodeal-android-sdk/banner/build/outputs/apk/debug/banner-debug.apk 52 | retention-days: 14 53 | 54 | - name: Upload apk to SauceLabs 55 | run: | 56 | curl -u "${{ secrets.SAUCELABS_USERNAME }}:${{ secrets.SAUCELABS_ACCESS_KEY }}" --location \ 57 | --request POST 'https://api.eu-central-1.saucelabs.com/v1/storage/upload' \ 58 | --form 'payload=@"appodeal-android-sdk/banner/build/outputs/apk/debug/banner-debug.apk"' \ 59 | --form 'name="banner-debug.apk"' \ 60 | --form 'description="APD demo \n ${GITHUB_REF_NAME}"' 61 | 62 | # GITHUB_REF_NAME=feature/auto_test 63 | # GITHUB_RUN_ID=10653731636 64 | # GITHUB_REPOSITORY=appodeal/appodeal-android-sdk 65 | # https://github.com/appodeal/appodeal-android-sdk/actions/runs/10653731636 66 | # GITHUB_TRIGGERING_ACTOR=johnlitvinov 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | *.iml 4 | .idea/* 5 | .DS_Store 6 | build/ 7 | captures/ 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Appodeal Android SDK 2 | 3 | [![](https://img.shields.io/badge/SDK%20version-%203.6.0-brightgreen)](https://docs.appodeal.com/android/get-started) 4 | 5 | # Examples 6 | 7 | * [Banner](https://github.com/appodeal/appodeal-android-sdk/tree/master/banner) 8 | * [Interstitial](https://github.com/appodeal/appodeal-android-sdk/tree/master/interstitial) 9 | * [MREC](https://github.com/appodeal/appodeal-android-sdk/tree/master/mrec) 10 | * [Rewarded Video](https://github.com/appodeal/appodeal-android-sdk/tree/master/rewarded) 11 | * [Native](https://github.com/appodeal/appodeal-android-sdk/tree/master/native) 12 | * [Services](https://github.com/appodeal/appodeal-android-sdk/tree/master/analytics) 13 | 14 | # Documentation 15 | 16 | The examples show only the most commonly used methods. Check out 17 | our [Get started](https://docs.appodeal.com/android/get-started) page for documentation on using the 18 | Appodeal SDK. 19 | 20 | If you have any comments or suggestions about the Appodeal SDK, please contact our support team 21 | support@appodeal.com. -------------------------------------------------------------------------------- /analytics/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /analytics/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'com.google.gms.google-services' 5 | } 6 | 7 | android { 8 | compileSdk 34 9 | namespace 'com.appodealstack.demo.analytics' 10 | 11 | defaultConfig { 12 | buildConfigField "String", "APP_KEY", "\"d908f77a97ae0993514bc8edba7e776a36593c77e5f44994\"" 13 | applicationId "com.appodealstack.demo" 14 | minSdkVersion 21 15 | targetSdkVersion 34 16 | versionCode 1 17 | versionName "1.0" 18 | } 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_11 27 | targetCompatibility JavaVersion.VERSION_11 28 | } 29 | kotlinOptions { 30 | jvmTarget = '11' 31 | } 32 | buildFeatures { 33 | viewBinding true 34 | buildConfig true 35 | } 36 | } 37 | 38 | dependencies { 39 | implementation 'androidx.core:core-ktx:1.13.1' 40 | implementation 'androidx.activity:activity-ktx:1.9.1' 41 | implementation 'androidx.appcompat:appcompat:1.7.0' 42 | implementation 'com.google.android.material:material:1.12.0' 43 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 44 | // Google billing library for Appodeal purchase validation 45 | implementation 'com.android.billingclient:billing-ktx:7.0.0' 46 | // Appodeal SDK 3.6.0.0 47 | implementation 'com.appodeal.ads:sdk:3.6.0.0' 48 | } -------------------------------------------------------------------------------- /analytics/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "785362040133", 4 | "firebase_url": "https://appodeal-sandbox.firebaseio.com", 5 | "project_id": "appodeal-sandbox", 6 | "storage_bucket": "appodeal-sandbox.appspot.com" 7 | }, 8 | "client": [ 9 | { 10 | "client_info": { 11 | "mobilesdk_app_id": "1:785362040133:android:77aa069cb9b77f91c57a2c", 12 | "android_client_info": { 13 | "package_name": "com.appodealstack.demo" 14 | } 15 | }, 16 | "oauth_client": [ 17 | { 18 | "client_id": "785362040133-9odke8a112ah3a9d2dbl9gd2lrpm809c.apps.googleusercontent.com", 19 | "client_type": 3 20 | } 21 | ], 22 | "api_key": [ 23 | { 24 | "current_key": "AIzaSyBwlWE5mcH3RTbABRPp7_bSio1vzsQmjqY" 25 | } 26 | ], 27 | "services": { 28 | "appinvite_service": { 29 | "other_platform_oauth_client": [ 30 | { 31 | "client_id": "785362040133-9odke8a112ah3a9d2dbl9gd2lrpm809c.apps.googleusercontent.com", 32 | "client_type": 3 33 | } 34 | ] 35 | } 36 | } 37 | } 38 | ], 39 | "configuration_version": "1" 40 | } -------------------------------------------------------------------------------- /analytics/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | -------------------------------------------------------------------------------- /analytics/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 18 | 21 | 22 | 29 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 44 | 45 | 48 | 49 | 52 | 53 | 56 | 57 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /analytics/src/main/java/com/appodealstack/demo/analytics/AnalyticsActivity.kt: -------------------------------------------------------------------------------- 1 | package com.appodealstack.demo.analytics 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import android.util.Log 6 | import android.widget.Toast 7 | import androidx.activity.viewModels 8 | import androidx.appcompat.app.AppCompatActivity 9 | import com.android.billingclient.api.BillingClient 10 | import com.android.billingclient.api.Purchase 11 | import com.appodeal.ads.Appodeal 12 | import com.appodeal.ads.AppodealServices 13 | import com.appodeal.ads.inapp.InAppPurchase 14 | import com.appodeal.ads.inapp.InAppPurchaseValidateCallback 15 | import com.appodeal.ads.initializing.ApdInitializationError 16 | import com.appodeal.ads.revenue.AdRevenueCallbacks 17 | import com.appodeal.ads.revenue.RevenueInfo 18 | import com.appodeal.ads.service.ServiceError 19 | import com.appodeal.ads.utils.Log.LogLevel 20 | import com.appodealstack.demo.analytics.databinding.ActivityAnalyticsBinding 21 | 22 | class AnalyticsActivity : AppCompatActivity() { 23 | 24 | private val viewModel by viewModels() 25 | 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | super.onCreate(savedInstanceState) 28 | val binding = ActivityAnalyticsBinding.inflate(layoutInflater) 29 | setContentView(binding.root) 30 | setUpAppodealSdk(binding) 31 | } 32 | 33 | private fun setUpAppodealSdk(binding: ActivityAnalyticsBinding) { 34 | Appodeal.setLogLevel(LogLevel.verbose) 35 | Appodeal.initialize( 36 | this, 37 | BuildConfig.APP_KEY, 38 | Appodeal.NONE 39 | ) { errors: List? -> 40 | val initResult = 41 | if (errors.isNullOrEmpty()) "successfully" else "with ${errors.size} errors" 42 | showToast("Appodeal initialized $initResult") 43 | errors?.forEach { 44 | Log.e(TAG, "onInitializationFinished: ", it) 45 | } 46 | } 47 | Appodeal.setAdRevenueCallbacks(object : AdRevenueCallbacks { 48 | override fun onAdRevenueReceive(revenueInfo: RevenueInfo) { 49 | // Called whenever SDK receives revenue information for an ad 50 | } 51 | }) 52 | with(binding) { 53 | validateInapp.setOnClickListener { viewModel.flowInAppPurchase(this@AnalyticsActivity) } 54 | validateSubscription.setOnClickListener { viewModel.flowSubsPurchase(this@AnalyticsActivity) } 55 | logEvent.setOnClickListener { logEvent() } 56 | } 57 | viewModel.purchases.observe(this) { purchases -> 58 | purchases.forEach { validatePurchase(it) } 59 | } 60 | } 61 | 62 | private fun logEvent() { 63 | val params = mapOf( 64 | "example_param_1" to "Param1 value", 65 | "example_param_2" to 123 66 | ) 67 | Appodeal.logEvent( 68 | eventName = "appodealstack_sdk_example_test_event", 69 | params = params, 70 | service = AppodealServices.APPSFLYER or AppodealServices.FIREBASE 71 | ) 72 | } 73 | 74 | private fun validatePurchase(purchase: Purchase) = purchase.products.forEach { productId -> 75 | val productDetails = 76 | viewModel.getProductDetails(productId) ?: error("Product details is null") 77 | val apdPurchaseBuilder = when (productDetails.productType) { 78 | BillingClient.ProductType.INAPP -> { 79 | InAppPurchase.newInAppBuilder().apply { 80 | productDetails.oneTimePurchaseOfferDetails?.let { 81 | withPrice(it.formattedPrice) 82 | withCurrency(it.priceCurrencyCode) 83 | } 84 | } 85 | } 86 | 87 | BillingClient.ProductType.SUBS -> { 88 | InAppPurchase.newSubscriptionBuilder().apply { 89 | productDetails.subscriptionOfferDetails?.let { 90 | val pricingPhase = it.first().pricingPhases.pricingPhaseList.first() 91 | withPrice(pricingPhase.formattedPrice) 92 | withCurrency(pricingPhase.priceCurrencyCode) 93 | } 94 | } 95 | } 96 | 97 | else -> error("Product type is incorrect") 98 | } 99 | val apdPurchase: InAppPurchase = apdPurchaseBuilder 100 | .withPublicKey(PUBLIC_KEY) 101 | .withSignature(purchase.signature) 102 | .withPurchaseData(purchase.originalJson) 103 | .withPurchaseToken(purchase.purchaseToken) 104 | .withPurchaseTimestamp(purchase.purchaseTime) 105 | .withDeveloperPayload(purchase.developerPayload) 106 | .withOrderId(purchase.orderId) 107 | .withSku(productId) 108 | .withAdditionalParams(mapOf("some_parameter" to "some_value")) 109 | .build() 110 | 111 | // Validate InApp purchase 112 | Appodeal.validateInAppPurchase(this, apdPurchase, object : InAppPurchaseValidateCallback { 113 | override fun onInAppPurchaseValidateSuccess( 114 | purchase: InAppPurchase, 115 | errors: List? 116 | ) { 117 | Log.v(TAG, "onInAppPurchaseValidateSuccess") 118 | errors?.forEach { error -> 119 | Log.e(TAG, "onInAppPurchaseValidateSuccess - $error") 120 | } 121 | } 122 | 123 | override fun onInAppPurchaseValidateFail( 124 | purchase: InAppPurchase, 125 | errors: List 126 | ) { 127 | Log.v(TAG, "onInAppPurchaseValidateFail") 128 | errors.forEach { error -> 129 | Log.e(TAG, "onInAppPurchaseValidateFail - $error") 130 | } 131 | } 132 | }) 133 | } 134 | } 135 | 136 | private fun Context.showToast(message: String) = 137 | Toast.makeText(applicationContext, message, Toast.LENGTH_SHORT).show() 138 | 139 | /** https://support.google.com/googleplay/android-developer/answer/186113 */ 140 | private const val PUBLIC_KEY = "YOUR_PUBLIC_KEY" 141 | private const val TAG = "AnalyticsActivity" -------------------------------------------------------------------------------- /analytics/src/main/java/com/appodealstack/demo/analytics/AnalyticsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.appodealstack.demo.analytics 2 | 3 | import android.app.Activity 4 | import android.app.Application 5 | import androidx.lifecycle.AndroidViewModel 6 | import androidx.lifecycle.LiveData 7 | import com.android.billingclient.api.Purchase 8 | 9 | class AnalyticsViewModel( 10 | application: Application, 11 | ) : AndroidViewModel(application) { 12 | 13 | private val billing = BillingUseCase(application, ID_COINS, ID_INFINITE_ACCESS_MONTHLY) 14 | val purchases: LiveData> = billing.purchases 15 | 16 | fun flowInAppPurchase(activity: Activity) = billing.flowInApp(activity, ID_COINS) 17 | 18 | fun flowSubsPurchase(activity: Activity) = 19 | billing.flowSubscription(activity, ID_INFINITE_ACCESS_MONTHLY) 20 | 21 | fun getProductDetails(productId: String) = billing.productDetails[productId] 22 | } 23 | 24 | private const val ID_COINS = "coins" 25 | private const val ID_INFINITE_ACCESS_MONTHLY = "infinite_access_monthly" 26 | -------------------------------------------------------------------------------- /analytics/src/main/java/com/appodealstack/demo/analytics/BillingUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.appodealstack.demo.analytics 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.util.Log 6 | import androidx.lifecycle.LiveData 7 | import androidx.lifecycle.MutableLiveData 8 | import com.android.billingclient.api.* 9 | 10 | class BillingUseCase( 11 | context: Context, 12 | private val inAppProductId: String, 13 | private val subsProductId: String 14 | ) { 15 | private val _productDetails = mutableMapOf() 16 | private val _purchases = MutableLiveData>() 17 | 18 | val productDetails: Map get() = _productDetails 19 | val purchases: LiveData> get() = _purchases 20 | 21 | private val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases -> 22 | debug("onPurchasesUpdated: ${billingResult.responseCode} ${billingResult.debugMessage}") 23 | when (billingResult.responseCode) { 24 | BillingClient.BillingResponseCode.OK -> { 25 | purchases?.let { 26 | processPurchaseList(it) 27 | _purchases.postValue(it) 28 | } ?: error("onPurchasesUpdated: Null Purchase List Returned from OK response!") 29 | } 30 | BillingClient.BillingResponseCode.USER_CANCELED -> debug("onPurchasesUpdated: User canceled the purchase") 31 | BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> debug("onPurchasesUpdated: The user already owns this item") 32 | BillingClient.BillingResponseCode.DEVELOPER_ERROR -> error( 33 | "onPurchasesUpdated: Developer error means that Google Play " + 34 | "does not recognize the configuration. If you are just getting started, " + 35 | "make sure you have configured the application correctly in the " + 36 | "Google Play Console. The SKU product ID must match and the APK you " + 37 | "are using must be signed with release keys." 38 | ) 39 | } 40 | } 41 | 42 | private val billingClientStateListener = object : BillingClientStateListener { 43 | override fun onBillingSetupFinished(billingResult: BillingResult) { 44 | debug("onBillingSetupFinished: ${billingResult.responseCode} ${billingResult.debugMessage}") 45 | if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { 46 | // The billing client is ready. You can query purchases here. 47 | // This doesn't mean that your app is set up correctly in the console -- it just 48 | // means that you have a connection to the Billing service. 49 | queryProductDetailsAsync() 50 | refreshPurchasesAsync() 51 | } 52 | } 53 | 54 | override fun onBillingServiceDisconnected() { 55 | debug("onBillingServiceDisconnected") 56 | } 57 | } 58 | 59 | private val billingClient = 60 | BillingClient.newBuilder(context) 61 | .setListener(purchasesUpdatedListener) 62 | .enablePendingPurchases() 63 | .build() 64 | .apply { startConnection(billingClientStateListener) } 65 | 66 | fun flowInApp(activity: Activity, productId: String) { 67 | val productDetails: ProductDetails = _productDetails[productId] ?: return 68 | val productDetailsParams = BillingFlowParams.ProductDetailsParams.newBuilder() 69 | .setProductDetails(productDetails) 70 | .build() 71 | flow(activity, productDetailsParams) 72 | } 73 | 74 | fun flowSubscription(activity: Activity, productId: String) { 75 | val productDetails: ProductDetails = _productDetails[productId] ?: return 76 | val offerToken = productDetails.subscriptionOfferDetails?.last()?.offerToken ?: return 77 | val productDetailsParams = BillingFlowParams.ProductDetailsParams.newBuilder() 78 | .setProductDetails(productDetails) 79 | .setOfferToken(offerToken) 80 | .build() 81 | flow(activity, productDetailsParams) 82 | } 83 | 84 | private fun flow( 85 | activity: Activity, 86 | productDetailsParams: BillingFlowParams.ProductDetailsParams 87 | ) { 88 | val billingFlowParams = BillingFlowParams.newBuilder() 89 | .setProductDetailsParamsList(listOf(productDetailsParams)) 90 | .build() 91 | val billingResult = billingClient.launchBillingFlow(activity, billingFlowParams) 92 | if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { 93 | debug("Flow billing success") 94 | } else { 95 | error("Flow billing failed: ${billingResult.debugMessage}") 96 | } 97 | } 98 | 99 | private fun queryProductDetailsAsync() { 100 | val detailsResponseListener = 101 | ProductDetailsResponseListener { billingResult, productDetailsList -> 102 | debug("onProductDetailsResponse: ${billingResult.responseCode} ${billingResult.debugMessage}") 103 | if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { 104 | if (productDetailsList.isEmpty()) { 105 | error( 106 | "onProductDetailsResponse: " + 107 | "Found null or empty SkuDetails. " + 108 | "Check to see if the SKUs you requested are correctly published " + 109 | "in the Google Play Console." 110 | ) 111 | } else { 112 | for (productDetails: ProductDetails in productDetailsList) { 113 | _productDetails[productDetails.productId] = productDetails 114 | } 115 | } 116 | } 117 | } 118 | billingClient.queryProductDetailsAsync( 119 | QueryProductDetailsParams.newBuilder().setProductList( 120 | listOf( 121 | QueryProductDetailsParams.Product.newBuilder() 122 | .setProductType(BillingClient.ProductType.INAPP) 123 | .setProductId(inAppProductId) 124 | .build() 125 | ) 126 | ).build(), 127 | detailsResponseListener 128 | ) 129 | billingClient.queryProductDetailsAsync( 130 | QueryProductDetailsParams.newBuilder().setProductList( 131 | listOf( 132 | QueryProductDetailsParams.Product.newBuilder() 133 | .setProductType(BillingClient.ProductType.SUBS) 134 | .setProductId(subsProductId) 135 | .build() 136 | ) 137 | ).build(), 138 | detailsResponseListener 139 | ) 140 | debug("Query product details started.") 141 | } 142 | 143 | private fun refreshPurchasesAsync() { 144 | val purchasesResponseListener = PurchasesResponseListener { billingResult, purchaseList -> 145 | if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) { 146 | error("Problem getting purchases: ${billingResult.debugMessage}") 147 | } else { 148 | processPurchaseList(purchaseList) 149 | } 150 | } 151 | billingClient.queryPurchasesAsync( 152 | QueryPurchasesParams 153 | .newBuilder() 154 | .setProductType(BillingClient.ProductType.INAPP) 155 | .build(), 156 | purchasesResponseListener 157 | ) 158 | billingClient.queryPurchasesAsync( 159 | QueryPurchasesParams 160 | .newBuilder() 161 | .setProductType(BillingClient.ProductType.SUBS) 162 | .build(), 163 | purchasesResponseListener 164 | ) 165 | debug("Refreshing purchases started.") 166 | } 167 | 168 | private fun processPurchaseList(purchases: List) { 169 | purchases 170 | .filter { purchase -> purchase.purchaseState == Purchase.PurchaseState.PURCHASED } 171 | .forEach { purchase -> 172 | val isConsumable = purchase.products.any { inAppProductId == it } 173 | if (isConsumable) { 174 | consumePurchase(purchase) 175 | } else if (!purchase.isAcknowledged) { 176 | acknowledgePurchase(purchase) 177 | } 178 | } 179 | } 180 | 181 | private fun consumePurchase(purchase: Purchase) { 182 | val consumeParams = 183 | ConsumeParams.newBuilder() 184 | .setPurchaseToken(purchase.purchaseToken) 185 | .build() 186 | billingClient.consumeAsync(consumeParams) { billingResult, _ -> 187 | if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { 188 | debug("Consumption flow successful. Delivering entitlement.") 189 | } else { 190 | error("Consumption flow error: ${billingResult.debugMessage}") 191 | } 192 | } 193 | debug("Consumption flow started.") 194 | } 195 | 196 | private fun acknowledgePurchase(purchase: Purchase) { 197 | val acknowledgeParams = AcknowledgePurchaseParams.newBuilder() 198 | .setPurchaseToken(purchase.purchaseToken) 199 | .build() 200 | billingClient.acknowledgePurchase(acknowledgeParams) { billingResult -> 201 | if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { 202 | debug("Acknowledge flow successful.") 203 | } else { 204 | error("Acknowledge flow error: ${billingResult.debugMessage}") 205 | } 206 | } 207 | debug("Acknowledge flow started.") 208 | } 209 | } 210 | 211 | private val TAG = BillingClient::class.java.simpleName 212 | private fun debug(message: String) = Log.d(TAG, message) 213 | private fun error(message: String) = Log.e(TAG, message) 214 | -------------------------------------------------------------------------------- /analytics/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /analytics/src/main/res/layout/activity_analytics.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 |