diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df56946 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# gradle directory +.gradle +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# built application files +*.apk +*.ap_ +*.jar + +!gradle-wrapper.jar \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..73303b4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +The Affise SDK is licensed under the MIT License. + +Copyright (c) 2022 Affise, Inc. | https://affise.com + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dee5806 --- /dev/null +++ b/README.md @@ -0,0 +1,780 @@ +# Affise Attribution Android library + +- [Description](#description) +- [Quick start](#quick-start) +- [Integration](#integration) + - [Integrate as file dependency](#integrate-as-file-dependency) + - [Initialize](#initialize) +- [Features](#features) + - [Device identifiers collection](#device-identifiers-collection) + - [Events tracking](#events-tracking) + - [Custom events tracking](#custom-events-tracking) + - [Predefined event parameters](#predefined-event-parameters) + - [Events buffering](#events-buffering) + - [Install referrer tracking](#install-referrer-tracking) + - [Advertising Identifier (google) tracking](#advertising-identifier-google-tracking) + - [Open Advertising Identifier (huawei) tracking](#open-advertising-identifier-huawei-tracking) + - [Push token tracking](#push-token-tracking) + - [Reinstall Uninstall tracking](#reinstall-uninstall-tracking) + - [APK preinstall tracking](#apk-preinstall-tracking) + - [Deeplinks](#deeplinks) + - [Offline mode](#offline-mode) + - [Disable tracking](#disable-tracking) + - [Disable background tracking](#disable-background-tracking) + - [GDPR right to be forgotten](#gdpr-right-to-be-forgotten) + - [Get referrer](#get-referrer) + - [Get referrer parameter](#get-referrer-parameter) + - [Referrer keys](#referrer-keys) + - [Web view tracking](#webview-tracking) + - [Initialize webview](#initialize-webview) + - [Events tracking JS](#events-tracking-js) + - [Predefined event parameters JS](#predefined-event-parameters-js) + - [Custom events JS](#custom-events-js) +- [Requirements](#requirements) + +# Description + +Affise SDK is a software you can use to collect app usage statistics, device identifiers, deeplink usage, track install +referrer. + +## Quick start + +## Integration + +### Integrate as file dependency + +Download latest Affise SDK (`attribution-release.aar`) +from [releases page](https://github.com/affise/sdk-android-family/releases) and place this binary to gradle application +module lib directory `app/libs/attribution-release.aar` + +Add library as gradle file dependency to application module build script +Add install referrer library + +For kotlin build script build.gradle.kts use: + +```kotlin +dependencies { + // ... + implementation(files("libs/attribution-release.aar")) + // Add install referrer + implementation("com.android.installreferrer:installreferrer:2.2") +} +``` + +For groovy build script build.gradle use: + +```groovy +dependencies { + // ... + implementation files('libs/attribution-release.aar') + // Add install referrer + implementation 'com.android.installreferrer:installreferrer:2.2' +} +``` + +### Initialize + +After library is added as dependency sync project with gradle files and initialize. + +For kotlin use: + +```kotlin +class App : Application() { + override fun onCreate() { + super.onCreate() + val properties = AffiseInitProperties( + "Your appId", //Change to your app id + !BuildConfig.DEBUG, //Add your custom rule to determine if this is a production build + null, //Change to your partParamName + null, //Change to your partParamNameToken + null, //Change to your appToken + "Your secretId", //Change to your secretId + emptyList(), // Types to handles interception of clicks on activity + false // Affise metrics + ) + Affise.init(this, properties) + } +} +``` + +For java use: + +```java +public class App extends Application { + @Override + public void onCreate() { + super.onCreate(); + + AffiseInitProperties properties = new AffiseInitProperties( + "Your appId", //Change to your app id + !BuildConfig.DEBUG, //Add your custom rule to determine if this is a production build + null, //Change to your partParamName + null, //Change to your partParamNameToken + null, //Change to your appToken + "Your secretId", //Change to your secretId + Collections.emptyList(), // Types to handles interception of clicks on activity + false // Affise metrics + ); + Affise.init(this, properties); + } +} +``` + + +### Requirements + +For a minimal working functionality your app needs to declare internet permission: + +```xml + + + + + +``` + +# Features + +### Device identifiers collection + +To match users with events and data library is sending, these identifiers are collected: + +- `AFFISE_APP_ID` +- `AFFISE_PKG_APP_NAME` +- `AFFISE_APP_NAME_DASHBOARD` +- `APP_VERSION` +- `APP_VERSION_RAW` +- `STORE` +- `TRACKER_TOKEN` +- `TRACKER_NAME` +- `FIRST_TRACKER_TOKEN` +- `FIRST_TRACKER_NAME` +- `LAST_TRACKER_TOKEN` +- `LAST_TRACKER_NAME` +- `OUTDATED_TRACKER_TOKEN` +- `INSTALLED_TIME` +- `FIRST_OPEN_TIME` +- `INSTALLED_HOUR` +- `FIRST_OPEN_HOUR` +- `INSTALL_BEGIN_TIME` +- `INSTALL_FINISH_TIME` +- `REFERRAL_TIME` +- `CREATED_TIME` +- `CREATED_TIME_MILLI` +- `CREATED_TIME_HOUR` +- `UNINSTALL_TIME` +- `REINSTALL_TIME` +- `LAST_SESSION_TIME` +- `CONNECTION_TYPE` +- `CPU_TYPE` +- `HARDWARE_NAME` +- `NETWORK_TYPE` +- `DEVICE_MANUFACTURER` +- `PROXY_IP_ADDRESS` +- `DEEPLINK_CLICK` +- `DEVICE_ATLAS_ID` +- `AFFISE_DEVICE_ID` +- `AFFISE_ALT_DEVICE_ID` +- `ADID` +- `ANDROID_ID` +- `ANDROID_ID_MD5` +- `MAC_SHA1` +- `MAC_MD5` +- `GAID_ADID` +- `GAID_ADID_MD5` +- `OAID` +- `OAID_MD5` +- `REFTOKEN` +- `REFTOKENS` +- `REFERRER` +- `USER_AGENT` +- `MCCODE` +- `MNCODE` +- `ISP` +- `REGION` +- `COUNTRY` +- `LANGUAGE` +- `DEVICE_NAME` +- `DEVICE_TYPE` +- `OS_NAME` +- `PLATFORM` +- `API_LEVEL_OS` +- `AFFISE_SDK_VERSION` +- `OS_VERSION` +- `RANDOM_USER_ID` +- `AFFISE_SDK_POS` +- `TIMEZONE_DEV` +- `LAST_TIME_SESSION` +- `TIME_SESSION` +- `AFFISE_SESSION_COUNT` +- `LIFETIME_SESSION_COUNT` +- `AFFISE_DEEPLINK` +- `AFFISE_PART_PARAM_NAME` +- `AFFISE_PART_PARAM_NAME_TOKEN` +- `AFFISE_APP_TOKEN` +- `LABEL` +- `AFFISE_SDK_SECRET_ID` +- `UUID` +- `AFFISE_APP_OPENED` +- `PUSHTOKEN` +- `EVENTS` +- `AFFISE_EVENTS_COUNT` + +### Events tracking + +For example, we want to track what items usually user adds to shopping cart. To send event first create it with +following code + +```kotlin +class Presenter { + fun onUserAddsItemsToCart(items: String) { + val items = JSONObject().apply { + put("items", "cookies, potato, milk") + } + Affise.sendEvent(AddToCartEvent(items, System.currentTimeMillis(), "groceries")) + } +} +``` + +For java use: + +```java +class Presenter { + void onUserAddsItemsToCart(String items) { + JSONObject items = new JSONObject(); + items.put("items", items); + Affise.sendEvent(new AddToCartEvent(items, System.currentTimeMillis(), "groceries")); + } +} +``` + +With above example you can implement other events: + +- `AchieveLevelEvent` +- `AddPaymentInfoEvent` +- `AddToCartEvent` +- `AddToWishlistEvent` +- `ClickAdvEvent` +- `CompleteRegistrationEvent` +- `CompleteStreamEvent` +- `CompleteTrialEvent` +- `CompleteTutorialEvent` +- `ContentItemsViewEvent` +- `DeepLinkedEvent` +- `InitiatePurchaseEvent` +- `InitiateStreamEvent` +- `InviteEvent` +- `LastAttributedTouchEvent` +- `ListViewEvent` +- `LoginEvent` +- `OpenedFromPushNotificationEvent` +- `PurchaseEvent` +- `RateEvent` +- `ReEngageEvent` +- `ReserveEvent` +- `SalesEvent` +- `SearchEvent` +- `ShareEvent` +- `SpendCreditsEvent` +- `StartRegistrationEvent` +- `StartTrialEvent` +- `StartTutorialEvent` +- `SubscribeEvent` +- `TravelBookingEvent` +- `UnlockAchievementEvent` +- `UnsubscribeEvent` +- `UnsubscriptionEvent` +- `UpdateEvent` +- `ViewAdvEvent` +- `ViewCartEvent` +- `ViewItemEvent` +- `ViewItemsEvent` + +### Custom events tracking + +Use any of custom events if default doesn't fit your scenario: + +- `CustomId01Event` +- `CustomId02Event` +- `CustomId03Event` +- `CustomId04Event` +- `CustomId05Event` +- `CustomId06Event` +- `CustomId07Event` +- `CustomId08Event` +- `CustomId09Event` +- `CustomId10Event` + +### Predefined event parameters + +To enrich your event with another dimension, you can use predefined parameters for most common cases. +Add it to any event: + +```kotlin +class Presenter { + fun onUserAddsItemsToCart(items: String) { + val items = JSONObject().apply { + put("items", "cookies, potato, milk") + } + + val event = AddToCartEvent(items, System.currentTimeMillis()).apply { + addPredefinedParameter(PredefinedParameters.DESCRIPTION, "best before 2029") + } + Affise.sendEvent(event) + } +} +``` + +For java use: + +```java +class Presenter { + void onUserAddsItemsToCart(String items) { + JSONObject items = new JSONObject(); + items.put("items", items); + + AddToCartEvent event = AddToCartEvent(items, System.currentTimeMillis()); + event.addPredefinedParameter(PredefinedParameters.DESCRIPTION, "best before 2029"); + + Affise.sendEvent(event); + } +} +``` +In examples above `PredefinedParameters.DESCRIPTION` is used, but many others is available: +- `ADREV_AD_TYPE` +- `CITY` +- `COUNTRY` +- `REGION` +- `CLASS` +- `CONTENT` +- `CONTENT_ID` +- `CONTENT_LIST` +- `CONTENT_TYPE` +- `CURRENCY` +- `CUSTOMER_USER_ID` +- `DATE_A` +- `DATE_B` +- `DEPARTING_ARRIVAL_DATE` +- `DEPARTING_DEPARTURE_DATE` +- `DESCRIPTION` +- `DESTINATION_A` +- `DESTINATION_B` +- `DESTINATION_LIST` +- `HOTEL_SCORE` +- `LEVEL` +- `MAX_RATING_VALUE` +- `NUM_ADULTS` +- `NUM_CHILDREN` +- `NUM_INFANTS` +- `ORDER_ID` +- `PAYMENT_INFO_AVAILABLE` +- `PREFERRED_NEIGHBORHOODS` +- `PREFERRED_NUM_STOPS` +- `PREFERRED_PRICE_RANGE` +- `PREFERRED_STAR_RATINGS` +- `PRICE` +- `PURCHASE_CURRENCY` +- `QUANTITY` +- `RATING_VALUE` +- `RECEIPT_ID` +- `REGISTRATION_METHOD` +- `RETURNING_ARRIVAL_DATE` +- `RETURNING_DEPARTURE_DATE` +- `REVENUE` +- `SCORE` +- `SEARCH_STRING` +- `SUBSCRIPTION_ID` +- `SUCCESS` +- `SUGGESTED_DESTINATIONS` +- `SUGGESTED_HOTELS` +- `TRAVEL_START` +- `TRAVEL_END` +- `USER_SCORE` +- `VALIDATED` +- `ACHIEVEMENT_ID` +- `COUPON_CODE` +- `CUSTOMER_SEGMENT` +- `DEEP_LINK` +- `EVENT_START` +- `EVENT_END` +- `LAT` +- `LONG` +- `NEW_VERSION` +- `OLD_VERSION` +- `REVIEW_TEXT` +- `TUTORIAL_ID` +- `VIRTUAL_CURRENCY_NAME` +- `PARAM_01` +- `PARAM_02` +- `PARAM_03` +- `PARAM_04` +- `PARAM_05` +- `PARAM_06` +- `PARAM_07` +- `PARAM_08` +- `PARAM_09` + +### Events buffering + +Affise library will send any pending events with first opportunity, +but if there is no network connection or device is disabled, events are kept locally for 7 days before deletion. + + +### Advertising Identifier (google) tracking + +Advertising Identifier (google) tracking is supported automatically, no actions needed + +### Open Advertising Identifier (huawei) tracking + +Open Advertising Identifier is supported automatically, no actions needed + +### Install referrer tracking + +Install referrer tracking is supported automatically, no actions needed + +### Push token tracking + +To let affise track push token you need to receive it from your push service provider, and pass to Affise library. +First add firebase integration to your app completing theese steps: https://firebase.google.com/docs/cloud-messaging/android/client + +After you have done with firebase inegration, add to your cloud messaging service `onNewToken` method `Affise.addPushToken(token)` + +```kotlin +class FirebaseCloudMessagingService : FirebaseMessagingService() { + override fun onNewToken(token: String) { + // New token generated + Affise.addPushToken(token) + } +} +``` +### Reinstall Uninstall tracking + +Affise automaticly track reinstall events by using silent-push technology, to make this feature work, pass push token when it is recreated by user and on you application starts up +```kotlin +Affise.addPushToken(token) +``` + +### APK preinstall tracking + +SDK is also supports scenario when APK is installed not from one of application markets, such as google play, huawei appgallery or amazon appstore +To use this feature, create file with name `partner_key` in your app assets directory, and write unique identifier inside, this key will be passed to our backend so you can track events by partner later in your Affise console. + +### Deeplinks + +To integrate applink support you need: + +- add intent filter to one of your activities, replacing YOUR_AFFISE_APP_ID with id from your affise personal cabinet + +```xml + + + + + + + +``` +- register applink callback right after Affise.init(..) + +for kotlin: +```kotlin +Affise.init(..) +Affise.registerDeeplinkCallback { uri -> + val screen = uri.getQueryParameter("screen") + if(screen == "special_offer") { + // open special offer activity + } else { + // open another activity + } + // return true if deeplink is handled successfully + true +} +``` + +for java: +```java +Affise.registerDeeplinkCallback(uri -> { + String screen = uri.getQueryParameter("screen"); + if (screen.equals("special_offer")) { + // open special offer screen + } else { + // open another activity + } + // return true if deeplink is handled successfully + return true; +}); +``` + +### Offline mode + +In some scenarious you would want to limit Affise network usage, to pause that activity call anywhere in your application following code after Affise init: + +```kotlin +Affise.init(..) +Affise.setOfflineModeEnabled(enabled = true) // to enable offline mode +Affise.setOfflineModeEnabled(enabled = false) // to disable offline mode +``` +While offline mode is enabled, your metrics and other events are kept locally, and will be delivered once offline mode is disabled. +Offline mode is persistent as Application lifecycle, and will be disabled with process termination automaticly. +To check current offline mode status call: + +```kotlin +Affise.isOfflineModeEnabled() // returns true or false describing current tracking state +``` + +### Disable tracking + +To disable any tracking activity, storing events and gathering device identifiers and metrics call anywhere in your application following code after Affise init: + +```kotlin +Affise.init(..) +Affise.setTrackingEnabled(enabled = true) // to enable tracking +Affise.setTrackingEnabled(enabled = false) // to disable tracking +``` + +By default tracking is enabled. + +While tracking mode is disabled, metrics and other identifiers is not generated locally. +Keep in mind that this flag is persistent until app reinstall, and don't forget to reactivate tracking when needed. +To check current status of tracking call: + +```kotlin +Affise.isTrackingEnabled() // returns true or false describing current tracking state +``` + +### Disable background tracking + +To disable any background tracking activity, storing events and gathering device identifiers and metrics call anywhere in your application following code after Affise init: + +```kotlin +Affise.init(..) +Affise.setBackgroundTrackingEnabled(enabled = true) // to enable background tracking +Affise.setBackgroundTrackingEnabled(enabled = false) // to disable background tracking +``` + +By default background tracking is enabled. + +While background tracking mode is disabled, metrics and other identifiers is not generated locally. +Background tracking mode is persistent as Application lifecycle, and will be re-enabled with process termination automatically. +To check current status of background tracking call: + +```kotlin +Affise.isBackgroundTrackingEnabled() // returns true or false describing current background tracking state +``` + +### GDPR right to be forgotten + +Under the EU's General Data Protection Regulation (GDPR): An individual has the right to have their personal data erased. +To provide this functionality to user, as the app developer, you can call + +```kotlin +Affise.init(..) +Affise.forget() // to forget users data +``` +After processing such request our backend servers will delete all users data. +To prevent library from generating new events, disable tracking just before calling Affise.forget: + +```kotlin +Affise.init(..) +Affise.setTrackingEnabled(enabled = false) +Affise.forget() // to forget users data +``` + +### Get referrer + +Use the next public method of SDK + +```kotlin +Affise.getReferrer() +``` + +### Get referrer parameter + +Use the next public method of SDK to get referrer parameter by + +For kotlin: +```kotlin +Affise.getReferrerValue(ReferrerKey.CLICK_ID) { value -> + +} +``` + +For java: +```java +Affise.getReferrerValue(ReferrerKey.CLICK_ID, value -> { + +}); +``` + +#### Referrer keys + +In examples above `ReferrerKey.CLICK_ID` is used, but many others is available: + +- `AD_ID` +- `CAMPAIGN_ID` +- `CLICK_ID` +- `AFFISE_AD` +- `AFFISE_AD_ID` +- `AFFISE_AD_TYPE` +- `AFFISE_ADSET` +- `AFFISE_ADSET_ID` +- `AFFISE_AFFC_ID` +- `AFFISE_CHANNEL` +- `AFFISE_CLICK_LOOK_BACK` +- `AFFISE_COST_CURRENCY` +- `AFFISE_COST_MODEL` +- `AFFISE_COST_VALUE` +- `AFFISE_DEEPLINK` +- `AFFISE_KEYWORDS` +- `AFFISE_MEDIA_TYPE` +- `AFFISE_MODEL` +- `AFFISE_OS` +- `AFFISE_PARTNER` +- `AFFISE_REF` +- `AFFISE_SITE_ID` +- `AFFISE_SUB_SITE_ID` +- `AFFC` +- `PID` +- `SUB_1` +- `SUB_2` +- `SUB_3` +- `SUB_4` +- `SUB_5` + +### Webview tracking +#### Initialize webview +To integrate the library into the JavaScript environment, we added a bridge between JavaScript and the native SDK. Now you can send events and use the functionality of the native library directly from Webview. +Here are step by step instructions: + + +```kotlin +// retreive webview from view hierarhy +val webView = findViewById(R.Id.your_webview_id) +// make sure javascript is enabled +webView.javaScriptEnabled = true +// initialize webview with Affise native library +Affise.registerWebView(webView) + +``` + +Other Javascript enviroment features is described below. + +#### Events tracking JS +after webview is initialized you send events from JavaScript enviroment + +```javascript +var event = new AddPaymentInfoEvent( + { card: 4138, type: 'phone' }, + Date.now(), + 'taxi' +); + +event.addPredefinedParameter('affise_p_purchase_currency', 'USD'); + +Affise.sendEvent(event) + +}); +``` + +Just like with native SDK, javascript enviroment also provides default events that can be passed from webview: +- `AchieveLevelEvent` +- `AddPaymentInfoEvent` +- `AddToCartEvent` +- `AddToWishlistEvent` +- `ClickAdvEvent` +- `CompleteRegistrationEvent` +- `CompleteStreamEvent` +- `CompleteTrialEvent` +- `CompleteTutorialEvent` +- `ContentItemsViewEvent` +- `ConvertedOfferEvent` +- `ConvertedOfferFromRetryEvent` +- `ConvertedTrialEvent` +- `ConvertedTrialFromRetryEvent` +- `CustomId01Event` +- `CustomId02Event` +- `CustomId03Event` +- `CustomId04Event` +- `CustomId05Event` +- `CustomId06Event` +- `CustomId07Event` +- `CustomId08Event` +- `CustomId09Event` +- `CustomId10Event` +- `DeepLinkedEvent` +- `FailedOfferFromRetryEvent` +- `FailedOfferiseEvent` +- `FailedSubscriptionEvent` +- `FailedSubscriptionFromRetryEvent` +- `FailedTrialEvent` +- `FailedTrialFromRetryEvent` +- `InitialOfferEvent` +- `InitialSubscriptionEvent` +- `InitialTrialEvent` +- `InitiatePurchaseEvent` +- `InitiateStreamEvent` +- `InviteEvent` +- `LastAttributedTouchEvent` +- `ListViewEvent` +- `LoginEvent` +- `OfferInRetryEvent` +- `OpenedFromPushNotificationEvent` +- `PurchaseEvent` +- `RateEvent` +- `ReEngageEvent` +- `ReactivatedSubscriptionEvent` +- `RenewedSubscriptionEvent` +- `RenewedSubscriptionFromRetryEvent` +- `ReserveEvent` +- `SalesEvent` +- `SearchEvent` +- `ShareEvent` +- `SpendCreditsEvent` +- `StartRegistrationEvent` +- `StartTrialEvent` +- `StartTutorialEvent` +- `SubscribeEvent` +- `SubscriptionEvent` +- `SubscriptionInRetryEvent` +- `TravelBookingEvent` +- `TrialInRetryEvent` +- `UnlockAchievementEvent` +- `UnsubscribeEvent` +- `UnsubscriptionEvent` +- `UpdateEvent` +- `ViewAdvEvent` +- `ViewCartEvent` +- `ViewItemEvent` +- `ViewItemsEvent` + +#### Predefined event parameters JS +Each event can be extended with custom event parameters. By calling `addPredefinedParameter` function you can pass predefined parameters name and value, for example: +```javascript +var event = ... + +event.addPredefinedParameter('affise_p_purchase_currency', 'USD'); + +Affise.sendEvent(event) + +}); +``` + +#### Custom events JS + +If above event functionality still limits your usecase, you can allways extend `Event` class to override fields you are missing + +```javascript +class AchieveLevelEvent extends Event { + constructor(level, timeStampMillis, userData) { + super('AchieveLevel'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_achieve_level: level, + affise_event_achieve_level_timestamp: timeStampMillis + }; + } +}}); +``` + diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..a07d548 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,87 @@ +import java.text.SimpleDateFormat + +plugins { + id 'com.android.application' + id 'kotlin-android' + id 'com.google.gms.google-services' + id 'kotlin-kapt' +} + +android { + compileSdk 33 + + buildFeatures { + viewBinding true + } + + defaultConfig { + applicationId "com.affise.app" + minSdk 21 + targetSdk 33 + versionCode 1 + versionName "1.0 build (${buildTime()})" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation 'androidx.appcompat:appcompat:1.4.1' + implementation 'androidx.constraintlayout:constraintlayout:2.1.3' + implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' + implementation 'androidx.core:core-ktx:1.7.0' + implementation 'com.google.android.material:material:1.5.0' + + implementation "com.google.firebase:firebase-core:20.0.2" + implementation 'com.google.firebase:firebase-messaging-ktx:23.0.0' + + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4" + + implementation "androidx.navigation:navigation-fragment-ktx:2.4.0" + implementation "androidx.navigation:navigation-ui-ktx:2.4.0" + + implementation "com.google.dagger:dagger:2.37" + implementation 'com.google.firebase:firebase-firestore-ktx:24.0.1' + kapt "com.google.dagger:dagger-compiler:2.37" + + implementation "com.google.dagger:dagger-android:2.37" + kapt "com.google.dagger:dagger-android-processor:2.37" + implementation "com.google.dagger:dagger-android-support:2.37" + + implementation 'com.github.bumptech.glide:glide:4.13.0' + kapt 'com.github.bumptech.glide:compiler:4.13.0' + + implementation(project("::attribution")) + + implementation 'com.google.android.flexbox:flexbox:3.0.0' + + implementation "com.fasterxml.jackson.core:jackson-core:2.11.1" + implementation "com.fasterxml.jackson.core:jackson-databind:2.11.1" + + testImplementation "junit:junit:$testJunit" + androidTestImplementation "androidx.test.ext:junit:$testAndroidxJunit" + androidTestImplementation "androidx.test.espresso:espresso-core:$testEspressoCore" +} + +static def buildTime() { + def df = new SimpleDateFormat("dd-MM-yyyy HH:mm") + df.setTimeZone(TimeZone.getTimeZone("UTC")) + return df.format(new Date()) +} \ No newline at end of file diff --git a/app/google-services.json b/app/google-services.json new file mode 100644 index 0000000..946400f --- /dev/null +++ b/app/google-services.json @@ -0,0 +1,39 @@ +{ + "project_info": { + "project_number": "1067582769202", + "project_id": "ma-sdk", + "storage_bucket": "ma-sdk.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:1067582769202:android:dcd5253dc319029f47942b", + "android_client_info": { + "package_name": "com.affise.app" + } + }, + "oauth_client": [ + { + "client_id": "1067582769202-j4rppfc1of6ob0ogl8hsmk8mtssv107a.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCkjqprruzV3tj0Zp6KOh19F2shCOGTMMg" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "1067582769202-j4rppfc1of6ob0ogl8hsmk8mtssv107a.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..51acf94 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/img1.png b/app/src/main/assets/img1.png new file mode 100644 index 0000000..e12846a Binary files /dev/null and b/app/src/main/assets/img1.png differ diff --git a/app/src/main/assets/img2.png b/app/src/main/assets/img2.png new file mode 100644 index 0000000..cf25980 Binary files /dev/null and b/app/src/main/assets/img2.png differ diff --git a/app/src/main/assets/img3.png b/app/src/main/assets/img3.png new file mode 100644 index 0000000..d2d5cdb Binary files /dev/null and b/app/src/main/assets/img3.png differ diff --git a/app/src/main/assets/img4.png b/app/src/main/assets/img4.png new file mode 100644 index 0000000..cee15f9 Binary files /dev/null and b/app/src/main/assets/img4.png differ diff --git a/app/src/main/assets/img5.png b/app/src/main/assets/img5.png new file mode 100644 index 0000000..775b1a4 Binary files /dev/null and b/app/src/main/assets/img5.png differ diff --git a/app/src/main/assets/index.html b/app/src/main/assets/index.html new file mode 100644 index 0000000..f969ce8 --- /dev/null +++ b/app/src/main/assets/index.html @@ -0,0 +1,1001 @@ + + + Test SDK + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + \ No newline at end of file diff --git a/app/src/main/assets/oaid.cert.pem b/app/src/main/assets/oaid.cert.pem new file mode 100644 index 0000000..33f8111 --- /dev/null +++ b/app/src/main/assets/oaid.cert.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFfDCCA2SgAwIBAgICA/EwDQYJKoZIhvcNAQELBQAwgYAxCzAJBgNVBAYTAkNO +MRAwDgYDVQQIDAdCZWlqaW5nMQwwCgYDVQQKDANNU0ExETAPBgNVBAsMCE9BSURf +U0RLMR4wHAYDVQQDDBVjb20uYnVuLm1paXRtZGlkLnNpZ24xHjAcBgkqhkiG9w0B +CQEWD21zYUBjYWljdC5hYy5jbjAgFw0yMTA2MDgxODA2MDNaGA8yMTc2MDUwMjE4 +MDYwM1owbTELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB0JlaWppbmcxEDAOBgNVBAcM +B0JlaWppbmcxCTAHBgNVBAoMADEeMBwGA1UEAwwVY29tLmV4YW1wbGUub2FpZHRl +c3QyMQ8wDQYJKoZIhvcNAQkBFgAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCyGC9T3GW4BSsvnVNqb8+HlnjJAQvTPd7H13m1f945uUsUYYMaaZuICYjb +4qefZUyQgyxBUTYfBOTpE7ccdcNoZgCp05RTng30zvkc4pjawJIkNeIC2NGlN7Rq +uO4Ka/06LSuq5N+bfL78E0MwZAinoxSswf/yNY5R1LJ2b3I7BnW7rVSb7KbB0Q4z +5KmuJ04hPVl7BtSdfVnvnRVTBDRm/cMgyupEl+CCGW8HUejzPHdHeHRK1rx7fuwQ +I+5jgMmDYRNXZD4AmJ0vClurQR5avN2xOuxB8HbtB+lQitsNnQCvYY1Xp4s7Ftfm +T6JzDNQ//vRgXxYrOaVFfoY/ugrO+SfZtqej+mdKR3KgOaAA75t8zcJfM6WGrbPJ +l3xHc2bhD4kGwRkTAcHJ7WQOIrWz85CPCfIoYh8nZlBlsCNamJJh6Pl2nAtmnfMt +DuYZJo/2QK6J/KFtywsYIekU6fiRsJaaunBf5sD9IxbvJcRRB+HRCsySKYEg8z7X +lh90fHimfUgT0Gt44/o+nzPh7Cq9Ay6dWtMxYFx78Z1epapOJdUNpZWIET8wQHri +JmbJ18TlidGRAW85kW2ka2cr4u6Ed68JQa+nN+kbS0dDeuvOBGOsiqqCy5aeA0oz +88HRWWIga02PLTZbEs4PdYrNzHWCISjnk34KpwwxYJUn6Zpy1wIDAQABoxAwDjAM +BgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQCEhPUIc+6sNQkVFCA/+ZvO +LO5P/DEJ/WVJODF+HIWiwcilwLUGJuWV3b4jO4VAJiDvrNJH60BVhmV/jt7eYOHF +99hOucbZRo+q4TrloXVZN/rVXzKqfHjv6y8Ss/YN45KanE/IeOxByRsMpKYollcs +DYOrF82uPHeurBuIqIVxYZO7qIM8y8G2TaMGL+//IpT8CnQBPCmI4OxHu3l0bnRp +FBHQQQSrEZHhbD+sbKkTQL5BJJJkx4gnxB24t42wYdYgaluj76l6fvspveEiURVk +J2Ag0oo8XWXC+kjK/+BedrLJ0b5fyGck/B+emgWPXNmkN26F3dgcDjx3ILpWGqI/ +amoyR4IA726IYXFQ1WBm/nOAiq7DZYbKhuNk2ZhS50+eLu030MqsuxMNeAjSXCvC +AQowuHMANLgSxHWJDKuEIpQYErGHS2QmeOyEA7sdmemVhZ+EgiB2QzaP2E13LDfd +xc0n+0IOYxdr3bT6HTvdhT02+H7Fl8aS3mp9adhAJ0IT1Xnh4o4rKuxlYH5fM+RG +4R4RaAB52tYxF6gOsud2KHWdQrt3gewP76FTzfizwJ8jkqBsIMAncQUHMyJwkLeg +KEDOuPr+54bK8jJbo+g3VBQ760f7WGRdNZYcb2ml6+rbVKb8twd52ZNLdM51CA0Z +mW2UsdCR0FgdAloOLI3KHQ== +-----END CERTIFICATE----- diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..053d148 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/affise/app/application/App.kt b/app/src/main/java/com/affise/app/application/App.kt new file mode 100644 index 0000000..954b112 --- /dev/null +++ b/app/src/main/java/com/affise/app/application/App.kt @@ -0,0 +1,49 @@ +package com.affise.app.application + +import android.content.SharedPreferences +import com.affise.app.dependencies.DaggerAppComponent +import com.affise.attribution.Affise +import com.affise.attribution.events.autoCatchingClick.AutoCatchingType +import com.affise.attribution.init.AffiseInitProperties +import com.google.firebase.FirebaseApp +import dagger.android.AndroidInjector +import dagger.android.DaggerApplication +import javax.inject.Inject + +class App : DaggerApplication() { + + @Inject + lateinit var preferences: SharedPreferences + + override fun onCreate() { + super.onCreate() + + FirebaseApp.initializeApp(this) + + val props = AffiseInitProperties( + affiseAppId = "AffiseDemoApp", + partParamName = "partParamName", + partParamNameToken = "partParamNameToken", + appToken = "app-token", + isProduction = false, + secretId = preferences.getString(SECRET_ID_KEY, "be07d122-3f3c-11ec-9bbc-0242ac130002"), + autoCatchingClickEvents = preferences.getStringSet(App.AUTO_CATCHING_TYPES_KEY, null) + ?.map { AutoCatchingType.valueOf(it) } + ?: emptyList(), + enabledMetrics = preferences.getBoolean(ENABLED_METRICS_KEY, false) + ) + + Affise.init(this, props) + } + + override fun applicationInjector(): AndroidInjector = + DaggerAppComponent.builder() + .application(this) + .build() + + companion object { + const val SECRET_ID_KEY = "SECRET_ID_KEY" + const val AUTO_CATCHING_TYPES_KEY = "AUTO_CATCHING_TYPES_KEY" + const val ENABLED_METRICS_KEY = "ENABLED_METRICS_KEY" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/dependencies/AppComponent.kt b/app/src/main/java/com/affise/app/dependencies/AppComponent.kt new file mode 100644 index 0000000..992c1f4 --- /dev/null +++ b/app/src/main/java/com/affise/app/dependencies/AppComponent.kt @@ -0,0 +1,22 @@ +package com.affise.app.dependencies + +import android.app.Application +import com.affise.app.application.App +import dagger.BindsInstance +import dagger.Component +import dagger.android.AndroidInjector +import javax.inject.Singleton + +@Singleton +@Component(modules = [AppModule::class]) +interface AppComponent : AndroidInjector { + + @Component.Builder + interface Builder { + + @BindsInstance + fun application(application: Application): Builder + + fun build(): AppComponent + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/dependencies/AppModule.kt b/app/src/main/java/com/affise/app/dependencies/AppModule.kt new file mode 100644 index 0000000..de71b38 --- /dev/null +++ b/app/src/main/java/com/affise/app/dependencies/AppModule.kt @@ -0,0 +1,41 @@ +package com.affise.app.dependencies + +import android.app.Application +import android.content.Context +import android.content.SharedPreferences +import com.affise.app.dependencies.activities.ActivityModule +import com.affise.app.dependencies.fragments.FragmentModule +import com.affise.app.dependencies.models.ViewModelModule +import com.affise.app.dependencies.repositories.RepositoryModule +import com.affise.app.dependencies.store.StoreModule +import com.affise.app.dependencies.usecases.UseCaseModule +import com.fasterxml.jackson.databind.ObjectMapper +import dagger.Module +import dagger.Provides +import dagger.android.support.AndroidSupportInjectionModule +import javax.inject.Singleton + +@Module( + includes = [ + AndroidSupportInjectionModule::class, + ActivityModule::class, + FragmentModule::class, + ViewModelModule::class, + RepositoryModule::class, + UseCaseModule::class, + StoreModule::class + ] +) +class AppModule { + + @Provides + @Singleton + fun mapper(): ObjectMapper = ObjectMapper() + + @Provides + @Singleton + fun sharedPreferences(app: Application): SharedPreferences = app.getSharedPreferences( + "com.affise.attribution", + Context.MODE_PRIVATE + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/dependencies/activities/ActivityModule.kt b/app/src/main/java/com/affise/app/dependencies/activities/ActivityModule.kt new file mode 100644 index 0000000..d677d63 --- /dev/null +++ b/app/src/main/java/com/affise/app/dependencies/activities/ActivityModule.kt @@ -0,0 +1,15 @@ +package com.affise.app.dependencies.activities + +import com.affise.app.dependencies.activities.modules.MainActivityModule +import com.affise.app.dependencies.scope.ActivityScope +import com.affise.app.ui.activity.main.MainActivity +import dagger.Module +import dagger.android.ContributesAndroidInjector + +@Module +abstract class ActivityModule { + + @ActivityScope + @ContributesAndroidInjector(modules = [MainActivityModule::class]) + abstract fun mainActivity(): MainActivity +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/dependencies/activities/modules/MainActivityModule.kt b/app/src/main/java/com/affise/app/dependencies/activities/modules/MainActivityModule.kt new file mode 100644 index 0000000..5a7d84b --- /dev/null +++ b/app/src/main/java/com/affise/app/dependencies/activities/modules/MainActivityModule.kt @@ -0,0 +1,20 @@ +package com.affise.app.dependencies.activities.modules + +import androidx.lifecycle.ViewModelProvider +import com.affise.app.dependencies.models.DaggerViewModelFactory +import com.affise.app.ui.activity.main.MainActivity +import com.affise.app.ui.activity.main.MainActivityContract +import com.affise.app.ui.activity.main.MainActivityViewModel +import dagger.Module +import dagger.Provides + +@Module +class MainActivityModule { + + @Provides + fun provideViewModel( + activity: MainActivity, + viewModelFactory: DaggerViewModelFactory + ): MainActivityContract.ViewModel = ViewModelProvider(activity, viewModelFactory) + .get(MainActivityViewModel::class.java) +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/dependencies/fragments/FragmentModule.kt b/app/src/main/java/com/affise/app/dependencies/fragments/FragmentModule.kt new file mode 100644 index 0000000..3008046 --- /dev/null +++ b/app/src/main/java/com/affise/app/dependencies/fragments/FragmentModule.kt @@ -0,0 +1,55 @@ +package com.affise.app.dependencies.fragments + +import com.affise.app.dependencies.fragments.modules.* +import com.affise.app.dependencies.scope.FragmentScope +import com.affise.app.ui.fragments.autoCatching.AutoCatchingFragment +import com.affise.app.ui.fragments.buttons.ButtonsFragment +import com.affise.app.ui.fragments.cart.CartFragment +import com.affise.app.ui.fragments.details.ProductDetailsFragment +import com.affise.app.ui.fragments.home.HomeFragment +import com.affise.app.ui.fragments.likes.LikesFragment +import com.affise.app.ui.fragments.menu.MenuFragment +import com.affise.app.ui.fragments.metrics.MetricsFragment +import com.affise.app.ui.fragments.settings.SettingsFragment +import dagger.Module +import dagger.android.ContributesAndroidInjector + +@Module +abstract class FragmentModule { + + @FragmentScope + @ContributesAndroidInjector(modules = [MenuFragmentModule::class]) + abstract fun menuFragment(): MenuFragment + + @FragmentScope + @ContributesAndroidInjector(modules = [HomeFragmentModule::class]) + abstract fun homeFragment(): HomeFragment + + @FragmentScope + @ContributesAndroidInjector(modules = [ProductDetailsFragmentModule::class]) + abstract fun productDetailsFragment(): ProductDetailsFragment + + @FragmentScope + @ContributesAndroidInjector(modules = [LikesFragmentModule::class]) + abstract fun likesFragment(): LikesFragment + + @FragmentScope + @ContributesAndroidInjector(modules = [CartFragmentModule::class]) + abstract fun cartFragment(): CartFragment + + @FragmentScope + @ContributesAndroidInjector(modules = [ButtonsFragmentModule::class]) + abstract fun buttonsFragment(): ButtonsFragment + + @FragmentScope + @ContributesAndroidInjector(modules = [SettingsFragmentModule::class]) + abstract fun settingsFragment(): SettingsFragment + + @FragmentScope + @ContributesAndroidInjector(modules = [AutoCatchingFragmentModule::class]) + abstract fun autoCatchingFragment(): AutoCatchingFragment + + @FragmentScope + @ContributesAndroidInjector(modules = [MetricsFragmentModule::class]) + abstract fun metricsFragment(): MetricsFragment +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/dependencies/fragments/modules/AutoCatchingFragmentModule.kt b/app/src/main/java/com/affise/app/dependencies/fragments/modules/AutoCatchingFragmentModule.kt new file mode 100644 index 0000000..afdb36d --- /dev/null +++ b/app/src/main/java/com/affise/app/dependencies/fragments/modules/AutoCatchingFragmentModule.kt @@ -0,0 +1,27 @@ +package com.affise.app.dependencies.fragments.modules + +import androidx.lifecycle.ViewModelProvider +import com.affise.app.dependencies.models.DaggerViewModelFactory +import com.affise.app.ui.activity.main.MenuHolder +import com.affise.app.ui.fragments.autoCatching.AutoCatchingContract +import com.affise.app.ui.fragments.autoCatching.AutoCatchingFragment +import com.affise.app.ui.fragments.autoCatching.AutoCatchingViewModel +import dagger.Module +import dagger.Provides + +@Module +class AutoCatchingFragmentModule { + + @Provides + fun provideMenuHolder( + fragment: AutoCatchingFragment + ): MenuHolder = fragment.requireActivity() as MenuHolder + + @Provides + fun provideViewModel( + fragment: AutoCatchingFragment, + viewModelFactory: DaggerViewModelFactory + ): AutoCatchingContract.ViewModel = ViewModelProvider( + fragment, viewModelFactory + ).get(AutoCatchingViewModel::class.java) +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/dependencies/fragments/modules/ButtonsFragmentModule.kt b/app/src/main/java/com/affise/app/dependencies/fragments/modules/ButtonsFragmentModule.kt new file mode 100644 index 0000000..b49dba8 --- /dev/null +++ b/app/src/main/java/com/affise/app/dependencies/fragments/modules/ButtonsFragmentModule.kt @@ -0,0 +1,27 @@ +package com.affise.app.dependencies.fragments.modules + +import androidx.lifecycle.ViewModelProvider +import com.affise.app.dependencies.models.DaggerViewModelFactory +import com.affise.app.ui.activity.main.MenuHolder +import com.affise.app.ui.fragments.buttons.ButtonsContract +import com.affise.app.ui.fragments.buttons.ButtonsFragment +import com.affise.app.ui.fragments.buttons.ButtonsViewModel +import dagger.Module +import dagger.Provides + +@Module +class ButtonsFragmentModule { + + @Provides + fun provideMenuHolder( + fragment: ButtonsFragment + ): MenuHolder = fragment.requireActivity() as MenuHolder + + @Provides + fun provideViewModel( + fragment: ButtonsFragment, + viewModelFactory: DaggerViewModelFactory + ): ButtonsContract.ViewModel = ViewModelProvider( + fragment, viewModelFactory + ).get(ButtonsViewModel::class.java) +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/dependencies/fragments/modules/CartFragmentModule.kt b/app/src/main/java/com/affise/app/dependencies/fragments/modules/CartFragmentModule.kt new file mode 100644 index 0000000..787fff7 --- /dev/null +++ b/app/src/main/java/com/affise/app/dependencies/fragments/modules/CartFragmentModule.kt @@ -0,0 +1,27 @@ +package com.affise.app.dependencies.fragments.modules + +import androidx.lifecycle.ViewModelProvider +import com.affise.app.dependencies.models.DaggerViewModelFactory +import com.affise.app.ui.activity.main.MenuHolder +import com.affise.app.ui.fragments.cart.CartContract +import com.affise.app.ui.fragments.cart.CartFragment +import com.affise.app.ui.fragments.cart.CartViewModel +import dagger.Module +import dagger.Provides + +@Module +class CartFragmentModule { + + @Provides + fun provideMenuHolder( + fragment: CartFragment + ): MenuHolder = fragment.requireActivity() as MenuHolder + + @Provides + fun provideViewModel( + fragment: CartFragment, + viewModelFactory: DaggerViewModelFactory + ): CartContract.ViewModel = ViewModelProvider( + fragment, viewModelFactory + ).get(CartViewModel::class.java) +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/dependencies/fragments/modules/HomeFragmentModule.kt b/app/src/main/java/com/affise/app/dependencies/fragments/modules/HomeFragmentModule.kt new file mode 100644 index 0000000..d55b491 --- /dev/null +++ b/app/src/main/java/com/affise/app/dependencies/fragments/modules/HomeFragmentModule.kt @@ -0,0 +1,27 @@ +package com.affise.app.dependencies.fragments.modules + +import androidx.lifecycle.ViewModelProvider +import com.affise.app.dependencies.models.DaggerViewModelFactory +import com.affise.app.ui.activity.main.MenuHolder +import com.affise.app.ui.fragments.home.HomeContract +import com.affise.app.ui.fragments.home.HomeFragment +import com.affise.app.ui.fragments.home.HomeViewModel +import dagger.Module +import dagger.Provides + +@Module +class HomeFragmentModule { + + @Provides + fun provideMenuHolder( + fragment: HomeFragment + ): MenuHolder = fragment.requireActivity() as MenuHolder + + @Provides + fun provideViewModel( + fragment: HomeFragment, + viewModelFactory: DaggerViewModelFactory + ): HomeContract.ViewModel = ViewModelProvider( + fragment, viewModelFactory + ).get(HomeViewModel::class.java) +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/dependencies/fragments/modules/LikesFragmentModule.kt b/app/src/main/java/com/affise/app/dependencies/fragments/modules/LikesFragmentModule.kt new file mode 100644 index 0000000..39891d5 --- /dev/null +++ b/app/src/main/java/com/affise/app/dependencies/fragments/modules/LikesFragmentModule.kt @@ -0,0 +1,27 @@ +package com.affise.app.dependencies.fragments.modules + +import androidx.lifecycle.ViewModelProvider +import com.affise.app.dependencies.models.DaggerViewModelFactory +import com.affise.app.ui.activity.main.MenuHolder +import com.affise.app.ui.fragments.likes.LikesContract +import com.affise.app.ui.fragments.likes.LikesFragment +import com.affise.app.ui.fragments.likes.LikesViewModel +import dagger.Module +import dagger.Provides + +@Module +class LikesFragmentModule { + + @Provides + fun provideMenuHolder( + fragment: LikesFragment + ): MenuHolder = fragment.requireActivity() as MenuHolder + + @Provides + fun provideViewModel( + fragment: LikesFragment, + viewModelFactory: DaggerViewModelFactory + ): LikesContract.ViewModel = ViewModelProvider( + fragment, viewModelFactory + ).get(LikesViewModel::class.java) +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/dependencies/fragments/modules/MenuFragmentModule.kt b/app/src/main/java/com/affise/app/dependencies/fragments/modules/MenuFragmentModule.kt new file mode 100644 index 0000000..0a87a79 --- /dev/null +++ b/app/src/main/java/com/affise/app/dependencies/fragments/modules/MenuFragmentModule.kt @@ -0,0 +1,27 @@ +package com.affise.app.dependencies.fragments.modules + +import androidx.lifecycle.ViewModelProvider +import com.affise.app.dependencies.models.DaggerViewModelFactory +import com.affise.app.ui.activity.main.MenuHolder +import com.affise.app.ui.fragments.menu.MenuContract +import com.affise.app.ui.fragments.menu.MenuFragment +import com.affise.app.ui.fragments.menu.MenuViewModel +import dagger.Module +import dagger.Provides + +@Module +class MenuFragmentModule { + + @Provides + fun provideMenuHolder( + fragment: MenuFragment + ): MenuHolder = fragment.requireActivity() as MenuHolder + + @Provides + fun provideViewModel( + fragment: MenuFragment, + viewModelFactory: DaggerViewModelFactory + ): MenuContract.ViewModel = ViewModelProvider( + fragment, viewModelFactory + ).get(MenuViewModel::class.java) +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/dependencies/fragments/modules/MetricsFragmentModule.kt b/app/src/main/java/com/affise/app/dependencies/fragments/modules/MetricsFragmentModule.kt new file mode 100644 index 0000000..e4bccbc --- /dev/null +++ b/app/src/main/java/com/affise/app/dependencies/fragments/modules/MetricsFragmentModule.kt @@ -0,0 +1,27 @@ +package com.affise.app.dependencies.fragments.modules + +import androidx.lifecycle.ViewModelProvider +import com.affise.app.dependencies.models.DaggerViewModelFactory +import com.affise.app.ui.activity.main.MenuHolder +import com.affise.app.ui.fragments.metrics.MetricsContract +import com.affise.app.ui.fragments.metrics.MetricsFragment +import com.affise.app.ui.fragments.metrics.MetricsViewModel +import dagger.Module +import dagger.Provides + +@Module +class MetricsFragmentModule { + + @Provides + fun provideMenuHolder( + fragment: MetricsFragment + ): MenuHolder = fragment.requireActivity() as MenuHolder + + @Provides + fun provideViewModel( + fragment: MetricsFragment, + viewModelFactory: DaggerViewModelFactory + ): MetricsContract.ViewModel = ViewModelProvider( + fragment, viewModelFactory + ).get(MetricsViewModel::class.java) +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/dependencies/fragments/modules/ProductDetailsFragmentModule.kt b/app/src/main/java/com/affise/app/dependencies/fragments/modules/ProductDetailsFragmentModule.kt new file mode 100644 index 0000000..f7d71ac --- /dev/null +++ b/app/src/main/java/com/affise/app/dependencies/fragments/modules/ProductDetailsFragmentModule.kt @@ -0,0 +1,21 @@ +package com.affise.app.dependencies.fragments.modules + +import androidx.lifecycle.ViewModelProvider +import com.affise.app.dependencies.models.DaggerViewModelFactory +import com.affise.app.ui.fragments.details.ProductDetailsContract +import com.affise.app.ui.fragments.details.ProductDetailsFragment +import com.affise.app.ui.fragments.details.ProductDetailsViewModel +import dagger.Module +import dagger.Provides + +@Module +class ProductDetailsFragmentModule { + + @Provides + fun provideViewModel( + fragment: ProductDetailsFragment, + viewModelFactory: DaggerViewModelFactory + ): ProductDetailsContract.ViewModel = ViewModelProvider( + fragment, viewModelFactory + ).get(ProductDetailsViewModel::class.java) +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/dependencies/fragments/modules/SettingsFragmentModule.kt b/app/src/main/java/com/affise/app/dependencies/fragments/modules/SettingsFragmentModule.kt new file mode 100644 index 0000000..ffe2076 --- /dev/null +++ b/app/src/main/java/com/affise/app/dependencies/fragments/modules/SettingsFragmentModule.kt @@ -0,0 +1,27 @@ +package com.affise.app.dependencies.fragments.modules + +import androidx.lifecycle.ViewModelProvider +import com.affise.app.dependencies.models.DaggerViewModelFactory +import com.affise.app.ui.activity.main.MenuHolder +import com.affise.app.ui.fragments.settings.SettingsContract +import com.affise.app.ui.fragments.settings.SettingsFragment +import com.affise.app.ui.fragments.settings.SettingsViewModel +import dagger.Module +import dagger.Provides + +@Module +class SettingsFragmentModule { + + @Provides + fun provideMenuHolder( + fragment: SettingsFragment + ): MenuHolder = fragment.requireActivity() as MenuHolder + + @Provides + fun provideViewModel( + fragment: SettingsFragment, + viewModelFactory: DaggerViewModelFactory + ): SettingsContract.ViewModel = ViewModelProvider( + fragment, viewModelFactory + ).get(SettingsViewModel::class.java) +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/dependencies/models/DaggerViewModelFactory.kt b/app/src/main/java/com/affise/app/dependencies/models/DaggerViewModelFactory.kt new file mode 100644 index 0000000..8fe227c --- /dev/null +++ b/app/src/main/java/com/affise/app/dependencies/models/DaggerViewModelFactory.kt @@ -0,0 +1,26 @@ +package com.affise.app.dependencies.models + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import javax.inject.Inject +import javax.inject.Provider +import javax.inject.Singleton + +@Singleton +class DaggerViewModelFactory @Inject constructor( + private val creators: Map, @JvmSuppressWildcards Provider> +) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + val creator = creators[modelClass] ?: creators.asIterable() + .firstOrNull { modelClass.isAssignableFrom(it.key) }?.value + ?: throw IllegalArgumentException("Unknown model class $modelClass") + + return try { + creator.get() as T + } catch (e: Exception) { + throw RuntimeException(e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/dependencies/models/ViewModelKey.kt b/app/src/main/java/com/affise/app/dependencies/models/ViewModelKey.kt new file mode 100644 index 0000000..3633100 --- /dev/null +++ b/app/src/main/java/com/affise/app/dependencies/models/ViewModelKey.kt @@ -0,0 +1,13 @@ +package com.affise.app.dependencies.models + +import androidx.lifecycle.ViewModel +import dagger.MapKey +import kotlin.reflect.KClass + +@Target( + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER +) +@MapKey +annotation class ViewModelKey(val value: KClass) \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/dependencies/models/ViewModelModule.kt b/app/src/main/java/com/affise/app/dependencies/models/ViewModelModule.kt new file mode 100644 index 0000000..5006f0a --- /dev/null +++ b/app/src/main/java/com/affise/app/dependencies/models/ViewModelModule.kt @@ -0,0 +1,70 @@ +package com.affise.app.dependencies.models + +import androidx.lifecycle.ViewModel +import com.affise.app.ui.activity.main.MainActivityViewModel +import com.affise.app.ui.fragments.autoCatching.AutoCatchingViewModel +import com.affise.app.ui.fragments.buttons.ButtonsViewModel +import com.affise.app.ui.fragments.cart.CartViewModel +import com.affise.app.ui.fragments.details.ProductDetailsViewModel +import com.affise.app.ui.fragments.home.HomeViewModel +import com.affise.app.ui.fragments.likes.LikesViewModel +import com.affise.app.ui.fragments.menu.MenuViewModel +import com.affise.app.ui.fragments.metrics.MetricsViewModel +import com.affise.app.ui.fragments.settings.SettingsViewModel +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap + +@Module +interface ViewModelModule { + + @Binds + @IntoMap + @ViewModelKey(MainActivityViewModel::class) + fun mainActivityViewModel(viewModel: MainActivityViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(MenuViewModel::class) + fun menuViewModel(viewModel: MenuViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(HomeViewModel::class) + fun homeViewModel(viewModel: HomeViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(ProductDetailsViewModel::class) + fun productDetailsViewModel(viewModel: ProductDetailsViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(ButtonsViewModel::class) + fun buttonsViewModel(viewModel: ButtonsViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(LikesViewModel::class) + fun likesViewModel(viewModel: LikesViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(CartViewModel::class) + fun cartViewModel(viewModel: CartViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(SettingsViewModel::class) + fun settingsViewModel(viewModel: SettingsViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(AutoCatchingViewModel::class) + fun autoCatchingViewModel(viewModel: AutoCatchingViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(MetricsViewModel::class) + fun metricsViewModel(viewModel: MetricsViewModel): ViewModel +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/dependencies/repositories/RepositoryModule.kt b/app/src/main/java/com/affise/app/dependencies/repositories/RepositoryModule.kt new file mode 100644 index 0000000..afceeb2 --- /dev/null +++ b/app/src/main/java/com/affise/app/dependencies/repositories/RepositoryModule.kt @@ -0,0 +1,18 @@ +package com.affise.app.dependencies.repositories + +import com.affise.app.repositories.ProductRepository +import com.affise.app.repositories.ProductRepositoryImpl +import com.affise.app.storage.Storage +import dagger.Module +import dagger.Provides +import dagger.Reusable + +@Module +class RepositoryModule { + + @Reusable + @Provides + fun productRepository( + storage: Storage + ): ProductRepository = ProductRepositoryImpl(storage) +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/dependencies/scope/ActivityScope.kt b/app/src/main/java/com/affise/app/dependencies/scope/ActivityScope.kt new file mode 100644 index 0000000..02568c9 --- /dev/null +++ b/app/src/main/java/com/affise/app/dependencies/scope/ActivityScope.kt @@ -0,0 +1,7 @@ +package com.affise.app.dependencies.scope + +import javax.inject.Scope + +@Scope +@Retention(AnnotationRetention.RUNTIME) +annotation class ActivityScope \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/dependencies/scope/FragmentScope.kt b/app/src/main/java/com/affise/app/dependencies/scope/FragmentScope.kt new file mode 100644 index 0000000..7711cf9 --- /dev/null +++ b/app/src/main/java/com/affise/app/dependencies/scope/FragmentScope.kt @@ -0,0 +1,7 @@ +package com.affise.app.dependencies.scope + +import javax.inject.Scope + +@Scope +@Retention(AnnotationRetention.RUNTIME) +annotation class FragmentScope \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/dependencies/store/StoreModule.kt b/app/src/main/java/com/affise/app/dependencies/store/StoreModule.kt new file mode 100644 index 0000000..d240385 --- /dev/null +++ b/app/src/main/java/com/affise/app/dependencies/store/StoreModule.kt @@ -0,0 +1,15 @@ +package com.affise.app.dependencies.store + +import com.affise.app.storage.FakeStorageImpl +import com.affise.app.storage.Storage +import dagger.Module +import dagger.Provides +import javax.inject.Singleton + +@Module +class StoreModule { + + @Singleton + @Provides + fun productRepository(): Storage = FakeStorageImpl() +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/dependencies/usecases/UseCaseModule.kt b/app/src/main/java/com/affise/app/dependencies/usecases/UseCaseModule.kt new file mode 100644 index 0000000..194be8e --- /dev/null +++ b/app/src/main/java/com/affise/app/dependencies/usecases/UseCaseModule.kt @@ -0,0 +1,18 @@ +package com.affise.app.dependencies.usecases + +import com.affise.app.repositories.ProductRepository +import com.affise.app.usecase.ProductUseCase +import com.affise.app.usecase.ProductUseCaseImpl +import dagger.Module +import dagger.Provides +import dagger.Reusable + +@Module +class UseCaseModule { + + @Reusable + @Provides + fun productRepository( + repository: ProductRepository + ): ProductUseCase = ProductUseCaseImpl(repository) +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/entity/Colors.kt b/app/src/main/java/com/affise/app/entity/Colors.kt new file mode 100644 index 0000000..f8d6c5e --- /dev/null +++ b/app/src/main/java/com/affise/app/entity/Colors.kt @@ -0,0 +1,10 @@ +package com.affise.app.entity + +import com.affise.app.R + +enum class Colors(val colorRes: Int) { + WHITE(R.color.item_white), + BLACK(R.color.item_black), + BLUE(R.color.item_blue), + YELLOW(R.color.item_yellow) +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/entity/ProductEntity.kt b/app/src/main/java/com/affise/app/entity/ProductEntity.kt new file mode 100644 index 0000000..31fa44e --- /dev/null +++ b/app/src/main/java/com/affise/app/entity/ProductEntity.kt @@ -0,0 +1,17 @@ +package com.affise.app.entity + +import com.affise.app.ui.fragments.details.adapters.SizeType +import java.math.BigDecimal + +data class ProductEntity( + val id: Long, + val price: BigDecimal?, + val unit: String?, + val name: String, + val description: String, + val rating: Double, + val size_type: SizeType, + val size: Double, + val color: Colors, + val images: List +) \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/extensions/Context.kt b/app/src/main/java/com/affise/app/extensions/Context.kt new file mode 100644 index 0000000..07c4bf2 --- /dev/null +++ b/app/src/main/java/com/affise/app/extensions/Context.kt @@ -0,0 +1,27 @@ +package com.affise.app.extensions + +import android.app.Activity +import android.content.Context +import android.util.TypedValue +import android.view.inputmethod.InputMethodManager + +fun Context.convertDpToPixels(dp: Float): Int { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + dp, + resources.displayMetrics + ).toInt() +} + +fun Activity.hideKeyboard() { + val viewFocus = currentFocus + + val token = viewFocus?.windowToken + ?: window.decorView.rootView.windowToken + ?: return + + val imm = (getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager) + ?: return + + imm.hideSoftInputFromWindow(token, 0) +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/extensions/IOWithErrorHandling.kt b/app/src/main/java/com/affise/app/extensions/IOWithErrorHandling.kt new file mode 100644 index 0000000..b7fec16 --- /dev/null +++ b/app/src/main/java/com/affise/app/extensions/IOWithErrorHandling.kt @@ -0,0 +1,12 @@ +package com.affise.app.extensions + +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlin.coroutines.CoroutineContext + +fun Dispatchers.IOWithErrorHandling(handler: (Throwable) -> Unit): CoroutineContext { + return SupervisorJob() + IO + CoroutineExceptionHandler { _, throwable -> + handler.invoke(throwable) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/receivers/PushService.kt b/app/src/main/java/com/affise/app/receivers/PushService.kt new file mode 100644 index 0000000..1ac68cc --- /dev/null +++ b/app/src/main/java/com/affise/app/receivers/PushService.kt @@ -0,0 +1,11 @@ +package com.affise.app.receivers + +import com.affise.attribution.Affise +import com.google.firebase.messaging.FirebaseMessagingService + +class PushService : FirebaseMessagingService() { + + override fun onNewToken(token: String) { + Affise.addPushToken(token) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/repositories/ProductRepository.kt b/app/src/main/java/com/affise/app/repositories/ProductRepository.kt new file mode 100644 index 0000000..42855bd --- /dev/null +++ b/app/src/main/java/com/affise/app/repositories/ProductRepository.kt @@ -0,0 +1,25 @@ +package com.affise.app.repositories + +import com.affise.app.entity.ProductEntity +import com.affise.app.ui.fragments.cart.adapters.ProductCartEntity +import com.affise.app.ui.fragments.home.adapters.ProductNewEntity +import com.affise.app.ui.fragments.home.adapters.ProductPopularEntity +import com.affise.app.ui.fragments.likes.adapters.ProductLikeEntity + +interface ProductRepository { + suspend fun getPopularProducts(): List + suspend fun getNewProducts(): List + suspend fun getLikeProducts(): List + + suspend fun getProductDetails(id: Long): ProductEntity? + + suspend fun inLike(productId: Long): Boolean + suspend fun inCart(productId: Long): Boolean + + suspend fun updateLike(productId: Long?): Boolean + suspend fun updateCart(productId: Long?): Boolean + + suspend fun getCart(): List + suspend fun removeOnCart(productId: Long): List + suspend fun addToCart(productId: Long): List +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/repositories/ProductRepositoryImpl.kt b/app/src/main/java/com/affise/app/repositories/ProductRepositoryImpl.kt new file mode 100644 index 0000000..d256600 --- /dev/null +++ b/app/src/main/java/com/affise/app/repositories/ProductRepositoryImpl.kt @@ -0,0 +1,41 @@ +package com.affise.app.repositories + +import com.affise.app.entity.ProductEntity +import com.affise.app.storage.Storage +import com.affise.app.ui.fragments.cart.adapters.ProductCartEntity +import com.affise.app.ui.fragments.home.adapters.ProductNewEntity +import com.affise.app.ui.fragments.home.adapters.ProductPopularEntity +import com.affise.app.ui.fragments.likes.adapters.ProductLikeEntity +import javax.inject.Inject + +class ProductRepositoryImpl @Inject constructor( + private val storage: Storage +) : ProductRepository { + + override suspend fun getPopularProducts( + ): List = storage.getPopularProducts() + + override suspend fun getNewProducts(): List = storage.getNewProducts() + + override suspend fun getLikeProducts(): List = storage.getLikeProducts() + + override suspend fun getProductDetails(id: Long): ProductEntity? = storage.getProductDetails(id) + + override suspend fun inLike(productId: Long): Boolean = storage.inLike(productId) + + override suspend fun inCart(productId: Long): Boolean = storage.inCart(productId) + + override suspend fun updateLike(productId: Long?): Boolean = storage.updateLike(productId) + + override suspend fun updateCart(productId: Long?): Boolean = storage.updateCart(productId) + + override suspend fun getCart(): List = storage.getCart() + + override suspend fun removeOnCart( + productId: Long + ): List = storage.removeOnCart(productId) + + override suspend fun addToCart( + productId: Long + ): List = storage.addToCart(productId) +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/storage/FakeStorageImpl.kt b/app/src/main/java/com/affise/app/storage/FakeStorageImpl.kt new file mode 100644 index 0000000..ae2600b --- /dev/null +++ b/app/src/main/java/com/affise/app/storage/FakeStorageImpl.kt @@ -0,0 +1,174 @@ +package com.affise.app.storage + +import com.affise.app.entity.Colors +import com.affise.app.entity.ProductEntity +import com.affise.app.ui.fragments.cart.adapters.ProductCartEntity +import com.affise.app.ui.fragments.details.adapters.SizeType +import com.affise.app.ui.fragments.home.adapters.ProductNewEntity +import com.affise.app.ui.fragments.home.adapters.ProductPopularEntity +import com.affise.app.ui.fragments.likes.adapters.ProductLikeEntity +import javax.inject.Inject + +class FakeStorageImpl @Inject constructor() : Storage { + + private val product1 = ProductEntity( + 1, + 10.toBigDecimal(), + "$", + "Air Max 270 G", + "The Nike Air max 270 G Huarache Run DNA ch. 1 reimagines not only the original Air", + 3.8, + SizeType.US, + 9.0, + Colors.BLACK, + listOf("img1.png") + ) + + private val product2 = ProductEntity( + 2, + 20.toBigDecimal(), + "$", + "Nike Ultra Zoom", + "The Nike Ultra Zoom Huarache Run DNA ch. 1 reimagines not only the original Air", + 4.8, + SizeType.US, + 8.0, + Colors.WHITE, + listOf("img2.png", "img2.png") + ) + + private val product3 = ProductEntity( + 3, + 18.6.toBigDecimal(), + "$", + "Nike Power pro", + "The Nike Ultra Zoom Huarache Run DNA ch. 1 reimagines not only the original Air", + 4.1, + SizeType.US, + 7.0, + Colors.BLUE, + listOf("img3.png", "img3.png", "img3.png") + ) + + private val product4 = ProductEntity( + 4, + 999.toBigDecimal(), + "$", + "Nike Air", + "The Nike Air Huarache Run DNA ch. 1 reimagines not only the original Air", + 2.1, + SizeType.EURO, + 45.0, + Colors.YELLOW, + listOf("img4.png", "img4.png", "img4.png", "img4.png") + ) + + private val product5 = ProductEntity( + 5, + 202.22.toBigDecimal(), + "$", + "Nike Power", + "The Nike Power Huarache Run DNA ch. 1 reimagines not only the original Air", + 4.6, + SizeType.US, + 9.0, + Colors.BLACK, + listOf("img5.png", "img5.png", "img5.png", "img5.png", "img5.png") + ) + + private val allProducts = listOf( + product1, product2, product3, product4, product5 + ) + + private val popularProducts = mutableListOf( + product1, product2, product3, product4, product5 + ) + + private val newProducts = mutableListOf( + product1, product2, product3, product4, product5 + ) + + private val likeProducts = mutableListOf( + product1, product3 + ) + + private val cartProducts = mutableListOf( + ProductCartEntity(product4, 1) + ) + + override suspend fun getPopularProducts(): List = popularProducts + .map { + ProductPopularEntity(it, inLike(it.id), inCart(it.id)) + } + + override suspend fun getNewProducts(): List = newProducts + .map { + ProductNewEntity(it, inLike(it.id), inCart(it.id)) + } + + override suspend fun getLikeProducts(): List = likeProducts + .map { + ProductLikeEntity(it, true, inCart(it.id)) + } + + override suspend fun getProductDetails( + id: Long + ): ProductEntity? = allProducts.find { it.id == id } + + override suspend fun inLike(productId: Long): Boolean = likeProducts + .any { it.id == productId } + + override suspend fun inCart(productId: Long): Boolean = cartProducts + .any { it.product.id == productId } + + override suspend fun updateLike(productId: Long?): Boolean { + productId ?: return false + + likeProducts.find { it.id == productId } + ?.let { + likeProducts.remove(it) + } + ?: allProducts.find { it.id == productId }?.let { + likeProducts.add(it) + } + + return inLike(productId) + } + + override suspend fun updateCart(productId: Long?): Boolean { + productId ?: return false + + cartProducts.find { it.product.id == productId } + ?.let { + cartProducts.remove(it) + } + ?: allProducts.find { it.id == productId }?.let { + cartProducts.add(ProductCartEntity(it, 1)) + } + + return inCart(productId) + } + + override suspend fun getCart(): List = cartProducts + .map { ProductCartEntity(it.product, it.count) } + + override suspend fun removeOnCart(productId: Long): List { + cartProducts.find { it.product.id == productId }?.let { + if (it.count > 1) { + it.count = it.count - 1 + } else { + cartProducts.remove(it) + } + } + + return getCart() + } + + override suspend fun addToCart(productId: Long): List { + cartProducts.find { it.product.id == productId }?.let { + it.count = it.count + 1 + } + + return getCart() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/storage/Storage.kt b/app/src/main/java/com/affise/app/storage/Storage.kt new file mode 100644 index 0000000..15e1944 --- /dev/null +++ b/app/src/main/java/com/affise/app/storage/Storage.kt @@ -0,0 +1,25 @@ +package com.affise.app.storage + +import com.affise.app.entity.ProductEntity +import com.affise.app.ui.fragments.cart.adapters.ProductCartEntity +import com.affise.app.ui.fragments.home.adapters.ProductNewEntity +import com.affise.app.ui.fragments.home.adapters.ProductPopularEntity +import com.affise.app.ui.fragments.likes.adapters.ProductLikeEntity + +interface Storage { + suspend fun getPopularProducts(): List + suspend fun getNewProducts(): List + suspend fun getLikeProducts(): List + + suspend fun getProductDetails(id: Long): ProductEntity? + + suspend fun inLike(productId: Long): Boolean + suspend fun inCart(productId: Long): Boolean + + suspend fun updateLike(productId: Long?): Boolean + suspend fun updateCart(productId: Long?): Boolean + + suspend fun getCart(): List + suspend fun removeOnCart(productId: Long): List + suspend fun addToCart(productId: Long): List +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/activity/main/MainActivity.kt b/app/src/main/java/com/affise/app/ui/activity/main/MainActivity.kt new file mode 100644 index 0000000..7406ae7 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/activity/main/MainActivity.kt @@ -0,0 +1,76 @@ +package com.affise.app.ui.activity.main + +import android.os.Bundle +import androidx.appcompat.app.AlertDialog +import androidx.navigation.Navigation +import com.affise.app.R +import com.affise.app.databinding.ActivityMainBinding +import com.affise.app.ui.fragments.menu.adapters.Menu +import com.affise.attribution.Affise +import dagger.android.support.DaggerAppCompatActivity +import javax.inject.Inject + +class MainActivity : DaggerAppCompatActivity(), MenuHolder { + + @Inject + lateinit var viewModel: MainActivityViewModel + + lateinit var binding: ActivityMainBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + Affise.registerDeeplinkCallback { + AlertDialog.Builder(this) + .setMessage(it.toString()) + .create().show() + true + } + + with(ActivityMainBinding.inflate(layoutInflater)) { + setContentView(root) + + binding = this + } + + Navigation.findNavController(this, R.id.general_menu_holder).apply { + graph = navInflater.inflate(R.navigation.menu) + } + } + + override fun changeMenuSate() { + binding.generalMenuNavigation.let { + if (it.isShown) { + binding.generalDrawer.closeDrawers() + } else { + binding.generalDrawer.openDrawer(it) + } + } + } + + override fun selectMenu(menuItem: Menu.MenuItem?) { + menuItem ?: return + + binding.generalMenuNavigation.let { + if (it.isShown) { + binding.generalDrawer.closeDrawers() + } + } + + Navigation.findNavController(findViewById(R.id.general_holder)).apply { + graph = navInflater.inflate(menuItem.graph) + } + } + + override fun onBackPressed() { + binding.generalMenuNavigation.let { + if (it.isShown) { + binding.generalDrawer.closeDrawers() + + return + } + } + + super.onBackPressed() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/activity/main/MainActivityContract.kt b/app/src/main/java/com/affise/app/ui/activity/main/MainActivityContract.kt new file mode 100644 index 0000000..53ef93f --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/activity/main/MainActivityContract.kt @@ -0,0 +1,5 @@ +package com.affise.app.ui.activity.main + +interface MainActivityContract { + interface ViewModel +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/activity/main/MainActivityViewModel.kt b/app/src/main/java/com/affise/app/ui/activity/main/MainActivityViewModel.kt new file mode 100644 index 0000000..d5b73f7 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/activity/main/MainActivityViewModel.kt @@ -0,0 +1,21 @@ +package com.affise.app.ui.activity.main + +import androidx.lifecycle.ViewModel +import com.affise.attribution.Affise +import com.google.firebase.messaging.FirebaseMessaging +import javax.inject.Inject + +class MainActivityViewModel @Inject constructor() : ViewModel(), MainActivityContract.ViewModel { + + init { + FirebaseMessaging.getInstance().token.addOnCompleteListener { + try { + it.result?.toString()?.let { newToken -> + Affise.addPushToken(newToken) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/activity/main/MenuHolder.kt b/app/src/main/java/com/affise/app/ui/activity/main/MenuHolder.kt new file mode 100644 index 0000000..ce338b1 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/activity/main/MenuHolder.kt @@ -0,0 +1,10 @@ +package com.affise.app.ui.activity.main + +import com.affise.app.ui.fragments.menu.adapters.Menu + +interface MenuHolder { + + fun changeMenuSate() + + fun selectMenu(menuItem: Menu.MenuItem?) +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/adapters/CenterDecoration.kt b/app/src/main/java/com/affise/app/ui/adapters/CenterDecoration.kt new file mode 100644 index 0000000..6c482d7 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/adapters/CenterDecoration.kt @@ -0,0 +1,52 @@ +package com.affise.app.ui.adapters + +import android.graphics.Rect +import android.view.View +import androidx.annotation.Px +import androidx.core.view.doOnPreDraw +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView + +class CenterDecoration(@Px private val spacing: Int) : RecyclerView.ItemDecoration() { + + private var firstViewWidth = -1 + + private var lastViewWidth = -1 + + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + + val adapterPosition = (view.layoutParams as RecyclerView.LayoutParams).viewAdapterPosition + val lm = parent.layoutManager as LinearLayoutManager + + if (adapterPosition == 0) { + + if (view.width != firstViewWidth) { + view.doOnPreDraw { parent.invalidateItemDecorations() } + } + firstViewWidth = view.width + outRect.left = parent.width / 2 - view.width / 2 + + if (lm.itemCount > 1) { + outRect.right = spacing / 2 + } else { + outRect.right = outRect.left + } + } else if (adapterPosition == lm.itemCount - 1) { + if (view.width != lastViewWidth) { + view.doOnPreDraw { parent.invalidateItemDecorations() } + } + lastViewWidth = view.width + outRect.right = parent.width / 2 - view.width / 2 + outRect.left = spacing / 2 + } else { + outRect.left = spacing / 2 + outRect.right = spacing / 2 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/adapters/CenterSnapHelper.kt b/app/src/main/java/com/affise/app/ui/adapters/CenterSnapHelper.kt new file mode 100644 index 0000000..588d097 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/adapters/CenterSnapHelper.kt @@ -0,0 +1,180 @@ +package com.affise.app.ui.adapters + +import android.os.Handler +import android.os.Looper +import android.view.View +import androidx.recyclerview.widget.LinearSnapHelper +import androidx.recyclerview.widget.OrientationHelper +import androidx.recyclerview.widget.RecyclerView + +class CenterSnapHelper : LinearSnapHelper() { + + private var verticalHelper: OrientationHelper? = null + + private var horizontalHelper: OrientationHelper? = null + + private var scrolled = false + + private var recyclerView: RecyclerView? = null + + private val scrollListener = object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + + if (newState == RecyclerView.SCROLL_STATE_IDLE && scrolled) { + if (recyclerView.layoutManager != null) { + val view = findSnapView(recyclerView.layoutManager) + if (view != null) { + val out = calculateDistanceToFinalSnap(recyclerView.layoutManager!!, view) + if (out != null) { + recyclerView.smoothScrollBy(out[0], out[1]) + } + } + } + scrolled = false + } else { + scrolled = true + } + } + } + + fun scrollTo(position: Int, smooth: Boolean, again: Boolean = false) { + if (recyclerView?.layoutManager != null) { + val viewHolder = recyclerView!!.findViewHolderForAdapterPosition(position) + + if (viewHolder != null) { + val distances = calculateDistanceToFinalSnap( + recyclerView!!.layoutManager!!, + viewHolder.itemView + ) + if (smooth) { + recyclerView!!.smoothScrollBy(distances!![0], distances[1]) + } else { + recyclerView!!.scrollBy(distances!![0], distances[1]) + } + } else { + Handler(Looper.getMainLooper()).postDelayed({ + scrollTo(position, smooth, again = true) + }, 100) + + if (!again) { + if (smooth) { + recyclerView!!.smoothScrollToPosition(position) + } else { + recyclerView!!.scrollToPosition(position) + } + } + } + } + } + + override fun findSnapView(layoutManager: RecyclerView.LayoutManager?): View? { + if (layoutManager == null) { + return null + } + if (layoutManager.canScrollVertically()) { + return findCenterView(layoutManager, getVerticalHelper(layoutManager)) + } else if (layoutManager.canScrollHorizontally()) { + return findCenterView(layoutManager, getHorizontalHelper(layoutManager)) + } + + return null + } + + override fun attachToRecyclerView(recyclerView: RecyclerView?) { + this.recyclerView = recyclerView + + recyclerView?.addOnScrollListener(scrollListener) + } + + override fun calculateDistanceToFinalSnap( + layoutManager: RecyclerView.LayoutManager, + targetView: View + ): IntArray? { + val out = IntArray(2) + + if (layoutManager.canScrollHorizontally()) { + out[0] = distanceToCenter(layoutManager, targetView, getHorizontalHelper(layoutManager)) + } else { + out[0] = 0 + } + if (layoutManager.canScrollVertically()) { + out[1] = distanceToCenter(layoutManager, targetView, getVerticalHelper(layoutManager)) + } else { + out[1] = 0 + } + + return out + } + + private fun findCenterView( + layoutManager: RecyclerView.LayoutManager, + helper: OrientationHelper + ): View? { + val childCount = layoutManager.childCount + if (childCount == 0) { + return null + } + var closestChild: View? = null + val center: Int = if (layoutManager.clipToPadding) { + helper.startAfterPadding + helper.totalSpace / 2 + } else { + helper.end / 2 + } + var absClosest = Integer.MAX_VALUE + + for (i in 0 until childCount) { + val child = layoutManager.getChildAt(i) + val childCenter = if (helper == horizontalHelper) { + (child!!.x + child.width / 2).toInt() + } else { + (child!!.y + child.height / 2).toInt() + } + val absDistance = Math.abs(childCenter - center) + + if (absDistance < absClosest) { + absClosest = absDistance + closestChild = child + } + } + + return closestChild + } + + private fun distanceToCenter( + layoutManager: RecyclerView.LayoutManager, + targetView: View, + helper: OrientationHelper + ): Int { + val childCenter = if (helper == horizontalHelper) { + (targetView.x + targetView.width / 2).toInt() + } else { + (targetView.y + targetView.height / 2).toInt() + } + val containerCenter = if (layoutManager.clipToPadding) { + helper.startAfterPadding + helper.totalSpace / 2 + } else { + helper.end / 2 + } + + return childCenter - containerCenter + } + + private fun getVerticalHelper(layoutManager: RecyclerView.LayoutManager): OrientationHelper { + if (verticalHelper == null || verticalHelper!!.layoutManager !== layoutManager) { + verticalHelper = OrientationHelper.createVerticalHelper(layoutManager) + } + + return verticalHelper!! + } + + private fun getHorizontalHelper( + layoutManager: RecyclerView.LayoutManager + ): OrientationHelper { + if (horizontalHelper == null || horizontalHelper!!.layoutManager !== layoutManager) { + horizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager) + } + + return horizontalHelper!! + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/adapters/SpaceDecoration.kt b/app/src/main/java/com/affise/app/ui/adapters/SpaceDecoration.kt new file mode 100644 index 0000000..0b66f63 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/adapters/SpaceDecoration.kt @@ -0,0 +1,58 @@ +package com.affise.app.ui.adapters + +import android.content.Context +import android.graphics.Rect +import android.view.View +import androidx.annotation.IntRange +import androidx.annotation.Px +import androidx.recyclerview.widget.RecyclerView +import com.affise.app.extensions.convertDpToPixels + +class SpaceDecoration( + private val paddingTop: Float = 0f, + private val paddingEnd: Float = 0f, + private val paddingBottom: Float = 0f, + private val paddingStart: Float = 0f, +) : RecyclerView.ItemDecoration() { + + private lateinit var appearance: Appearance + + override fun getItemOffsets( + outRect: Rect, + itemView: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + ensureAppearance(parent.context) + + parent.findContainingViewHolder(itemView)?.let { viewHolder -> + val position = viewHolder.adapterPosition + val lastPosition = parent.adapter?.itemCount?.let { it - 1 } + + val extraPaddingTop = if (position != 0) appearance.paddingTop else 0 + val extraPaddingStart = if (position != 0) appearance.paddingStart else 0 + val extraPaddingBottom = if (position != lastPosition) appearance.paddingBottom else 0 + val extraPaddingEnd = if (position != lastPosition) appearance.paddingEnd else 0 + + outRect.set(extraPaddingStart, extraPaddingTop, extraPaddingEnd, extraPaddingBottom) + } + } + + private fun ensureAppearance(context: Context) { + if (!this::appearance.isInitialized) { + appearance = Appearance( + paddingTop = context.convertDpToPixels(paddingTop), + paddingEnd = context.convertDpToPixels(paddingEnd), + paddingBottom = context.convertDpToPixels(paddingBottom), + paddingStart = context.convertDpToPixels(paddingStart), + ) + } + } + + private class Appearance( + @param:IntRange(from = 0) @param:Px @field:IntRange(from = 0) @field:Px val paddingTop: Int, + @param:IntRange(from = 0) @param:Px @field:IntRange(from = 0) @field:Px val paddingEnd: Int, + @param:IntRange(from = 0) @param:Px @field:IntRange(from = 0) @field:Px val paddingBottom: Int, + @param:IntRange(from = 0) @param:Px @field:IntRange(from = 0) @field:Px val paddingStart: Int + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/autoCatching/AutoCatchingContract.kt b/app/src/main/java/com/affise/app/ui/fragments/autoCatching/AutoCatchingContract.kt new file mode 100644 index 0000000..7f1fc17 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/autoCatching/AutoCatchingContract.kt @@ -0,0 +1,13 @@ +package com.affise.app.ui.fragments.autoCatching + +import androidx.lifecycle.LiveData +import com.affise.attribution.events.autoCatchingClick.AutoCatchingType + +interface AutoCatchingContract { + + interface ViewModel { + fun setSelected(position: Int, checked: Boolean) + + val checkedItems: LiveData> + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/autoCatching/AutoCatchingFragment.kt b/app/src/main/java/com/affise/app/ui/fragments/autoCatching/AutoCatchingFragment.kt new file mode 100644 index 0000000..2165305 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/autoCatching/AutoCatchingFragment.kt @@ -0,0 +1,64 @@ +package com.affise.app.ui.fragments.autoCatching + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import com.affise.app.R +import com.affise.app.databinding.FragmentAutoCatchingBinding +import com.affise.app.ui.activity.main.MenuHolder +import com.affise.attribution.events.autoCatchingClick.AutoCatchingType +import dagger.android.support.DaggerFragment +import javax.inject.Inject + +class AutoCatchingFragment : DaggerFragment() { + + @Inject + lateinit var viewModel: AutoCatchingContract.ViewModel + + @Inject + lateinit var menuHolder: MenuHolder + + lateinit var binding: FragmentAutoCatchingBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = FragmentAutoCatchingBinding.inflate(layoutInflater).apply { + binding = this + + menuSate.setOnClickListener { + menuHolder.changeMenuSate() + } + + changeTypes.setOnClickListener { + val checkedItems = viewModel.checkedItems.value ?: emptyList() + + val checked = AutoCatchingType.values().map { + checkedItems.contains(it) + }.toBooleanArray() + + val items: Array = AutoCatchingType.values() + .map { it.name } + .toTypedArray() + + AlertDialog.Builder(requireContext()) + .setTitle(R.string.select_auto_catching_types_title) + .setPositiveButton(R.string.ok, null) + .setMultiChoiceItems( + items, + checked + ) { _, which, isChecked -> + viewModel.setSelected(which, isChecked) + }.show() + } + + testButton.setOnClickListener { } + testText.setOnClickListener { } + testImageButton.setOnClickListener { } + testImage.setOnClickListener { } + testGroup.setOnClickListener { } + }.root +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/autoCatching/AutoCatchingViewModel.kt b/app/src/main/java/com/affise/app/ui/fragments/autoCatching/AutoCatchingViewModel.kt new file mode 100644 index 0000000..c186817 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/autoCatching/AutoCatchingViewModel.kt @@ -0,0 +1,34 @@ +package com.affise.app.ui.fragments.autoCatching + +import android.content.SharedPreferences +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.affise.app.application.App +import com.affise.attribution.Affise +import com.affise.attribution.events.autoCatchingClick.AutoCatchingType +import javax.inject.Inject + +class AutoCatchingViewModel @Inject constructor( + private val preferences: SharedPreferences +) : ViewModel(), AutoCatchingContract.ViewModel { + + override val checkedItems = MutableLiveData( + preferences.getStringSet(App.AUTO_CATCHING_TYPES_KEY, null) + ?.map { AutoCatchingType.valueOf(it) } + ?: emptyList() + ) + + override fun setSelected(position: Int, checked: Boolean) { + checkedItems.value = checkedItems.value?.toMutableList()?.apply { + val item = AutoCatchingType.values()[position] + + if (checked) add(item) else remove(item) + + preferences.edit() + .putStringSet(App.AUTO_CATCHING_TYPES_KEY, this.map { it.name }.toSet()) + .apply() + + Affise.setAutoCatchingTypes(this) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/buttons/ButtonsContract.kt b/app/src/main/java/com/affise/app/ui/fragments/buttons/ButtonsContract.kt new file mode 100644 index 0000000..e8ef5ed --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/buttons/ButtonsContract.kt @@ -0,0 +1,5 @@ +package com.affise.app.ui.fragments.buttons + +interface ButtonsContract { + interface ViewModel +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/buttons/ButtonsFragment.kt b/app/src/main/java/com/affise/app/ui/fragments/buttons/ButtonsFragment.kt new file mode 100644 index 0000000..f53dcc8 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/buttons/ButtonsFragment.kt @@ -0,0 +1,104 @@ +package com.affise.app.ui.fragments.buttons + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.webkit.WebChromeClient +import android.webkit.WebViewClient +import androidx.core.view.isVisible +import com.affise.app.databinding.FragmentMainButtonsBinding +import com.affise.app.extensions.hideKeyboard +import com.affise.app.ui.activity.main.MenuHolder +import com.affise.app.ui.fragments.buttons.adapters.EventsAdapter +import com.affise.app.ui.fragments.buttons.adapters.ItemCallback +import com.affise.app.ui.fragments.buttons.factories.DefaultEventsFactory +import com.affise.attribution.Affise +import com.google.android.material.tabs.TabLayout +import dagger.android.support.DaggerFragment +import javax.inject.Inject + +class ButtonsFragment : DaggerFragment() { + + @Inject + lateinit var viewModel: ButtonsContract.ViewModel + + @Inject + lateinit var menuHolder: MenuHolder + + lateinit var binding: FragmentMainButtonsBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = FragmentMainButtonsBinding.inflate(layoutInflater).apply { + binding = this + + menuSate.setOnClickListener { + menuHolder.changeMenuSate() + } + + with(binding.webView) { + settings.javaScriptEnabled = true + webChromeClient = WebChromeClient() + webViewClient = WebViewClient() + } + + tabEvents.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { + override fun onTabSelected(tab: TabLayout.Tab?) { + when (tab?.text) { + "events" -> { + binding.eventsRecyclerView.isVisible = true + binding.webView.isVisible = false + } + "Web events" -> { + binding.eventsRecyclerView.isVisible = false + binding.webView.isVisible = true + } + else -> Unit + } + } + + override fun onTabUnselected(tab: TabLayout.Tab?) { + } + + override fun onTabReselected(tab: TabLayout.Tab?) { + } + }) + + binding.sendEventsButton.setOnClickListener { + Affise.sendEvents() + } + + binding.eventsRecyclerView.adapter = EventsAdapter(ItemCallback()) { + Affise.sendEvent(it) + }.apply { + submitList(DefaultEventsFactory().createEvents()) + } + }.root + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + Affise.registerWebView(binding.webView) + + try { + binding.webView.loadUrl("file:///android_asset/index.html") + } catch (e: Exception) { + e.printStackTrace() + } + } + + override fun onResume() { + super.onResume() + + requireActivity().hideKeyboard() + } + + override fun onDestroyView() { + Affise.unregisterWebView() + + super.onDestroyView() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/buttons/ButtonsViewModel.kt b/app/src/main/java/com/affise/app/ui/fragments/buttons/ButtonsViewModel.kt new file mode 100644 index 0000000..31258ff --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/buttons/ButtonsViewModel.kt @@ -0,0 +1,6 @@ +package com.affise.app.ui.fragments.buttons + +import androidx.lifecycle.ViewModel +import javax.inject.Inject + +class ButtonsViewModel @Inject constructor() : ViewModel(), ButtonsContract.ViewModel \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/buttons/adapters/EventViewHolder.kt b/app/src/main/java/com/affise/app/ui/fragments/buttons/adapters/EventViewHolder.kt new file mode 100644 index 0000000..80b893c --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/buttons/adapters/EventViewHolder.kt @@ -0,0 +1,94 @@ +package com.affise.app.ui.fragments.buttons.adapters + +import androidx.recyclerview.widget.RecyclerView +import com.affise.app.databinding.ListItemEventBinding +import com.affise.attribution.events.Event +import com.affise.attribution.events.predefined.* +import com.affise.attribution.events.subscription.* + +class EventViewHolder( + private val binding: ListItemEventBinding, + private val onClick: (Event) -> Unit +) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: Event) { + when (item) { + is AchieveLevelEvent -> bind(item, item.getName()) + is AddPaymentInfoEvent -> bind(item, item.getName()) + is AddToCartEvent -> bind(item, item.getName()) + is AddToWishlistEvent -> bind(item, item.getName()) + is ClickAdvEvent -> bind(item, item.getName()) + is CompleteRegistrationEvent -> bind(item, item.getName()) + is CompleteStreamEvent -> bind(item, item.getName()) + is CompleteTrialEvent -> bind(item, item.getName()) + is CompleteTutorialEvent -> bind(item, item.getName()) + is ContentItemsViewEvent -> bind(item, item.getName()) + is CustomId01Event -> bind(item, item.getName()) + is CustomId02Event -> bind(item, item.getName()) + is CustomId03Event -> bind(item, item.getName()) + is CustomId04Event -> bind(item, item.getName()) + is CustomId05Event -> bind(item, item.getName()) + is CustomId06Event -> bind(item, item.getName()) + is CustomId07Event -> bind(item, item.getName()) + is CustomId08Event -> bind(item, item.getName()) + is CustomId09Event -> bind(item, item.getName()) + is CustomId10Event -> bind(item, item.getName()) + is DeepLinkedEvent -> bind(item, item.getName()) + is InitiatePurchaseEvent -> bind(item, item.getName()) + is InitiateStreamEvent -> bind(item, item.getName()) + is InviteEvent -> bind(item, item.getName()) + is LastAttributedTouchEvent -> bind(item, item.getName()) + is ListViewEvent -> bind(item, item.getName()) + is LoginEvent -> bind(item, item.getName()) + is OpenedFromPushNotificationEvent -> bind(item, item.getName()) + is PurchaseEvent -> bind(item, item.getName()) + is RateEvent -> bind(item, item.getName()) + is ReEngageEvent -> bind(item, item.getName()) + is ReserveEvent -> bind(item, item.getName()) + is SalesEvent -> bind(item, item.getName()) + is SearchEvent -> bind(item, item.getName()) + is ShareEvent -> bind(item, item.getName()) + is SpendCreditsEvent -> bind(item, item.getName()) + is StartRegistrationEvent -> bind(item, item.getName()) + is StartTrialEvent -> bind(item, item.getName()) + is StartTutorialEvent -> bind(item, item.getName()) + is SubscribeEvent -> bind(item, item.getName()) + is TravelBookingEvent -> bind(item, item.getName()) + is UnlockAchievementEvent -> bind(item, item.getName()) + is UnsubscribeEvent -> bind(item, item.getName()) + is UpdateEvent -> bind(item, item.getName()) + is ViewAdvEvent -> bind(item, item.getName()) + is ViewCartEvent -> bind(item, item.getName()) + is ViewItemEvent -> bind(item, item.getName()) + is ViewItemsEvent -> bind(item, item.getName()) + + is InitialSubscriptionEvent -> bind(item, item.subtype) + is InitialTrialEvent -> bind(item, item.subtype) + is InitialOfferEvent -> bind(item, item.subtype) + is ConvertedTrialEvent -> bind(item, item.subtype) + is ConvertedOfferEvent -> bind(item, item.subtype) + is TrialInRetryEvent -> bind(item, item.subtype) + is OfferInRetryEvent -> bind(item, item.subtype) + is SubscriptionInRetryEvent -> bind(item, item.subtype) + is RenewedSubscriptionEvent -> bind(item, item.subtype) + is FailedSubscriptionFromRetryEvent -> bind(item, item.subtype) + is FailedOfferFromRetryEvent -> bind(item, item.subtype) + is FailedTrialFromRetryEvent -> bind(item, item.subtype) + is FailedSubscriptionEvent -> bind(item, item.subtype) + is FailedOfferiseEvent -> bind(item, item.subtype) + is FailedTrialEvent -> bind(item, item.subtype) + is ReactivatedSubscriptionEvent -> bind(item, item.subtype) + is RenewedSubscriptionFromRetryEvent -> bind(item, item.subtype) + is ConvertedOfferFromRetryEvent -> bind(item, item.subtype) + is ConvertedTrialFromRetryEvent -> bind(item, item.subtype) + is UnsubscriptionEvent -> bind(item, item.subtype) + } + } + + private fun bind(item: Event, title: String) { + binding.eventButton.setOnClickListener { + onClick.invoke(item) + } + + binding.eventButton.text = title + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/buttons/adapters/EventsAdapter.kt b/app/src/main/java/com/affise/app/ui/fragments/buttons/adapters/EventsAdapter.kt new file mode 100644 index 0000000..57f3597 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/buttons/adapters/EventsAdapter.kt @@ -0,0 +1,28 @@ +package com.affise.app.ui.fragments.buttons.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import com.affise.app.databinding.ListItemEventBinding +import com.affise.attribution.events.Event + +class EventsAdapter( + itemCallback: DiffUtil.ItemCallback, + private val onClick: (Event) -> Unit +) : ListAdapter(itemCallback) { + + override fun onCreateViewHolder( + parent: ViewGroup, viewType: Int + ): EventViewHolder = LayoutInflater.from(parent.context) + .let { + ListItemEventBinding.inflate(it, parent, false) + } + .let { + EventViewHolder(it, onClick) + } + + override fun onBindViewHolder(holder: EventViewHolder, position: Int) { + holder.bind(getItem(position)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/buttons/adapters/ItemCallback.kt b/app/src/main/java/com/affise/app/ui/fragments/buttons/adapters/ItemCallback.kt new file mode 100644 index 0000000..2579076 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/buttons/adapters/ItemCallback.kt @@ -0,0 +1,16 @@ +package com.affise.app.ui.fragments.buttons.adapters + +import androidx.recyclerview.widget.DiffUtil +import com.affise.attribution.events.Event + +class ItemCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: Event, + newItem: Event + ): Boolean = oldItem::class == newItem::class + + override fun areContentsTheSame( + oldItem: Event, + newItem: Event + ): Boolean = true +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/buttons/factories/DefaultEventsFactory.kt b/app/src/main/java/com/affise/app/ui/fragments/buttons/factories/DefaultEventsFactory.kt new file mode 100644 index 0000000..c17e0a3 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/buttons/factories/DefaultEventsFactory.kt @@ -0,0 +1,832 @@ +package com.affise.app.ui.fragments.buttons.factories + +import com.affise.attribution.events.Event +import com.affise.attribution.events.predefined.AchieveLevelEvent +import com.affise.attribution.events.predefined.AddPaymentInfoEvent +import com.affise.attribution.events.predefined.AddToCartEvent +import com.affise.attribution.events.predefined.AddToWishlistEvent +import com.affise.attribution.events.predefined.ClickAdvEvent +import com.affise.attribution.events.predefined.CompleteRegistrationEvent +import com.affise.attribution.events.predefined.CompleteStreamEvent +import com.affise.attribution.events.predefined.CompleteTrialEvent +import com.affise.attribution.events.predefined.CompleteTutorialEvent +import com.affise.attribution.events.predefined.ContentItemsViewEvent +import com.affise.attribution.events.predefined.CustomId01Event +import com.affise.attribution.events.predefined.CustomId02Event +import com.affise.attribution.events.predefined.CustomId03Event +import com.affise.attribution.events.predefined.CustomId04Event +import com.affise.attribution.events.predefined.CustomId05Event +import com.affise.attribution.events.predefined.CustomId06Event +import com.affise.attribution.events.predefined.CustomId07Event +import com.affise.attribution.events.predefined.CustomId08Event +import com.affise.attribution.events.predefined.CustomId09Event +import com.affise.attribution.events.predefined.CustomId10Event +import com.affise.attribution.events.predefined.DeepLinkedEvent +import com.affise.attribution.events.predefined.InitiatePurchaseEvent +import com.affise.attribution.events.predefined.InitiateStreamEvent +import com.affise.attribution.events.predefined.InviteEvent +import com.affise.attribution.events.predefined.LastAttributedTouchEvent +import com.affise.attribution.events.predefined.ListViewEvent +import com.affise.attribution.events.predefined.LoginEvent +import com.affise.attribution.events.predefined.OpenedFromPushNotificationEvent +import com.affise.attribution.events.predefined.PredefinedParameters +import com.affise.attribution.events.predefined.PurchaseEvent +import com.affise.attribution.events.predefined.RateEvent +import com.affise.attribution.events.predefined.ReEngageEvent +import com.affise.attribution.events.predefined.ReserveEvent +import com.affise.attribution.events.predefined.SalesEvent +import com.affise.attribution.events.predefined.SearchEvent +import com.affise.attribution.events.predefined.ShareEvent +import com.affise.attribution.events.predefined.SpendCreditsEvent +import com.affise.attribution.events.predefined.StartRegistrationEvent +import com.affise.attribution.events.predefined.StartTrialEvent +import com.affise.attribution.events.predefined.StartTutorialEvent +import com.affise.attribution.events.predefined.SubscribeEvent +import com.affise.attribution.events.predefined.TouchType +import com.affise.attribution.events.predefined.TravelBookingEvent +import com.affise.attribution.events.predefined.UnlockAchievementEvent +import com.affise.attribution.events.predefined.UnsubscribeEvent +import com.affise.attribution.events.predefined.UpdateEvent +import com.affise.attribution.events.predefined.ViewAdvEvent +import com.affise.attribution.events.predefined.ViewCartEvent +import com.affise.attribution.events.predefined.ViewItemEvent +import com.affise.attribution.events.predefined.ViewItemsEvent +import com.affise.attribution.events.subscription.* +import org.json.JSONArray +import org.json.JSONObject + +class DefaultEventsFactory : EventsFactory { + override fun createEvents(): List { + return listOf( + createAchieveLevelEvent(), + createAddPaymentInfoEvent(), + createAddToCartEvent(), + createAddToWishlistEvent(), + createClickAdvEvent(), + createCompleteRegistrationEvent(), + createCompleteStreamEvent(), + createCompleteTrialEvent(), + createCompleteTutorialEvent(), + createContentItemsViewEvent(), + createCustomId01Event(), + createCustomId02Event(), + createCustomId03Event(), + createCustomId04Event(), + createCustomId05Event(), + createCustomId06Event(), + createCustomId07Event(), + createCustomId08Event(), + createCustomId09Event(), + createCustomId10Event(), + createDeepLinkedEvent(), + createInitiatePurchaseEvent(), + createInitiateStreamEvent(), + createInviteEvent(), + createLastAttributedTouchEvent(), + createListViewEvent(), + createLoginEvent(), + createOpenedFromPushNotificationEvent(), + createPurchaseEvent(), + createRateEvent(), + createReEngageEvent(), + createReserveEvent(), + createSalesEvent(), + createSearchEvent(), + createShareEvent(), + createSpendCreditsEvent(), + createStartRegistrationEvent(), + createStartTrialEvent(), + createStartTutorialEvent(), + createSubscribeEvent(), + createTravelBookingEvent(), + createUnlockAchievementEvent(), + createUnsubscribeEvent(), + createUpdateEvent(), + createViewAdvEvent(), + createViewCartEvent(), + createViewItemEvent(), + createViewItemsEvent(), + createInitialSubscriptionEvent(), + createInitialTrialEvent(), + createInitialOfferEvent(), + createConvertedTrialEvent(), + createConvertedOfferEvent(), + createTrialInRetryEvent(), + createOfferInRetryEvent(), + createSubscriptionInRetryEvent(), + createRenewedSubscriptionEvent(), + createFailedSubscriptionFromRetryEvent(), + createFailedOfferFromRetryEvent(), + createFailedTrialFromRetryEvent(), + createFailedSubscriptionEvent(), + createFailedOfferiseEvent(), + createFailedTrialEvent(), + createReactivatedSubscriptionEvent(), + createRenewedSubscriptionFromRetryEvent(), + createConvertedOfferFromRetryEvent(), + createConvertedTrialFromRetryEvent(), + createUnsubscriptionEvent() + ) + } + + private fun createUnsubscriptionEvent() = UnsubscriptionEvent( + JSONObject().apply { + put("affise_event_revenue", 2.99) + put("affise_event_currency", "USD") + put("affise_event_product_id", 278459628375) + }, + "Unsubscription" + ).apply { + addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $") + } + + private fun createConvertedTrialFromRetryEvent() = ConvertedTrialFromRetryEvent( + JSONObject().apply { + put("affise_event_revenue", 2.99) + put("affise_event_currency", "USD") + put("affise_event_product_id", 278459628375) + }, + "Subscription Plus" + ).apply { + addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $") + } + + private fun createConvertedOfferFromRetryEvent() = ConvertedOfferFromRetryEvent( + JSONObject().apply { + put("affise_event_revenue", 2.99) + put("affise_event_currency", "USD") + put("affise_event_product_id", 278459628375) + }, + "Subscription Plus" + ).apply { + addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $") + } + + private fun createRenewedSubscriptionFromRetryEvent() = RenewedSubscriptionFromRetryEvent( + JSONObject().apply { + put("affise_event_revenue", 2.99) + put("affise_event_currency", "USD") + put("affise_event_product_id", 278459628375) + }, + "Subscription Plus" + ).apply { + addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $") + } + + private fun createReactivatedSubscriptionEvent() = ReactivatedSubscriptionEvent( + JSONObject().apply { + put("affise_event_revenue", 2.99) + put("affise_event_currency", "USD") + put("affise_event_product_id", 278459628375) + }, + "Subscription Plus" + ).apply { + addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $") + } + + private fun createFailedTrialEvent() = FailedTrialEvent( + JSONObject().apply { + put("affise_event_revenue", 2.99) + put("affise_event_currency", "USD") + put("affise_event_product_id", 278459628375) + }, + "Subscription Plus" + ).apply { + addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $") + } + + private fun createFailedOfferiseEvent() = FailedOfferiseEvent( + JSONObject().apply { + put("affise_event_revenue", 2.99) + put("affise_event_currency", "USD") + put("affise_event_product_id", 278459628375) + }, + "Subscription Plus" + ).apply { + addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $") + } + + private fun createFailedSubscriptionEvent() = FailedSubscriptionEvent( + JSONObject().apply { + put("affise_event_revenue", 2.99) + put("affise_event_currency", "USD") + put("affise_event_product_id", 278459628375) + }, + "Subscription Plus" + ).apply { + addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $") + } + + private fun createFailedTrialFromRetryEvent() = FailedTrialFromRetryEvent( + JSONObject().apply { + put("affise_event_revenue", 2.99) + put("affise_event_currency", "USD") + put("affise_event_product_id", 278459628375) + }, + "Subscription Plus" + ).apply { + addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $") + } + + private fun createFailedOfferFromRetryEvent() = FailedOfferFromRetryEvent( + JSONObject().apply { + put("affise_event_revenue", 2.99) + put("affise_event_currency", "USD") + put("affise_event_product_id", 278459628375) + }, + "Subscription Plus" + ).apply { + addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $") + } + + private fun createFailedSubscriptionFromRetryEvent() = FailedSubscriptionFromRetryEvent( + JSONObject().apply { + put("affise_event_revenue", 2.99) + put("affise_event_currency", "USD") + put("affise_event_product_id", 278459628375) + }, + "Subscription Plus" + ).apply { + addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $") + } + + private fun createRenewedSubscriptionEvent() = RenewedSubscriptionEvent( + JSONObject().apply { + put("affise_event_revenue", 2.99) + put("affise_event_currency", "USD") + put("affise_event_product_id", 278459628375) + }, + "Subscription Plus" + ).apply { + addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $") + } + + private fun createTrialInRetryEvent() = TrialInRetryEvent( + JSONObject().apply { + put("affise_event_revenue", 2.99) + put("affise_event_currency", "USD") + put("affise_event_product_id", 278459628375) + }, + "Subscription Plus" + ).apply { + addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $") + } + + private fun createOfferInRetryEvent() = OfferInRetryEvent( + JSONObject().apply { + put("affise_event_revenue", 2.99) + put("affise_event_currency", "USD") + put("affise_event_product_id", 278459628375) + }, + "Subscription Plus" + ).apply { + addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $") + } + + private fun createSubscriptionInRetryEvent() = SubscriptionInRetryEvent( + JSONObject().apply { + put("affise_event_revenue", 2.99) + put("affise_event_currency", "USD") + put("affise_event_product_id", 278459628375) + }, + "Subscription Plus" + ).apply { + addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $") + } + + private fun createInitialOfferEvent() = InitialOfferEvent( + JSONObject().apply { + put("affise_event_revenue", 2.99) + put("affise_event_currency", "USD") + put("affise_event_product_id", 278459628375) + }, + "Subscription Plus" + ).apply { + addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $") + } + + private fun createInitialTrialEvent() = InitialTrialEvent( + JSONObject().apply { + put("affise_event_revenue", 2.99) + put("affise_event_currency", "USD") + put("affise_event_product_id", 278459628375) + }, + "Subscription Plus" + ).apply { + addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $") + } + + private fun createInitialSubscriptionEvent() = InitialSubscriptionEvent( + JSONObject().apply { + put("affise_event_revenue", 2.99) + put("affise_event_currency", "USD") + put("affise_event_product_id", 278459628375) + }, + "Subscription Plus" + ).apply { + addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $") + } + + private fun createConvertedTrialEvent() = ConvertedTrialEvent( + JSONObject().apply { + put("affise_event_revenue", 2.99) + put("affise_event_currency", "USD") + put("affise_event_product_id", 278459628375) + }, + "Subscription Plus" + ).apply { + addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $") + } + + private fun createConvertedOfferEvent() = ConvertedOfferEvent( + JSONObject().apply { + put("affise_event_revenue", 2.99) + put("affise_event_currency", "USD") + put("affise_event_product_id", 278459628375) + }, + "Subscription Plus" + ).apply { + addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $") + } + + private fun createAchieveLevelEvent(): Event { + val level = JSONObject().apply { + put("old_level", 69) + put("new_level", 70) + } + return AchieveLevelEvent(level, System.currentTimeMillis(), "warlock").apply { + addPredefinedParameter(PredefinedParameters.DEEP_LINK, "https://new-game.lt") + addPredefinedParameter(PredefinedParameters.SCORE, "25013") + addPredefinedParameter(PredefinedParameters.LEVEL, "70") + addPredefinedParameter(PredefinedParameters.SUCCESS, "true") + addPredefinedParameter(PredefinedParameters.TUTORIAL_ID, "12") + } + } + + private fun createAddPaymentInfoEvent(): Event { + val data = JSONObject().apply { + put("card", 4138) + put("type", "phone") + } + return AddPaymentInfoEvent(data, System.currentTimeMillis(), "taxi").apply { + addPredefinedParameter(PredefinedParameters.PURCHASE_CURRENCY, "USD") + } + } + + private fun createAddToCartEvent(): Event { + val products = + listOf("milk", "cookies", "meat", "vegetables").shuffled().take(1).joinToString() + val items = JSONObject().apply { + put("items", products) + } + return AddToCartEvent(items, System.currentTimeMillis()).apply { + addPredefinedParameter(PredefinedParameters.DESCRIPTION, "best before 2029") + } + } + + private fun createAddToWishlistEvent(): Event { + val wishes = listOf("car", "house", "sdk").shuffled().take(1).joinToString() + val items = JSONObject().apply { + put("items", wishes) + } + return AddToWishlistEvent(items, System.currentTimeMillis(), "next year").apply { + addPredefinedParameter(PredefinedParameters.COUNTRY, "Russia") + addPredefinedParameter(PredefinedParameters.CITY, "Voronezh") + addPredefinedParameter(PredefinedParameters.LAT, "42") + addPredefinedParameter(PredefinedParameters.LONG, "24") + } + } + + private fun createClickAdvEvent(): Event { + return ClickAdvEvent("facebook-meta", System.currentTimeMillis(), "header").apply { + addPredefinedParameter(PredefinedParameters.PARAM_01, "PARAM_01") + addPredefinedParameter(PredefinedParameters.PARAM_02, "PARAM_02") + addPredefinedParameter(PredefinedParameters.PARAM_03, "PARAM_03") + addPredefinedParameter(PredefinedParameters.PARAM_04, "PARAM_04") + addPredefinedParameter(PredefinedParameters.PARAM_05, "PARAM_05") + addPredefinedParameter(PredefinedParameters.PARAM_06, "PARAM_06") + addPredefinedParameter(PredefinedParameters.PARAM_07, "PARAM_07") + addPredefinedParameter(PredefinedParameters.PARAM_08, "PARAM_08") + addPredefinedParameter(PredefinedParameters.PARAM_09, "PARAM_09") + addPredefinedParameter(PredefinedParameters.PARAM_10, "PARAM_10") + } + } + + private fun createCompleteRegistrationEvent(): Event { + val data = JSONObject().apply { + put("email", "dog@gmail.com") + } + return CompleteRegistrationEvent(data, System.currentTimeMillis(), "promo").apply { + addPredefinedParameter(PredefinedParameters.VALIDATED, "02.11.2021") + addPredefinedParameter(PredefinedParameters.REVIEW_TEXT, "approve") + addPredefinedParameter(PredefinedParameters.CUSTOMER_SEGMENT, "DOG") + } + } + + private fun createCompleteStreamEvent(): Event { + val data = JSONObject().apply { + put("streamer", "cat") + put("max_viewers", "29") + } + return CompleteStreamEvent(data, System.currentTimeMillis(), "23 hours").apply { + addPredefinedParameter(PredefinedParameters.REVENUE, "225522 $") + } + } + + private fun createCompleteTrialEvent(): Event { + val data = JSONObject().apply { + put("player", "ghost") + } + return CompleteTrialEvent(data, System.currentTimeMillis(), "time").apply { + addPredefinedParameter(PredefinedParameters.REGISTRATION_METHOD, "SMS") + } + } + + private fun createCompleteTutorialEvent(): Event { + val data = JSONObject().apply { + put("name", "intro") + } + return CompleteTutorialEvent(data, System.currentTimeMillis(), "intro").apply { + addPredefinedParameter(PredefinedParameters.REGISTRATION_METHOD, "SMS") + } + } + + private fun createContentItemsViewEvent(): Event { + val data = listOf( + JSONObject().apply { + put("item", "book") + }, + JSONObject().apply { + put("item", "guitar") + } + ) + return ContentItemsViewEvent(data, "personal").apply { + addPredefinedParameter(PredefinedParameters.CONTENT, "Greatest Hits") + addPredefinedParameter(PredefinedParameters.CONTENT_ID, "2561") + addPredefinedParameter(PredefinedParameters.CONTENT_LIST, "songs, videos") + addPredefinedParameter(PredefinedParameters.CONTENT_TYPE, "MATURE") + addPredefinedParameter(PredefinedParameters.CURRENCY, "USD") + } + } + + private fun createCustomId01Event(): Event { + return CustomId01Event("custom", System.currentTimeMillis(), "custom").apply { + addPredefinedParameter(PredefinedParameters.PARAM_01, "param1") + } + } + + private fun createCustomId02Event(): Event { + return CustomId02Event("custom", System.currentTimeMillis(), "custom").apply { + addPredefinedParameter(PredefinedParameters.PARAM_01, "param1") + } + } + + private fun createCustomId03Event(): Event { + return CustomId03Event("custom", System.currentTimeMillis(), "custom").apply { + addPredefinedParameter(PredefinedParameters.PARAM_01, "param1") + } + } + + private fun createCustomId04Event(): Event { + return CustomId04Event("custom", System.currentTimeMillis(), "custom").apply { + addPredefinedParameter(PredefinedParameters.PARAM_01, "param1") + } + } + + private fun createCustomId05Event(): Event { + return CustomId05Event("custom", System.currentTimeMillis(), "custom").apply { + addPredefinedParameter(PredefinedParameters.PARAM_01, "param1") + } + } + + private fun createCustomId06Event(): Event { + return CustomId06Event("custom", System.currentTimeMillis(), "custom").apply { + addPredefinedParameter(PredefinedParameters.PARAM_01, "param1") + } + } + + private fun createCustomId07Event(): Event { + return CustomId07Event("custom", System.currentTimeMillis(), "custom").apply { + addPredefinedParameter(PredefinedParameters.PARAM_01, "param1") + } + } + + private fun createCustomId08Event(): Event { + return CustomId08Event("custom", System.currentTimeMillis(), "custom").apply { + addPredefinedParameter(PredefinedParameters.PARAM_01, "param1") + } + } + + private fun createCustomId09Event(): Event { + return CustomId09Event("custom", System.currentTimeMillis(), "custom").apply { + addPredefinedParameter(PredefinedParameters.PARAM_01, "param1") + } + } + + private fun createCustomId10Event(): Event { + return CustomId10Event("custom", System.currentTimeMillis(), "custom").apply { + addPredefinedParameter(PredefinedParameters.PARAM_01, "param1") + } + } + + private fun createDeepLinkedEvent(): Event { + return DeepLinkedEvent(true, "referrer: google").apply { + addPredefinedParameter(PredefinedParameters.ADREV_AD_TYPE, "interstitial") + addPredefinedParameter(PredefinedParameters.REGION, "ASIA") + addPredefinedParameter(PredefinedParameters.CLASS, "student") + } + } + + private fun createInitiatePurchaseEvent(): Event { + val data = JSONObject().apply { + put("payment", "card") + } + return InitiatePurchaseEvent(data, System.currentTimeMillis(), "best price").apply { + addPredefinedParameter(PredefinedParameters.ORDER_ID, "23123") + addPredefinedParameter(PredefinedParameters.PRICE, "2.19$") + addPredefinedParameter(PredefinedParameters.QUANTITY, "1") + } + } + + private fun createInitiateStreamEvent(): Event { + val data = JSONObject().apply { + put("streamer", "car") + put("date", "02.03.2021") + } + return InitiateStreamEvent(data, System.currentTimeMillis(), "shooter").apply { + addPredefinedParameter(PredefinedParameters.COUPON_CODE, "25XLKM") + addPredefinedParameter(PredefinedParameters.VIRTUAL_CURRENCY_NAME, "BTC") + } + } + + private fun createInviteEvent(): Event { + val data = JSONObject().apply { + put("invitation", "mail") + put("date", "02.03.2021") + } + return InviteEvent(data, System.currentTimeMillis(), "dancing").apply { + addPredefinedParameter(PredefinedParameters.PARAM_01, "param1") + } + } + + private fun createLastAttributedTouchEvent(): Event { + val touchData = JSONObject().apply { + put("header", 2) + } + return LastAttributedTouchEvent( + TouchType.CLICK, + System.currentTimeMillis(), + touchData, + "tablet" + ).apply { + addPredefinedParameter(PredefinedParameters.SUBSCRIPTION_ID, "lasAK22") + } + } + + private fun createListViewEvent(): Event { + val data = JSONObject().apply { + put("clothes", 2) + } + return ListViewEvent(data, "items").apply { + addPredefinedParameter(PredefinedParameters.PAYMENT_INFO_AVAILABLE, "card") + addPredefinedParameter(PredefinedParameters.SEARCH_STRING, "best car wash") + addPredefinedParameter(PredefinedParameters.SUGGESTED_DESTINATIONS, "crete, spain") + addPredefinedParameter( + PredefinedParameters.SUGGESTED_HOTELS, + "beach resort, marina spa" + ) + } + } + + private fun createLoginEvent(): Event { + val data = JSONObject().apply { + put("email", "cat@gmail.com") + } + return LoginEvent(data, System.currentTimeMillis(), "web").apply { + addPredefinedParameter(PredefinedParameters.PARAM_01, "param1") + } + } + + private fun createOpenedFromPushNotificationEvent(): Event { + return OpenedFromPushNotificationEvent("silent", "active").apply { + addPredefinedParameter(PredefinedParameters.PARAM_01, "param1") + } + } + + private fun createPurchaseEvent(): Event { + val data = JSONObject().apply { + put("phone", 1) + put("case", 1) + } + return PurchaseEvent(data, System.currentTimeMillis(), "apple").apply { + addPredefinedParameter(PredefinedParameters.ORDER_ID, "23123") + addPredefinedParameter(PredefinedParameters.PRICE, "2.19$") + addPredefinedParameter(PredefinedParameters.QUANTITY, "1") + } + } + + private fun createRateEvent(): Event { + val data = JSONObject().apply { + put("rating", 5) + } + return RateEvent(data, System.currentTimeMillis(), "no bugs").apply { + addPredefinedParameter(PredefinedParameters.PREFERRED_NEIGHBORHOODS, "2") + addPredefinedParameter(PredefinedParameters.PREFERRED_NUM_STOPS, "4") + addPredefinedParameter(PredefinedParameters.PREFERRED_PRICE_RANGE, "10-22") + addPredefinedParameter(PredefinedParameters.PREFERRED_STAR_RATINGS, "4.6") + } + } + + private fun createReEngageEvent(): Event { + return ReEngageEvent("data", "web").apply { + addPredefinedParameter(PredefinedParameters.CUSTOMER_USER_ID, "4") + } + } + + private fun createReserveEvent(): Event { + val data = JSONObject().apply { + put("ticket", "movie") + } + val data2 = JSONObject().apply { + put("food", "coke") + } + return ReserveEvent(listOf(data, data2), System.currentTimeMillis(), "discount").apply { + addPredefinedParameter(PredefinedParameters.ORDER_ID, "23123") + addPredefinedParameter(PredefinedParameters.PRICE, "2.19$") + addPredefinedParameter(PredefinedParameters.QUANTITY, "1") + } + } + + private fun createSalesEvent(): Event { + val data = JSONObject().apply { + put("phone", 1) + put("case", 1) + } + return SalesEvent(data, System.currentTimeMillis(), "apple").apply { + addPredefinedParameter(PredefinedParameters.ORDER_ID, "23123") + addPredefinedParameter(PredefinedParameters.PRICE, "2.19$") + addPredefinedParameter(PredefinedParameters.QUANTITY, "1") + } + } + + private fun createSearchEvent(): Event { + val data = JSONArray().apply { + put("eco milk") + put("grass") + } + return SearchEvent(data, System.currentTimeMillis(), "browser").apply { + addPredefinedParameter(PredefinedParameters.PARAM_01, "param1") + } + } + + private fun createShareEvent(): Event { + val data = JSONObject().apply { + put("post_id", "252242") + put("post_img", "img.webp") + } + + return ShareEvent(data, System.currentTimeMillis(), "telegram").apply { + addPredefinedParameter(PredefinedParameters.RECEIPT_ID, "22") + } + } + + private fun createSpendCreditsEvent(): Event { + return SpendCreditsEvent(2142, System.currentTimeMillis(), "boosters").apply { + addPredefinedParameter(PredefinedParameters.PARAM_01, "param1") + } + } + + private fun createStartRegistrationEvent(): Event { + val data = JSONObject().apply { + put("time", "19:22:11") + } + return StartRegistrationEvent(data, System.currentTimeMillis(), "referrer").apply { + addPredefinedParameter(PredefinedParameters.PARAM_01, "param1") + } + } + + private fun createStartTrialEvent(): Event { + val data = JSONObject().apply { + put("time", "19:22:11") + } + return StartTrialEvent(data, System.currentTimeMillis(), "30-days").apply { + addPredefinedParameter(PredefinedParameters.PARAM_01, "param1") + } + } + + private fun createStartTutorialEvent(): Event { + val data = JSONObject().apply { + put("time", "19:22:11") + put("reward", "22") + } + + return StartTutorialEvent(data, System.currentTimeMillis(), "video-feature").apply { + addPredefinedParameter(PredefinedParameters.PARAM_01, "param1") + } + } + + private fun createSubscribeEvent(): Event { + val data = JSONObject().apply { + put("streamer", "cat") + } + return SubscribeEvent(data, System.currentTimeMillis(), "wire").apply { + addPredefinedParameter(PredefinedParameters.PARAM_01, "param1") + } + } + + private fun createTravelBookingEvent(): Event { + val data = JSONArray().apply { + put("may") + put("august") + } + return TravelBookingEvent(data, "booking").apply { + addPredefinedParameter(PredefinedParameters.NUM_ADULTS, "1") + addPredefinedParameter(PredefinedParameters.NUM_CHILDREN, "2") + addPredefinedParameter(PredefinedParameters.NUM_INFANTS, "1") + addPredefinedParameter(PredefinedParameters.DATE_A, "30.12.2020") + addPredefinedParameter(PredefinedParameters.DATE_B, "01.01.2021") + addPredefinedParameter(PredefinedParameters.DEPARTING_ARRIVAL_DATE, "01.01.2021") + addPredefinedParameter(PredefinedParameters.DEPARTING_DEPARTURE_DATE, "30.12.2020") + addPredefinedParameter(PredefinedParameters.DESTINATION_A, "usa") + addPredefinedParameter(PredefinedParameters.DESTINATION_B, "mexico") + addPredefinedParameter(PredefinedParameters.DESTINATION_LIST, "usa, mexico") + addPredefinedParameter(PredefinedParameters.HOTEL_SCORE, "5") + addPredefinedParameter(PredefinedParameters.TRAVEL_START, "01.12.2020") + addPredefinedParameter(PredefinedParameters.TRAVEL_END, "01.12.2021") + } + } + + private fun createUnlockAchievementEvent(): Event { + val data = JSONObject().apply { + put("achievement", "new level") + } + return UnlockAchievementEvent(data, System.currentTimeMillis(), "best damage").apply { + addPredefinedParameter(PredefinedParameters.USER_SCORE, "12552") + addPredefinedParameter(PredefinedParameters.ACHIEVEMENT_ID, "1334-1225-ASDZ") + } + } + + private fun createUnsubscribeEvent(): Event { + val data = JSONObject().apply { + put("reason", "span") + } + return UnsubscribeEvent(data, System.currentTimeMillis(), "02.01.2021").apply { + addPredefinedParameter(PredefinedParameters.PARAM_01, "param1") + } + } + + private fun createUpdateEvent(): Event { + val data = JSONArray().apply { + put("com.facebook") + } + return UpdateEvent(data, "firmware").apply { + addPredefinedParameter(PredefinedParameters.EVENT_START, "01.02.2021") + addPredefinedParameter(PredefinedParameters.EVENT_END, "01.01.2022") + addPredefinedParameter(PredefinedParameters.NEW_VERSION, "8") + addPredefinedParameter(PredefinedParameters.OLD_VERSION, "1.8") + } + } + + private fun createViewAdvEvent(): Event { + val data = JSONObject().apply { + put("source", "amazon") + } + return ViewAdvEvent(data, System.currentTimeMillis(), "skip").apply { + addPredefinedParameter(PredefinedParameters.RETURNING_ARRIVAL_DATE, "01.12.2021") + addPredefinedParameter(PredefinedParameters.RETURNING_DEPARTURE_DATE, "01.12.2020") + } + } + + private fun createViewCartEvent(): Event { + val data = JSONObject().apply { + put("cart_value", "25.22$") + put("cart_items", "2") + } + return ViewCartEvent(data, "main").apply { + addPredefinedParameter(PredefinedParameters.PARAM_01, "param1") + } + } + + private fun createViewItemEvent(): Event { + val data = JSONObject().apply { + put("section_name", "header") + } + return ViewItemEvent(data, "main").apply { + addPredefinedParameter(PredefinedParameters.MAX_RATING_VALUE, "5") + addPredefinedParameter(PredefinedParameters.RATING_VALUE, "4.9") + } + } + + private fun createViewItemsEvent(): Event { + val data = JSONArray().apply { + put(JSONObject().apply { put("section_name", "header") }) + put(JSONObject().apply { put("section_name", "section-1") }) + put(JSONObject().apply { put("section_name", "section-2") }) + put(JSONObject().apply { put("section_name", "footer") }) + } + return ViewItemsEvent(data, "main").apply { + addPredefinedParameter(PredefinedParameters.MAX_RATING_VALUE, "5") + addPredefinedParameter(PredefinedParameters.RATING_VALUE, "4.9") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/buttons/factories/EventsFactory.kt b/app/src/main/java/com/affise/app/ui/fragments/buttons/factories/EventsFactory.kt new file mode 100644 index 0000000..5ab7106 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/buttons/factories/EventsFactory.kt @@ -0,0 +1,7 @@ +package com.affise.app.ui.fragments.buttons.factories + +import com.affise.attribution.events.Event + +interface EventsFactory { + fun createEvents(): List +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/cart/CartContract.kt b/app/src/main/java/com/affise/app/ui/fragments/cart/CartContract.kt new file mode 100644 index 0000000..5bd89bd --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/cart/CartContract.kt @@ -0,0 +1,14 @@ +package com.affise.app.ui.fragments.cart + +import androidx.lifecycle.LiveData +import com.affise.app.entity.ProductEntity + +interface CartContract { + interface ViewModel { + val stateData: LiveData + + fun update() + fun addProduct(product: ProductEntity) + fun removeProduct(product: ProductEntity) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/cart/CartFragment.kt b/app/src/main/java/com/affise/app/ui/fragments/cart/CartFragment.kt new file mode 100644 index 0000000..b610ebd --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/cart/CartFragment.kt @@ -0,0 +1,116 @@ +package com.affise.app.ui.fragments.cart + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.navigation.fragment.findNavController +import com.affise.app.R +import com.affise.app.databinding.FragmentMainCartBinding +import com.affise.app.entity.ProductEntity +import com.affise.app.extensions.hideKeyboard +import com.affise.app.ui.activity.main.MenuHolder +import com.affise.app.ui.fragments.cart.adapters.AdapterCart +import com.affise.app.ui.fragments.details.ProductDetailsFragment +import dagger.android.support.DaggerFragment +import java.math.BigDecimal +import javax.inject.Inject + +class CartFragment : DaggerFragment() { + + @Inject + lateinit var viewModel: CartContract.ViewModel + + @Inject + lateinit var menuHolder: MenuHolder + + @Inject + lateinit var adapter: AdapterCart + + lateinit var binding: FragmentMainCartBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = FragmentMainCartBinding.inflate(layoutInflater).apply { + binding = this + + recycles.adapter = adapter.apply { + onClickProduct = { clickProduct(it.product) } + onClickPlus = { viewModel.addProduct(it.product) } + onClickMinus = { viewModel.removeProduct(it.product) } + } + + refresh.setOnRefreshListener { + viewModel.update() + } + + menuSate.setOnClickListener { + menuHolder.changeMenuSate() + } + + notification.setOnClickListener { + + } + + checkout.setOnClickListener { + + } + }.root + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.stateData.observe(viewLifecycleOwner) { + when (it) { + is CartState.DataState -> { + binding.refresh.isRefreshing = false + adapter.submitList(it.cartProducts) + + binding.totalPrice.text = it.cartProducts + .fold(BigDecimal.ZERO) { c, v -> + c + ((v.product.price ?: BigDecimal.ZERO) * v.count.toBigDecimal()) + } + .takeIf { sum -> sum > BigDecimal.ZERO } + ?.toPlainString() + ?.let { price -> + it.cartProducts + .firstOrNull() + ?.product + ?.unit + ?.let { unit -> "$unit$price" } + ?: price + } + ?: "" + } + CartState.ErrorState -> { + binding.refresh.isRefreshing = false + } + CartState.LoadingState -> { + binding.refresh.isRefreshing = false + } + CartState.UpdateState -> { + binding.refresh.isRefreshing = true + } + } + } + } + + override fun onResume() { + super.onResume() + + viewModel.update() + + requireActivity().hideKeyboard() + } + + private fun clickProduct(productEntity: ProductEntity) { + findNavController().navigate( + R.id.action_cartFragment_to_productDetailsFragment, + Bundle().apply { + putLong(ProductDetailsFragment.PRODUCT_DETAILS_EXTRA_KEY, productEntity.id) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/cart/CartState.kt b/app/src/main/java/com/affise/app/ui/fragments/cart/CartState.kt new file mode 100644 index 0000000..9181ff8 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/cart/CartState.kt @@ -0,0 +1,12 @@ +package com.affise.app.ui.fragments.cart + +import com.affise.app.ui.fragments.cart.adapters.ProductCartEntity + +sealed class CartState { + object LoadingState : CartState() + object UpdateState : CartState() + object ErrorState : CartState() + data class DataState( + val cartProducts: List + ) : CartState() +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/cart/CartViewModel.kt b/app/src/main/java/com/affise/app/ui/fragments/cart/CartViewModel.kt new file mode 100644 index 0000000..8a3a91c --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/cart/CartViewModel.kt @@ -0,0 +1,90 @@ +package com.affise.app.ui.fragments.cart + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.affise.app.entity.ProductEntity +import com.affise.app.extensions.IOWithErrorHandling +import com.affise.app.usecase.ProductUseCase +import com.affise.attribution.Affise +import com.affise.attribution.events.predefined.ViewCartEvent +import com.fasterxml.jackson.databind.ObjectMapper +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.json.JSONArray +import org.json.JSONObject +import javax.inject.Inject + +class CartViewModel @Inject constructor( + private val useCase: ProductUseCase, + private val objectMapper: ObjectMapper +) : ViewModel(), CartContract.ViewModel { + + override val stateData = MutableLiveData() + + init { + updateData() + } + + override fun update() { + val currentState = stateData.value + if (currentState == CartState.LoadingState || currentState == CartState.UpdateState) { + return + } + stateData.value = CartState.UpdateState + + updateData() + } + + private fun updateData() { + stateData.value = CartState.UpdateState + + viewModelScope.launch(Dispatchers.IOWithErrorHandling { + stateData.postValue(CartState.ErrorState) + }) { + delay(300) + + val items = useCase.getCart() + + val itemsCart = items + .map { objectMapper.writeValueAsString(it) } + .map { JSONObject(it) } + + Affise.sendEvent( + ViewCartEvent( + JSONObject().apply { + put("cart", JSONArray(itemsCart)) + }, + "Cart" + ) + ) + + stateData.postValue( + CartState.DataState(items) + ) + } + } + + override fun addProduct(product: ProductEntity) { + stateData.value = CartState.UpdateState + + viewModelScope.launch(Dispatchers.IOWithErrorHandling { + }) { + stateData.postValue( + CartState.DataState(useCase.addToCart(product.id)) + ) + } + } + + override fun removeProduct(product: ProductEntity) { + stateData.value = CartState.UpdateState + + viewModelScope.launch(Dispatchers.IOWithErrorHandling { + }) { + stateData.postValue( + CartState.DataState(useCase.removeOnCart(product.id)) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/cart/adapters/AdapterCart.kt b/app/src/main/java/com/affise/app/ui/fragments/cart/adapters/AdapterCart.kt new file mode 100644 index 0000000..6592d22 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/cart/adapters/AdapterCart.kt @@ -0,0 +1,96 @@ +package com.affise.app.ui.fragments.cart.adapters + +import android.net.Uri +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.affise.app.R +import com.affise.app.databinding.ItemRecyclerCartBinding +import com.affise.app.entity.ProductEntity +import com.bumptech.glide.Glide +import javax.inject.Inject + +data class ProductCartEntity( + val product: ProductEntity, + var count: Int +) + +class AdapterCart @Inject constructor( +) : ListAdapter(ProductCartCallback()) { + + var onClickProduct: (ProductCartEntity) -> Unit = {} + + var onClickPlus: (ProductCartEntity) -> Unit = {} + + var onClickMinus: (ProductCartEntity) -> Unit = {} + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ProductCartViewHolder = LayoutInflater.from(parent.context) + .let { + ItemRecyclerCartBinding.inflate(it, parent, false) + } + .let { + ProductCartViewHolder(it, onClickProduct, onClickPlus, onClickMinus) + } + + override fun onBindViewHolder(holder: ProductCartViewHolder, position: Int) { + holder.bind(getItem(position)) + } +} + +class ProductCartCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: ProductCartEntity, + newItem: ProductCartEntity + ): Boolean = oldItem.product.id == newItem.product.id + + override fun areContentsTheSame( + oldItem: ProductCartEntity, + newItem: ProductCartEntity + ): Boolean = oldItem.product.name == newItem.product.name && + oldItem.product.price == newItem.product.price && + oldItem.product.unit == newItem.product.unit && + oldItem.product.rating == newItem.product.rating && + oldItem.count == newItem.count +} + +class ProductCartViewHolder( + private val binding: ItemRecyclerCartBinding, + private val onClickProduct: (ProductCartEntity) -> Unit, + private val onClickPlus: (ProductCartEntity) -> Unit, + private val onClickMinus: (ProductCartEntity) -> Unit +) : RecyclerView.ViewHolder(binding.root) { + + fun bind(product: ProductCartEntity) = with(binding) { + Glide.with(imageView) + .load(Uri.parse("file:///android_asset/${product.product.images.firstOrNull()}")) + .error(R.drawable.img_product_test) + .into(imageView) + + name.text = product.product.name + + price.text = product.product.price?.toPlainString()?.let { price -> + product.product.unit?.let { unit -> + "$unit$price" + } ?: price + } ?: "" + + count.text = product.count.toString() + + main.setOnClickListener { + onClickProduct.invoke(product) + } + + plus.setOnClickListener { + onClickPlus.invoke(product) + } + + minus.setOnClickListener { + onClickMinus.invoke(product) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/details/ProductDetailsContract.kt b/app/src/main/java/com/affise/app/ui/fragments/details/ProductDetailsContract.kt new file mode 100644 index 0000000..3190be9 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/details/ProductDetailsContract.kt @@ -0,0 +1,16 @@ +package com.affise.app.ui.fragments.details + +import androidx.lifecycle.LiveData + +interface ProductDetailsContract { + interface ViewModel { + val stateData: LiveData + val currentGroup: LiveData + + fun setItemId(id: Long?) + fun clickLike(id: Long?) + fun selectGroup(group: ProductDetailsActionGroup) + fun clickCart() + fun clickBuyNow() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/details/ProductDetailsFragment.kt b/app/src/main/java/com/affise/app/ui/fragments/details/ProductDetailsFragment.kt new file mode 100644 index 0000000..191fb4d --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/details/ProductDetailsFragment.kt @@ -0,0 +1,364 @@ +package com.affise.app.ui.fragments.details + +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.RecyclerView +import androidx.viewpager2.widget.ViewPager2 +import com.affise.app.R +import com.affise.app.databinding.FragmentMainProductDetailsBinding +import com.affise.app.entity.Colors +import com.affise.app.entity.ProductEntity +import com.affise.app.extensions.hideKeyboard +import com.affise.app.ui.adapters.CenterDecoration +import com.affise.app.ui.adapters.CenterSnapHelper +import com.affise.app.ui.adapters.SpaceDecoration +import com.affise.app.ui.fragments.details.adapters.* +import com.google.android.material.card.MaterialCardView +import com.google.android.material.textview.MaterialTextView +import dagger.android.support.DaggerFragment +import javax.inject.Inject + +class ProductDetailsFragment : DaggerFragment() { + + @Inject + lateinit var viewModel: ProductDetailsContract.ViewModel + + @Inject + lateinit var adapterImages: AdapterImages + + @Inject + lateinit var adapterColors: AdapterColors + + @Inject + lateinit var adapterSize: AdapterSize + + @Inject + lateinit var adapterImagePages: AdapterImagePages + + lateinit var binding: FragmentMainProductDetailsBinding + + private val snapHelper = CenterSnapHelper() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = FragmentMainProductDetailsBinding.inflate(layoutInflater).apply { + binding = this + + images.adapter = adapterImages + + images.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + super.onPageSelected(position) + + adapterImagePages.setSelected(position) + } + }) + + imagePages.adapter = adapterImagePages + + imagePages.addItemDecoration(SpaceDecoration(paddingEnd = 5f)) + + colors.adapter = adapterColors + + size.adapter = adapterSize.apply { + submitList(Size.getAllSize()) + } + + size.setHasFixedSize(true) + + snapHelper.attachToRecyclerView(size) + + size.addItemDecoration( + DividerItemDecoration(requireContext(), RecyclerView.HORIZONTAL).apply { + setDrawable( + ContextCompat.getDrawable( + requireContext(), + R.drawable.adapter_size_decoration + )!! + ) + }) + + size.addItemDecoration(CenterDecoration(0)) + + productDetailsErrorBack.setOnClickListener { + requireActivity().onBackPressed() + } + + productDetailsBack.setOnClickListener { + requireActivity().onBackPressed() + } + + productDetailsLike.setOnClickListener { + viewModel.clickLike(getProductId()) + } + + productDetailsRefresh.setOnRefreshListener { + viewModel.setItemId(getProductId()) + } + + actionColor.setOnClickListener { + viewModel.selectGroup(ProductDetailsActionGroup.COLORS) + } + + actionSize.setOnClickListener { + viewModel.selectGroup(ProductDetailsActionGroup.SIZE) + } + + actionReviews.setOnClickListener { + viewModel.selectGroup(ProductDetailsActionGroup.REVIEWS) + } + + actionSizeEuro.setOnClickListener { + adapterSize.currentType = SizeType.EURO + + setSelectedSize(actionSizeEuro) + setUnSelectedSize(actionSizeUs) + setUnSelectedSize(actionSizeAsia) + } + + actionSizeUs.setOnClickListener { + adapterSize.currentType = SizeType.US + + setUnSelectedSize(actionSizeEuro) + setSelectedSize(actionSizeUs) + setUnSelectedSize(actionSizeAsia) + } + + actionSizeAsia.setOnClickListener { + adapterSize.currentType = SizeType.ASIA + + setUnSelectedSize(actionSizeEuro) + setUnSelectedSize(actionSizeUs) + setSelectedSize(actionSizeAsia) + } + + cart.setOnClickListener { + viewModel.clickCart() + } + + buyNow.setOnClickListener { + viewModel.clickBuyNow() + } + }.root + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.setItemId(getProductId()) + + viewModel.stateData.observe(viewLifecycleOwner) { + when (it) { + is ProductDetailsState.DataState -> { + binding.productDetailsLoading.isVisible = false + binding.productDetailsRefresh.isVisible = true + binding.productDetailsRefresh.isRefreshing = false + binding.productDetailsError.isVisible = false + + val images = it.productsDetails.images + + adapterImages.submitList(images) + + if (images.size > 1) { + val pages = images.mapIndexed { index, image -> + ImagePagesItem(image, binding.images.currentItem == index) + } + + adapterImagePages.submitList(pages) + + binding.imagePages.isVisible = true + } else { + binding.imagePages.isVisible = false + } + + adapterColors.submitList( + Colors.values().map { color -> + ColorItem(color, it.productsDetails.color == color) + } + ) + + val sizeValue = Size.getAllSize().first() { size -> + it.productsDetails.size == when (it.productsDetails.size_type) { + SizeType.EURO -> size.sizeEuro + SizeType.US -> size.sizeUs + SizeType.ASIA -> size.sizeAsia + } + } + adapterSize.currentType = it.productsDetails.size_type + adapterSize.currentSize = sizeValue + adapterSize.enabledSize = listOf(sizeValue) + + when (it.productsDetails.size_type) { + SizeType.EURO -> { + setSelectedSize(binding.actionSizeEuro) + setUnSelectedSize(binding.actionSizeUs) + setUnSelectedSize(binding.actionSizeAsia) + } + SizeType.US -> { + setUnSelectedSize(binding.actionSizeEuro) + setSelectedSize(binding.actionSizeUs) + setUnSelectedSize(binding.actionSizeAsia) + } + SizeType.ASIA -> { + setUnSelectedSize(binding.actionSizeEuro) + setUnSelectedSize(binding.actionSizeUs) + setSelectedSize(binding.actionSizeAsia) + } + } + + val position = Size.getAllSize().indexOf(sizeValue) + snapHelper.scrollTo(position, false) + + binding.productDetailsLike.setImageResource( + if (it.inLike) R.drawable.ic_like_full else R.drawable.ic_like + ) + + binding.cartImage.setImageResource( + if (it.inCart) R.drawable.ic_cart_full else R.drawable.ic_cart + ) + + showDetails(it.productsDetails) + } + is ProductDetailsState.ErrorState -> { + binding.productDetailsLoading.isVisible = false + binding.productDetailsRefresh.isVisible = false + binding.productDetailsError.isVisible = true + binding.productDetailsErrorRefresh.isVisible = it.retry + } + ProductDetailsState.LoadingState -> { + binding.productDetailsLoading.isVisible = true + binding.productDetailsRefresh.isVisible = false + binding.productDetailsError.isVisible = false + } + ProductDetailsState.UpdateState -> { + binding.productDetailsLoading.isVisible = false + binding.productDetailsRefresh.isVisible = true + binding.productDetailsRefresh.isRefreshing = true + binding.productDetailsError.isVisible = false + } + } + } + + viewModel.currentGroup.observe(viewLifecycleOwner) { + when (it) { + null -> Unit + ProductDetailsActionGroup.COLORS -> showColorsGroup() + ProductDetailsActionGroup.SIZE -> showSizeGroup() + ProductDetailsActionGroup.REVIEWS -> showReviewsGroup() + } + } + } + + override fun onResume() { + super.onResume() + + requireActivity().hideKeyboard() + } + + private fun showDetails(productsDetails: ProductEntity) = with(binding) { + price.text = productsDetails.price?.toPlainString()?.let { price -> + productsDetails.unit?.let { unit -> + "$unit$price" + } ?: price + } ?: "" + + name.text = productsDetails.name + description.text = productsDetails.description + rating.text = getString(R.string.rating_with_value, productsDetails.rating.toString()) + } + + private fun showColorsGroup() = with(binding) { + setSelected(actionColorTitle, actionColor) + setUnSelected(actionSizeTitle, actionSize) + setUnSelected(actionReviewsTitle, actionReviews) + + colors.isVisible = true + reviews.isVisible = false + sizeGroup.isVisible = false + } + + private fun showSizeGroup() = with(binding) { + setUnSelected(actionColorTitle, actionColor) + setSelected(actionSizeTitle, actionSize) + setUnSelected(actionReviewsTitle, actionReviews) + + colors.isVisible = false + reviews.isVisible = false + sizeGroup.isVisible = true + } + + private fun showReviewsGroup() = with(binding) { + setUnSelected(actionColorTitle, actionColor) + setUnSelected(actionSizeTitle, actionSize) + setSelected(actionReviewsTitle, actionReviews) + + colors.isVisible = false + reviews.isVisible = true + sizeGroup.isVisible = false + } + + private fun setSelected(textView: MaterialTextView, cardView: MaterialCardView) { + with(textView) { + setBackgroundResource(R.color.ebony_clay) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + setTextAppearance(R.style.Selected) + } else { + @Suppress("DEPRECATION") + setTextAppearance(requireContext(), R.style.Selected) + } + } + + cardView.alpha = 1f + } + + private fun setUnSelected(textView: MaterialTextView, cardView: MaterialCardView) { + with(textView) { + setBackgroundResource(android.R.color.transparent) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + setTextAppearance(R.style.UnSelected) + } else { + @Suppress("DEPRECATION") + setTextAppearance(requireContext(), R.style.UnSelected) + } + } + + cardView.alpha = 0.3f + } + + private fun setSelectedSize(textView: MaterialTextView) { + with(textView) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + setTextAppearance(R.style.EnabledSize) + } else { + @Suppress("DEPRECATION") + setTextAppearance(requireContext(), R.style.EnabledSize) + } + } + } + + private fun setUnSelectedSize(textView: MaterialTextView) { + with(textView) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + setTextAppearance(R.style.DisabledSize) + } else { + @Suppress("DEPRECATION") + setTextAppearance(requireContext(), R.style.DisabledSize) + } + } + } + + private fun getProductId() = arguments?.getLong(PRODUCT_DETAILS_EXTRA_KEY) + + companion object { + const val PRODUCT_DETAILS_EXTRA_KEY = "product_details" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/details/ProductDetailsState.kt b/app/src/main/java/com/affise/app/ui/fragments/details/ProductDetailsState.kt new file mode 100644 index 0000000..635670a --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/details/ProductDetailsState.kt @@ -0,0 +1,14 @@ +package com.affise.app.ui.fragments.details + +import com.affise.app.entity.ProductEntity + +sealed class ProductDetailsState { + object LoadingState : ProductDetailsState() + object UpdateState : ProductDetailsState() + data class ErrorState(val retry: Boolean) : ProductDetailsState() + data class DataState( + val productsDetails: ProductEntity, + val inLike: Boolean, + val inCart: Boolean + ) : ProductDetailsState() +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/details/ProductDetailsViewModel.kt b/app/src/main/java/com/affise/app/ui/fragments/details/ProductDetailsViewModel.kt new file mode 100644 index 0000000..c079e51 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/details/ProductDetailsViewModel.kt @@ -0,0 +1,140 @@ +package com.affise.app.ui.fragments.details + +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.affise.app.entity.ProductEntity +import com.affise.app.extensions.IOWithErrorHandling +import com.affise.app.usecase.ProductUseCase +import com.affise.attribution.Affise +import com.affise.attribution.events.predefined.AddToCartEvent +import com.affise.attribution.events.predefined.AddToWishlistEvent +import com.affise.attribution.events.predefined.ViewItemEvent +import com.fasterxml.jackson.databind.ObjectMapper +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.json.JSONObject +import javax.inject.Inject + +enum class ProductDetailsActionGroup { + COLORS, + SIZE, + REVIEWS +} + +class ProductDetailsViewModel @Inject constructor( + private val useCase: ProductUseCase, + private val objectMapper: ObjectMapper +) : ViewModel(), ProductDetailsContract.ViewModel { + + private var currentProductId: Long? = null + + private val isLike = MutableLiveData() + + private val isCart = MutableLiveData() + + private val product = MutableLiveData() + + override val stateData = MediatorLiveData().apply { + val observe: (Any) -> Unit = { + val inLike = isLike.value + val inCart = isCart.value + val product = product.value + + if (inLike != null && inCart != null && product != null) { + postValue(ProductDetailsState.DataState(product, inLike, inCart)) + } + } + + addSource(isLike, observe) + addSource(isCart, observe) + addSource(product, observe) + } + + override val currentGroup = MutableLiveData(ProductDetailsActionGroup.SIZE) + + override fun setItemId(id: Long?) { + currentProductId = id + + if (id == null) { + stateData.postValue(ProductDetailsState.ErrorState(false)) + + return + } + + stateData.value = if (stateData.value is ProductDetailsState.DataState) { + ProductDetailsState.UpdateState + } else { + ProductDetailsState.LoadingState + } + + viewModelScope.launch(Dispatchers.IOWithErrorHandling { + stateData.postValue(ProductDetailsState.ErrorState(true)) + }) { + useCase.getProductDetails(id)?.let { + val productData = it + val likeData = useCase.inLike(id) + val cartData = useCase.inCart(id) + + Affise.sendEvent( + ViewItemEvent( + JSONObject().apply { + put("product", JSONObject(objectMapper.writeValueAsString(productData))) + put("isLike", likeData) + put("inCart", cartData) + }, + "product details" + ) + ) + + product.postValue(productData) + isLike.postValue(likeData) + isCart.postValue(cartData) + } ?: stateData.postValue(ProductDetailsState.ErrorState(false)) + } + } + + override fun selectGroup(group: ProductDetailsActionGroup) { + if (currentGroup.value != group) { + currentGroup.postValue(group) + } + } + + override fun clickLike(id: Long?) { + viewModelScope.launch(Dispatchers.IOWithErrorHandling { + }) { + (stateData.value as? ProductDetailsState.DataState)?.let { + Affise.sendEvent( + AddToWishlistEvent( + JSONObject(objectMapper.writeValueAsString(it.productsDetails)), + System.currentTimeMillis(), + "Add to wishlist in Product details" + ) + ) + } + + isLike.postValue(useCase.updateLike(currentProductId)) + } + } + + override fun clickCart() { + viewModelScope.launch(Dispatchers.IOWithErrorHandling { + }) { + (stateData.value as? ProductDetailsState.DataState)?.let { + Affise.sendEvent( + AddToCartEvent( + JSONObject(objectMapper.writeValueAsString(it.productsDetails)), + System.currentTimeMillis(), + "Add to cart in Product details" + ) + ) + } + + isCart.postValue(useCase.updateCart(currentProductId)) + } + } + + override fun clickBuyNow() { + } +} diff --git a/app/src/main/java/com/affise/app/ui/fragments/details/adapters/AdapterColors.kt b/app/src/main/java/com/affise/app/ui/fragments/details/adapters/AdapterColors.kt new file mode 100644 index 0000000..cfe1902 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/details/adapters/AdapterColors.kt @@ -0,0 +1,62 @@ +package com.affise.app.ui.fragments.details.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.affise.app.databinding.ItemRecyclerColorBinding +import com.affise.app.entity.Colors +import javax.inject.Inject + +data class ColorItem(val color: Colors, val enabled: Boolean) + +class AdapterColors @Inject constructor( +) : ListAdapter(ColorCallback()) { + + val onSelected: (ColorItem) -> Unit = {} + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ColorViewHolder = LayoutInflater.from(parent.context) + .let { + ItemRecyclerColorBinding.inflate(it, parent, false) + } + .let { + ColorViewHolder(it) + } + + override fun onBindViewHolder(holder: ColorViewHolder, position: Int) { + holder.bind(getItem(position), onSelected) + } +} + +class ColorCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: ColorItem, + newItem: ColorItem + ): Boolean = oldItem.color == newItem.color + + override fun areContentsTheSame( + oldItem: ColorItem, + newItem: ColorItem + ): Boolean = oldItem.enabled == newItem.enabled +} + +class ColorViewHolder( + private val binding: ItemRecyclerColorBinding, +) : RecyclerView.ViewHolder(binding.root) { + + fun bind(color: ColorItem, onSelected: (ColorItem) -> Unit) = with(binding) { + root.setOnClickListener { + onSelected.invoke(color) + } + + root.isEnabled = color.enabled + + root.alpha = if (color.enabled) 1f else 0.3f + + image.setImageResource(color.color.colorRes) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/details/adapters/AdapterImagePages.kt b/app/src/main/java/com/affise/app/ui/fragments/details/adapters/AdapterImagePages.kt new file mode 100644 index 0000000..333101a --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/details/adapters/AdapterImagePages.kt @@ -0,0 +1,87 @@ +package com.affise.app.ui.fragments.details.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.affise.app.R +import com.affise.app.databinding.ItemRecyclerPageBinding +import com.affise.app.extensions.convertDpToPixels +import javax.inject.Inject + +class ImagePagesItem(val name: String, var isSelected: Boolean = false) + +class AdapterImagePages @Inject constructor( +) : ListAdapter(PageCallback()) { + + private var selection = -1 + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ImagePageViewHolder = LayoutInflater.from(parent.context) + .let { + ItemRecyclerPageBinding.inflate(it, parent, false) + } + .let { + ImagePageViewHolder(it) + } + + override fun onBindViewHolder(holder: ImagePageViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + fun getSelectedPosition(): Int = selection + + fun setSelected(position: Int) { + if (selection == position) return + + if (selection in 0 until itemCount) { + getItem(selection)?.isSelected = false + + notifyItemChanged(selection) + } + + if (position in 0 until itemCount) { + getItem(position)?.isSelected = true + + notifyItemChanged(position) + } + + selection = position + } +} + +class PageCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: ImagePagesItem, + newItem: ImagePagesItem + ): Boolean = oldItem.name == newItem.name + + override fun areContentsTheSame( + oldItem: ImagePagesItem, + newItem: ImagePagesItem + ): Boolean = oldItem.isSelected == newItem.isSelected +} + +class ImagePageViewHolder( + private val binding: ItemRecyclerPageBinding, +) : RecyclerView.ViewHolder(binding.root) { + + fun bind(item: ImagePagesItem) = with(binding) { + root.setCardBackgroundColor( + ContextCompat.getColor( + root.context, + if (item.isSelected) R.color.ebony_clay else R.color.gray_c4 + ) + ) + + root.layoutParams = root.layoutParams.apply { + width = root.context.convertDpToPixels( + if (item.isSelected) 26f else 10f + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/details/adapters/AdapterImages.kt b/app/src/main/java/com/affise/app/ui/fragments/details/adapters/AdapterImages.kt new file mode 100644 index 0000000..c1ea1c5 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/details/adapters/AdapterImages.kt @@ -0,0 +1,55 @@ +package com.affise.app.ui.fragments.details.adapters + +import android.net.Uri +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.affise.app.R +import com.affise.app.databinding.ItemRecyclerImageBinding +import com.bumptech.glide.Glide +import javax.inject.Inject + +class AdapterImages @Inject constructor( +) : ListAdapter(ImageCallback()) { + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ImageViewHolder = LayoutInflater.from(parent.context) + .let { + ItemRecyclerImageBinding.inflate(it, parent, false) + } + .let { + ImageViewHolder(it) + } + + override fun onBindViewHolder(holder: ImageViewHolder, position: Int) { + holder.bind(getItem(position)) + } +} + +class ImageCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: String, + newItem: String + ): Boolean = oldItem == newItem + + override fun areContentsTheSame( + oldItem: String, + newItem: String + ): Boolean = true +} + +class ImageViewHolder( + private val binding: ItemRecyclerImageBinding, +) : RecyclerView.ViewHolder(binding.root) { + + fun bind(product: String) = with(binding) { + Glide.with(root) + .load(Uri.parse("file:///android_asset/${product}")) + .error(R.drawable.img_product_test) + .into(root) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/details/adapters/AdapterSize.kt b/app/src/main/java/com/affise/app/ui/fragments/details/adapters/AdapterSize.kt new file mode 100644 index 0000000..33ddd66 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/details/adapters/AdapterSize.kt @@ -0,0 +1,117 @@ +package com.affise.app.ui.fragments.details.adapters + +import android.annotation.SuppressLint +import android.os.Build +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isInvisible +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.affise.app.R +import com.affise.app.databinding.ItemRecyclerSizeBinding +import javax.inject.Inject + +enum class SizeType { + EURO, + US, + ASIA +} + +data class Size(val sizeUs: Double, val sizeEuro: Double, val sizeAsia: Double) { + companion object { + fun getAllSize(): List = listOf( + Size(6.0, 38.0, 24.0), + Size(6.5, 38.5, 24.5), + Size(7.0, 39.0, 25.0), + Size(7.5, 40.0, 25.5), + Size(8.0, 41.0, 26.0), + Size(8.5, 42.0, 26.5), + Size(9.0, 43.0, 27.0), + Size(9.5, 43.5, 27.5), + Size(10.0, 44.0, 28.0), + Size(10.5, 44.5, 28.5), + Size(11.0, 45.0, 29.0), + Size(11.5, 45.5, 29.5), + Size(12.0, 46.0, 30.0), + ) + } +} + +@SuppressLint("NotifyDataSetChanged") +class AdapterSize @Inject constructor( +) : ListAdapter(SizeCallback()) { + + var currentType: SizeType = SizeType.US + set(value) { + field = value + + notifyDataSetChanged() + } + + var enabledSize: List = emptyList() + set(value) { + field = value + + notifyDataSetChanged() + } + + var currentSize: Size = Size(8.0, 41.0, 26.0) + set(value) { + field = value + + notifyDataSetChanged() + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): SizeViewHolder = LayoutInflater.from(parent.context) + .let { + ItemRecyclerSizeBinding.inflate(it, parent, false) + } + .let { + SizeViewHolder(it) + } + + override fun onBindViewHolder(holder: SizeViewHolder, position: Int) { + val size = getItem(position) + holder.bind(size, currentType, size in enabledSize, size == currentSize) + } +} + +class SizeCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: Size, + newItem: Size + ): Boolean = oldItem.sizeUs == newItem.sizeUs + + override fun areContentsTheSame( + oldItem: Size, + newItem: Size + ): Boolean = true +} + +class SizeViewHolder( + private val binding: ItemRecyclerSizeBinding, +) : RecyclerView.ViewHolder(binding.root) { + + fun bind(size: Size, type: SizeType, enabled: Boolean, isCurrent: Boolean) = with(binding) { + current.isInvisible = !isCurrent + + value.text = when (type) { + SizeType.EURO -> size.sizeEuro + SizeType.US -> size.sizeUs + SizeType.ASIA -> size.sizeAsia + }.toString() + + val textAppearance = if (enabled) R.style.EnabledSize else R.style.DisabledSize + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + value.setTextAppearance(textAppearance) + } else { + @Suppress("DEPRECATION") + value.setTextAppearance(itemView.context, textAppearance) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/home/HomeContract.kt b/app/src/main/java/com/affise/app/ui/fragments/home/HomeContract.kt new file mode 100644 index 0000000..5efdc21 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/home/HomeContract.kt @@ -0,0 +1,16 @@ +package com.affise.app.ui.fragments.home + +import androidx.lifecycle.LiveData +import com.affise.app.entity.ProductEntity + +interface HomeContract { + interface ViewModel { + val stateData: LiveData + + fun setSearch(value: String) + + fun update() + fun clickLike(productEntity: ProductEntity) + fun clickAdd(productEntity: ProductEntity) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/home/HomeFragment.kt b/app/src/main/java/com/affise/app/ui/fragments/home/HomeFragment.kt new file mode 100644 index 0000000..3470919 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/home/HomeFragment.kt @@ -0,0 +1,137 @@ +package com.affise.app.ui.fragments.home + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.core.widget.addTextChangedListener +import androidx.navigation.fragment.findNavController +import com.affise.app.R +import com.affise.app.databinding.FragmentMainHomeBinding +import com.affise.app.entity.ProductEntity +import com.affise.app.extensions.hideKeyboard +import com.affise.app.ui.activity.main.MenuHolder +import com.affise.app.ui.adapters.SpaceDecoration +import com.affise.app.ui.fragments.details.ProductDetailsFragment +import com.affise.app.ui.fragments.home.adapters.AdapterNew +import com.affise.app.ui.fragments.home.adapters.AdapterPopular +import dagger.android.support.DaggerFragment +import javax.inject.Inject + +class HomeFragment : DaggerFragment() { + + @Inject + lateinit var viewModel: HomeContract.ViewModel + + @Inject + lateinit var menuHolder: MenuHolder + + @Inject + lateinit var adapterPopular: AdapterPopular + + @Inject + lateinit var adapterNew: AdapterNew + + lateinit var binding: FragmentMainHomeBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = FragmentMainHomeBinding.inflate(layoutInflater).apply { + binding = this + + homeMenu.setOnClickListener { + menuHolder.changeMenuSate() + } + + homeNotification.setOnClickListener { + + } + + homeErrorRefresh.setOnClickListener { + viewModel.update() + } + + homeRefresh.setOnRefreshListener { + viewModel.update() + } + + homeSearchSetting.setOnClickListener { + + } + + popularMore.setOnClickListener { + + } + + homeSearchValue.addTextChangedListener { + viewModel.setSearch(it.toString()) + } + + recyclerPopular.adapter = adapterPopular.apply { + onClickProduct = { clickProduct(it.product) } + onClickLike = { viewModel.clickLike(it.product) } + onClickAdd = { viewModel.clickAdd(it.product) } + } + + recyclerNew.adapter = adapterNew.apply { + onClickProduct = { clickProduct(it.product) } + onClickLike = { viewModel.clickLike(it.product) } + onClickAdd = { viewModel.clickAdd(it.product) } + } + + recyclerPopular.addItemDecoration(SpaceDecoration(paddingEnd = 16f)) + recyclerNew.addItemDecoration(SpaceDecoration(paddingBottom = 16f)) + }.root + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.stateData.observe(viewLifecycleOwner) { + when (it) { + is HomeState.DataState -> { + binding.homeLoading.isVisible = false + binding.homeError.isVisible = false + binding.homeRefresh.isRefreshing = false + + adapterPopular.submitList(it.popularProducts) + adapterNew.submitList(it.newProducts) + } + HomeState.ErrorState -> { + binding.homeLoading.isVisible = false + binding.homeError.isVisible = true + binding.homeRefresh.isRefreshing = false + } + HomeState.LoadingState -> { + binding.homeLoading.isVisible = true + binding.homeError.isVisible = false + binding.homeRefresh.isRefreshing = false + } + HomeState.UpdateState -> { + binding.homeLoading.isVisible = false + binding.homeError.isVisible = false + binding.homeRefresh.isRefreshing = true + } + } + } + } + + override fun onResume() { + super.onResume() + + requireActivity().hideKeyboard() + + viewModel.update() + } + + private fun clickProduct(productEntity: ProductEntity) { + findNavController().navigate( + R.id.action_homeFragment_to_productDetailsFragment, + Bundle().apply { + putLong(ProductDetailsFragment.PRODUCT_DETAILS_EXTRA_KEY, productEntity.id) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/home/HomeState.kt b/app/src/main/java/com/affise/app/ui/fragments/home/HomeState.kt new file mode 100644 index 0000000..eee2207 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/home/HomeState.kt @@ -0,0 +1,14 @@ +package com.affise.app.ui.fragments.home + +import com.affise.app.ui.fragments.home.adapters.ProductNewEntity +import com.affise.app.ui.fragments.home.adapters.ProductPopularEntity + +sealed class HomeState { + object LoadingState : HomeState() + object UpdateState : HomeState() + object ErrorState : HomeState() + data class DataState( + val popularProducts: List, + val newProducts: List, + ) : HomeState() +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/home/HomeViewModel.kt b/app/src/main/java/com/affise/app/ui/fragments/home/HomeViewModel.kt new file mode 100644 index 0000000..b2d0ace --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/home/HomeViewModel.kt @@ -0,0 +1,117 @@ +package com.affise.app.ui.fragments.home + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.affise.app.entity.ProductEntity +import com.affise.app.extensions.IOWithErrorHandling +import com.affise.app.usecase.ProductUseCase +import com.affise.attribution.Affise +import com.affise.attribution.events.predefined.AddToCartEvent +import com.affise.attribution.events.predefined.AddToWishlistEvent +import com.affise.attribution.events.predefined.SearchEvent +import com.affise.attribution.events.predefined.ViewItemsEvent +import com.fasterxml.jackson.databind.ObjectMapper +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.json.JSONArray +import org.json.JSONObject +import javax.inject.Inject + +class HomeViewModel @Inject constructor( + private val useCase: ProductUseCase, + private val objectMapper: ObjectMapper +) : ViewModel(), HomeContract.ViewModel { + + override val stateData = MutableLiveData(HomeState.LoadingState) + + init { + updateData() + } + + override fun update() { + val currentState = stateData.value + if (currentState == HomeState.LoadingState || currentState == HomeState.UpdateState) { + return + } + + stateData.value = HomeState.UpdateState + + updateData() + } + + private fun updateData() { + viewModelScope.launch(Dispatchers.IOWithErrorHandling { + stateData.postValue(HomeState.ErrorState) + }) { + delay(300) + updateProducts() + } + } + + override fun setSearch(value: String) { + Affise.sendEvent( + SearchEvent(JSONArray(), System.currentTimeMillis(), "search by '$value'") + ) + } + + override fun clickLike(productEntity: ProductEntity) { + viewModelScope.launch(Dispatchers.IOWithErrorHandling { + }) { + Affise.sendEvent( + AddToWishlistEvent( + JSONObject(objectMapper.writeValueAsString(productEntity)), + System.currentTimeMillis(), + "Add to wishlist on home" + ) + ) + + useCase.updateLike(productEntity.id) + updateProducts() + } + } + + override fun clickAdd(productEntity: ProductEntity) { + viewModelScope.launch(Dispatchers.IOWithErrorHandling { + }) { + Affise.sendEvent( + AddToCartEvent( + JSONObject(objectMapper.writeValueAsString(productEntity)), + System.currentTimeMillis(), + "Add to cart on home" + ) + ) + + + useCase.updateCart(productEntity.id) + updateProducts() + } + } + + private suspend fun updateProducts() { + val popularProducts = useCase.getPopularProducts() + + val newProducts = useCase.getNewProducts() + + val itemsPopular = popularProducts + .map { objectMapper.writeValueAsString(it) } + .map { JSONObject(it) } + + val itemsNew = newProducts + .map { objectMapper.writeValueAsString(it) } + .map { JSONObject(it) } + + Affise.sendEvent( + ViewItemsEvent(JSONArray(itemsPopular), "popularProducts") + ) + + Affise.sendEvent( + ViewItemsEvent(JSONArray(itemsNew), "newProducts") + ) + + stateData.postValue( + HomeState.DataState(popularProducts, newProducts) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/home/adapters/AdapterNew.kt b/app/src/main/java/com/affise/app/ui/fragments/home/adapters/AdapterNew.kt new file mode 100644 index 0000000..83b7751 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/home/adapters/AdapterNew.kt @@ -0,0 +1,103 @@ +package com.affise.app.ui.fragments.home.adapters + +import android.net.Uri +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.affise.app.R +import com.affise.app.databinding.ItemRecyclerNewBinding +import com.affise.app.entity.ProductEntity +import com.bumptech.glide.Glide +import javax.inject.Inject + +data class ProductNewEntity( + val product: ProductEntity, + val inLike: Boolean, + val inCart: Boolean +) + +class AdapterNew @Inject constructor( +) : ListAdapter(ProductNewCallback()) { + + var onClickProduct: (ProductNewEntity) -> Unit = {} + + var onClickLike: (ProductNewEntity) -> Unit = {} + + var onClickAdd: (ProductNewEntity) -> Unit = {} + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ProductNewViewHolder = LayoutInflater.from(parent.context) + .let { + ItemRecyclerNewBinding.inflate(it, parent, false) + } + .let { + ProductNewViewHolder(it, onClickProduct, onClickLike, onClickAdd) + } + + override fun onBindViewHolder(holder: ProductNewViewHolder, position: Int) { + holder.bind(getItem(position)) + } +} + +class ProductNewCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: ProductNewEntity, + newItem: ProductNewEntity + ): Boolean = oldItem.product.id == newItem.product.id + + override fun areContentsTheSame( + oldItem: ProductNewEntity, + newItem: ProductNewEntity + ): Boolean = oldItem.product.name == newItem.product.name && + oldItem.product.price == newItem.product.price && + oldItem.product.unit == newItem.product.unit && + oldItem.inCart == newItem.inCart && + oldItem.inLike == newItem.inLike +} + +class ProductNewViewHolder( + private val binding: ItemRecyclerNewBinding, + private val onClickProduct: (ProductNewEntity) -> Unit, + private val onClickLike: (ProductNewEntity) -> Unit, + private val onClickAdd: (ProductNewEntity) -> Unit +) : RecyclerView.ViewHolder(binding.root) { + + fun bind(product: ProductNewEntity) = with(binding) { + Glide.with(imageView) + .load(Uri.parse("file:///android_asset/${product.product.images.firstOrNull()}")) + .error(R.drawable.img_product_test) + .into(imageView) + + name.text = product.product.name + + price.text = product.product.price?.toPlainString()?.let { price -> + product.product.unit?.let { unit -> + "$unit$price" + } ?: price + } ?: "" + + like.setImageResource( + if (product.inLike) R.drawable.ic_like_full else R.drawable.ic_like + ) + + add.setImageResource( + if (product.inCart) R.drawable.ic_cart_full else R.drawable.ic_cart + ) + + main.setOnClickListener { + onClickProduct.invoke(product) + } + + like.setOnClickListener { + onClickLike.invoke(product) + } + + add.setOnClickListener { + onClickAdd.invoke(product) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/home/adapters/AdapterPopular.kt b/app/src/main/java/com/affise/app/ui/fragments/home/adapters/AdapterPopular.kt new file mode 100644 index 0000000..fe9aa51 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/home/adapters/AdapterPopular.kt @@ -0,0 +1,103 @@ +package com.affise.app.ui.fragments.home.adapters + +import android.net.Uri +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.affise.app.R +import com.affise.app.databinding.ItemRecyclerPopularBinding +import com.affise.app.entity.ProductEntity +import com.bumptech.glide.Glide +import javax.inject.Inject + +data class ProductPopularEntity( + val product: ProductEntity, + val inLike: Boolean, + val inCart: Boolean +) + +class AdapterPopular @Inject constructor( +) : ListAdapter(ProductPopularCallback()) { + + var onClickProduct: (ProductPopularEntity) -> Unit = {} + + var onClickLike: (ProductPopularEntity) -> Unit = {} + + var onClickAdd: (ProductPopularEntity) -> Unit = {} + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ProductPopularViewHolder = LayoutInflater.from(parent.context) + .let { + ItemRecyclerPopularBinding.inflate(it, parent, false) + } + .let { + ProductPopularViewHolder(it, onClickProduct, onClickLike, onClickAdd) + } + + override fun onBindViewHolder(holder: ProductPopularViewHolder, position: Int) { + holder.bind(getItem(position)) + } +} + +class ProductPopularCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: ProductPopularEntity, + newItem: ProductPopularEntity + ): Boolean = oldItem.product.id == newItem.product.id + + override fun areContentsTheSame( + oldItem: ProductPopularEntity, + newItem: ProductPopularEntity + ): Boolean = oldItem.product.name == newItem.product.name && + oldItem.product.price == newItem.product.price && + oldItem.product.unit == newItem.product.unit && + oldItem.inCart == newItem.inCart && + oldItem.inLike == newItem.inLike +} + +class ProductPopularViewHolder( + private val binding: ItemRecyclerPopularBinding, + private val onClickProduct: (ProductPopularEntity) -> Unit, + private val onClickLike: (ProductPopularEntity) -> Unit, + private val onClickAdd: (ProductPopularEntity) -> Unit +) : RecyclerView.ViewHolder(binding.root) { + + fun bind(product: ProductPopularEntity) = with(binding) { + Glide.with(imageView) + .load(Uri.parse("file:///android_asset/${product.product.images.firstOrNull()}")) + .error(R.drawable.img_product_test) + .into(imageView) + + name.text = product.product.name + + price.text = product.product.price?.toPlainString()?.let { price -> + product.product.unit?.let { unit -> + "$unit$price" + } ?: price + } ?: "" + + like.setImageResource( + if (product.inLike) R.drawable.ic_like_full else R.drawable.ic_like + ) + + add.setImageResource( + if (product.inCart) R.drawable.ic_cart_full else R.drawable.ic_cart + ) + + main.setOnClickListener { + onClickProduct.invoke(product) + } + + like.setOnClickListener { + onClickLike.invoke(product) + } + + add.setOnClickListener { + onClickAdd.invoke(product) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/likes/LikesContract.kt b/app/src/main/java/com/affise/app/ui/fragments/likes/LikesContract.kt new file mode 100644 index 0000000..d5063e1 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/likes/LikesContract.kt @@ -0,0 +1,15 @@ +package com.affise.app.ui.fragments.likes + +import androidx.lifecycle.LiveData +import com.affise.app.entity.ProductEntity + +interface LikesContract { + + interface ViewModel { + val stateData: LiveData + + fun update() + fun clickLike(product: ProductEntity) + fun clickAdd(product: ProductEntity) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/likes/LikesFragment.kt b/app/src/main/java/com/affise/app/ui/fragments/likes/LikesFragment.kt new file mode 100644 index 0000000..aa32a98 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/likes/LikesFragment.kt @@ -0,0 +1,103 @@ +package com.affise.app.ui.fragments.likes + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.navigation.fragment.findNavController +import com.affise.app.R +import com.affise.app.databinding.FragmentMainLikesBinding +import com.affise.app.entity.ProductEntity +import com.affise.app.extensions.hideKeyboard +import com.affise.app.ui.activity.main.MenuHolder +import com.affise.app.ui.fragments.details.ProductDetailsFragment +import com.affise.app.ui.fragments.likes.adapters.AdapterLikes +import dagger.android.support.DaggerFragment +import javax.inject.Inject + +class LikesFragment : DaggerFragment() { + + @Inject + lateinit var viewModel: LikesContract.ViewModel + + @Inject + lateinit var menuHolder: MenuHolder + + @Inject + lateinit var adapter: AdapterLikes + + lateinit var binding: FragmentMainLikesBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = FragmentMainLikesBinding.inflate(layoutInflater).apply { + binding = this + + menuSate.setOnClickListener { + menuHolder.changeMenuSate() + } + + recycles.adapter = adapter.apply { + onClickProduct = { clickProduct(it.product) } + onClickLike = { viewModel.clickLike(it.product) } + onClickAdd = { viewModel.clickAdd(it.product) } + } + + refresh.setOnRefreshListener { + viewModel.update() + } + + notification.setOnClickListener { + + } + + searchSetting.setOnClickListener { + + } + + checkout.setOnClickListener { + + } + }.root + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.stateData.observe(viewLifecycleOwner) { + when (it) { + is LikesState.DataState -> { + adapter.submitList(it.likeProducts) + binding.refresh.isRefreshing = false + } + LikesState.ErrorState -> { + binding.refresh.isRefreshing = false + } + LikesState.LoadingState -> { + binding.refresh.isRefreshing = true + } + LikesState.UpdateState -> { + binding.refresh.isRefreshing = false + } + } + } + } + + override fun onResume() { + super.onResume() + + viewModel.update() + + requireActivity().hideKeyboard() + } + + private fun clickProduct(productEntity: ProductEntity) { + findNavController().navigate( + R.id.action_likesFragment_to_productDetailsFragment, + Bundle().apply { + putLong(ProductDetailsFragment.PRODUCT_DETAILS_EXTRA_KEY, productEntity.id) + } + ) + } +} diff --git a/app/src/main/java/com/affise/app/ui/fragments/likes/LikesState.kt b/app/src/main/java/com/affise/app/ui/fragments/likes/LikesState.kt new file mode 100644 index 0000000..af7b121 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/likes/LikesState.kt @@ -0,0 +1,12 @@ +package com.affise.app.ui.fragments.likes + +import com.affise.app.ui.fragments.likes.adapters.ProductLikeEntity + +sealed class LikesState { + object LoadingState : LikesState() + object UpdateState : LikesState() + object ErrorState : LikesState() + data class DataState( + val likeProducts: List + ) : LikesState() +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/likes/LikesViewModel.kt b/app/src/main/java/com/affise/app/ui/fragments/likes/LikesViewModel.kt new file mode 100644 index 0000000..557244f --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/likes/LikesViewModel.kt @@ -0,0 +1,99 @@ +package com.affise.app.ui.fragments.likes + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.affise.app.entity.ProductEntity +import com.affise.app.extensions.IOWithErrorHandling +import com.affise.app.usecase.ProductUseCase +import com.affise.attribution.Affise +import com.affise.attribution.events.predefined.AddToCartEvent +import com.affise.attribution.events.predefined.AddToWishlistEvent +import com.affise.attribution.events.predefined.ViewItemsEvent +import com.fasterxml.jackson.databind.ObjectMapper +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.json.JSONArray +import org.json.JSONObject +import javax.inject.Inject + +class LikesViewModel @Inject constructor( + private val useCase: ProductUseCase, + private val objectMapper: ObjectMapper +) : ViewModel(), LikesContract.ViewModel { + + override val stateData = MutableLiveData(LikesState.LoadingState) + + init { + updateData() + } + + override fun update() { + val currentState = stateData.value + if (currentState == LikesState.LoadingState || currentState == LikesState.UpdateState) { + return + } + stateData.value = LikesState.UpdateState + + updateData() + } + + private fun updateData() { + viewModelScope.launch(Dispatchers.IOWithErrorHandling { + stateData.postValue(LikesState.ErrorState) + }) { + delay(300) + + updateProducts() + } + } + + override fun clickLike(product: ProductEntity) { + viewModelScope.launch(Dispatchers.IOWithErrorHandling { + }) { + Affise.sendEvent( + AddToWishlistEvent( + JSONObject(objectMapper.writeValueAsString(product)), + System.currentTimeMillis(), + "Add to wishlist in Product details" + ) + ) + + useCase.updateLike(product.id) + updateProducts() + } + } + + override fun clickAdd(product: ProductEntity) { + viewModelScope.launch(Dispatchers.IOWithErrorHandling { + }) { + Affise.sendEvent( + AddToCartEvent( + JSONObject(objectMapper.writeValueAsString(product)), + System.currentTimeMillis(), + "Add to cart in Product details" + ) + ) + + useCase.updateCart(product.id) + updateProducts() + } + } + + private suspend fun updateProducts() { + val likeProducts = useCase.getLikeProducts() + + val itemsWishlist = likeProducts + .map { objectMapper.writeValueAsString(it) } + .map { JSONObject(it) } + + Affise.sendEvent( + ViewItemsEvent(JSONArray(itemsWishlist), "itemsWishlist") + ) + + stateData.postValue( + LikesState.DataState(likeProducts) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/likes/adapters/AdapterLikes.kt b/app/src/main/java/com/affise/app/ui/fragments/likes/adapters/AdapterLikes.kt new file mode 100644 index 0000000..66902b7 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/likes/adapters/AdapterLikes.kt @@ -0,0 +1,103 @@ +package com.affise.app.ui.fragments.likes.adapters + +import android.net.Uri +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.affise.app.R +import com.affise.app.databinding.ItemRecyclerLikeBinding +import com.affise.app.entity.ProductEntity +import com.bumptech.glide.Glide +import javax.inject.Inject + +data class ProductLikeEntity( + val product: ProductEntity, + val inLike: Boolean, + val inCart: Boolean +) + +class AdapterLikes @Inject constructor( +) : ListAdapter(ProductLikeCallback()) { + + var onClickProduct: (ProductLikeEntity) -> Unit = {} + + var onClickLike: (ProductLikeEntity) -> Unit = {} + + var onClickAdd: (ProductLikeEntity) -> Unit = {} + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ProductLikeViewHolder = LayoutInflater.from(parent.context) + .let { + ItemRecyclerLikeBinding.inflate(it, parent, false) + } + .let { + ProductLikeViewHolder(it, onClickProduct, onClickLike, onClickAdd) + } + + override fun onBindViewHolder(holder: ProductLikeViewHolder, position: Int) { + holder.bind(getItem(position)) + } +} + +class ProductLikeCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: ProductLikeEntity, + newItem: ProductLikeEntity + ): Boolean = oldItem.product.id == newItem.product.id + + override fun areContentsTheSame( + oldItem: ProductLikeEntity, + newItem: ProductLikeEntity + ): Boolean = oldItem.product.name == newItem.product.name && + oldItem.product.price == newItem.product.price && + oldItem.product.unit == newItem.product.unit && + oldItem.inCart == newItem.inCart && + oldItem.inLike == newItem.inLike +} + +class ProductLikeViewHolder( + private val binding: ItemRecyclerLikeBinding, + private val onClickProduct: (ProductLikeEntity) -> Unit, + private val onClickLike: (ProductLikeEntity) -> Unit, + private val onClickAdd: (ProductLikeEntity) -> Unit +) : RecyclerView.ViewHolder(binding.root) { + + fun bind(product: ProductLikeEntity) = with(binding) { + Glide.with(imageView) + .load(Uri.parse("file:///android_asset/${product.product.images.firstOrNull()}")) + .error(R.drawable.img_product_test) + .into(imageView) + + name.text = product.product.name + + price.text = product.product.price?.toPlainString()?.let { price -> + product.product.unit?.let { unit -> + "$unit$price" + } ?: price + } ?: "" + + like.setImageResource( + if (product.inLike) R.drawable.ic_like_full else R.drawable.ic_like + ) + + add.setImageResource( + if (product.inCart) R.drawable.ic_cart_full else R.drawable.ic_cart + ) + + main.setOnClickListener { + onClickProduct.invoke(product) + } + + like.setOnClickListener { + onClickLike.invoke(product) + } + + add.setOnClickListener { + onClickAdd.invoke(product) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/menu/MenuContract.kt b/app/src/main/java/com/affise/app/ui/fragments/menu/MenuContract.kt new file mode 100644 index 0000000..4ef300a --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/menu/MenuContract.kt @@ -0,0 +1,13 @@ +package com.affise.app.ui.fragments.menu + +import androidx.lifecycle.LiveData +import com.affise.app.ui.fragments.menu.adapters.Menu + +interface MenuContract { + interface ViewModel { + val menu: LiveData> + val currentMenu: LiveData + + fun setCurrentMenu(item: Menu.MenuItem) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/menu/MenuFragment.kt b/app/src/main/java/com/affise/app/ui/fragments/menu/MenuFragment.kt new file mode 100644 index 0000000..8158edd --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/menu/MenuFragment.kt @@ -0,0 +1,51 @@ +package com.affise.app.ui.fragments.menu + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.affise.app.databinding.FragmentMainMenuBinding +import com.affise.app.ui.activity.main.MenuHolder +import com.affise.app.ui.fragments.menu.adapters.AdapterMenu +import dagger.android.support.DaggerFragment +import javax.inject.Inject + +class MenuFragment : DaggerFragment() { + + @Inject + lateinit var viewModel: MenuContract.ViewModel + + @Inject + lateinit var menuHolder: MenuHolder + + @Inject + lateinit var adapterMenu: AdapterMenu + + lateinit var binding: FragmentMainMenuBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = FragmentMainMenuBinding.inflate(layoutInflater).apply { + binding = this + + menu.adapter = adapterMenu.apply { + onClick = viewModel::setCurrentMenu + } + }.root + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.menu.observe(viewLifecycleOwner) { + adapterMenu.submitList(it) + } + + viewModel.currentMenu.observe(viewLifecycleOwner) { + adapterMenu.current = it + + menuHolder.selectMenu(it) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/menu/MenuViewModel.kt b/app/src/main/java/com/affise/app/ui/fragments/menu/MenuViewModel.kt new file mode 100644 index 0000000..6570bce --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/menu/MenuViewModel.kt @@ -0,0 +1,40 @@ +package com.affise.app.ui.fragments.menu + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.affise.app.R +import com.affise.app.ui.fragments.menu.adapters.Menu +import javax.inject.Inject + +class MenuViewModel @Inject constructor() : ViewModel(), MenuContract.ViewModel { + + private val menuItems = listOf( + Menu.MenuItem(R.drawable.ic_menu_home, R.string.menu_home, R.navigation.main), + Menu.MenuItem(R.drawable.ic_menu_favorite, R.string.menu_wishlist, R.navigation.like), + Menu.MenuItem(R.drawable.ic_menu_shopping, R.string.menu_cart, R.navigation.cart), + Menu.MenuDivider, + Menu.MenuItem( + R.drawable.ic_menu_buttons, + R.string.menu_test_buttons, + R.navigation.test_buttons + ), + Menu.MenuItem( + R.drawable.ic_menu_auto_cathing, + R.string.menu_test_auto_cathing, + R.navigation.test_auto_cathing + ), + Menu.MenuItem(R.drawable.ic_menu_metrics, R.string.menu_test_metrics, R.navigation.metrics), + Menu.MenuDivider, + Menu.MenuItem(R.drawable.ic_munu_settings, R.string.menu_settings, R.navigation.settings), + ) + + override val menu = MutableLiveData(menuItems) + + override val currentMenu = MutableLiveData( + menuItems.mapNotNull { it as? Menu.MenuItem }.first() + ) + + override fun setCurrentMenu(item: Menu.MenuItem) { + currentMenu.postValue(item) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/menu/adapters/AdapterMenu.kt b/app/src/main/java/com/affise/app/ui/fragments/menu/adapters/AdapterMenu.kt new file mode 100644 index 0000000..0e32fbb --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/menu/adapters/AdapterMenu.kt @@ -0,0 +1,113 @@ +package com.affise.app.ui.fragments.menu.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.affise.app.databinding.ItemRecyclerMenuBinding +import com.affise.app.databinding.ItemRecyclerMenuDividerBinding +import javax.inject.Inject + +sealed class Menu { + data class MenuItem(val iconRes: Int, val nameRes: Int, val graph: Int) : Menu() + object MenuDivider : Menu() +} + +class AdapterMenu @Inject constructor( +) : ListAdapter(MenuCallback()) { + + private var items: List = emptyList() + + var onClick: (Menu.MenuItem) -> Unit = {} + + var current: Menu? = null + set(value) { + val oldValue = field + field = value + + items.indexOf(oldValue) + .takeIf { it >= 0 } + ?.let { + notifyItemChanged(it) + } + + items.indexOf(value) + .takeIf { it >= 0 } + ?.let { + notifyItemChanged(it) + } + } + + override fun submitList(list: List?) { + items = list ?: emptyList() + + super.submitList(list) + } + + override fun getItemViewType(position: Int): Int = when (items[position]) { + Menu.MenuDivider -> 0 + is Menu.MenuItem -> 1 + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): RecyclerView.ViewHolder = LayoutInflater.from(parent.context) + .let { + when (viewType) { + 0 -> { + val binding = ItemRecyclerMenuDividerBinding.inflate(it, parent, false) + MenuDividerViewHolder(binding) + } + 1 -> { + val binding = ItemRecyclerMenuBinding.inflate(it, parent, false) + MenuItemViewHolder(binding, onClick) + } + else -> throw NotImplementedError() + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + if (holder is MenuItemViewHolder) { + + val item = getItem(position) as Menu.MenuItem + holder.bind(item, current == item) + } + } +} + +class MenuCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: Menu, + newItem: Menu + ): Boolean = false + + override fun areContentsTheSame( + oldItem: Menu, + newItem: Menu + ): Boolean = false +} + +class MenuDividerViewHolder( + binding: ItemRecyclerMenuDividerBinding +) : RecyclerView.ViewHolder(binding.root) + +class MenuItemViewHolder( + private val binding: ItemRecyclerMenuBinding, + private val onClick: (Menu.MenuItem) -> Unit +) : RecyclerView.ViewHolder(binding.root) { + + fun bind(item: Menu.MenuItem, isCurrent: Boolean) = with(binding) { + name.setText(item.nameRes) + + itemView.setOnClickListener { + onClick.invoke(item) + } + + icon.setImageResource(item.iconRes) + + selected.isVisible = isCurrent + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/metrics/MetricsContract.kt b/app/src/main/java/com/affise/app/ui/fragments/metrics/MetricsContract.kt new file mode 100644 index 0000000..f7bef12 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/metrics/MetricsContract.kt @@ -0,0 +1,11 @@ +package com.affise.app.ui.fragments.metrics + +import androidx.lifecycle.LiveData + +class MetricsContract { + interface ViewModel { + val enabled: LiveData + + fun setMetricsEnabled(metricsEnabled: Boolean) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/metrics/MetricsFragment.kt b/app/src/main/java/com/affise/app/ui/fragments/metrics/MetricsFragment.kt new file mode 100644 index 0000000..545e4dd --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/metrics/MetricsFragment.kt @@ -0,0 +1,40 @@ +package com.affise.app.ui.fragments.metrics + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.affise.app.databinding.FragmentMainMetricsBinding +import com.affise.app.ui.activity.main.MenuHolder +import dagger.android.support.DaggerFragment +import javax.inject.Inject + +class MetricsFragment : DaggerFragment() { + + @Inject + lateinit var viewModel: MetricsContract.ViewModel + + @Inject + lateinit var menuHolder: MenuHolder + + lateinit var binding: FragmentMainMetricsBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = FragmentMainMetricsBinding.inflate(layoutInflater).apply { + binding = this + + menuSate.setOnClickListener { + menuHolder.changeMenuSate() + } + viewModel.enabled.observe(viewLifecycleOwner) { + metrics.isChecked = it + } + + metrics.setOnCheckedChangeListener { _, isChecked -> + viewModel.setMetricsEnabled(isChecked) + } + }.root +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/metrics/MetricsViewModel.kt b/app/src/main/java/com/affise/app/ui/fragments/metrics/MetricsViewModel.kt new file mode 100644 index 0000000..6f2602f --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/metrics/MetricsViewModel.kt @@ -0,0 +1,29 @@ +package com.affise.app.ui.fragments.metrics + +import android.content.SharedPreferences +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.affise.app.application.App +import com.affise.attribution.Affise +import javax.inject.Inject + +class MetricsViewModel @Inject constructor( + private val preferences: SharedPreferences +) : ViewModel(), MetricsContract.ViewModel { + + override val enabled = MutableLiveData(preferences.getBoolean(App.ENABLED_METRICS_KEY, false)) + + override fun setMetricsEnabled(metricsEnabled: Boolean) { + enabled.value?.let { + if (it != metricsEnabled) { + preferences.edit() + .putBoolean(App.ENABLED_METRICS_KEY, metricsEnabled) + .apply() + + Affise.setEnabledMetrics(metricsEnabled) + + enabled.value = metricsEnabled + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/settings/SettingsContract.kt b/app/src/main/java/com/affise/app/ui/fragments/settings/SettingsContract.kt new file mode 100644 index 0000000..ca455ac --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/settings/SettingsContract.kt @@ -0,0 +1,19 @@ +package com.affise.app.ui.fragments.settings + +import androidx.lifecycle.LiveData + +interface SettingsContract { + interface ViewModel { + val offlineModeState: LiveData + val backgroundTrackingModeState: LiveData + val trackingModeState: LiveData + val pushToken: LiveData + fun setSecretId(secretId: String) + fun onSetOfflineModeCheckboxClicked(isChecked: Boolean) + fun onSetBackgroundTrackingModeCheckboxClicked(isChecked: Boolean) + fun onSetTrackingModeCheckboxClicked(isChecked: Boolean) + fun onGDPRForgetButtonClicked() + fun onCrashApplicationAffiseButtonClicked() + fun onCrashApplicationButtonClicked() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/settings/SettingsFragment.kt b/app/src/main/java/com/affise/app/ui/fragments/settings/SettingsFragment.kt new file mode 100644 index 0000000..4cf8c25 --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/settings/SettingsFragment.kt @@ -0,0 +1,96 @@ +package com.affise.app.ui.fragments.settings + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import com.affise.app.R +import com.affise.app.databinding.FragmentMainSettingsBinding +import com.affise.app.extensions.hideKeyboard +import com.affise.app.ui.activity.main.MenuHolder +import dagger.android.support.DaggerFragment +import javax.inject.Inject + +class SettingsFragment : DaggerFragment() { + + @Inject + lateinit var viewModel: SettingsContract.ViewModel + + @Inject + lateinit var menuHolder: MenuHolder + + lateinit var binding: FragmentMainSettingsBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = FragmentMainSettingsBinding.inflate(layoutInflater).apply { + binding = this + }.root + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.menuSate.setOnClickListener { + menuHolder.changeMenuSate() + } + + binding.save.setOnClickListener { + viewModel.setSecretId(binding.secretIdValue.text.toString()) + + binding.secretIdValue.setText("") + + AlertDialog.Builder(requireContext()) + .setMessage(R.string.change_secrat_id_complate) + .setPositiveButton(R.string.ok, null) + .show() + } + + binding.setOfflineModeSwitch.apply { + viewModel.offlineModeState.observe(viewLifecycleOwner, ::setChecked) + setOnClickListener { + viewModel.onSetOfflineModeCheckboxClicked(isChecked) + } + } + + binding.setBackgroundTrackingModeSwitch.apply { + viewModel.backgroundTrackingModeState.observe(viewLifecycleOwner, ::setChecked) + setOnClickListener { + viewModel.onSetBackgroundTrackingModeCheckboxClicked(isChecked) + } + } + + binding.setTrackingModeSwitch.apply { + viewModel.trackingModeState.observe(viewLifecycleOwner, ::setChecked) + setOnClickListener { + viewModel.onSetTrackingModeCheckboxClicked(isChecked) + } + } + + binding.gdprForgetButton.setOnClickListener { + viewModel.onGDPRForgetButtonClicked() + } + + binding.testApplicationCrashAffiseButton.setOnClickListener { + viewModel.onCrashApplicationAffiseButtonClicked() + } + + binding.testApplicationCrashButton.setOnClickListener { + viewModel.onCrashApplicationButtonClicked() + } + + viewModel.pushToken.observe( + viewLifecycleOwner, + binding.pushTokenInputLayoutInputEditText::setText + ) + + } + + override fun onResume() { + super.onResume() + + requireActivity().hideKeyboard() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/ui/fragments/settings/SettingsViewModel.kt b/app/src/main/java/com/affise/app/ui/fragments/settings/SettingsViewModel.kt new file mode 100644 index 0000000..7fd6c5f --- /dev/null +++ b/app/src/main/java/com/affise/app/ui/fragments/settings/SettingsViewModel.kt @@ -0,0 +1,83 @@ +package com.affise.app.ui.fragments.settings + +import android.content.SharedPreferences +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.affise.app.application.App +import com.affise.attribution.Affise +import com.google.android.gms.tasks.Task +import com.google.firebase.messaging.FirebaseMessaging +import java.lang.IllegalStateException +import javax.inject.Inject + +class SettingsViewModel @Inject constructor( + private val preferences: SharedPreferences +) : ViewModel(), SettingsContract.ViewModel { + private val _offlineModeState = MutableLiveData() + private val _backgroundTrackingModeState = MutableLiveData() + private val _trackingModeState = MutableLiveData() + private val _pushToken = MutableLiveData() + + override val offlineModeState: LiveData + get() = _offlineModeState + + override val backgroundTrackingModeState: LiveData + get() = _backgroundTrackingModeState + + override val trackingModeState: LiveData + get() = _trackingModeState + + override val pushToken: LiveData + get() = _pushToken + + init { + _offlineModeState.value = Affise.isOfflineModeEnabled() + _backgroundTrackingModeState.value = Affise.isBackgroundTrackingEnabled() + _trackingModeState.value = Affise.isTrackingEnabled() + + FirebaseMessaging.getInstance().token.apply { + addOnSuccessListener { + _pushToken.postValue(it?.toString()) + } + addOnFailureListener { + _pushToken.postValue("Failed to retrieve token: " + it.message) + } + } + } + + override fun setSecretId(secretId: String) { + preferences.edit() + .putString(App.SECRET_ID_KEY, secretId) + .apply() + + Affise.setSecretId(secretId) + } + + override fun onSetOfflineModeCheckboxClicked(isChecked: Boolean) { + Affise.setOfflineModeEnabled(isChecked) + _offlineModeState.value = Affise.isOfflineModeEnabled() + } + + override fun onSetBackgroundTrackingModeCheckboxClicked(isChecked: Boolean) { + Affise.setBackgroundTrackingEnabled(isChecked) + _backgroundTrackingModeState.value = Affise.isBackgroundTrackingEnabled() + } + + override fun onSetTrackingModeCheckboxClicked(isChecked: Boolean) { + Affise.setTrackingEnabled(isChecked) + _trackingModeState.value = Affise.isTrackingEnabled() + } + + override fun onGDPRForgetButtonClicked() { + Affise.forget("Demo App forget event") + } + + override fun onCrashApplicationAffiseButtonClicked() { + Affise.crashApplication() + } + + override fun onCrashApplicationButtonClicked() { + throw IllegalStateException("Crash caused by demo app") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/usecase/ProductUseCase.kt b/app/src/main/java/com/affise/app/usecase/ProductUseCase.kt new file mode 100644 index 0000000..e05d959 --- /dev/null +++ b/app/src/main/java/com/affise/app/usecase/ProductUseCase.kt @@ -0,0 +1,25 @@ +package com.affise.app.usecase + +import com.affise.app.entity.ProductEntity +import com.affise.app.ui.fragments.cart.adapters.ProductCartEntity +import com.affise.app.ui.fragments.home.adapters.ProductNewEntity +import com.affise.app.ui.fragments.home.adapters.ProductPopularEntity +import com.affise.app.ui.fragments.likes.adapters.ProductLikeEntity + +interface ProductUseCase { + suspend fun getPopularProducts(): List + suspend fun getNewProducts(): List + suspend fun getLikeProducts(): List + + suspend fun getProductDetails(id: Long): ProductEntity? + + suspend fun inLike(productId: Long): Boolean + suspend fun inCart(productId: Long): Boolean + + suspend fun updateLike(productId: Long?): Boolean + suspend fun updateCart(productId: Long?): Boolean + + suspend fun getCart(): List + suspend fun removeOnCart(productId: Long): List + suspend fun addToCart(productId: Long): List +} \ No newline at end of file diff --git a/app/src/main/java/com/affise/app/usecase/ProductUseCaseImpl.kt b/app/src/main/java/com/affise/app/usecase/ProductUseCaseImpl.kt new file mode 100644 index 0000000..366d4a4 --- /dev/null +++ b/app/src/main/java/com/affise/app/usecase/ProductUseCaseImpl.kt @@ -0,0 +1,43 @@ +package com.affise.app.usecase + +import com.affise.app.entity.ProductEntity +import com.affise.app.repositories.ProductRepository +import com.affise.app.ui.fragments.cart.adapters.ProductCartEntity +import com.affise.app.ui.fragments.home.adapters.ProductNewEntity +import com.affise.app.ui.fragments.home.adapters.ProductPopularEntity +import com.affise.app.ui.fragments.likes.adapters.ProductLikeEntity +import javax.inject.Inject + +class ProductUseCaseImpl @Inject constructor( + private val repository: ProductRepository +) : ProductUseCase { + + override suspend fun getPopularProducts( + ): List = repository.getPopularProducts() + + override suspend fun getNewProducts(): List = repository.getNewProducts() + + override suspend fun getProductDetails( + id: Long + ): ProductEntity? = repository.getProductDetails(id) + + override suspend fun getLikeProducts(): List = repository.getLikeProducts() + + override suspend fun inLike(productId: Long): Boolean = repository.inLike(productId) + + override suspend fun inCart(productId: Long): Boolean = repository.inCart(productId) + + override suspend fun updateLike(productId: Long?): Boolean = repository.updateLike(productId) + + override suspend fun updateCart(productId: Long?): Boolean = repository.updateCart(productId) + + override suspend fun getCart(): List = repository.getCart() + + override suspend fun removeOnCart( + productId: Long + ): List = repository.removeOnCart(productId) + + override suspend fun addToCart( + productId: Long + ): List = repository.addToCart(productId) +} \ No newline at end of file diff --git a/app/src/main/res/drawable/adapter_size_decoration.xml b/app/src/main/res/drawable/adapter_size_decoration.xml new file mode 100644 index 0000000..eb659f4 --- /dev/null +++ b/app/src/main/res/drawable/adapter_size_decoration.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button.xml b/app/src/main/res/drawable/button.xml new file mode 100644 index 0000000..3641b92 --- /dev/null +++ b/app/src/main/res/drawable/button.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml new file mode 100644 index 0000000..6ef19e6 --- /dev/null +++ b/app/src/main/res/drawable/ic_add.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_add_2.xml b/app/src/main/res/drawable/ic_add_2.xml new file mode 100644 index 0000000..b1b71f3 --- /dev/null +++ b/app/src/main/res/drawable/ic_add_2.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_back.xml b/app/src/main/res/drawable/ic_back.xml new file mode 100644 index 0000000..d1cfd10 --- /dev/null +++ b/app/src/main/res/drawable/ic_back.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_shopping_cart_24.xml b/app/src/main/res/drawable/ic_baseline_shopping_cart_24.xml new file mode 100644 index 0000000..cf0dcc8 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_shopping_cart_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_bg_item_details.xml b/app/src/main/res/drawable/ic_bg_item_details.xml new file mode 100644 index 0000000..a3f9652 --- /dev/null +++ b/app/src/main/res/drawable/ic_bg_item_details.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_cart.xml b/app/src/main/res/drawable/ic_cart.xml new file mode 100644 index 0000000..d7407de --- /dev/null +++ b/app/src/main/res/drawable/ic_cart.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_cart_full.xml b/app/src/main/res/drawable/ic_cart_full.xml new file mode 100644 index 0000000..74e5f8d --- /dev/null +++ b/app/src/main/res/drawable/ic_cart_full.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_count_minus.xml b/app/src/main/res/drawable/ic_count_minus.xml new file mode 100644 index 0000000..9e51c7f --- /dev/null +++ b/app/src/main/res/drawable/ic_count_minus.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_count_plus.xml b/app/src/main/res/drawable/ic_count_plus.xml new file mode 100644 index 0000000..95dc633 --- /dev/null +++ b/app/src/main/res/drawable/ic_count_plus.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_ellipsis.xml b/app/src/main/res/drawable/ic_ellipsis.xml new file mode 100644 index 0000000..2acda31 --- /dev/null +++ b/app/src/main/res/drawable/ic_ellipsis.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_home_logo.xml b/app/src/main/res/drawable/ic_home_logo.xml new file mode 100644 index 0000000..28abd70 --- /dev/null +++ b/app/src/main/res/drawable/ic_home_logo.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_home_menu.xml b/app/src/main/res/drawable/ic_home_menu.xml new file mode 100644 index 0000000..a937c49 --- /dev/null +++ b/app/src/main/res/drawable/ic_home_menu.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_home_notification.xml b/app/src/main/res/drawable/ic_home_notification.xml new file mode 100644 index 0000000..75ee4f8 --- /dev/null +++ b/app/src/main/res/drawable/ic_home_notification.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_like.xml b/app/src/main/res/drawable/ic_like.xml new file mode 100644 index 0000000..0b7c1f8 --- /dev/null +++ b/app/src/main/res/drawable/ic_like.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_like_full.xml b/app/src/main/res/drawable/ic_like_full.xml new file mode 100644 index 0000000..ffb3233 --- /dev/null +++ b/app/src/main/res/drawable/ic_like_full.xml @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_menu_auto_cathing.xml b/app/src/main/res/drawable/ic_menu_auto_cathing.xml new file mode 100644 index 0000000..eb91e5d --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_auto_cathing.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_menu_buttons.xml b/app/src/main/res/drawable/ic_menu_buttons.xml new file mode 100644 index 0000000..cf9d994 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_buttons.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_menu_favorite.xml b/app/src/main/res/drawable/ic_menu_favorite.xml new file mode 100644 index 0000000..5cea312 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_favorite.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_menu_home.xml b/app/src/main/res/drawable/ic_menu_home.xml new file mode 100644 index 0000000..fa4da8e --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_home.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_menu_metrics.xml b/app/src/main/res/drawable/ic_menu_metrics.xml new file mode 100644 index 0000000..e0e9834 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_metrics.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_menu_shopping.xml b/app/src/main/res/drawable/ic_menu_shopping.xml new file mode 100644 index 0000000..ce1bc3c --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_shopping.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_munu_settings.xml b/app/src/main/res/drawable/ic_munu_settings.xml new file mode 100644 index 0000000..c607376 --- /dev/null +++ b/app/src/main/res/drawable/ic_munu_settings.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_popular_background.xml b/app/src/main/res/drawable/ic_popular_background.xml new file mode 100644 index 0000000..e94dfcc --- /dev/null +++ b/app/src/main/res/drawable/ic_popular_background.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_rating_star.xml b/app/src/main/res/drawable/ic_rating_star.xml new file mode 100644 index 0000000..9938f8b --- /dev/null +++ b/app/src/main/res/drawable/ic_rating_star.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 0000000..4a9fa9b --- /dev/null +++ b/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_size_current.xml b/app/src/main/res/drawable/ic_size_current.xml new file mode 100644 index 0000000..a3c3d79 --- /dev/null +++ b/app/src/main/res/drawable/ic_size_current.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/img_product_test.png b/app/src/main/res/drawable/img_product_test.png new file mode 100644 index 0000000..bcad4d6 Binary files /dev/null and b/app/src/main/res/drawable/img_product_test.png differ diff --git a/app/src/main/res/drawable/img_product_test_2.png b/app/src/main/res/drawable/img_product_test_2.png new file mode 100644 index 0000000..30b4250 Binary files /dev/null and b/app/src/main/res/drawable/img_product_test_2.png differ diff --git a/app/src/main/res/drawable/menu_bg.xml b/app/src/main/res/drawable/menu_bg.xml new file mode 100644 index 0000000..fcdb2a5 --- /dev/null +++ b/app/src/main/res/drawable/menu_bg.xml @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/menu_divider.xml b/app/src/main/res/drawable/menu_divider.xml new file mode 100644 index 0000000..7d9cea4 --- /dev/null +++ b/app/src/main/res/drawable/menu_divider.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/menu_ripple_rectangle.xml b/app/src/main/res/drawable/menu_ripple_rectangle.xml new file mode 100644 index 0000000..3198830 --- /dev/null +++ b/app/src/main/res/drawable/menu_ripple_rectangle.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ripple.xml b/app/src/main/res/drawable/ripple.xml new file mode 100644 index 0000000..6bc9fe0 --- /dev/null +++ b/app/src/main/res/drawable/ripple.xml @@ -0,0 +1,3 @@ + + diff --git a/app/src/main/res/drawable/ripple_rectangle.xml b/app/src/main/res/drawable/ripple_rectangle.xml new file mode 100644 index 0000000..57d52b6 --- /dev/null +++ b/app/src/main/res/drawable/ripple_rectangle.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ripple_top_left_corner.xml b/app/src/main/res/drawable/ripple_top_left_corner.xml new file mode 100644 index 0000000..7e79822 --- /dev/null +++ b/app/src/main/res/drawable/ripple_top_left_corner.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selected_menu.xml b/app/src/main/res/drawable/selected_menu.xml new file mode 100644 index 0000000..402216d --- /dev/null +++ b/app/src/main/res/drawable/selected_menu.xml @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..b10d97d --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_auto_catching.xml b/app/src/main/res/layout/fragment_auto_catching.xml new file mode 100644 index 0000000..712ba88 --- /dev/null +++ b/app/src/main/res/layout/fragment_auto_catching.xml @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main_buttons.xml b/app/src/main/res/layout/fragment_main_buttons.xml new file mode 100644 index 0000000..f8fc79b --- /dev/null +++ b/app/src/main/res/layout/fragment_main_buttons.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main_cart.xml b/app/src/main/res/layout/fragment_main_cart.xml new file mode 100644 index 0000000..9ffcd27 --- /dev/null +++ b/app/src/main/res/layout/fragment_main_cart.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main_home.xml b/app/src/main/res/layout/fragment_main_home.xml new file mode 100644 index 0000000..7673406 --- /dev/null +++ b/app/src/main/res/layout/fragment_main_home.xml @@ -0,0 +1,243 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main_likes.xml b/app/src/main/res/layout/fragment_main_likes.xml new file mode 100644 index 0000000..705d70a --- /dev/null +++ b/app/src/main/res/layout/fragment_main_likes.xml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main_menu.xml b/app/src/main/res/layout/fragment_main_menu.xml new file mode 100644 index 0000000..4569cfb --- /dev/null +++ b/app/src/main/res/layout/fragment_main_menu.xml @@ -0,0 +1,37 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main_metrics.xml b/app/src/main/res/layout/fragment_main_metrics.xml new file mode 100644 index 0000000..06d614f --- /dev/null +++ b/app/src/main/res/layout/fragment_main_metrics.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main_product_details.xml b/app/src/main/res/layout/fragment_main_product_details.xml new file mode 100644 index 0000000..87caf6c --- /dev/null +++ b/app/src/main/res/layout/fragment_main_product_details.xml @@ -0,0 +1,446 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main_settings.xml b/app/src/main/res/layout/fragment_main_settings.xml new file mode 100644 index 0000000..db7d314 --- /dev/null +++ b/app/src/main/res/layout/fragment_main_settings.xml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_recycler_cart.xml b/app/src/main/res/layout/item_recycler_cart.xml new file mode 100644 index 0000000..5eba127 --- /dev/null +++ b/app/src/main/res/layout/item_recycler_cart.xml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_recycler_color.xml b/app/src/main/res/layout/item_recycler_color.xml new file mode 100644 index 0000000..d1de7fe --- /dev/null +++ b/app/src/main/res/layout/item_recycler_color.xml @@ -0,0 +1,25 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_recycler_image.xml b/app/src/main/res/layout/item_recycler_image.xml new file mode 100644 index 0000000..293cf98 --- /dev/null +++ b/app/src/main/res/layout/item_recycler_image.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_recycler_like.xml b/app/src/main/res/layout/item_recycler_like.xml new file mode 100644 index 0000000..2d45b0b --- /dev/null +++ b/app/src/main/res/layout/item_recycler_like.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_recycler_menu.xml b/app/src/main/res/layout/item_recycler_menu.xml new file mode 100644 index 0000000..b643807 --- /dev/null +++ b/app/src/main/res/layout/item_recycler_menu.xml @@ -0,0 +1,43 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_recycler_menu_divider.xml b/app/src/main/res/layout/item_recycler_menu_divider.xml new file mode 100644 index 0000000..66b347c --- /dev/null +++ b/app/src/main/res/layout/item_recycler_menu_divider.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_recycler_new.xml b/app/src/main/res/layout/item_recycler_new.xml new file mode 100644 index 0000000..64d8c52 --- /dev/null +++ b/app/src/main/res/layout/item_recycler_new.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_recycler_page.xml b/app/src/main/res/layout/item_recycler_page.xml new file mode 100644 index 0000000..ffa9e54 --- /dev/null +++ b/app/src/main/res/layout/item_recycler_page.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_recycler_popular.xml b/app/src/main/res/layout/item_recycler_popular.xml new file mode 100644 index 0000000..98cbd33 --- /dev/null +++ b/app/src/main/res/layout/item_recycler_popular.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_recycler_size.xml b/app/src/main/res/layout/item_recycler_size.xml new file mode 100644 index 0000000..ec53f0e --- /dev/null +++ b/app/src/main/res/layout/item_recycler_size.xml @@ -0,0 +1,39 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_event.xml b/app/src/main/res/layout/list_item_event.xml new file mode 100644 index 0000000..c601dbd --- /dev/null +++ b/app/src/main/res/layout/list_item_event.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..61b10c2 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..1c98874 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..5f1aa6b Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..3d88bce Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..6e36781 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..142db91 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..007cace Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..b655c17 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..6029d88 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..a7832c2 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..112ef04 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..a87ebff Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..22e4b66 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..8a2f6e9 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..bd3ae74 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/navigation/cart.xml b/app/src/main/res/navigation/cart.xml new file mode 100644 index 0000000..bf608fc --- /dev/null +++ b/app/src/main/res/navigation/cart.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/general.xml b/app/src/main/res/navigation/general.xml new file mode 100644 index 0000000..e95480c --- /dev/null +++ b/app/src/main/res/navigation/general.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/like.xml b/app/src/main/res/navigation/like.xml new file mode 100644 index 0000000..dc82054 --- /dev/null +++ b/app/src/main/res/navigation/like.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/main.xml b/app/src/main/res/navigation/main.xml new file mode 100644 index 0000000..2fe9bb2 --- /dev/null +++ b/app/src/main/res/navigation/main.xml @@ -0,0 +1,23 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/menu.xml b/app/src/main/res/navigation/menu.xml new file mode 100644 index 0000000..2bb56b4 --- /dev/null +++ b/app/src/main/res/navigation/menu.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/metrics.xml b/app/src/main/res/navigation/metrics.xml new file mode 100644 index 0000000..c0bb432 --- /dev/null +++ b/app/src/main/res/navigation/metrics.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/settings.xml b/app/src/main/res/navigation/settings.xml new file mode 100644 index 0000000..1fb400d --- /dev/null +++ b/app/src/main/res/navigation/settings.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/test_auto_cathing.xml b/app/src/main/res/navigation/test_auto_cathing.xml new file mode 100644 index 0000000..8bac082 --- /dev/null +++ b/app/src/main/res/navigation/test_auto_cathing.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/test_buttons.xml b/app/src/main/res/navigation/test_buttons.xml new file mode 100644 index 0000000..e8a6c4b --- /dev/null +++ b/app/src/main/res/navigation/test_buttons.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-v29/styles.xml b/app/src/main/res/values-v29/styles.xml new file mode 100644 index 0000000..d0114ea --- /dev/null +++ b/app/src/main/res/values-v29/styles.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..781b952 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,27 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + + #646464 + #C4C4C4 + #E5E5E5 + #ECECEC + #F8F8F8 + #F2F2F2 + #F5F5F5 + + #D3E8FF + #2D2942 + + #F4F0ED + #17181A + #56ADBD + #AC972C + #C1DDFA + \ No newline at end of file diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..c5d5899 --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..1c7b737 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,56 @@ + + Affise SDK showcase + Send events + Web events + Affise events demo + Back + Affise web events demo + Retry + Error + Search here… + Popular + Ratting %s + Reviews + Size + Colors + EURO + Asia + US + Buy Now + Home + Wishlist + Cart + Test buttons + My Wishlist + Checkout + My Cart + Total + Settings + Secret id + Settings + Save + OK + Change secret id successful + Affise demo + Test auto catching + Select types + Types + Button + Text + ImageButton + Image + Group + Item + 50$ + Metrics + Metrics enabled + Metrics settings + Set offline mode + Set background tracking mode + Set tracking mode + Send GDPR Forget event + Crash application with affise library + Crash application with demo app + Current push token + unknown + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..e8b0b7b --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..ce61ed3 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,19 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..ba07e7d --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/app/src/test/java/com/affise/attribution/ExampleUnitTest.kt b/app/src/test/java/com/affise/attribution/ExampleUnitTest.kt new file mode 100644 index 0000000..3e0a690 --- /dev/null +++ b/app/src/test/java/com/affise/attribution/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.affise.attribution + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/attribution/.gitignore b/attribution/.gitignore new file mode 100644 index 0000000..7eeb97a --- /dev/null +++ b/attribution/.gitignore @@ -0,0 +1,2 @@ +/build +!src/main/java/com/affise/attribution/build/ \ No newline at end of file diff --git a/attribution/build.gradle b/attribution/build.gradle new file mode 100644 index 0000000..bb2897a --- /dev/null +++ b/attribution/build.gradle @@ -0,0 +1,82 @@ +import java.text.SimpleDateFormat + +plugins { + id 'com.android.library' + id 'kotlin-android' +} + +android { + namespace 'com.affise.attribution' + compileSdk 33 + + defaultConfig { + minSdk 21 + targetSdk 33 + versionCode 4 + versionName "$affiseVersion build (${buildTime()})" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + all { + buildConfigField("long", "VERSION_CODE", "${defaultConfig.versionCode}") + buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}\"") + } + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + debug { + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + testOptions { + unitTests.includeAndroidResources = true + } + packagingOptions { + exclude 'META-INF/DEPENDENCIES' + exclude 'META-INF/NOTICE' + exclude 'META-INF/LICENSE' + exclude 'META-INF/LICENSE.txt' + exclude 'META-INF/license.txt' + exclude 'META-INF/NOTICE.txt' + exclude 'META-INF/notice.txt' + } +} + +dependencies { + //Kotlin + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + + //Installreferrer + implementation "com.android.installreferrer:installreferrer:2.2" + + //Tests + androidTestImplementation "androidx.test.espresso:espresso-core:$testEspressoCore" + androidTestImplementation "androidx.test.ext:junit:$testAndroidxJunit" + testImplementation "com.google.truth:truth:$testTruth" + testImplementation "io.mockk:mockk:$testMockk" + testImplementation 'androidx.test:core:1.5.0' + testImplementation "junit:junit:$testJunit" + testImplementation 'org.json:json:20210307' +} + +tasks.withType(Test).configureEach { + maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 +} + +static def buildTime() { + def df = new SimpleDateFormat("dd-MM-yyyy HH:mm") + df.setTimeZone(TimeZone.getTimeZone("UTC")) + return df.format(new Date()) +} diff --git a/attribution/consumer-rules.pro b/attribution/consumer-rules.pro new file mode 100644 index 0000000..e19ecef --- /dev/null +++ b/attribution/consumer-rules.pro @@ -0,0 +1 @@ +-keep class com.affise.attribution.** { *; } \ No newline at end of file diff --git a/attribution/proguard-rules.pro b/attribution/proguard-rules.pro new file mode 100644 index 0000000..d1a7f23 --- /dev/null +++ b/attribution/proguard-rules.pro @@ -0,0 +1,23 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +-keep class com.affise.attribution.** { *; } \ No newline at end of file diff --git a/attribution/src/main/AndroidManifest.xml b/attribution/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f86e2f0 --- /dev/null +++ b/attribution/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/attribution/src/main/assets/affise.js b/attribution/src/main/assets/affise.js new file mode 100644 index 0000000..42fecb9 --- /dev/null +++ b/attribution/src/main/assets/affise.js @@ -0,0 +1,895 @@ +var Affise = class { + static sendEvent(event) { + AffiseBridge.sendEvent(JSON.stringify(event)); + } +}; + +class PredefinedParameters{ + constructor(name, value) { + this.name = name; + this.value = value; + } +} + +class Event { + constructor(name) { + this.affise_event_id = this._generateUUID(); + this.affise_event_name = name; + this.affise_event_category = 'web'; + this.affise_event_timestamp = Date.now(); + } + + addPredefinedParameter(key, value) { + if (typeof this.affise_parameters === 'undefined') { + this.affise_parameters = {}; + } + this.affise_parameters[key] = value; + } + + _generateUUID() { + var d = new Date().getTime(); + var d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now()*1000)) || 0; + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random() * 16; + if(d > 0){ + r = (d + r)%16 | 0; + d = Math.floor(d/16); + } else { + r = (d2 + r)%16 | 0; + d2 = Math.floor(d2/16); + } + return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); + }); + } +} + +class SubscriptionEvent extends Event { + constructor(data, userData, subscriptionKey, subscriptionSubKey) { + super(subscriptionKey); + + var event_data = {affise_event_type:subscriptionSubKey}; + + for(var key in data){ + event_data[key] = data[key]; + } + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = event_data; + } +} + +class AchieveLevelEvent extends Event { + constructor(level, timeStampMillis, userData) { + super('AchieveLevel'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_achieve_level: level, + affise_event_achieve_level_timestamp: timeStampMillis + }; + } +} + +class AddPaymentInfoEvent extends Event { + constructor(paymentInfo, timeStampMillis, userData) { + super('AddPaymentInfo'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_add_payment_info: paymentInfo, + affise_event_add_payment_info_timestamp: timeStampMillis + }; + } +} + +class AddToCartEvent extends Event { + constructor(addToCartObject, timeStampMillis, userData) { + super('AddToCart'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_add_to_cart: addToCartObject, + affise_event_add_to_cart_timestamp: timeStampMillis + }; + } +} + +class AddToWishlistEvent extends Event { + constructor(wishList, timeStampMillis, userData) { + super('AddToWishlist'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_add_to_wishlist: wishList, + affise_event_add_to_wishlist_timestamp: timeStampMillis + }; + } +} + +class ClickAdvEvent extends Event { + constructor(advertisement, timeStampMillis, userData) { + super('ClickAdv'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_click_adv: advertisement, + affise_event_click_adv_timestamp: timeStampMillis + }; + } +} + +class CompleteRegistrationEvent extends Event { + constructor(registration, timeStampMillis, userData) { + super('CompleteRegistration'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_complete_registration: registration, + affise_event_complete_registration_timestamp: timeStampMillis + }; + } +} + +class CompleteStreamEvent extends Event { + constructor(stream, timeStampMillis, userData) { + super('CompleteStream'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_complete_stream: stream, + affise_event_complete_stream_timestamp: timeStampMillis + }; + } +} + +class CompleteTrialEvent extends Event { + constructor(trial, timeStampMillis, userData) { + super('CompleteTrial'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_complete_trial: trial, + affise_event_complete_trial_timestamp: timeStampMillis + }; + } +} + +class CompleteTutorialEvent extends Event { + constructor(tutorial, timeStampMillis, userData) { + super('CompleteTutorial'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_complete_tutorial: tutorial, + affise_event_complete_tutorial_timestamp: timeStampMillis + }; + } +} + +class ContentItemsViewEvent extends Event { + constructor(objects, userData) { + super('ContentItemsView'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_content_items_view: objects + }; + } +} + +class CustomId01Event extends Event { + constructor(custom, timeStampMillis, userData) { + super('CustomId01'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_custom_id_01: custom, + affise_event_custom_id_01_timestamp: timeStampMillis + }; + } +} + +class CustomId02Event extends Event { + constructor(custom, timeStampMillis, userData) { + super('CustomId02'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_custom_id_02: custom, + affise_event_custom_id_02_timestamp: timeStampMillis + }; + } +} + +class CustomId03Event extends Event { + constructor(custom, timeStampMillis, userData) { + super('CustomId03'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_custom_id_03: custom, + affise_event_custom_id_03_timestamp: timeStampMillis + }; + } +} + +class CustomId04Event extends Event { + constructor(custom, timeStampMillis, userData) { + super('CustomId04'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_custom_id_04: custom, + affise_event_custom_id_04_timestamp: timeStampMillis + }; + } +} + +class CustomId05Event extends Event { + constructor(custom, timeStampMillis, userData) { + super('CustomId05'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_custom_id_05: custom, + affise_event_custom_id_05_timestamp: timeStampMillis + }; + } +} + +class CustomId06Event extends Event { + constructor(custom, timeStampMillis, userData) { + super('CustomId06'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_custom_id_06: custom, + affise_event_custom_id_06_timestamp: timeStampMillis + }; + } +} + +class CustomId07Event extends Event { + constructor(custom, timeStampMillis, userData) { + super('CustomId07'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_custom_id_07: custom, + affise_event_custom_id_07_timestamp: timeStampMillis + }; + } +} + +class CustomId08Event extends Event { + constructor(custom, timeStampMillis, userData) { + super('CustomId08'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_custom_id_08: custom, + affise_event_custom_id_08_timestamp: timeStampMillis + }; + } +} + +class CustomId09Event extends Event { + constructor(custom, timeStampMillis, userData) { + super('CustomId09'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_custom_id_09: custom, + affise_event_custom_id_09_timestamp: timeStampMillis + }; + } +} + +class CustomId10Event extends Event { + constructor(custom, timeStampMillis, userData) { + super('CustomId10'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_custom_id_10: custom, + affise_event_custom_id_10_timestamp: timeStampMillis + }; + } +} + +class DeepLinkedEvent extends Event { + constructor(isLinked, userData) { + super('DeepLinked'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_deep_linked: isLinked + }; + } +} + +class InitiatePurchaseEvent extends Event { + constructor(purchaseData, timeStampMillis, userData) { + super('InitiatePurchase'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_initiate_purchase: purchaseData, + affise_event_initiate_purchase_timestamp: timeStampMillis + }; + } +} + +class InitiateStreamEvent extends Event { + constructor(stream, timeStampMillis, userData) { + super('InitiateStream'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_initiate_stream: stream, + affise_event_initiate_stream_timestamp: timeStampMillis + }; + } +} + +class InviteEvent extends Event { + constructor(invite, timeStampMillis, userData) { + super('Invite'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_invite: invite, + affise_event_invite_timestamp: timeStampMillis + }; + } +} + +class LastAttributedTouchEvent extends Event { + constructor(touchType, timeStampMillis, touchData, userData) { + super('LastAttributedTouch'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_last_attributed_touch_type: touchType, + affise_event_last_attributed_touch_timestamp: timeStampMillis, + affise_event_last_attributed_touch_data: touchData + }; + } +} + +class ListViewEvent extends Event { + constructor(list, userData) { + super('ListView'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_list_view: list + }; + } +} + +class LoginEvent extends Event { + constructor(login, timeStampMillis, userData) { + super('Login'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_login: login, + affise_event_login_timestamp: timeStampMillis + }; + } +} + +class OpenedFromPushNotificationEvent extends Event { + constructor(details, userData) { + super('OpenedFromPushNotification'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_opened_from_push_notification: details + }; + } +} + +class PurchaseEvent extends Event { + constructor(purchaseData, timeStampMillis, userData) { + super('Purchase'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_purchase: purchaseData, + affise_event_purchase_timestamp: timeStampMillis + }; + } +} + +class RateEvent extends Event { + constructor(rate, timeStampMillis, userData) { + super('Rate'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_rate: rate, + affise_event_rate_timestamp: timeStampMillis + }; + } +} + +class ReEngageEvent extends Event { + constructor(reEngage, userData) { + super('ReEngage'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_re_engage: reEngage + }; + } +} + +class ReserveEvent extends Event { + constructor(reserve, timeStampMillis, userData) { + super('Reserve'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_reserve: reserve, + affise_event_reserve_timestamp: timeStampMillis + }; + } +} + +class SalesEvent extends Event { + constructor(sales, timeStampMillis, userData) { + super('Sales'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_sales: sales, + affise_event_sales_timestamp: timeStampMillis + }; + } +} + +class SearchEvent extends Event { + constructor(search, timeStampMillis, userData) { + super('Search'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_search: search, + affise_event_search_timestamp: timeStampMillis + }; + } +} + +class ShareEvent extends Event { + constructor(share, timeStampMillis, userData) { + super('Share'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_share: share, + affise_event_share_timestamp: timeStampMillis + }; + } +} + +class SpendCreditsEvent extends Event { + constructor(credits, timeStampMillis, userData) { + super('SpendCredits'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_spend_credits: credits, + affise_event_spend_credits_timestamp: timeStampMillis + }; + } +} + +class StartRegistrationEvent extends Event { + constructor(registration, timeStampMillis, userData) { + super('StartRegistration'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_start_registration: registration, + affise_event_start_registration_timestamp: timeStampMillis + }; + } +} + +class StartTrialEvent extends Event { + constructor(trial, timeStampMillis, userData) { + super('StartTrial'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_start_trial: trial, + affise_event_start_trial_timestamp: timeStampMillis + }; + } +} + +class StartTutorialEvent extends Event { + constructor(tutorial, timeStampMillis, userData) { + super('StartTutorial'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_start_tutorial: tutorial, + affise_event_start_tutorial_timestamp: timeStampMillis + }; + } +} + +class SubscribeEvent extends Event { + constructor(tutorial, timeStampMillis, userData) { + super('Subscribe'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_subscribe: tutorial, + affise_event_subscribe_timestamp: timeStampMillis + }; + } +} + +class TravelBookingEvent extends Event { + constructor(details, userData) { + super('TravelBooking'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_travel_booking: details + }; + } +} + +class UnlockAchievementEvent extends Event { + constructor(achievement, timeStampMillis, userData) { + super('UnlockAchievement'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_unlock_achievement: achievement, + affise_event_unlock_achievement_timestamp: timeStampMillis + }; + } +} + +class UnsubscribeEvent extends Event { + constructor(unsubscribe, timeStampMillis, userData) { + super('Unsubscribe'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_unsubscribe: unsubscribe, + affise_event_unsubscribe_timestamp: timeStampMillis + }; + } +} + +class UpdateEvent extends Event { + constructor(details, userData) { + super('Update'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_update: details + }; + } +} + +class ViewAdvEvent extends Event { + constructor(ad, timeStampMillis, userData) { + super('ViewAdv'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_view_adv: ad, + affise_event_view_adv_timestamp: timeStampMillis + }; + } +} + +class ViewCartEvent extends Event { + constructor(objects, userData) { + super('ViewCart'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_view_cart: objects + }; + } +} + +class ViewItemEvent extends Event { + constructor(item, userData) { + super('ViewItem'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_view_item: item + }; + } +} + +class ViewItemsEvent extends Event { + constructor(items, userData) { + super('ViewItems'); + + this.affise_event_first_for_user = false; + this.affise_event_user_data = userData; + this.affise_event_data = { + affise_event_view_items: items + }; + } +} + +class InitialSubscriptionEvent extends SubscriptionEvent { + constructor(data, userData) { + super( + data, + userData, + 'affise_subscription_activation', + 'affise_sub_initial_subscription' + ); + } +} + +class InitialTrialEvent extends SubscriptionEvent { + constructor(data, userData) { + super( + data, + userData, + 'affise_subscription_activation', + 'affise_sub_initial_trial' + ); + } +} + +class InitialOfferEvent extends SubscriptionEvent { + constructor(data, userData) { + super( + data, + userData, + 'affise_subscription_activation', + 'affise_sub_initial_offer' + ); + } +} + +class ConvertedTrialEvent extends SubscriptionEvent { + constructor(data, userData) { + super( + data, + userData, + 'affise_subscription_first_conversion', + 'affise_sub_converted_trial' + ); + } +} + +class ConvertedOfferEvent extends SubscriptionEvent { + constructor(data, userData) { + super( + data, + userData, + 'affise_subscription_first_conversion', + 'affise_sub_converted_offer' + ); + } +} + +class TrialInRetryEvent extends SubscriptionEvent { + constructor(data, userData) { + super( + data, + userData, + 'affise_subscription_entered_billing_retry', + 'affise_sub_trial_in_retry' + ); + } +} + +class OfferInRetryEvent extends SubscriptionEvent { + constructor(data, userData) { + super( + data, + userData, + 'affise_subscription_entered_billing_retry', + 'affise_sub_offer_in_retry' + ); + } +} + +class SubscriptionInRetryEvent extends SubscriptionEvent { + constructor(data, userData) { + super( + data, + userData, + 'affise_subscription_entered_billing_retry', + 'affise_sub_subscription_in_retry' + ); + } +} + +class RenewedSubscriptionEvent extends SubscriptionEvent { + constructor(data, userData) { + super( + data, + userData, + 'affise_subscription_renewal', + 'affise_sub_renewed_subscription' + ); + } +} + +class FailedSubscriptionFromRetryEvent extends SubscriptionEvent { + constructor(data, userData) { + super( + data, + userData, + 'affise_subscription_cancellation', + 'affise_sub_failed_subscription_from_retry' + ); + } +} + +class FailedOfferFromRetryEvent extends SubscriptionEvent { + constructor(data, userData) { + super( + data, + userData, + 'affise_subscription_cancellation', + 'affise_sub_failed_offer_from_retry' + ); + } +} + +class FailedTrialFromRetryEvent extends SubscriptionEvent { + constructor(data, userData) { + super( + data, + userData, + 'affise_subscription_cancellation', + 'affise_sub_failed_trial_from_retry' + ); + } +} + +class FailedSubscriptionEvent extends SubscriptionEvent { + constructor(data, userData) { + super( + data, + userData, + 'affise_subscription_cancellation', + 'affise_sub_failed_subscription' + ); + } +} + +class FailedOfferiseEvent extends SubscriptionEvent { + constructor(data, userData) { + super( + data, + userData, + 'affise_subscription_cancellation', + 'affise_sub_failed_offer' + ); + } +} + +class FailedTrialEvent extends SubscriptionEvent { + constructor(data, userData) { + super( + data, + userData, + 'affise_subscription_cancellation', + 'affise_sub_failed_trial' + ); + } +} + +class ReactivatedSubscriptionEvent extends SubscriptionEvent { + constructor(data, userData) { + super( + data, + userData, + 'affise_subscription_reactivation', + 'affise_sub_reactivated_subscription' + ); + } +} + +class RenewedSubscriptionFromRetryEvent extends SubscriptionEvent { + constructor(data, userData) { + super( + data, + userData, + 'affise_subscription_renewal_from_billing_retry', + 'affise_sub_renewed_subscription_from_retry' + ); + } +} + +class ConvertedOfferFromRetryEvent extends SubscriptionEvent { + constructor(data, userData) { + super( + data, + userData, + 'affise_subscription_renewal_from_billing_retry', + 'affise_sub_converted_offer_from_retry' + ); + } +} + +class ConvertedTrialFromRetryEvent extends SubscriptionEvent { + constructor(data, userData) { + super( + data, + userData, + 'affise_subscription_renewal_from_billing_retry', + 'affise_sub_converted_trial_from_retry' + ); + } +} + +class UnsubscriptionEvent extends SubscriptionEvent { + constructor(data, userData) { + super( + data, + userData, + 'affise_unsubscription', + 'affise_sub_unsubscription' + ); + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/Affise.kt b/attribution/src/main/java/com/affise/attribution/Affise.kt new file mode 100644 index 0000000..0a37abe --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/Affise.kt @@ -0,0 +1,228 @@ +package com.affise.attribution + +import android.app.Application +import android.net.Uri +import android.webkit.WebView +import com.affise.attribution.deeplink.OnDeeplinkCallback +import com.affise.attribution.events.Event +import com.affise.attribution.referrer.ReferrerKey +import com.affise.attribution.events.autoCatchingClick.AutoCatchingType +import com.affise.attribution.events.predefined.GDPREvent +import com.affise.attribution.init.AffiseInitProperties +import com.affise.attribution.parameters.PushTokenProvider +import com.affise.attribution.platform.SdkPlatform +import com.affise.attribution.referrer.OnReferrerCallback + +/** + * Entry point to initialise Affise Attribution library + */ +object Affise { + + /** + * Api to communication with Affise + */ + private var api: AffiseApi? = null + + /** + * Init [AffiseComponent] with [app] and [initProperties] + */ + @JvmStatic + @Synchronized + fun init( + app: Application, + initProperties: AffiseInitProperties + ) { + //Check creating AffiseComponent + if (api == null) { + //Create AffiseComponent + api = AffiseComponent(app, initProperties) + } + } + + /** + * Send events + */ + @JvmStatic + fun sendEvents() { + api?.eventsManager?.sendEvents() + } + + /** + * Store and send [event] + */ + @JvmStatic + fun sendEvent(event: Event) { + api?.storeEventUseCase?.storeEvent(event) + } + + /** + * Add [pushToken] + */ + @JvmStatic + fun addPushToken(pushToken: String) { + api?.sharedPreferences?.edit()?.let { + it.putString(PushTokenProvider.KEY_APP_PUSHTOKEN, pushToken) + it.commit() + } + } + + /** + * Register [webView] to WebBridge + */ + @JvmStatic + fun registerWebView(webView: WebView) { + api?.webBridgeManager?.registerWebView(webView) + } + + /** + * Unregister webView on WebBridge + */ + @JvmStatic + fun unregisterWebView() { + api?.webBridgeManager?.unregisterWebView() + } + + /** + * Register [callback] for deeplink + */ + @JvmStatic + fun registerDeeplinkCallback(callback: OnDeeplinkCallback) { + api?.deeplinkManager?.setDeeplinkCallback(callback) + } + + /** + * Set new secretId + */ + @JvmStatic + fun setSecretId(secretId: String) { + api?.initPropertiesStorage?.updateSecretId(secretId) + } + + /** + * Send enabled autoCatching types + */ + @JvmStatic + fun setAutoCatchingTypes(types: List?) { + api?.autoCatchingClickProvider?.setTypes(types) + } + + /** + * Sets offline mode to [enabled] state + * + * When enabled, no network activity should be triggered by this library, + * but background work is not paused. When offline mode is enabled, + * all recorded events should be sent + */ + @JvmStatic + fun setOfflineModeEnabled(enabled: Boolean) { + api?.preferencesUseCase?.setOfflineModeEnabled(enabled) + } + + /** + * Returns current offline mode state + */ + @JvmStatic + fun isOfflineModeEnabled(): Boolean = api?.preferencesUseCase?.isOfflineModeEnabled() ?: false + + /** + * Sets background tracking mode to [enabled] state + * + * When disabled, library should not generate any tracking events while in background + */ + @JvmStatic + fun setBackgroundTrackingEnabled(enabled: Boolean) { + api?.preferencesUseCase?.setBackgroundTrackingEnabled(enabled) + } + + /** + * Returns current background tracking state + */ + @JvmStatic + fun isBackgroundTrackingEnabled(): Boolean = + api?.preferencesUseCase?.isBackgroundTrackingEnabled() ?: false + + /** + * Sets offline mode to [enabled] state + * + * When disabled, library should not generate any tracking events + */ + @JvmStatic + fun setTrackingEnabled(enabled: Boolean) { + api?.preferencesUseCase?.setTrackingEnabled(enabled) + } + + /** + * Returns current tracking state + */ + @JvmStatic + fun isTrackingEnabled(): Boolean = api?.preferencesUseCase?.isTrackingEnabled() ?: false + + /** + * Erases all user data from mobile and sends [GDPREvent] + */ + @JvmStatic + fun forget(userData: String) { + api?.sendGDPREventUseCase?.registerForgetMeEvent(userData) + } + + /** + * Set [enabled] collect metrics + * + * When disabled, library should not generate any metrics events, + * but will send the saved metrics events + */ + @JvmStatic + fun setEnabledMetrics(enabled: Boolean) { + api?.metricsManager?.setEnabledMetrics(enabled) + } + + @JvmStatic + fun crashApplication() { + api?.crashApplicationUseCase?.crash() + } + + /** + * Get referrer + */ + @JvmStatic + fun getReferrer(): String? = api?.installReferrerProvider?.provide() + + + /** + * Get referrer Value + */ + @JvmStatic + fun getReferrerValue(key: ReferrerKey, callback: OnReferrerCallback?) { + api?.retrieveInstallReferrerUseCase?.getReferrerValue(key, callback) + } + + object _crossPlatform { + /** + * Handle Deeplink [uri] for cross platform + */ + @JvmStatic + fun handleDeeplink(uri: String) { + api?.deeplinkManager?.handleDeeplink(Uri.parse(uri)) + } + + @JvmStatic + fun start() { + api?.sessionManager?.sessionStart() + } + + @JvmStatic + fun react() { + SdkPlatform.react() + } + + @JvmStatic + fun flutter() { + SdkPlatform.flutter() + } + + @JvmStatic + fun unity() { + SdkPlatform.unity() + } + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/AffiseApi.kt b/attribution/src/main/java/com/affise/attribution/AffiseApi.kt new file mode 100644 index 0000000..08552af --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/AffiseApi.kt @@ -0,0 +1,44 @@ +package com.affise.attribution + +import android.content.SharedPreferences +import com.affise.attribution.deeplink.DeeplinkManagerImpl +import com.affise.attribution.events.EventsManager +import com.affise.attribution.events.StoreEventUseCase +import com.affise.attribution.events.autoCatchingClick.AutoCatchingClickProvider +import com.affise.attribution.init.InitPropertiesStorage +import com.affise.attribution.init.SetPropertiesWhenAppInitializedUseCase +import com.affise.attribution.logs.LogsManager +import com.affise.attribution.metrics.MetricsManager +import com.affise.attribution.parameters.InstallReferrerProvider +import com.affise.attribution.session.SessionManager +import com.affise.attribution.test.CrashApplicationUseCase +import com.affise.attribution.usecase.RetrieveInstallReferrerUseCase +import com.affise.attribution.usecase.EraseUserDataUseCaseImpl +import com.affise.attribution.usecase.FirstAppOpenUseCase +import com.affise.attribution.usecase.PreferencesUseCaseImpl +import com.affise.attribution.usecase.SendGDPREventUseCaseImpl +import com.affise.attribution.webBridge.WebBridgeManager + +/** + * Library api contract + */ +internal interface AffiseApi { + val setPropertiesWhenInitUseCase: SetPropertiesWhenAppInitializedUseCase + val firstAppOpenUseCase: FirstAppOpenUseCase + val sessionManager: SessionManager + val eventsManager: EventsManager + val storeEventUseCase: StoreEventUseCase + val sharedPreferences: SharedPreferences + val logsManager: LogsManager + val webBridgeManager: WebBridgeManager + val deeplinkManager: DeeplinkManagerImpl + val initPropertiesStorage: InitPropertiesStorage + val autoCatchingClickProvider: AutoCatchingClickProvider + val preferencesUseCase: PreferencesUseCaseImpl + val eraseUserDataUseCase: EraseUserDataUseCaseImpl + val sendGDPREventUseCase: SendGDPREventUseCaseImpl + val metricsManager: MetricsManager + val crashApplicationUseCase: CrashApplicationUseCase + val installReferrerProvider: InstallReferrerProvider + val retrieveInstallReferrerUseCase: RetrieveInstallReferrerUseCase +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/AffiseComponent.kt b/attribution/src/main/java/com/affise/attribution/AffiseComponent.kt new file mode 100644 index 0000000..6f9287d --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/AffiseComponent.kt @@ -0,0 +1,456 @@ +package com.affise.attribution + +import android.app.Application +import android.content.Context +import android.content.SharedPreferences +import android.os.Build +import com.affise.attribution.build.BuildConfigPropertiesProviderImpl +import com.affise.attribution.converter.* +import com.affise.attribution.converter.JsonObjectToMetricsEventConverter +import com.affise.attribution.build.BuildConfigPropertiesProvider +import com.affise.attribution.executors.ExecutorServiceProviderImpl +import com.affise.attribution.logs.LogsManager +import com.affise.attribution.deeplink.DeeplinkClickRepository +import com.affise.attribution.deeplink.DeeplinkClickRepositoryImpl +import com.affise.attribution.deeplink.DeeplinkManagerImpl +import com.affise.attribution.deeplink.InstallReferrerToDeeplinkUriConverter +import com.affise.attribution.events.* +import com.affise.attribution.events.autoCatchingClick.AutoCatchingClickProvider +import com.affise.attribution.init.* +import com.affise.attribution.internal.* +import com.affise.attribution.logs.* +import com.affise.attribution.metrics.* +import com.affise.attribution.modules.AffiseModuleManager +import com.affise.attribution.network.CloudRepository +import com.affise.attribution.network.CloudRepositoryImpl +import com.affise.attribution.network.HttpClientImpl +import com.affise.attribution.parameters.InstallReferrerProvider +import com.affise.attribution.parameters.UserAgentProvider +import com.affise.attribution.parameters.base.PropertiesProviderFactory +import com.affise.attribution.parameters.factory.PostBackModelFactory +import com.affise.attribution.preferences.ApplicationLifecyclePreferencesRepositoryImpl +import com.affise.attribution.preferences.ApplicationLifetimePreferencesRepositoryImpl +import com.affise.attribution.referrer.AffiseReferrerDataToStringConverter +import com.affise.attribution.session.CurrentActiveActivityCountProvider +import com.affise.attribution.session.CurrentActiveActivityCountProviderImpl +import com.affise.attribution.session.SessionManager +import com.affise.attribution.session.SessionManagerImpl +import com.affise.attribution.storages.* +import com.affise.attribution.test.CrashApplicationUseCase +import com.affise.attribution.test.CrashApplicationUseCaseImpl +import com.affise.attribution.usecase.* +import com.affise.attribution.utils.ActivityActionsManager +import com.affise.attribution.utils.ActivityActionsManagerImpl +import com.affise.attribution.utils.EncryptedSharedPreferences +import com.affise.attribution.webBridge.WebBridgeManager + +internal class AffiseComponent( + private val app: Application, + initProperties: AffiseInitProperties, +) : AffiseApi { + + /** + * PostBackModelFactory + */ + private val postBackModelFactory: PostBackModelFactory by lazy { + PropertiesProviderFactory( + buildConfigPropertiesProvider, + app, + firstAppOpenUseCase, + retrieveInstallReferrerUseCase, + sessionManager, + sharedPreferences, + initPropertiesStorage, + stringToMD5Converter, + stringToSHA1Converter, + StringToSHA256Converter(), + logsManager, + isDeeplinkClickRepository, + installReferrerProvider + ).create() + } + + private val stringToMD5Converter: StringToMD5Converter by lazy { + StringToMD5Converter(logsManager) + } + + private val stringToSHA1Converter: StringToSHA1Converter by lazy { + StringToSHA1Converter() + } + + private val converterToBase64: ConverterToBase64 by lazy { + ConverterToBase64() + } + + private val buildConfigPropertiesProvider: BuildConfigPropertiesProvider by lazy { + BuildConfigPropertiesProviderImpl() + } + + /** + * ActivityCountProvider + */ + private val activityCountProvider: CurrentActiveActivityCountProvider by lazy { + CurrentActiveActivityCountProviderImpl(activityActionsManager) + } + + /** + * Provides [EventsStorage] + */ + private val eventsStorage: EventsStorage by lazy { + EventsStorageImpl(app, logsManager) + } + + /** + * Provides [InternalEventsStorage] + */ + private val internalEventsStorage: InternalEventsStorage by lazy { + InternalEventsStorageImpl(app, logsManager) + } + + /** + * Provides [Converter] from [Event] to [SerializedEvent] + */ + private val eventToSerializedEventConverter: Converter by lazy { + EventToSerializedEventConverter() + } + + /** + * Provides [Converter] from [InternalEvent] to [SerializedEvent] + */ + private val internalEventToSerializedEventConverter: Converter by lazy { + InternalEventToSerializedEventConverter() + } + + /** + * EventsRepository + */ + private val eventsRepository: EventsRepository by lazy { + EventsRepositoryImpl( + converterToBase64, + eventToSerializedEventConverter, + logsManager, + eventsStorage + ) + } + + /** + * InternalEventsRepository + */ + private val internalEventsRepository: InternalEventsRepository by lazy { + InternalEventsRepositoryImpl( + converterToBase64, + internalEventToSerializedEventConverter, + logsManager, + internalEventsStorage + ) + } + + private val logStorage: LogsStorage by lazy { + LogsStorageImpl(app) + } + + /** + * LogsRepository + */ + private val logsRepository: LogsRepository by lazy { + LogsRepositoryImpl(converterToBase64, LogToSerializedLogConverter(), logStorage) + } + + private val metricsStorage: MetricsStorage by lazy { + MetricsStorageImpl(app, JsonObjectToMetricsEventConverter()) + } + + /** + * MetricsRepository + */ + private val metricsRepository: MetricsRepository by lazy { + MetricsRepositoryImpl( + converterToBase64, + eventToSerializedEventConverter, + metricsStorage + ) + } + + /** + * MetricsUseCase + */ + private val metricsUseCase: MetricsUseCase by lazy { + MetricsUseCaseImpl(ExecutorServiceProviderImpl("Metrics Worker"), metricsRepository) + } + + /** + * MetricsManager + */ + override val metricsManager: MetricsManager by lazy { + MetricsManagerImpl(activityActionsManager, metricsUseCase, stringToSHA1Converter) + } + + /** + * Provides [CrashApplicationUseCase] + */ + override val crashApplicationUseCase: CrashApplicationUseCase by lazy { + CrashApplicationUseCaseImpl() + } + + /** + * CloudRepository + */ + private val cloudRepository: CloudRepository by lazy { + CloudRepositoryImpl( + HttpClientImpl(), + postBackModelFactory.getProvider(), + PostBackModelToJsonStringConverter() + ) + } + + private val gdprEventRepository: GDPREventRepository by lazy { + GDPREventRepository(sharedPreferences, eventToSerializedEventConverter) + } + + /** + * SendDataToServerUseCase + */ + private val sendDataToServerUseCase: SendDataToServerUseCase by lazy { + SendDataToServerUseCaseImpl( + postBackModelFactory, + cloudRepository, + eventsRepository, + internalEventsRepository, + ExecutorServiceProviderImpl("Sending Worker"), + logsRepository, + metricsRepository, + logsManager, + preferencesUseCase + ) + } + + /** + * StoreLogsUseCase + */ + private val storeLogsUseCase: StoreLogsUseCase by lazy { + StoreLogsUseCaseImpl( + ExecutorServiceProviderImpl("Log Worker"), + logsRepository + ) + } + + /** + * Provides [IsFirstForUserStorage] + */ + private val isFirstForUserStorage: IsFirstForUserStorage by lazy { + IsFirstForUserStorageImpl(app, logsManager) + } + + /** + * Provides [IsFirstForUserUseCase] + */ + private val isFirstForUserUseCase: IsFirstForUserUseCase by lazy { + IsFirstForUserUseCaseImpl(isFirstForUserStorage) + } + + /** + * SetPropertiesWhenInitUseCase + */ + override val setPropertiesWhenInitUseCase: SetPropertiesWhenAppInitializedUseCase by lazy { + SetPropertiesWhenAppInitializedUseCaseImpl(initPropertiesStorage) + } + + /** + * FirstAppOpenUseCase + */ + override val firstAppOpenUseCase: FirstAppOpenUseCase by lazy { + FirstAppOpenUseCase(sharedPreferences, activityCountProvider) + } + + /** + * StoreEventUseCase + */ + override val storeEventUseCase: StoreEventUseCase by lazy { + StoreEventUseCaseImpl( + ExecutorServiceProviderImpl("Event Worker"), + eventsRepository, + eventsManager, + preferencesUseCase, + activityCountProvider, + logsManager, + isFirstForUserUseCase + ) + } + + /** + * StoreInternalEventUseCase + */ + private val storeInternalEventUseCase: StoreInternalEventUseCase by lazy { + StoreInternalEventUseCaseImpl( + ExecutorServiceProviderImpl("Internal Event Worker"), + internalEventsRepository + ) + } + + /** + * RetrieveInstallReferrerUseCase + */ + override val retrieveInstallReferrerUseCase by lazy { + RetrieveInstallReferrerUseCase( + sharedPreferences, + AffiseReferrerDataToStringConverter(), + StringToAffiseReferrerDataConverter(logsManager), + app, + deeplinkManager, + logsManager, + InstallReferrerToDeeplinkUriConverter() + ) + } + + override val installReferrerProvider by lazy { + InstallReferrerProvider(app, retrieveInstallReferrerUseCase) + } + + /** + * InitPropertiesStorage + */ + override val initPropertiesStorage: InitPropertiesStorage = InitPropertiesStorageImpl() + + /** + * provides [PreferencesUseCaseImpl] + */ + override val preferencesUseCase: PreferencesUseCaseImpl by lazy { + PreferencesUseCaseImpl( + ApplicationLifecyclePreferencesRepositoryImpl(), + ApplicationLifetimePreferencesRepositoryImpl(sharedPreferences) + ) + } + + /** + * provides [EraseUserDataUseCaseImpl] + */ + override val eraseUserDataUseCase: EraseUserDataUseCaseImpl by lazy { + EraseUserDataUseCaseImpl(eventsRepository, gdprEventRepository) + } + + /** + * DeeplinkClickRepository + */ + private val isDeeplinkClickRepository: DeeplinkClickRepository = DeeplinkClickRepositoryImpl() + + /** + * ActivityActionsListeners + */ + private val activityActionsManager: ActivityActionsManager by lazy { + ActivityActionsManagerImpl(app, logsManager) + } + + /** + * SharedPreferences + */ + override val sharedPreferences: SharedPreferences by lazy { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + EncryptedSharedPreferences(app, PREFERENCES_ENCRYPTED_FILE_NAME) + } else { + app.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE) + } + } + + /** + * LogsManager + */ + override val logsManager: LogsManager by lazy { + LogsManagerImpl(storeLogsUseCase) + } + + /** + * SessionManager + */ + override val sessionManager: SessionManager by lazy { + SessionManagerImpl( + sharedPreferences, + activityCountProvider, + storeInternalEventUseCase + ) + } + + /** + * EventsManager + */ + override val eventsManager: EventsManager by lazy { + EventsManager(sendDataToServerUseCase, activityCountProvider) + } + + /** + * WebBridgeManager + */ + override val webBridgeManager: WebBridgeManager by lazy { + WebBridgeManager(storeEventUseCase) + } + + /** + * DeeplinkManager + */ + override val deeplinkManager: DeeplinkManagerImpl by lazy { + DeeplinkManagerImpl( + initPropertiesStorage, + isDeeplinkClickRepository, + activityActionsManager + ) + } + + /** + * AutoCatchingClickProvider + */ + override val autoCatchingClickProvider: AutoCatchingClickProvider = AutoCatchingClickProvider( + storeEventUseCase, activityActionsManager + ) + + override val sendGDPREventUseCase: SendGDPREventUseCaseImpl by lazy { + SendGDPREventUseCaseImpl( + gdprEventRepository, + ExecutorServiceProviderImpl("GDPR Event Worker"), + cloudRepository, + postBackModelFactory, + eraseUserDataUseCase + ) + } + + private val moduleManager: AffiseModuleManager by lazy { + AffiseModuleManager( + app, + logsManager, + postBackModelFactory + ) + } + + /** + * Init properties + */ + init { + sendGDPREventUseCase.sendForgetMeEvent() + firstAppOpenUseCase.onAppCreated() + sessionManager.init() + retrieveInstallReferrerUseCase.startInstallReferrerRetrieve(onFinished = { + eventsManager.init() + }) + setPropertiesWhenInitUseCase.init(initProperties) + deeplinkManager.init() + autoCatchingClickProvider.init(initProperties.autoCatchingClickEvents) + metricsManager.setEnabledMetrics(initProperties.enabledMetrics) + + AffiseThreadUncaughtExceptionHandlerImpl( + Thread.getDefaultUncaughtExceptionHandler(), + logsManager + ) + .also(Thread::setDefaultUncaughtExceptionHandler) + + moduleManager.init( + dependencies = listOf( + buildConfigPropertiesProvider, + stringToMD5Converter, + stringToSHA1Converter, + ) + ) + } + + companion object { + private const val PREFERENCES_FILE_NAME = "com.affise.attribution" + private const val PREFERENCES_ENCRYPTED_FILE_NAME = "com.affise.attribution.encrypted" + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/build/BuildConfigPropertiesProvider.kt b/attribution/src/main/java/com/affise/attribution/build/BuildConfigPropertiesProvider.kt new file mode 100644 index 0000000..c30762d --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/build/BuildConfigPropertiesProvider.kt @@ -0,0 +1,38 @@ +package com.affise.attribution.build + +/** + * Wrapper on android Build config to retrieve data from + * created for testing purposes + */ +interface BuildConfigPropertiesProvider { + + /** + * @returns SDK_INT + */ + fun getSDKVersion(): Int + + /** + * @returns RELEASE name + */ + fun getReleaseName(): String? + + /** + * @returns SUPPORTED_ABIs + */ + fun getSupportedABIs(): List + + /** + * @returns HARDWARE + */ + fun getHardware(): String? + + /** + * @returns MANUFACTURER + */ + fun getManufacturer(): String? + + /** + * @returns CODENAME + */ + fun getCodeName(): String? +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/build/BuildConfigPropertiesProviderImpl.kt b/attribution/src/main/java/com/affise/attribution/build/BuildConfigPropertiesProviderImpl.kt new file mode 100644 index 0000000..92c63ae --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/build/BuildConfigPropertiesProviderImpl.kt @@ -0,0 +1,39 @@ +package com.affise.attribution.build + +import android.os.Build + +/** + * Implementation for [BuildConfigPropertiesProvider] + */ +class BuildConfigPropertiesProviderImpl : BuildConfigPropertiesProvider { + + /** + * @returns SDK_INT from Build.VERSION + */ + override fun getSDKVersion() = Build.VERSION.SDK_INT + + /** + * @returns RELEASE from Build.VERSION + */ + override fun getReleaseName() = Build.VERSION.RELEASE.toString() + + /** + * @returns SUPPORTED_ABIs form Build + */ + override fun getSupportedABIs() = Build.SUPPORTED_ABIS.filterNotNull().toList() + + /** + * @returns HARDWARE from Build + */ + override fun getHardware() = Build.HARDWARE.toString() + + /** + * @returns MANUFACTURER from Build + */ + override fun getManufacturer() = Build.MANUFACTURER.toString() + + /** + * @returns CODENAME from Build.VERSION + */ + override fun getCodeName() = Build.VERSION.CODENAME.toString() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/converter/Converter.kt b/attribution/src/main/java/com/affise/attribution/converter/Converter.kt new file mode 100644 index 0000000..d8eb2eb --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/converter/Converter.kt @@ -0,0 +1,15 @@ +package com.affise.attribution.converter + +/** + * Interface for convert object to another object + * + * @param T object type in + * @param R object type out + */ +interface Converter { + + /** + * Convert value [from] to R + */ + fun convert(from: T): R +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/converter/ConverterToBase64.kt b/attribution/src/main/java/com/affise/attribution/converter/ConverterToBase64.kt new file mode 100644 index 0000000..f30e313 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/converter/ConverterToBase64.kt @@ -0,0 +1,16 @@ +package com.affise.attribution.converter + +import android.util.Base64 + +/** + * Convert string to Base64 [encoded string] + */ +class ConverterToBase64 : Converter { + + /** + * Convert [from] String to encode string with Base64 + */ + override fun convert( + from: String + ): String = Base64.encodeToString(from.toByteArray(), Base64.NO_WRAP) +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/converter/JsonObjectToMetricsEventConverter.kt b/attribution/src/main/java/com/affise/attribution/converter/JsonObjectToMetricsEventConverter.kt new file mode 100644 index 0000000..0163b41 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/converter/JsonObjectToMetricsEventConverter.kt @@ -0,0 +1,47 @@ +package com.affise.attribution.converter + +import com.affise.attribution.metrics.MetricsClickData +import com.affise.attribution.metrics.MetricsData +import com.affise.attribution.metrics.MetricsEvent +import org.json.JSONArray +import org.json.JSONObject + +/** + * Converter from JSONObject to MetricsEvent + */ +internal class JsonObjectToMetricsEventConverter : Converter { + + /** + * Convert [from] JSONObject to MetricsEvent + */ + override fun convert(from: JSONObject): MetricsEvent = MetricsEvent( + from.optLong(MetricsEvent.KEY_DATE) + ).also { + val data = from.optJSONArray(MetricsEvent.KEY_DATA) ?: JSONArray() + + it.data = (0 until data.length()) + .map { positionMetricsData -> + MetricsData().apply { + val saveMetricsData = data.optJSONObject(positionMetricsData) + + activityName = saveMetricsData.optString(MetricsEvent.KEY_ACTIVITY_NAME) + + openTime = saveMetricsData.optLong(MetricsEvent.KEY_OPEN_TIME) + + clicksData = saveMetricsData.optJSONArray(MetricsEvent.KEY_CLICKS_DATA) + ?.let { clicksData -> + (0 until clicksData.length()).map { positionClicksData -> + val saveClicksData = + clicksData.optJSONObject(positionClicksData) + + MetricsClickData().apply { + name = saveClicksData.optString(MetricsEvent.KEY_NAME) + + count = saveClicksData.optInt(MetricsEvent.KEY_COUNT) + } + } + }?.toMutableList() + } + }.toMutableList() + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/converter/LogToSerializedLogConverter.kt b/attribution/src/main/java/com/affise/attribution/converter/LogToSerializedLogConverter.kt new file mode 100644 index 0000000..f09afd7 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/converter/LogToSerializedLogConverter.kt @@ -0,0 +1,53 @@ +package com.affise.attribution.converter + +import com.affise.attribution.events.predefined.AffiseLog +import com.affise.attribution.logs.SerializedLog +import com.affise.attribution.utils.generateUUID +import com.affise.attribution.utils.timestamp +import org.json.JSONObject + +/** + * Converter AffiseLog to SerializedLog + */ +class LogToSerializedLogConverter : Converter { + + /** + * Convert [from] AffiseLog to SerializedLog + */ + override fun convert(from: AffiseLog): SerializedLog { + //Generate id + val id = generateUUID().toString() + + //Type of log + val type = from.name.type + + //Generate parameters + val value: Any = when (from) { + is AffiseLog.NetworkLog -> from.jsonObject + else -> from.value + } + + //Create JSONObject for parameters + val parameters = JSONObject().apply { + put(type, value) + } + + //Generate data + val json = JSONObject().apply { + //Add id + put("affise_sdkevent_id", id) + + //Add name + put("affise_sdkevent_name", "affise_event_sdklog") + + //Add timestam + put("affise_sdkevent_timestamp", timestamp()) + + //Add parameters + put("affise_sdkevent_parameters", parameters) + } + + //Create serialized object + return SerializedLog(id, from.name.type, json) + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/converter/PostBackModelToJsonStringConverter.kt b/attribution/src/main/java/com/affise/attribution/converter/PostBackModelToJsonStringConverter.kt new file mode 100644 index 0000000..c384edb --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/converter/PostBackModelToJsonStringConverter.kt @@ -0,0 +1,80 @@ +package com.affise.attribution.converter + +import com.affise.attribution.network.entity.PostBackModel +import com.affise.attribution.parameters.Parameters +import org.json.JSONArray +import org.json.JSONObject + +/** + * Converter List to String + */ +class PostBackModelToJsonStringConverter : Converter, String> { + + /** + * Convert [from] list of PostBackModel to json string + */ + override fun convert(from: List): String { + //Create jsonArray + val jsonArray = JSONArray() + + //Create jsonObject for all objects in list + from.forEach { model -> + val jsonObject = parameters(model) + jsonArray.put(jsonObject) + } + + //Create json string + return jsonArray + .toString() + .replace("\\/", "/") + } + + /** + * Create parameters map of object + */ + private fun parameters(obj: PostBackModel) = JSONObject().apply { + //Parameters + obj.parameters.forEach { + put(it.key, it.value) + } + + //Events + val eventsArray = JSONArray() + obj.events?.forEach { event -> + eventsArray.put(event.data) + } + put(Parameters.AFFISE_EVENTS_COUNT, eventsArray.length()) + put(EVENTS_KEY, eventsArray) + + //SdkEvents + val sdkEventsArray = JSONArray() + obj.internalEvents?.forEach { event -> + sdkEventsArray.put(event.data) + } + put(Parameters.AFFISE_INTERNAL_EVENTS_COUNT, sdkEventsArray.length()) + put(INTERNAL_EVENTS_KEY, sdkEventsArray) + + //Logs + val logsArray = JSONArray() + obj.logs?.forEach { log -> + logsArray.put(log.data) + } + put(Parameters.AFFISE_SDK_EVENTS_COUNT, logsArray.length()) + put(SDK_EVENTS_KEY, logsArray) + + //Metrics + val metricsArray = JSONArray() + obj.metrics?.forEach { metric -> + metricsArray.put(metric.data) + } + put(Parameters.AFFISE_METRICS_EVENTS_COUNT, metricsArray.length()) + put(METRICS_EVENTS_KEY, metricsArray) + } + + companion object { + private const val EVENTS_KEY = "events" + private const val SDK_EVENTS_KEY = "sdk_events" + private const val METRICS_EVENTS_KEY = "metrics_events" + private const val INTERNAL_EVENTS_KEY = "internal_events" + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/converter/StringToAffiseReferrerDataConverter.kt b/attribution/src/main/java/com/affise/attribution/converter/StringToAffiseReferrerDataConverter.kt new file mode 100644 index 0000000..937e919 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/converter/StringToAffiseReferrerDataConverter.kt @@ -0,0 +1,40 @@ +package com.affise.attribution.converter + +import com.affise.attribution.logs.LogsManager +import com.affise.attribution.referrer.AffiseReferrerData +import org.json.JSONObject + +/** + * Converter from [String] to [AffiseReferrerData] + * + * @property logsManager for error logging + */ +class StringToAffiseReferrerDataConverter( + private val logsManager: LogsManager +) : Converter { + + /** + * Convert [from] String to json AffiseReferrerData + */ + override fun convert(from: String): AffiseReferrerData? { + return try { + //Create JSONObject + val j = JSONObject(from) + + //Create referrer data + AffiseReferrerData( + installReferrer = j.getString(AffiseReferrerData.KEYS.installReferrer), + referrerClickTimestampSeconds = j.getLong(AffiseReferrerData.KEYS.referrerClickTimestampSeconds), + installBeginTimestampSeconds = j.getLong(AffiseReferrerData.KEYS.installBeginTimestampSeconds), + referrerClickTimestampServerSeconds = j.getLong(AffiseReferrerData.KEYS.referrerClickTimestampServerSeconds), + installBeginTimestampServerSeconds = j.getLong(AffiseReferrerData.KEYS.installBeginTimestampServerSeconds), + installVersion = j.getString(AffiseReferrerData.KEYS.installVersion), + googlePlayInstantParam = j.getBoolean(AffiseReferrerData.KEYS.googlePlayInstantParam), + ) + } catch (e: Exception) { + //log error + logsManager.addSdkError(e) + null + } + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/converter/StringToMD5Converter.kt b/attribution/src/main/java/com/affise/attribution/converter/StringToMD5Converter.kt new file mode 100644 index 0000000..3d1eb26 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/converter/StringToMD5Converter.kt @@ -0,0 +1,61 @@ +package com.affise.attribution.converter + +import com.affise.attribution.logs.LogsManager +import java.math.BigInteger +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException + +/** + * Converter String to Md5 String + * + * @property logsManager for error logging + */ +class StringToMD5Converter( + private val logsManager: LogsManager +) : Converter { + + /** + * Convert [from] to Md5 + * + * @return value of md5 + */ + override fun convert(from: String) = generateMd5(from) + + /** + * Generate md5 from [value] + * + * @return value of md5 + */ + private fun generateMd5(value: String): String { + //Get digits + val digits = try { + with(MessageDigest.getInstance(ALGORITHM_NAME)) { + reset() + update(value.toByteArray()) + digest() + } + } catch (e: NoSuchAlgorithmException) { + //Log error + logsManager.addSdkError(e) + + byteArrayOf() + } + + //Convert digits to BigInteger + val bigInt = BigInteger(1, digits) + + //Create stringBuilder + val stringBuilder = StringBuilder(bigInt.toString(16)) + + //Filling stringBuilder + while (stringBuilder.length < 32) { + stringBuilder.insert(0, "0") + } + + return stringBuilder.toString() + } + + companion object { + private const val ALGORITHM_NAME = "MD5" + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/converter/StringToSHA1Converter.kt b/attribution/src/main/java/com/affise/attribution/converter/StringToSHA1Converter.kt new file mode 100644 index 0000000..75267e3 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/converter/StringToSHA1Converter.kt @@ -0,0 +1,31 @@ +package com.affise.attribution.converter + +import java.security.MessageDigest + +/** + * Converter String to Sha1 String + */ +class StringToSHA1Converter : Converter { + + /** + * Convert [from] to Sha1 + * + * @return value of Sha1 + */ + override fun convert(from: String) = generateSha1(from) + + /** + * Generate sha1 from [value] + * + * @return value of Sha1 + */ + private fun generateSha1(value: String) = MessageDigest.getInstance(ALGORITHM_NAME) + .digest(value.toByteArray()) + .joinToString("") { + "%02x".format(it) + } + + companion object { + private const val ALGORITHM_NAME = "SHA-1" + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/converter/StringToSHA256Converter.kt b/attribution/src/main/java/com/affise/attribution/converter/StringToSHA256Converter.kt new file mode 100644 index 0000000..e0ec4ed --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/converter/StringToSHA256Converter.kt @@ -0,0 +1,31 @@ +package com.affise.attribution.converter + +import java.security.MessageDigest + +/** + * Converter String to Sha256 String + */ +class StringToSHA256Converter : Converter { + + /** + * Convert [from] to Sha256 + * + * @return value of Sha256 + */ + override fun convert(from: String) = generateSha256(from) + + /** + * Generate sha1 from [value] + * + * @return value of Sha256 + */ + private fun generateSha256(value: String) = MessageDigest.getInstance(ALGORITHM_NAME) + .digest(value.toByteArray()) + .joinToString("") { + "%02x".format(it) + } + + companion object { + private const val ALGORITHM_NAME = "SHA-256" + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/deeplink/DeeplinkClickRepository.kt b/attribution/src/main/java/com/affise/attribution/deeplink/DeeplinkClickRepository.kt new file mode 100644 index 0000000..7461bbf --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/deeplink/DeeplinkClickRepository.kt @@ -0,0 +1,26 @@ +package com.affise.attribution.deeplink + +/** + * Storage for isDeeplink model to determine if app was opened by deeplink + */ +interface DeeplinkClickRepository { + /** + * Sets flag [isDeeplink] describing if app was opened by deeplink + */ + fun setDeeplinkClick(isDeeplink: Boolean) + + /** + * Returns flag describing if app was opened by deeplink + */ + fun isDeeplinkClick(): Boolean + + /** + * Store deeplink that has been used to open this app + */ + fun setDeeplink(deeplink: String) + + /** + * returns deeplink that has been used to open this app or null + */ + fun getDeeplink(): String? +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/deeplink/DeeplinkClickRepositoryImpl.kt b/attribution/src/main/java/com/affise/attribution/deeplink/DeeplinkClickRepositoryImpl.kt new file mode 100644 index 0000000..2eb3351 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/deeplink/DeeplinkClickRepositoryImpl.kt @@ -0,0 +1,24 @@ +package com.affise.attribution.deeplink + +/** + * Implementation of [DeeplinkClickRepository] + */ +class DeeplinkClickRepositoryImpl : DeeplinkClickRepository { + private var isDeeplinkClick: Boolean = false + private var _deeplink: String? = null + override fun setDeeplinkClick(isDeeplink: Boolean) { + this.isDeeplinkClick = isDeeplink + } + + override fun isDeeplinkClick(): Boolean { + return isDeeplinkClick + } + + override fun setDeeplink(deeplink: String) { + _deeplink = deeplink + } + + override fun getDeeplink(): String? { + return _deeplink + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/deeplink/DeeplinkManager.kt b/attribution/src/main/java/com/affise/attribution/deeplink/DeeplinkManager.kt new file mode 100644 index 0000000..8e47047 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/deeplink/DeeplinkManager.kt @@ -0,0 +1,23 @@ +package com.affise.attribution.deeplink + +import android.net.Uri + +/** + * Manager that coordinates deeplink related tasks + */ +interface DeeplinkManager { + /** + * Needs to be called upon app init to properly initialize manager + */ + fun init() + + /** + * Sets [callback] to invoke when app receives deeplink + */ + fun setDeeplinkCallback(callback: OnDeeplinkCallback) + + /** + * Process [uri] as deeplink, returns [Boolean] indicating if deeplink is processed successfully + */ + fun handleDeeplink(uri: Uri): Boolean +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/deeplink/DeeplinkManagerImpl.kt b/attribution/src/main/java/com/affise/attribution/deeplink/DeeplinkManagerImpl.kt new file mode 100644 index 0000000..ff3d939 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/deeplink/DeeplinkManagerImpl.kt @@ -0,0 +1,61 @@ +package com.affise.attribution.deeplink + +import android.net.Uri +import com.affise.attribution.init.InitPropertiesStorage +import com.affise.attribution.parameters.DeeplinkClickPropertyProvider +import com.affise.attribution.utils.ActivityActionsManager +import com.affise.attribution.utils.ActivityLifecycleCallback + +/** + * Implementation for [DeeplinkManager] + * + * @property initProperties model to retrieve affise app id from + * @property isDeeplinkRepository repository that stores isDeeplinkClick property, used by [DeeplinkClickPropertyProvider] + * @property activityActionsManager listeners for changes activity + */ +internal class DeeplinkManagerImpl( + private val initProperties: InitPropertiesStorage, + private val isDeeplinkRepository: DeeplinkClickRepository, + private val activityActionsManager: ActivityActionsManager +) : DeeplinkManager { + + /** + * Listener for resume activities + */ + private var onResumeSubscription: ActivityLifecycleCallback? = null + + /** + * Callback that is going to be triggered when deeplink is received by application + */ + private var deeplinkCallback: OnDeeplinkCallback? = null + + @Synchronized + override fun init() { + //Check started listener for resume activities + if (onResumeSubscription == null) { + //Create listener for resume activities + onResumeSubscription = ActivityLifecycleCallback { + with(it.intent) { + data?.let { uri -> + if (handleDeeplink(uri)) { + this.data = null + } + } + } + }.apply { + //Add listener for resume activities + activityActionsManager.addOnActivityResumedListener(this) + } + } + } + + override fun setDeeplinkCallback(callback: OnDeeplinkCallback) { + deeplinkCallback = callback + } + + override fun handleDeeplink(uri: Uri): Boolean { + isDeeplinkRepository.setDeeplinkClick(true) + isDeeplinkRepository.setDeeplink(uri.toString()) + return deeplinkCallback?.handleDeeplink(uri) ?: false + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/deeplink/InstallReferrerToDeeplinkUriConverter.kt b/attribution/src/main/java/com/affise/attribution/deeplink/InstallReferrerToDeeplinkUriConverter.kt new file mode 100644 index 0000000..bd004dd --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/deeplink/InstallReferrerToDeeplinkUriConverter.kt @@ -0,0 +1,38 @@ +package com.affise.attribution.deeplink + +import android.net.Uri +import com.affise.attribution.converter.Converter + +/** + * Implementation of [Converter] + * + * Converts uri string to [Uri], extracting deeplink from uri parameter + */ +class InstallReferrerToDeeplinkUriConverter : Converter { + override fun convert(from: String): Uri? { + val uri = Uri.parse(from) + return extractDeeplinkFromAbsolute(uri) ?: extractDeeplinkFromRelative(from) + } + + private fun extractDeeplinkFromRelative(from: String): Uri? = + try { + Uri.Builder() + .encodedQuery(from) + .build() + .let(::extractDeeplinkFromAbsolute) + } catch (e: Exception) { + null + } + + private fun extractDeeplinkFromAbsolute(from: Uri): Uri? = + try { + from.getQueryParameter(DEEPLINK_PARAM_NAME) + ?.let(Uri::parse) + } catch (e: Exception) { + null + } + + private companion object { + const val DEEPLINK_PARAM_NAME = "deeplink" + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/deeplink/OnDeeplinkCallback.kt b/attribution/src/main/java/com/affise/attribution/deeplink/OnDeeplinkCallback.kt new file mode 100644 index 0000000..740e8cb --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/deeplink/OnDeeplinkCallback.kt @@ -0,0 +1,13 @@ +package com.affise.attribution.deeplink + +import android.net.Uri + +/** + * Interface describing callback that is going to be triggered when deeplink is received by application + */ +fun interface OnDeeplinkCallback { + /** + * Triggered when new deeplink [uri] is received by application with + */ + fun handleDeeplink(uri: Uri): Boolean +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/Event.kt b/attribution/src/main/java/com/affise/attribution/events/Event.kt new file mode 100644 index 0000000..298bb3f --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/Event.kt @@ -0,0 +1,73 @@ +package com.affise.attribution.events + +import com.affise.attribution.events.predefined.PredefinedParameters +import org.json.JSONObject + +/** + * Base event + */ +abstract class Event { + + /** + * Event predefined parameters + */ + private val predefinedParameters = mutableMapOf() + + /** + * Is first for user, default false + */ + private var firstForUser: Boolean = false + + /** + * Serialize event to JSONObject + * + * @return JSONObject + */ + abstract fun serialize(): JSONObject + + /** + * Name of event + * + * @return name + */ + abstract fun getName(): String + + /** + * Category of event + * + * @return category + */ + abstract fun getCategory(): String + + /** + * User data + * + * @return userData + */ + abstract fun getUserData(): String? + + /** + * Is first for user, default false + * + * @return is first for user or not + */ + fun isFirstForUser(): Boolean = firstForUser + + fun setFirstForUser(firstForUser: Boolean) { + this.firstForUser = firstForUser + } + + /** + * Add predefined [parameter] with [value] to event + */ + fun addPredefinedParameter(parameter: PredefinedParameters, value: String) { + predefinedParameters[parameter] = value + } + + /** + * Get map of predefined parameter + * + * @return map of predefined parameter + */ + fun getPredefinedParameters(): Map = predefinedParameters.toMap() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/EventToSerializedEventConverter.kt b/attribution/src/main/java/com/affise/attribution/events/EventToSerializedEventConverter.kt new file mode 100644 index 0000000..6644460 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/EventToSerializedEventConverter.kt @@ -0,0 +1,55 @@ +package com.affise.attribution.events + +import com.affise.attribution.converter.Converter +import com.affise.attribution.parameters.Parameters +import com.affise.attribution.utils.generateUUID +import com.affise.attribution.utils.timestamp +import org.json.JSONObject + +/** + * Converter Event to SerializedEvent + */ +class EventToSerializedEventConverter : Converter { + + /** + * Convert [from] Event to SerializedEvent + */ + override fun convert(from: Event): SerializedEvent { + //Generate id + val id = generateUUID().toString() + + //Create JSONObject + val json = JSONObject().apply { + //Add Id + put(Parameters.AFFISE_EVENT_ID, id) + + //Add name + put(Parameters.AFFISE_EVENT_NAME, from.getName()) + + //Add category + put(Parameters.AFFISE_EVENT_CATEGORY, from.getCategory()) + + //Add timestamp + put(Parameters.AFFISE_EVENT_TIMESTAMP, timestamp()) + + //Add is first for user Or not + put(Parameters.AFFISE_EVENT_FIRST_FOR_USER, from.isFirstForUser()) + + //Add user data + put(Parameters.AFFISE_EVENT_USER_DATA, from.getUserData()) + + //Add event data + put(Parameters.AFFISE_EVENT_DATA, from.serialize()) + //Add predefined parameters + put( + Parameters.AFFISE_PARAMETERS, + from.getPredefinedParameters() + .mapKeys { it.key.value } + .let(::JSONObject) + ) + } + + //Create SerializedEvent + return SerializedEvent(id, json) + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/EventsManager.kt b/attribution/src/main/java/com/affise/attribution/events/EventsManager.kt new file mode 100644 index 0000000..7a36c92 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/EventsManager.kt @@ -0,0 +1,108 @@ +package com.affise.attribution.events + +import com.affise.attribution.session.CurrentActiveActivityCountProvider +import com.affise.attribution.usecase.SendDataToServerUseCase +import java.util.Timer +import java.util.TimerTask + +/** + * Manager of Events + * + * @property sendDataToServerUseCase the use case sending data to server. + * @property activityCountProvider the provider observe count of open activity + */ +class EventsManager( + private val sendDataToServerUseCase: SendDataToServerUseCase, + private val activityCountProvider: CurrentActiveActivityCountProvider +) { + + /** + * Last session count + */ + private var lastSessionCount = 0L + + /** + * Timer fo repeat send events + */ + private var timer: Timer? = null + + private var isAllowed: Boolean = false + + /** + * Start manager + */ + fun init() { + subscribeToActivityEvents() + sendEventsOnStart() + } + + fun sendEventsOnStart() { + //Allow send events + isAllowed = true + + //Send events on activity started + sendEvents(withDelay = false) + + //Start timer fo repeat send events + startTimer() + } + + /** + * Subscribe to change open activity count + */ + private fun subscribeToActivityEvents() { + activityCountProvider.addActivityCountListener { count -> + //Check if activity closed + if (lastSessionCount == 1L && count == 0L) { + //Stop timer + stopTimer() + } + + lastSessionCount = count + } + } + + /** + * Send event + */ + fun sendEvents(withDelay: Boolean = true) { + sendDataToServerUseCase.send(withDelay) + } + + /** + * Start timer fo repeat send events + */ + private fun startTimer() { + //Stop timer if running + timer?.let { + stopTimer() + } + + //Create timer + timer = Timer() + + //Start timer + timer?.schedule(object : TimerTask() { + override fun run() { + //Send events + sendEvents() + + //Stop timer + stopTimer() + } + }, TIME_SEND_REPEAT) + } + + /** + * Stop timer fo repeat send events + */ + private fun stopTimer() { + //Stop timer + timer?.cancel() + timer = null + } + + companion object { + const val TIME_SEND_REPEAT = 15 * 1000L + } +} diff --git a/attribution/src/main/java/com/affise/attribution/events/EventsParams.kt b/attribution/src/main/java/com/affise/attribution/events/EventsParams.kt new file mode 100644 index 0000000..14ea58e --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/EventsParams.kt @@ -0,0 +1,7 @@ +package com.affise.attribution.events + +object EventsParams { + const val EVENTS_DIR_NAME = "affise-events" + const val EVENTS_STORE_TIME = 7 * 24 * 60 * 60 * 1000 + const val EVENTS_SEND_COUNT = 100 +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/EventsRepository.kt b/attribution/src/main/java/com/affise/attribution/events/EventsRepository.kt new file mode 100644 index 0000000..ca9657c --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/EventsRepository.kt @@ -0,0 +1,37 @@ +package com.affise.attribution.events + +/** + * Events repository interface + */ +internal interface EventsRepository { + + /** + * Has save events by [url] or not + */ + fun hasEvents(url: String): Boolean + + /** + * Event recording for each url + */ + fun storeEvent(event: Event, urls: List) + + /** + * Web Event recording for each url + */ + fun storeWebEvent(event: String, urls: List) + + /** + * Get event in dir + */ + fun getEvents(url: String): List + + /** + * Delete events in dir + */ + fun deleteEvent(ids: List, url: String) + + /** + * Removes all events + */ + fun clear() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/EventsRepositoryImpl.kt b/attribution/src/main/java/com/affise/attribution/events/EventsRepositoryImpl.kt new file mode 100644 index 0000000..fbcfd8b --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/EventsRepositoryImpl.kt @@ -0,0 +1,96 @@ +package com.affise.attribution.events + +import com.affise.attribution.converter.Converter +import com.affise.attribution.logs.LogsManager +import com.affise.attribution.parameters.Parameters +import com.affise.attribution.storages.EventsStorage +import org.json.JSONObject + +/** + * Events repository provide write, read and delete events. + * + * @property converterToBase64 convert string to encoding Base64 string + * @property converterToSerializedEvent to convert Event to SerializedEvent + * @property logsManager for error logging + * @property eventsStorage storage of events + */ +internal class EventsRepositoryImpl( + private val converterToBase64: Converter, + private val converterToSerializedEvent: Converter, + private val logsManager: LogsManager, + private val eventsStorage: EventsStorage +) : EventsRepository { + + /** + * Has save events by [url] or not + */ + override fun hasEvents(url: String) = eventsStorage.hasEvents( + converterToBase64.convert(url) + ) + + /** + * Store [event] by [urls] + */ + override fun storeEvent(event: Event, urls: List) { + //For al urls + urls.forEach { + //Save event + eventsStorage.saveEvent( + converterToBase64.convert(it), + converterToSerializedEvent.convert(event) + ) + } + } + + /** + * Store web[event] by [urls] + */ + override fun storeWebEvent(event: String, urls: List) = try { + //Create json + val data = JSONObject(event) + + //Gei event id + val id = data.getString(Parameters.AFFISE_EVENT_ID) + + //Create serialized event + val serializedEvent = SerializedEvent(id, data) + + //For al urls + urls.forEach { + //Save event + eventsStorage.saveEvent( + converterToBase64.convert(it), + serializedEvent + ) + } + } catch (throwable: Throwable) { + //Log error + logsManager.addUserError(throwable) + } + + /** + * Get serialized events by [url] + * + * @return list of serialized events + */ + override fun getEvents(url: String): List = eventsStorage.getEvents( + converterToBase64.convert(url) + ) + + /** + * Delete event for [url] by [ids] + */ + override fun deleteEvent(ids: List, url: String) { + eventsStorage.deleteEvent( + converterToBase64.convert(url), + ids + ) + } + + /** + * Removes all events + */ + override fun clear() { + eventsStorage.clear() + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/GDPREventRepository.kt b/attribution/src/main/java/com/affise/attribution/events/GDPREventRepository.kt new file mode 100644 index 0000000..ce1cf33 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/GDPREventRepository.kt @@ -0,0 +1,43 @@ +package com.affise.attribution.events + +import android.annotation.SuppressLint +import android.content.SharedPreferences +import com.affise.attribution.converter.Converter +import com.affise.attribution.events.predefined.GDPREvent +import org.json.JSONObject + +/** + * Stores [GDPREvent] indicating is forget me event should be sent + */ +internal class GDPREventRepository( + private val sharedPreferences: SharedPreferences, + private val eventToSerializedEventConverter: Converter +) { + @SuppressLint("ApplySharedPref") + fun setEvent(event: GDPREvent) { + sharedPreferences.edit().apply { + val serialized = eventToSerializedEventConverter.convert(event) + putString(GDPR_EVENT_ID_KEY, serialized.id) + putString(GDPR_EVENT_CONTENT_KEY, serialized.data.toString()) + }.commit() + } + + fun getEvent(): SerializedEvent? { + val id = sharedPreferences.getString(GDPR_EVENT_ID_KEY, null) ?: return null + val content = sharedPreferences.getString(GDPR_EVENT_CONTENT_KEY, null) ?: return null + return SerializedEvent(id, JSONObject(content)) + } + + @SuppressLint("ApplySharedPref") + fun clear() { + sharedPreferences.edit().apply { + remove(GDPR_EVENT_ID_KEY) + remove(GDPR_EVENT_CONTENT_KEY) + }.commit() + } + + companion object { + private const val GDPR_EVENT_ID_KEY = "com.affise.attribution.usecase.GDPR_EVENT.id" + private const val GDPR_EVENT_CONTENT_KEY = "com.affise.attribution.usecase.GDPR_EVENT.content" + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/IsFirstForUserUseCase.kt b/attribution/src/main/java/com/affise/attribution/events/IsFirstForUserUseCase.kt new file mode 100644 index 0000000..6bcb93e --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/IsFirstForUserUseCase.kt @@ -0,0 +1,17 @@ +package com.affise.attribution.events + +/** + * Event use case for IsFirstForUser + */ +interface IsFirstForUserUseCase { + + /** + * Update IsFirstForUser + */ + fun updateEvent(event: Event) + + /** + * Update IsFirstForUser for webBridge + */ + fun updateWebEvent(event: String): String +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/IsFirstForUserUseCaseImpl.kt b/attribution/src/main/java/com/affise/attribution/events/IsFirstForUserUseCaseImpl.kt new file mode 100644 index 0000000..0bf5bff --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/IsFirstForUserUseCaseImpl.kt @@ -0,0 +1,55 @@ +package com.affise.attribution.events + +import com.affise.attribution.parameters.Parameters +import com.affise.attribution.storages.IsFirstForUserStorage +import org.json.JSONObject + +/** + * Event use case for IsFirstForUser + * + * @property isFirstForUserStorage storage of already send events + */ +internal class IsFirstForUserUseCaseImpl( + private val isFirstForUserStorage: IsFirstForUserStorage +) : IsFirstForUserUseCase { + + /** + * Cache of already send events + */ + private var cache: MutableList = mutableListOf() + + init { + // Init cache of already send events + cache = isFirstForUserStorage.getEventsNames().toMutableList() + } + + /** + * Update IsFirstForUser + */ + override fun updateEvent(event: Event) { + val eventClass = event.javaClass.simpleName + if (cache.contains(eventClass)) { + event.setFirstForUser(false) + } else { + cache.add(eventClass) + isFirstForUserStorage.add(eventClass) + event.setFirstForUser(true) + } + } + + /** + * Update IsFirstForUser + */ + override fun updateWebEvent(event: String): String { + val eventJson = JSONObject(event) + val eventClass = eventJson.classOfEvent()?.simpleName ?: return event + if (cache.contains(eventClass)) { + eventJson.putOpt(Parameters.AFFISE_EVENT_FIRST_FOR_USER, false) + } else { + cache.add(eventClass) + isFirstForUserStorage.add(eventClass) + eventJson.putOpt(Parameters.AFFISE_EVENT_FIRST_FOR_USER, true) + } + return eventJson.toString() + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/JsonEventClass.kt b/attribution/src/main/java/com/affise/attribution/events/JsonEventClass.kt new file mode 100644 index 0000000..79db679 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/JsonEventClass.kt @@ -0,0 +1,96 @@ +package com.affise.attribution.events + +import com.affise.attribution.events.predefined.* +import com.affise.attribution.events.subscription.* +import com.affise.attribution.parameters.Parameters +import org.json.JSONObject + +fun JSONObject.classOfEvent(): Class? { + val name = this.optString(Parameters.AFFISE_EVENT_NAME) + val subtype = this.optJSONObject(Parameters.AFFISE_EVENT_DATA)?.optString(SubscriptionParameters.AFFISE_SUBSCRIPTION_EVENT_TYPE_KEY) + return when (name) { + "AchieveLevel" -> AchieveLevelEvent::class.java + "AddPaymentInfo" -> AddPaymentInfoEvent::class.java + "AddToCart" -> AddToCartEvent::class.java + "AddToWishlist" -> AddToWishlistEvent::class.java + "ClickAdv" -> ClickAdvEvent::class.java + "CompleteRegistration" -> CompleteRegistrationEvent::class.java + "CompleteStream" -> CompleteStreamEvent::class.java + "CompleteTrial" -> CompleteTrialEvent::class.java + "CompleteTutorial" -> CompleteTutorialEvent::class.java + "ContentItemsView" -> ContentItemsViewEvent::class.java + "CustomId01" -> CustomId01Event::class.java + "CustomId02" -> CustomId02Event::class.java + "CustomId03" -> CustomId03Event::class.java + "CustomId04" -> CustomId04Event::class.java + "CustomId05" -> CustomId05Event::class.java + "CustomId06" -> CustomId06Event::class.java + "CustomId07" -> CustomId07Event::class.java + "CustomId08" -> CustomId08Event::class.java + "CustomId09" -> CustomId09Event::class.java + "CustomId10" -> CustomId10Event::class.java + "DeepLinked" -> DeepLinkedEvent::class.java + "InitiatePurchase" -> InitiatePurchaseEvent::class.java + "InitiateStream" -> InitiateStreamEvent::class.java + "Invite" -> InviteEvent::class.java + "LastAttributedTouch" -> LastAttributedTouchEvent::class.java + "ListView" -> ListViewEvent::class.java + "Login" -> LoginEvent::class.java + "OpenedFromPushNotification" -> OpenedFromPushNotificationEvent::class.java + "Purchase" -> PurchaseEvent::class.java + "Rate" -> RateEvent::class.java + "ReEngage" -> ReEngageEvent::class.java + "Reserve" -> ReserveEvent::class.java + "Sales" -> SalesEvent::class.java + "Search" -> SearchEvent::class.java + "Share" -> ShareEvent::class.java + "SpendCredits" -> SpendCreditsEvent::class.java + "StartRegistration" -> StartRegistrationEvent::class.java + "StartTrial" -> StartTrialEvent::class.java + "StartTutorial" -> StartTutorialEvent::class.java + "Subscribe" -> SubscribeEvent::class.java + "TravelBooking" -> TravelBookingEvent::class.java + "UnlockAchievement" -> UnlockAchievementEvent::class.java + "Unsubscribe" -> UnsubscribeEvent::class.java + "Update" -> UpdateEvent::class.java + "ViewAdv" -> ViewAdvEvent::class.java + "ViewCart" -> ViewCartEvent::class.java + "ViewItem" -> ViewItemEvent::class.java + "ViewItems" -> ViewItemsEvent::class.java + SubscriptionParameters.AFFISE_UNSUBSCRIPTION, + SubscriptionParameters.AFFISE_SUBSCRIPTION_ACTIVATION, + SubscriptionParameters.AFFISE_SUBSCRIPTION_CANCELLATION, + SubscriptionParameters.AFFISE_SUBSCRIPTION_ENTERED_BILLING_RETRY, + SubscriptionParameters.AFFISE_SUBSCRIPTION_FIRST_CONVERSION, + SubscriptionParameters.AFFISE_SUBSCRIPTION_REACTIVATION, + SubscriptionParameters.AFFISE_SUBSCRIPTION_RENEWAL, + SubscriptionParameters.AFFISE_SUBSCRIPTION_RENEWAL_FROM_BILLING_RETRY -> subscriptionEventClass(subtype) + else -> null + } +} + +private fun subscriptionEventClass(subtype: String?): Class? { + return when (subtype) { + SubscriptionParameters.AFFISE_SUB_INITIAL_SUBSCRIPTION -> InitialSubscriptionEvent::class.java + SubscriptionParameters.AFFISE_SUB_INITIAL_TRIAL -> InitialTrialEvent::class.java + SubscriptionParameters.AFFISE_SUB_INITIAL_OFFER -> InitialOfferEvent::class.java + SubscriptionParameters.AFFISE_SUB_FAILED_TRIAL -> FailedTrialEvent::class.java + SubscriptionParameters.AFFISE_SUB_FAILED_OFFERISE -> FailedOfferiseEvent::class.java + SubscriptionParameters.AFFISE_SUB_FAILED_SUBSCRIPTION -> FailedSubscriptionEvent::class.java + SubscriptionParameters.AFFISE_SUB_FAILED_TRIAL_FROM_RETRY -> FailedTrialFromRetryEvent::class.java + SubscriptionParameters.AFFISE_SUB_FAILED_OFFER_FROM_RETRY -> FailedOfferFromRetryEvent::class.java + SubscriptionParameters.AFFISE_SUB_FAILED_SUBSCRIPTION_FROM_RETRY -> FailedSubscriptionFromRetryEvent::class.java + SubscriptionParameters.AFFISE_SUB_TRIAL_IN_RETRY -> TrialInRetryEvent::class.java + SubscriptionParameters.AFFISE_SUB_OFFER_IN_RETRY -> OfferInRetryEvent::class.java + SubscriptionParameters.AFFISE_SUB_SUBSCRIPTION_IN_RETRY -> SubscriptionInRetryEvent::class.java + SubscriptionParameters.AFFISE_SUB_CONVERTED_TRIAL -> ConvertedTrialEvent::class.java + SubscriptionParameters.AFFISE_SUB_CONVERTED_OFFER -> ConvertedOfferEvent::class.java + SubscriptionParameters.AFFISE_SUB_REACTIVATED_SUBSCRIPTION -> ReactivatedSubscriptionEvent::class.java + SubscriptionParameters.AFFISE_SUB_RENEWED_SUBSCRIPTION -> RenewedSubscriptionEvent::class.java + SubscriptionParameters.AFFISE_SUB_CONVERTED_TRIAL_FROM_RETRY -> ConvertedTrialFromRetryEvent::class.java + SubscriptionParameters.AFFISE_SUB_CONVERTED_OFFER_FROM_RETRY -> ConvertedOfferFromRetryEvent::class.java + SubscriptionParameters.AFFISE_SUB_RENEWED_SUBSCRIPTION_FROM_RETRY -> RenewedSubscriptionFromRetryEvent::class.java + SubscriptionParameters.AFFISE_SUB_UNSUBSCRIPTION -> UnsubscriptionEvent::class.java + else -> null + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/NativeEvent.kt b/attribution/src/main/java/com/affise/attribution/events/NativeEvent.kt new file mode 100644 index 0000000..c4cf8d0 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/NativeEvent.kt @@ -0,0 +1,5 @@ +package com.affise.attribution.events + +abstract class NativeEvent : Event() { + override fun getCategory(): String = "native" +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/SerializedEvent.kt b/attribution/src/main/java/com/affise/attribution/events/SerializedEvent.kt new file mode 100644 index 0000000..84b6b8c --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/SerializedEvent.kt @@ -0,0 +1,18 @@ +package com.affise.attribution.events + +import org.json.JSONObject + +/** + * Serialized event to store + */ +data class SerializedEvent( + /** + * Event id + */ + val id: String, + + /** + * Event data + */ + val data: JSONObject +) \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/StoreEventUseCase.kt b/attribution/src/main/java/com/affise/attribution/events/StoreEventUseCase.kt new file mode 100644 index 0000000..809db51 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/StoreEventUseCase.kt @@ -0,0 +1,17 @@ +package com.affise.attribution.events + +/** + * Event use case for store events + */ +interface StoreEventUseCase { + + /** + * Store event + */ + fun storeEvent(event: Event) + + /** + * Store web event + */ + fun storeWebEvent(event: String) +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/StoreEventUseCaseImpl.kt b/attribution/src/main/java/com/affise/attribution/events/StoreEventUseCaseImpl.kt new file mode 100644 index 0000000..b23e07c --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/StoreEventUseCaseImpl.kt @@ -0,0 +1,83 @@ +package com.affise.attribution.events + +import com.affise.attribution.events.predefined.GDPREvent +import com.affise.attribution.executors.ExecutorServiceProvider +import com.affise.attribution.logs.LogsManager +import com.affise.attribution.network.CloudConfig +import com.affise.attribution.preferences.models.BackgroundTrackingDisabledException +import com.affise.attribution.usecase.PreferencesUseCaseImpl +import com.affise.attribution.preferences.models.TrackingDisabledException +import com.affise.attribution.session.CurrentActiveActivityCountProvider + +/** + * Event use case for store events on device + * + * @property executorServiceProvider an Executor that provides methods to manage termination and methods + * @property repository the events repository provide write, read and delete events. + * @property eventsManager the manager to work with events + */ +internal class StoreEventUseCaseImpl( + private val executorServiceProvider: ExecutorServiceProvider, + private val repository: EventsRepository, + private val eventsManager: EventsManager, + private val preferencesUseCase: PreferencesUseCaseImpl, + private val activityCountProvider: CurrentActiveActivityCountProvider, + private val logsManager: LogsManager, + private val isFirstForUserUseCase: IsFirstForUserUseCase +) : StoreEventUseCase { + + /** + * Store [event] from app + */ + override fun storeEvent(event: Event) { + if (isTrackingEnabled() || event !is GDPREvent) { + executorServiceProvider.provideExecutorService().execute { + //Update event for isFirstForUser + isFirstForUserUseCase.updateEvent(event) + + //Save event + repository.storeEvent(event, CloudConfig.getUrls()) + + //Send events + eventsManager.sendEvents() + } + } + } + + /** + * Store [event] from webBridge + */ + override fun storeWebEvent(event: String) { + if (isTrackingEnabled() || event != GDPREvent.EVENT_NAME) { + executorServiceProvider.provideExecutorService().execute { + //Update event for isFirstForUser + val updatedEvent = isFirstForUserUseCase.updateWebEvent(event) + + //Save event + repository.storeWebEvent(updatedEvent, CloudConfig.getUrls()) + + //Send events + eventsManager.sendEvents() + } + } + } + + private fun isTrackingEnabled(): Boolean { + if (!preferencesUseCase.isTrackingEnabled()) { + logsManager.addSdkError(TrackingDisabledException()) + return false + } + if (isAppInBackground()) { + if (!preferencesUseCase.isBackgroundTrackingEnabled()) { + logsManager.addSdkError(BackgroundTrackingDisabledException()) + return false + } + } + + return true + } + + private fun isAppInBackground(): Boolean { + return activityCountProvider.getActivityCount() >= 0 + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/autoCatchingClick/AutoCatchingClickData.kt b/attribution/src/main/java/com/affise/attribution/events/autoCatchingClick/AutoCatchingClickData.kt new file mode 100644 index 0000000..db7010a --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/autoCatchingClick/AutoCatchingClickData.kt @@ -0,0 +1,24 @@ +package com.affise.attribution.events.autoCatchingClick + +class AutoCatchingClickData( + + /** + * View id + */ + val id: String? = null, + + /** + * View text + */ + val text: String? = null, + + /** + * View tag + */ + val tag: String? = null, + + /** + * View type + */ + val typeView: String? = null, +) \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/autoCatchingClick/AutoCatchingClickEvent.kt b/attribution/src/main/java/com/affise/attribution/events/autoCatchingClick/AutoCatchingClickEvent.kt new file mode 100644 index 0000000..fa6cc85 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/autoCatchingClick/AutoCatchingClickEvent.kt @@ -0,0 +1,46 @@ +package com.affise.attribution.events.autoCatchingClick + +import com.affise.attribution.converter.StringToSHA1Converter +import com.affise.attribution.events.Event +import org.json.JSONArray +import org.json.JSONObject + +class AutoCatchingClickEvent( + private val isGroup: Boolean, + private val data: List, + private val activityName: String, + private val timeStampMillis: Long = System.currentTimeMillis() +) : Event() { + + override fun serialize(): JSONObject = JSONObject().apply { + val serializeData = JSONArray().apply { + data.forEach { + val item = JSONObject().apply { + put("id", it.id ?: "") + put("tag", it.tag ?: "") + put("text", it.text ?: "") + put("view", it.typeView ?: "") + } + + put(item) + } + } + + put("affise_event_auto_catching_group", isGroup) + put("affise_event_auto_catching_activity", activityName) + put("affise_event_auto_catching_click", serializeData) + put("affise_event_auto_catching_click_timestamp", timeStampMillis) + } + + override fun getName(): String = "AutoCatchingClickEvent_${getEventSha1()}" + + override fun getCategory(): String = "autoNative" + + override fun getUserData(): String = "Auto generate even on click" + + private fun getEventSha1() = StringToSHA1Converter().convert( + data.fold(activityName) { acc, value -> + acc + (value.id ?: "") + } + ) +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/autoCatchingClick/AutoCatchingClickProvider.kt b/attribution/src/main/java/com/affise/attribution/events/autoCatchingClick/AutoCatchingClickProvider.kt new file mode 100644 index 0000000..0847dae --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/autoCatchingClick/AutoCatchingClickProvider.kt @@ -0,0 +1,157 @@ +package com.affise.attribution.events.autoCatchingClick + +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import com.affise.attribution.events.StoreEventUseCase +import com.affise.attribution.utils.ActivityActionsManager +import com.affise.attribution.utils.ActivityClickCallback + +/** + * The provider that handles the interception of clicks on activity. + * Collects data on pressed views depending on the selected types for auto catching + * + * @property storeEventUseCase use case for store events + * @property activityActionsManager manager for handling events occurring on the activity + */ +internal class AutoCatchingClickProvider( + private val storeEventUseCase: StoreEventUseCase, + private val activityActionsManager: ActivityActionsManager +) { + + /** + * Enabled types of auto catching click + */ + private var types: List? = null + + /** + * Listener to clicks on activities + */ + private var activityClickCallback: ActivityClickCallback? = null + + /** + * Start provider + */ + @Synchronized + fun init(types: List?) { + this.types = types + + //Check calcback created + if (activityClickCallback == null) { + //Create calback + activityClickCallback = ActivityClickCallback { activity, view -> + //Crate and check data from view + getDataView(view)?.let { data -> + //Store AutoCatchingClick event + storeEventUseCase.storeEvent( + AutoCatchingClickEvent( + view is ViewGroup, + data, + activity::class.java.simpleName + ) + ) + } + }.apply { + //Add calback + activityActionsManager.addOnActivityClickListener(this) + } + } + } + + /** + * Set types of auto catching click + */ + fun setTypes(types: List?) { + this.types = types + } + + /** + * Get data from [view] + */ + private fun getDataView(view: View, isRoot: Boolean = true): List? { + //Check enabled types for AutoCatchingClick + val types = types ?: return null + + //Get id name from resource + val idName = view.id + .takeIf { + //Take only view if it has name + it != View.NO_ID + } + ?.runCatching { + //Find name in resources + view.resources?.getResourceEntryName(this) + } + ?.getOrNull() + ?.ifEmpty { null } + + //Get tag from view + val tag = view.tag?.toString() + + //View name + val canonicalName = view::class.java.canonicalName ?: "" + + //View contentDescription + val description = view.contentDescription?.toString() + + //Get view type and return data from view + return when (view) { + is TextView -> listOf( + when { + //iCheck ib Button add to AutoCatchingClick event + canonicalName.contains(KEY_BUTTON) -> if (types.contains(AutoCatchingType.BUTTON)) { + AutoCatchingClickData( + idName, view.text?.toString(), tag, AutoCatchingType.BUTTON.name + ) + } else return null + //iCheck ib Text add to AutoCatchingClick event + types.contains(AutoCatchingType.TEXT) -> AutoCatchingClickData( + idName, view.text?.toString(), tag, AutoCatchingType.TEXT.name + ) + else -> return null + } + ) + is ImageView -> listOf( + when { + //iCheck ib Image Button add to AutoCatchingClick event + canonicalName.contains(KEY_BUTTON) -> if (types.contains(AutoCatchingType.IMAGE_BUTTON)) { + AutoCatchingClickData( + idName, description, tag, AutoCatchingType.IMAGE_BUTTON.name + ) + } else return null + //iCheck ib Image add to AutoCatchingClick event + types.contains(AutoCatchingType.IMAGE) -> AutoCatchingClickData( + idName, description, tag, AutoCatchingType.IMAGE.name + ) + else -> return null + } + ) + is ViewGroup -> { + //iCheck ib View Group add to AutoCatchingClick event + if (types.contains(AutoCatchingType.GROUP)) { + mutableListOf().apply { + if (isRoot) { + add( + AutoCatchingClickData( + idName, description, tag, AutoCatchingType.GROUP.name + ) + ) + } + addAll( + (0 until view.childCount).flatMap { + //Get data from all child views + getDataView(view.getChildAt(it), false) ?: emptyList() + } + ) + } + } else return null + } + else -> return null + } + } + + companion object { + private const val KEY_BUTTON = "Button" + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/autoCatchingClick/AutoCatchingType.kt b/attribution/src/main/java/com/affise/attribution/events/autoCatchingClick/AutoCatchingType.kt new file mode 100644 index 0000000..049c17d --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/autoCatchingClick/AutoCatchingType.kt @@ -0,0 +1,11 @@ +package com.affise.attribution.events.autoCatchingClick + +enum class AutoCatchingType { + BUTTON, + TEXT, + + IMAGE_BUTTON, + IMAGE, + + GROUP +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/AchieveLevelEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/AchieveLevelEvent.kt new file mode 100644 index 0000000..71fafbe --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/AchieveLevelEvent.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * When a user has some achieve level event. + * + * @property level the JSON Object describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class AchieveLevelEvent( + private val level: JSONObject, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize AchieveLevelEvent to JSONObject + * + * @return JSONObject of AchieveLevelEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_achieve_level", level) + put("affise_event_achieve_level_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "AchieveLevel" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/AddPaymentInfoEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/AddPaymentInfoEvent.kt new file mode 100644 index 0000000..971af6a --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/AddPaymentInfoEvent.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event AddPaymentInfo + * + * @property paymentInfo the JSON Object describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class AddPaymentInfoEvent( + private val paymentInfo: JSONObject, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize AddPaymentInfoEvent to JSONObject + * + * @return JSONObject of AddPaymentInfoEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_add_payment_info", paymentInfo) + put("affise_event_add_payment_info_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "AddPaymentInfo" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/AddToCartEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/AddToCartEvent.kt new file mode 100644 index 0000000..c6d38af --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/AddToCartEvent.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event AddToCart + * + * @property addToCartObject the JSON Object describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class AddToCartEvent( + private val addToCartObject: JSONObject?, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize AddToCartEvent to JSONObject + * + * @return JSONObject of AddToCartEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_add_to_cart", addToCartObject) + put("affise_event_add_to_cart_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "AddToCart" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/AddToWishlistEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/AddToWishlistEvent.kt new file mode 100644 index 0000000..ff487db --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/AddToWishlistEvent.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event AddToWishlist + * + * @property wishList the JSON Object describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class AddToWishlistEvent( + private val wishList: JSONObject, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize AddToWishlistEvent to JSONObject + * + * @return JSONObject of AddToWishlistEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_add_to_wishlist", wishList) + put("affise_event_add_to_wishlist_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "AddToWishlist" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/ClickAdvEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/ClickAdvEvent.kt new file mode 100644 index 0000000..1df69be --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/ClickAdvEvent.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event ClickAdv + * + * @property advertisement the describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class ClickAdvEvent( + private val advertisement: String, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize ClickAdvEvent to JSONObject + * + * @return JSONObject of ClickAdvEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_click_adv", advertisement) + put("affise_event_click_adv_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "ClickAdv" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/CompleteRegistrationEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/CompleteRegistrationEvent.kt new file mode 100644 index 0000000..8150ce9 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/CompleteRegistrationEvent.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event CompleteRegistration + * + * @property registration the JSON Object describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class CompleteRegistrationEvent( + private val registration: JSONObject, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize CompleteRegistration to JSONObject + * + * @return JSONObject of CompleteRegistration + */ + override fun serialize() = JSONObject().apply { + put("affise_event_complete_registration", registration) + put("affise_event_complete_registration_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "CompleteRegistration" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/CompleteStreamEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/CompleteStreamEvent.kt new file mode 100644 index 0000000..bcbbdd5 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/CompleteStreamEvent.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event CompleteStream + * + * @property stream the JSON Object describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class CompleteStreamEvent( + private val stream: JSONObject, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize CompleteStreamEvent to JSONObject + * + * @return JSONObject of CompleteStreamEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_complete_stream", stream) + put("affise_event_complete_stream_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "CompleteStream" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/CompleteTrialEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/CompleteTrialEvent.kt new file mode 100644 index 0000000..c204b85 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/CompleteTrialEvent.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event CompleteTrial + * + * @property trial the JSON Object describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class CompleteTrialEvent( + private val trial: JSONObject, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize CompleteTrialEvent to JSONObject + * + * @return JSONObject of CompleteTrialEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_complete_trial", trial) + put("affise_event_complete_trial_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "CompleteTrial" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/CompleteTutorialEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/CompleteTutorialEvent.kt new file mode 100644 index 0000000..f319fb0 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/CompleteTutorialEvent.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event CompleteTutorial + * + * @property tutorial the JSON Object describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class CompleteTutorialEvent( + private val tutorial: JSONObject, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize CompleteTutorialEvent to JSONObject + * + * @return JSONObject of CompleteTutorialEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_complete_tutorial", tutorial) + put("affise_event_complete_tutorial_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "CompleteTutorial" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/ContentItemsViewEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/ContentItemsViewEvent.kt new file mode 100644 index 0000000..4bd1b03 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/ContentItemsViewEvent.kt @@ -0,0 +1,44 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONArray +import org.json.JSONObject + +/** + * Event ContentItemsView + * + * @property objects the list of JSON Object describing the meaning of the event. + * @property userData any custom string data. + */ +class ContentItemsViewEvent( + private val objects: List, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize ContentItemsViewEvent to JSONObject + * + * @return JSONObject of ContentItemsViewEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_content_items_view", JSONArray().apply { + objects.forEach { + put(it) + } + }) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "ContentItemsView" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId01Event.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId01Event.kt new file mode 100644 index 0000000..1ae9f83 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId01Event.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event CustomId01 + * + * @property custom the describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class CustomId01Event( + private val custom: String, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize CustomId01Event to JSONObject + * + * @return JSONObject of CustomId01Event + */ + override fun serialize() = JSONObject().apply { + put("affise_event_custom_id_01", custom) + put("affise_event_custom_id_01_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "CustomId01" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId02Event.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId02Event.kt new file mode 100644 index 0000000..96d9c17 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId02Event.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event CustomId02 + * + * @property custom the describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class CustomId02Event( + private val custom: String, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize CustomId02Event to JSONObject + * + * @return JSONObject of CustomId02Event + */ + override fun serialize() = JSONObject().apply { + put("affise_event_custom_id_02", custom) + put("affise_event_custom_id_02_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "CustomId02" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId03Event.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId03Event.kt new file mode 100644 index 0000000..10819a6 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId03Event.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event CustomId03 + * + * @property custom the describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class CustomId03Event( + private val custom: String, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize CustomId03Event to JSONObject + * + * @return JSONObject of CustomId03Event + */ + override fun serialize() = JSONObject().apply { + put("affise_event_custom_id_03", custom) + put("affise_event_custom_id_03_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "CustomId03" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId04Event.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId04Event.kt new file mode 100644 index 0000000..65a5954 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId04Event.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event CustomId04 + * + * @property custom the describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class CustomId04Event( + private val custom: String, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize CustomId04Event to JSONObject + * + * @return JSONObject of CustomId04Event + */ + override fun serialize() = JSONObject().apply { + put("affise_event_custom_id_04", custom) + put("affise_event_custom_id_04_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "CustomId04" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId05Event.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId05Event.kt new file mode 100644 index 0000000..f247fcf --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId05Event.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event CustomId05 + * + * @property custom the describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class CustomId05Event( + private val custom: String, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize CustomId05Event to JSONObject + * + * @return JSONObject of CustomId05Event + */ + override fun serialize() = JSONObject().apply { + put("affise_event_custom_id_05", custom) + put("affise_event_custom_id_05_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "CustomId05" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId06Event.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId06Event.kt new file mode 100644 index 0000000..9598b4c --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId06Event.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event CustomId06 + * + * @property custom the describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class CustomId06Event( + private val custom: String, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize CustomId06Event to JSONObject + * + * @return JSONObject of CustomId06Event + */ + override fun serialize() = JSONObject().apply { + put("affise_event_custom_id_06", custom) + put("affise_event_custom_id_06_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "CustomId06" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId07Event.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId07Event.kt new file mode 100644 index 0000000..877cb72 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId07Event.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event CustomId07 + * + * @property custom the describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class CustomId07Event( + private val custom: String, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize CustomId07Event to JSONObject + * + * @return JSONObject of CustomId07Event + */ + override fun serialize() = JSONObject().apply { + put("affise_event_custom_id_07", custom) + put("affise_event_custom_id_07_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "CustomId07" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId08Event.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId08Event.kt new file mode 100644 index 0000000..752b91b --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId08Event.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event CustomId08 + * + * @property custom the describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class CustomId08Event( + private val custom: String, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize CustomId08Event to JSONObject + * + * @return JSONObject of CustomId08Event + */ + override fun serialize() = JSONObject().apply { + put("affise_event_custom_id_08", custom) + put("affise_event_custom_id_08_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "CustomId08" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId09Event.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId09Event.kt new file mode 100644 index 0000000..4158d09 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId09Event.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event CustomId09 + * + * @property custom the describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class CustomId09Event( + private val custom: String, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize CustomId09Event to JSONObject + * + * @return JSONObject of CustomId09Event + */ + override fun serialize() = JSONObject().apply { + put("affise_event_custom_id_09", custom) + put("affise_event_custom_id_09_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "CustomId09" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId10Event.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId10Event.kt new file mode 100644 index 0000000..b6bc48b --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/CustomId10Event.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event CustomId10 + * + * @property custom the describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class CustomId10Event( + private val custom: String, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize CustomId10Event to JSONObject + * + * @return JSONObject of CustomId10Event + */ + override fun serialize() = JSONObject().apply { + put("affise_event_custom_id_10", custom) + put("affise_event_custom_id_10_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "CustomId10" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/DeepLinkedEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/DeepLinkedEvent.kt new file mode 100644 index 0000000..314036a --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/DeepLinkedEvent.kt @@ -0,0 +1,39 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event DeepLinked + * + * @property isLinked event from link or nor + * @property userData any custom string data. + */ +class DeepLinkedEvent( + private val isLinked: Boolean, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize DeepLinkedEvent to JSONObject + * + * @return JSONObject of DeepLinkedEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_deep_linked", isLinked) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "DeepLinked" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/GDPREvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/GDPREvent.kt new file mode 100644 index 0000000..07d51c6 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/GDPREvent.kt @@ -0,0 +1,41 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event GDPR + * + * @property userData any custom string data. + */ +internal class GDPREvent( + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize DeepLinkedEvent to JSONObject + * + * @return JSONObject of DeepLinkedEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_gpdr", true) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = EVENT_NAME + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData + + companion object { + const val EVENT_NAME = "GDPR" + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/InitiatePurchaseEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/InitiatePurchaseEvent.kt new file mode 100644 index 0000000..681e328 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/InitiatePurchaseEvent.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event InitiatePurchase + * + * @property purchaseData the JSON Object describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class InitiatePurchaseEvent( + private val purchaseData: JSONObject, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize InitiatePurchaseEvent to JSONObject + * + * @return JSONObject of InitiatePurchaseEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_initiate_purchase", purchaseData) + put("affise_event_initiate_purchase_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "InitiatePurchase" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/InitiateStreamEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/InitiateStreamEvent.kt new file mode 100644 index 0000000..37c2d54 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/InitiateStreamEvent.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event InitiateStream + * + * @property stream the JSON Object describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class InitiateStreamEvent( + private val stream: JSONObject, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize InitiateStreamEvent to JSONObject + * + * @return JSONObject of InitiateStreamEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_initiate_stream", stream) + put("affise_event_initiate_stream_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "InitiateStream" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/InviteEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/InviteEvent.kt new file mode 100644 index 0000000..113961c --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/InviteEvent.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event Invite + * + * @property invite the JSON Object describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class InviteEvent( + private val invite: JSONObject, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize InviteEvent to JSONObject + * + * @return JSONObject of InviteEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_invite", invite) + put("affise_event_invite_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "Invite" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/LastAttributedTouchEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/LastAttributedTouchEvent.kt new file mode 100644 index 0000000..2b69405 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/LastAttributedTouchEvent.kt @@ -0,0 +1,45 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event LastAttributedTouch + * + * @property touchType type in CLICK, WEB_TO_APP_AUTO_REDIRECT, IMPRESSION + * @property timeStampMillis the timestamp event in milliseconds. + * @property touchData the JSON Object describing the meaning of the event. + * @property userData any custom string data. + */ +class LastAttributedTouchEvent( + private val touchType: TouchType, + private val timeStampMillis: Long, + private val touchData: JSONObject, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize LastAttributedTouchEvent to JSONObject + * + * @return JSONObject of LastAttributedTouchEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_last_attributed_touch_type", touchType) + put("affise_event_last_attributed_touch_timestamp", timeStampMillis) + put("affise_event_last_attributed_touch_data", touchData) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "LastAttributedTouch" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/ListViewEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/ListViewEvent.kt new file mode 100644 index 0000000..029417f --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/ListViewEvent.kt @@ -0,0 +1,39 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event ListView + * + * @property list the JSON Object describing the meaning of the event. + * @property userData any custom string data. + */ +class ListViewEvent( + private val list: JSONObject, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize ListViewEvent to JSONObject + * + * @return JSONObject of ListViewEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_list_view", list) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "ListView" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/LogEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/LogEvent.kt new file mode 100644 index 0000000..8b1b8db --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/LogEvent.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import org.json.JSONObject + +/** + * Base Affise log event + * + * @property name the type log in NETWORK, DEVICEDATA, USERDATA, SDKLOG. + * @property value the event data. + */ +sealed class AffiseLog(val name: AffiseLogType, val value: String) { + + /** + * Log of network errors contains [jsonObject] + */ + class NetworkLog(val jsonObject: JSONObject) : AffiseLog(AffiseLogType.NETWORK, jsonObject.toString()) + + /** + * Log of device data errors contains any string [value] + */ + class DevicedataLog(value: String) : AffiseLog(AffiseLogType.DEVICEDATA, value) + + /** + * Log of user data contains any string [value] + */ + class UserdataLog(value: String) : AffiseLog(AffiseLogType.USERDATA, value) + + /** + * Log of sdk errors contains any string [value] + */ + class SdkLog(value: String) : AffiseLog(AffiseLogType.SDKLOG, value) +} + +/** + * Type of log + */ +enum class AffiseLogType(val type: String) { + NETWORK("affise_sdklog_network"), + DEVICEDATA("affise_sdklog_ddata"), + USERDATA("affise_sdklog_udata"), + SDKLOG("affise_sdklog_main") +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/LoginEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/LoginEvent.kt new file mode 100644 index 0000000..4100058 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/LoginEvent.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event Login + * + * @property login the JSON Object describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class LoginEvent( + private val login: JSONObject, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize LoginEvent to JSONObject + * + * @return JSONObject of LoginEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_login", login) + put("affise_event_login_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "Login" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/OpenedFromPushNotificationEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/OpenedFromPushNotificationEvent.kt new file mode 100644 index 0000000..fa6c017 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/OpenedFromPushNotificationEvent.kt @@ -0,0 +1,39 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event OpenedFromPushNotification + * + * @property details the describing the meaning of the event. + * @property userData any custom string data. + */ +class OpenedFromPushNotificationEvent( + private val details: String, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize OpenedFromPushNotification to JSONObject + * + * @return JSONObject of OpenedFromPushNotification + */ + override fun serialize() = JSONObject().apply { + put("affise_event_opened_from_push_notification", details) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "OpenedFromPushNotification" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/PredefinedParameters.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/PredefinedParameters.kt new file mode 100644 index 0000000..630d2ea --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/PredefinedParameters.kt @@ -0,0 +1,83 @@ +package com.affise.attribution.events.predefined + +/** + * Type of predefined parameters contains + * + * @property value the key of parameter + */ +enum class PredefinedParameters(val value: String) { + ADREV_AD_TYPE("affise_p_adrev_ad_type" ), + CITY("affise_p_city" ), + COUNTRY("affise_p_country" ), + REGION("affise_p_region" ), + CLASS("affise_p_class" ), + CONTENT("affise_p_content" ), + CONTENT_ID("affise_p_content_id" ), + CONTENT_LIST("affise_p_content_list" ), + CONTENT_TYPE("affise_p_content_type" ), + CURRENCY("affise_p_currency" ), + CUSTOMER_USER_ID("affise_p_customer_user_id" ), + DATE_A("affise_p_date_a" ), + DATE_B("affise_p_date_b" ), + DEPARTING_ARRIVAL_DATE("affise_p_departing_arrival_date" ), + DEPARTING_DEPARTURE_DATE("affise_p_departing_departure_date" ), + DESCRIPTION("affise_p_description" ), + DESTINATION_A("affise_p_destination_a" ), + DESTINATION_B("affise_p_destination_b" ), + DESTINATION_LIST("affise_p_destination_list" ), + HOTEL_SCORE("affise_p_hotel_score" ), + LEVEL("affise_p_level" ), + MAX_RATING_VALUE("affise_p_max_rating_value" ), + NUM_ADULTS("affise_p_num_adults" ), + NUM_CHILDREN("affise_p_num_children" ), + NUM_INFANTS("affise_p_num_infants" ), + ORDER_ID("affise_p_order_id" ), + PAYMENT_INFO_AVAILABLE("affise_p_payment_info_available" ), + PREFERRED_NEIGHBORHOODS("affise_p_preferred_neighborhoods" ), + PREFERRED_NUM_STOPS("affise_p_preferred_num_stops" ), + PREFERRED_PRICE_RANGE("affise_p_preferred_price_range" ), + PREFERRED_STAR_RATINGS("affise_p_preferred_star_ratings" ), + PRICE("affise_p_price" ), + PURCHASE_CURRENCY("affise_p_purchase_currency" ), + QUANTITY("affise_p_quantity" ), + RATING_VALUE("affise_p_rating_value" ), + RECEIPT_ID("affise_p_receipt_id" ), + REGISTRATION_METHOD("affise_p_registration_method" ), + RETURNING_ARRIVAL_DATE("affise_p_returning_arrival_date" ), + RETURNING_DEPARTURE_DATE("affise_p_returning_departure_date" ), + REVENUE("affise_p_revenue" ), + SCORE("affise_p_score" ), + SEARCH_STRING("affise_p_search_string" ), + SUBSCRIPTION_ID("affise_p_subscription_id" ), + SUCCESS("affise_p_success" ), + SUGGESTED_DESTINATIONS("affise_p_suggested_destinations" ), + SUGGESTED_HOTELS("affise_p_suggested_hotels" ), + TRAVEL_START("affise_p_travel_start" ), + TRAVEL_END("affise_p_travel_end" ), + USER_SCORE("affise_p_user_score" ), + VALIDATED("affise_p_validated" ), + ACHIEVEMENT_ID("affise_p_achievement_id" ), + COUPON_CODE("affise_p_coupon_code" ), + CUSTOMER_SEGMENT("affise_p_customer_segment" ), + DEEP_LINK("affise_p_deep_link" ), + EVENT_START("affise_p_event_start" ), + EVENT_END("affise_p_event_end" ), + LAT("affise_p_lat" ), + LONG("affise_p_long" ), + NEW_VERSION("affise_p_new_version" ), + OLD_VERSION("affise_p_old_version" ), + PARAM_01("affise_p_param_01" ), + PARAM_02("affise_p_param_02" ), + PARAM_03("affise_p_param_03" ), + PARAM_04("affise_p_param_04" ), + PARAM_05("affise_p_param_05" ), + PARAM_06("affise_p_param_06" ), + PARAM_07("affise_p_param_07" ), + PARAM_08("affise_p_param_08" ), + PARAM_09("affise_p_param_09" ), + PARAM_10("affise_p_param_10" ), + REVIEW_TEXT("affise_p_review_text" ), + TUTORIAL_ID("affise_p_tutorial_id" ), + VIRTUAL_CURRENCY_NAME("affise_p_virtual_currency_name" ) + +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/PurchaseEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/PurchaseEvent.kt new file mode 100644 index 0000000..7fb8a39 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/PurchaseEvent.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event Purchase use + * + * @property purchaseData the JSON Object describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class PurchaseEvent( + private val purchaseData: JSONObject, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize PurchaseEvent to JSONObject + * + * @return JSONObject of PurchaseEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_purchase", purchaseData) + put("affise_event_purchase_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "Purchase" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/RateEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/RateEvent.kt new file mode 100644 index 0000000..7621f3a --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/RateEvent.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event Rate use + * + * @property rate the JSON Object describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class RateEvent( + private val rate: JSONObject, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize RateEvent to JSONObject + * + * @return JSONObject of RateEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_rate", rate) + put("affise_event_rate_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "Rate" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/ReEngageEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/ReEngageEvent.kt new file mode 100644 index 0000000..6c0a032 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/ReEngageEvent.kt @@ -0,0 +1,39 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event Rate use + * + * @property reEngage the describing the meaning of the event. + * @property userData any custom string data. + */ +class ReEngageEvent( + private val reEngage: String, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize ReEngageEvent to JSONObject + * + * @return JSONObject of ReEngageEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_re_engage", reEngage) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "ReEngage" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/ReserveEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/ReserveEvent.kt new file mode 100644 index 0000000..bdff86e --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/ReserveEvent.kt @@ -0,0 +1,47 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONArray +import org.json.JSONObject + +/** + * Event Reserve + * + * @property reserve the list of JSON Object describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class ReserveEvent( + private val reserve: List, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize ReserveEvent to JSONObject + * + * @return JSONObject of ReserveEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_reserve", JSONArray().apply { + reserve.forEach { + put(it) + } + }) + put("affise_event_reserve_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "Reserve" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/SalesEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/SalesEvent.kt new file mode 100644 index 0000000..f563ebe --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/SalesEvent.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event Sales + * + * @property salesData the JSON Object describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class SalesEvent( + private val salesData: JSONObject, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize SalesEvent to JSONObject + * + * @return JSONObject of SalesEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_sales", salesData) + put("affise_event_sales_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "Sales" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/SearchEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/SearchEvent.kt new file mode 100644 index 0000000..9ce1fe4 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/SearchEvent.kt @@ -0,0 +1,43 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONArray +import org.json.JSONObject + +/** + * Event Search + * + * @property search the JSON array describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class SearchEvent( + private val search: JSONArray, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize SearchEvent to JSONObject + * + * @return JSONObject of SearchEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_search", search) + put("affise_event_search_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "Search" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/ShareEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/ShareEvent.kt new file mode 100644 index 0000000..11c72bb --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/ShareEvent.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event Share + * + * @property share the JSON Object describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class ShareEvent( + private val share: JSONObject, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize ShareEvent to JSONObject + * + * @return JSONObject of ShareEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_share", share) + put("affise_event_share_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "Share" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/SpendCreditsEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/SpendCreditsEvent.kt new file mode 100644 index 0000000..11f6fc8 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/SpendCreditsEvent.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event SpendCredits + * + * @property credits the value of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class SpendCreditsEvent( + private val credits: Long, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize SpendCreditsEvent to JSONObject + * + * @return JSONObject of SpendCreditsEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_spend_credits", credits) + put("affise_event_spend_credits_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "SpendCredits" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/StartRegistrationEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/StartRegistrationEvent.kt new file mode 100644 index 0000000..9b616ae --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/StartRegistrationEvent.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event StartRegistration + * + * @property registration the JSON Object describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class StartRegistrationEvent( + private val registration: JSONObject, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize StartRegistrationEvent to JSONObject + * + * @return JSONObject of StartRegistrationEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_start_registration", registration) + put("affise_event_start_registration_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "StartRegistration" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/StartTrialEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/StartTrialEvent.kt new file mode 100644 index 0000000..cfe639d --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/StartTrialEvent.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event StartTrial + * + * @property trial the JSON Object describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class StartTrialEvent( + private val trial: JSONObject, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize StartTrialEvent to JSONObject + * + * @return JSONObject of StartTrialEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_start_trial", trial) + put("affise_event_start_trial_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "StartTrial" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/StartTutorialEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/StartTutorialEvent.kt new file mode 100644 index 0000000..135913a --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/StartTutorialEvent.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event StartTutorial + * + * @property tutorial the JSON Object describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class StartTutorialEvent( + private val tutorial: JSONObject, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize StartTutorialEvent to JSONObject + * + * @return JSONObject of StartTutorialEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_start_tutorial", tutorial) + put("affise_event_start_tutorial_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "StartTutorial" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/SubscribeEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/SubscribeEvent.kt new file mode 100644 index 0000000..af5625e --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/SubscribeEvent.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event Subscribe + * + * @property subscribe the JSON Object describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class SubscribeEvent( + private val subscribe: JSONObject, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize SubscribeEvent to JSONObject + * + * @return JSONObject of SubscribeEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_subscribe", subscribe) + put("affise_event_subscribe_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "Subscribe" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/TouchType.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/TouchType.kt new file mode 100644 index 0000000..63be7ce --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/TouchType.kt @@ -0,0 +1,10 @@ +package com.affise.attribution.events.predefined + +/** + * Type of touch + */ +enum class TouchType { + CLICK, + WEB_TO_APP_AUTO_REDIRECT, + IMPRESSION +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/TravelBookingEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/TravelBookingEvent.kt new file mode 100644 index 0000000..aff76e2 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/TravelBookingEvent.kt @@ -0,0 +1,40 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONArray +import org.json.JSONObject + +/** + * Event TravelBooking + * + * @property details the JSON array describing the meaning of the event. + * @property userData any custom string data. + */ +class TravelBookingEvent( + private val details: JSONArray, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize TravelBookingEvent to JSONObject + * + * @return JSONObject of TravelBookingEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_travel_booking", details) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "TravelBooking" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/UnlockAchievementEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/UnlockAchievementEvent.kt new file mode 100644 index 0000000..b47b4a6 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/UnlockAchievementEvent.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event UnlockAchievement + * + * @property achievement the JSON Object describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class UnlockAchievementEvent( + private val achievement: JSONObject, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize UnlockAchievementEvent to JSONObject + * + * @return JSONObject of UnlockAchievementEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_unlock_achievement", achievement) + put("affise_event_unlock_achievement_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "UnlockAchievement" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/UnsubscribeEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/UnsubscribeEvent.kt new file mode 100644 index 0000000..acc205f --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/UnsubscribeEvent.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event Unsubscribe + * + * @property unsubscribe the JSON Object describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class UnsubscribeEvent( + private val unsubscribe: JSONObject, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize UnsubscribeEvent to JSONObject + * + * @return JSONObject of UnsubscribeEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_unsubscribe", unsubscribe) + put("affise_event_unsubscribe_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "Unsubscribe" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/UpdateEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/UpdateEvent.kt new file mode 100644 index 0000000..f6c65af --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/UpdateEvent.kt @@ -0,0 +1,40 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONArray +import org.json.JSONObject + +/** + * Event Update + * + * @property details the JSON array describing the meaning of the event. + * @property userData any custom string data. + */ +class UpdateEvent( + private val details: JSONArray, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize UpdateEvent to JSONObject + * + * @return JSONObject of UpdateEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_update", details) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "Update" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/ViewAdvEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/ViewAdvEvent.kt new file mode 100644 index 0000000..26f4350 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/ViewAdvEvent.kt @@ -0,0 +1,42 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event ViewAdv + * + * @property ad the JSON Object describing the meaning of the event. + * @property timeStampMillis the timestamp event in milliseconds. + * @property userData any custom string data. + */ +class ViewAdvEvent( + private val ad: JSONObject, + private val timeStampMillis: Long, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize ViewAdvEvent to JSONObject + * + * @return JSONObject of ViewAdvEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_view_adv", ad) + put("affise_event_view_adv_timestamp", timeStampMillis) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "ViewAdv" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/ViewCartEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/ViewCartEvent.kt new file mode 100644 index 0000000..9ed4ebe --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/ViewCartEvent.kt @@ -0,0 +1,39 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event ViewCart + * + * @property objects the JSON Object describing the meaning of the event. + * @property userData any custom string data. + */ +class ViewCartEvent( + private val objects: JSONObject, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize ViewCartEvent to JSONObject + * + * @return JSONObject of ViewCartEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_view_cart", objects) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "ViewCart" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/ViewItemEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/ViewItemEvent.kt new file mode 100644 index 0000000..8c366e5 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/ViewItemEvent.kt @@ -0,0 +1,39 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Event ViewItem + * + * @property item the JSON Object describing the meaning of the event. + * @property userData any custom string data. + */ +class ViewItemEvent( + private val item: JSONObject, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize ViewItemEvent to JSONObject + * + * @return JSONObject of ViewItemEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_view_item", item) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "ViewItem" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/predefined/ViewItemsEvent.kt b/attribution/src/main/java/com/affise/attribution/events/predefined/ViewItemsEvent.kt new file mode 100644 index 0000000..b234a78 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/predefined/ViewItemsEvent.kt @@ -0,0 +1,40 @@ +package com.affise.attribution.events.predefined + +import com.affise.attribution.events.NativeEvent +import org.json.JSONArray +import org.json.JSONObject + +/** + * Event ViewItems + * + * @property items the JSON array describing the meaning of the event. + * @property userData any custom string data. + */ +class ViewItemsEvent( + private val items: JSONArray, + private val userData: String? = null +) : NativeEvent() { + + /** + * Serialize ViewItemsEvent to JSONObject + * + * @return JSONObject of ViewItemsEvent + */ + override fun serialize() = JSONObject().apply { + put("affise_event_view_items", items) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "ViewItems" + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/subscription/BaseSubscriptionEvent.kt b/attribution/src/main/java/com/affise/attribution/events/subscription/BaseSubscriptionEvent.kt new file mode 100644 index 0000000..99c7188 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/subscription/BaseSubscriptionEvent.kt @@ -0,0 +1,53 @@ +package com.affise.attribution.events.subscription + +import com.affise.attribution.events.NativeEvent +import org.json.JSONObject + +/** + * Base Event of Subscription use [data] of event and [userData] + */ +abstract class BaseSubscriptionEvent( + private val data: JSONObject, + private val userData: String? = null +) : NativeEvent() { + + /** + * Type of subscription + * + */ + abstract val type: String + + /** + * Subtype of subscription + */ + abstract val subtype: String + + /** + * Serialize SubscriptionEvent to JSONObject + * + * @return JSONObject of SubscriptionEvent + */ + override fun serialize() = JSONObject().apply { + //Add subtype + put(SubscriptionParameters.AFFISE_SUBSCRIPTION_EVENT_TYPE_KEY, subtype) + + //Add data + data.keys().forEach { key -> + put(key, data.get(key)) + } + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = type + + /** + * User data + * + * @return userData + */ + override fun getUserData() = userData +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/subscription/SubscriptionActivation.kt b/attribution/src/main/java/com/affise/attribution/events/subscription/SubscriptionActivation.kt new file mode 100644 index 0000000..0c87e9d --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/subscription/SubscriptionActivation.kt @@ -0,0 +1,60 @@ +package com.affise.attribution.events.subscription + +import org.json.JSONObject + +/** + * Event InitialSubscription use [data] of event and [userData] + */ +class InitialSubscriptionEvent( + data: JSONObject, + userData: String? = null +) : BaseSubscriptionEvent(data, userData) { + + /** + * Type of event + */ + override val type = SubscriptionParameters.AFFISE_SUBSCRIPTION_ACTIVATION + + /** + * Subtype of event + */ + override val subtype = SubscriptionParameters.AFFISE_SUB_INITIAL_SUBSCRIPTION +} + +/** + * Event InitialTrial use [data] of event and [userData] + */ +class InitialTrialEvent( + data: JSONObject, + userData: String? = null +) : BaseSubscriptionEvent(data, userData) { + + /** + * Type of event + */ + override val type = SubscriptionParameters.AFFISE_SUBSCRIPTION_ACTIVATION + + /** + * Subtype of event + */ + override val subtype = SubscriptionParameters.AFFISE_SUB_INITIAL_TRIAL +} + +/** + * Event of InitialOffer use [data] of event and [userData] + */ +class InitialOfferEvent( + data: JSONObject, + userData: String? = null +) : BaseSubscriptionEvent(data, userData) { + + /** + * Type of event + */ + override val type = SubscriptionParameters.AFFISE_SUBSCRIPTION_ACTIVATION + + /** + * Subtype of event + */ + override val subtype = SubscriptionParameters.AFFISE_SUB_INITIAL_OFFER +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/subscription/SubscriptionCancellation.kt b/attribution/src/main/java/com/affise/attribution/events/subscription/SubscriptionCancellation.kt new file mode 100644 index 0000000..dffcf5f --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/subscription/SubscriptionCancellation.kt @@ -0,0 +1,117 @@ +package com.affise.attribution.events.subscription + +import org.json.JSONObject + +/** + * Event FailedTrial use [data] of event and [userData] + */ +class FailedTrialEvent( + data: JSONObject, + userData: String? = null +) : BaseSubscriptionEvent(data, userData) { + + /** + * Type of event + */ + override val type = SubscriptionParameters.AFFISE_SUBSCRIPTION_CANCELLATION + + /** + * Subtype of event + */ + override val subtype = SubscriptionParameters.AFFISE_SUB_FAILED_TRIAL +} + +/** + * Event FailedOfferise use [data] of event and [userData] + */ +class FailedOfferiseEvent( + data: JSONObject, + userData: String? = null +) : BaseSubscriptionEvent(data, userData) { + + /** + * Type of event + */ + override val type = SubscriptionParameters.AFFISE_SUBSCRIPTION_CANCELLATION + + /** + * Subtype of event + */ + override val subtype = SubscriptionParameters.AFFISE_SUB_FAILED_OFFERISE +} + +/** + * Event FailedSubscription use [data] of event and [userData] + */ +class FailedSubscriptionEvent( + data: JSONObject, + userData: String? = null +) : BaseSubscriptionEvent(data, userData) { + + /** + * Type of event + */ + override val type = SubscriptionParameters.AFFISE_SUBSCRIPTION_CANCELLATION + + /** + * Subtype of event + */ + override val subtype = SubscriptionParameters.AFFISE_SUB_FAILED_SUBSCRIPTION +} + +/** + * Event FailedTrialFromRetry use [data] of event and [userData] + */ +class FailedTrialFromRetryEvent( + data: JSONObject, + userData: String? = null +) : BaseSubscriptionEvent(data, userData) { + + /** + * Type of event + */ + override val type = SubscriptionParameters.AFFISE_SUBSCRIPTION_CANCELLATION + + /** + * Subtype of event + */ + override val subtype = SubscriptionParameters.AFFISE_SUB_FAILED_TRIAL_FROM_RETRY +} + +/** + * Event FailedOfferFromRetry use [data] of event and [userData] + */ +class FailedOfferFromRetryEvent( + data: JSONObject, + userData: String? = null +) : BaseSubscriptionEvent(data, userData) { + + /** + * Type of event + */ + override val type = SubscriptionParameters.AFFISE_SUBSCRIPTION_CANCELLATION + + /** + * Subtype of event + */ + override val subtype = SubscriptionParameters.AFFISE_SUB_FAILED_OFFER_FROM_RETRY +} + +/** + * Event FailedSubscriptionFromRetry use [data] of event and [userData] + */ +class FailedSubscriptionFromRetryEvent( + data: JSONObject, + userData: String? = null +) : BaseSubscriptionEvent(data, userData) { + + /** + * Type of event + */ + override val type = SubscriptionParameters.AFFISE_SUBSCRIPTION_CANCELLATION + + /** + * Subtype of event + */ + override val subtype = SubscriptionParameters.AFFISE_SUB_FAILED_SUBSCRIPTION_FROM_RETRY +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/subscription/SubscriptionEnteredBillingRetry.kt b/attribution/src/main/java/com/affise/attribution/events/subscription/SubscriptionEnteredBillingRetry.kt new file mode 100644 index 0000000..5759d0c --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/subscription/SubscriptionEnteredBillingRetry.kt @@ -0,0 +1,60 @@ +package com.affise.attribution.events.subscription + +import org.json.JSONObject + +/** + * Event TrialInRetry use [data] of event and [userData] + */ +class TrialInRetryEvent( + data: JSONObject, + userData: String? = null +) : BaseSubscriptionEvent(data, userData) { + + /** + * Type of event + */ + override val type = SubscriptionParameters.AFFISE_SUBSCRIPTION_ENTERED_BILLING_RETRY + + /** + * Subtype of event + */ + override val subtype = SubscriptionParameters.AFFISE_SUB_TRIAL_IN_RETRY +} + +/** + * Event OfferInRetry use [data] of event and [userData] + */ +class OfferInRetryEvent( + data: JSONObject, + userData: String? = null +) : BaseSubscriptionEvent(data, userData) { + + /** + * Type of event + */ + override val type = SubscriptionParameters.AFFISE_SUBSCRIPTION_ENTERED_BILLING_RETRY + + /** + * Subtype of event + */ + override val subtype = SubscriptionParameters.AFFISE_SUB_OFFER_IN_RETRY +} + +/** + * Event SubscriptionInRetry use [data] of event and [userData] + */ +class SubscriptionInRetryEvent( + data: JSONObject, + userData: String? = null +) : BaseSubscriptionEvent(data, userData) { + + /** + * Type of event + */ + override val type = SubscriptionParameters.AFFISE_SUBSCRIPTION_ENTERED_BILLING_RETRY + + /** + * Subtype of event + */ + override val subtype = SubscriptionParameters.AFFISE_SUB_SUBSCRIPTION_IN_RETRY +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/subscription/SubscriptionFirstConversion.kt b/attribution/src/main/java/com/affise/attribution/events/subscription/SubscriptionFirstConversion.kt new file mode 100644 index 0000000..bc368cf --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/subscription/SubscriptionFirstConversion.kt @@ -0,0 +1,41 @@ +package com.affise.attribution.events.subscription + +import org.json.JSONObject + +/** + * Event ConvertedTrial use [data] of event and [userData] + */ +class ConvertedTrialEvent( + data: JSONObject, + userData: String? = null +) : BaseSubscriptionEvent(data, userData) { + + /** + * Type of event + */ + override val type = SubscriptionParameters.AFFISE_SUBSCRIPTION_FIRST_CONVERSION + + /** + * Subtype of event + */ + override val subtype = SubscriptionParameters.AFFISE_SUB_CONVERTED_TRIAL +} + +/** + * Event ConvertedOffer use [data] of event and [userData] + */ +class ConvertedOfferEvent( + data: JSONObject, + userData: String? = null +) : BaseSubscriptionEvent(data, userData) { + + /** + * Type of event + */ + override val type = SubscriptionParameters.AFFISE_SUBSCRIPTION_FIRST_CONVERSION + + /** + * Subtype of event + */ + override val subtype = SubscriptionParameters.AFFISE_SUB_CONVERTED_OFFER +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/subscription/SubscriptionParameters.kt b/attribution/src/main/java/com/affise/attribution/events/subscription/SubscriptionParameters.kt new file mode 100644 index 0000000..932700d --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/subscription/SubscriptionParameters.kt @@ -0,0 +1,44 @@ +package com.affise.attribution.events.subscription + +/** + * Subscription events parameters - key of parameter + */ +object SubscriptionParameters { + const val AFFISE_SUBSCRIPTION_EVENT_TYPE_KEY = "affise_event_type" + + const val AFFISE_SUBSCRIPTION_ACTIVATION = "affise_subscription_activation" + const val AFFISE_SUB_INITIAL_SUBSCRIPTION = "affise_sub_initial_subscription" + const val AFFISE_SUB_INITIAL_TRIAL = "affise_sub_initial_trial" + const val AFFISE_SUB_INITIAL_OFFER = "affise_sub_initial_offer" + + const val AFFISE_SUBSCRIPTION_FIRST_CONVERSION = "affise_subscription_first_conversion" + const val AFFISE_SUB_CONVERTED_TRIAL = "affise_sub_converted_trial" + const val AFFISE_SUB_CONVERTED_OFFER = "affise_sub_converted_offer" + + const val AFFISE_SUBSCRIPTION_ENTERED_BILLING_RETRY = "affise_subscription_entered_billing_retry" + const val AFFISE_SUB_TRIAL_IN_RETRY = "affise_sub_trial_in_retry" + const val AFFISE_SUB_OFFER_IN_RETRY = "affise_sub_offer_in_retry" + const val AFFISE_SUB_SUBSCRIPTION_IN_RETRY = "affise_sub_subscription_in_retry" + + const val AFFISE_SUBSCRIPTION_RENEWAL = "affise_subscription_renewal" + const val AFFISE_SUB_RENEWED_SUBSCRIPTION = "affise_sub_renewed_subscription" + + const val AFFISE_SUBSCRIPTION_CANCELLATION = "affise_subscription_cancellation" + const val AFFISE_SUB_FAILED_TRIAL = "affise_sub_failed_trial" + const val AFFISE_SUB_FAILED_OFFERISE = "affise_sub_failed_offer" + const val AFFISE_SUB_FAILED_SUBSCRIPTION = "affise_sub_failed_subscription" + const val AFFISE_SUB_FAILED_TRIAL_FROM_RETRY = "affise_sub_failed_trial_from_retry" + const val AFFISE_SUB_FAILED_OFFER_FROM_RETRY = "affise_sub_failed_offer_from_retry" + const val AFFISE_SUB_FAILED_SUBSCRIPTION_FROM_RETRY = "affise_sub_failed_subscription_from_retry" + + const val AFFISE_SUBSCRIPTION_RENEWAL_FROM_BILLING_RETRY = "affise_subscription_renewal_from_billing_retry" + const val AFFISE_SUB_CONVERTED_TRIAL_FROM_RETRY = "affise_sub_converted_trial_from_retry" + const val AFFISE_SUB_CONVERTED_OFFER_FROM_RETRY = "affise_sub_converted_offer_from_retry" + const val AFFISE_SUB_RENEWED_SUBSCRIPTION_FROM_RETRY = "affise_sub_renewed_subscription_from_retry" + + const val AFFISE_SUBSCRIPTION_REACTIVATION = "affise_subscription_reactivation" + const val AFFISE_SUB_REACTIVATED_SUBSCRIPTION = "affise_sub_reactivated_subscription" + + const val AFFISE_UNSUBSCRIPTION = "affise_unsubscription" + const val AFFISE_SUB_UNSUBSCRIPTION = "affise_sub_unsubscription" +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/subscription/SubscriptionReactivation.kt b/attribution/src/main/java/com/affise/attribution/events/subscription/SubscriptionReactivation.kt new file mode 100644 index 0000000..7bb3eb8 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/subscription/SubscriptionReactivation.kt @@ -0,0 +1,22 @@ +package com.affise.attribution.events.subscription + +import org.json.JSONObject + +/** + * Event ReactivatedSubscription use [data] of event and [userData] + */ +class ReactivatedSubscriptionEvent( + data: JSONObject, + userData: String? = null +) : BaseSubscriptionEvent(data, userData) { + + /** + * Type of event + */ + override val type = SubscriptionParameters.AFFISE_SUBSCRIPTION_REACTIVATION + + /** + * Subtype of event + */ + override val subtype = SubscriptionParameters.AFFISE_SUB_REACTIVATED_SUBSCRIPTION +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/subscription/SubscriptionRenewal.kt b/attribution/src/main/java/com/affise/attribution/events/subscription/SubscriptionRenewal.kt new file mode 100644 index 0000000..b9f239b --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/subscription/SubscriptionRenewal.kt @@ -0,0 +1,22 @@ +package com.affise.attribution.events.subscription + +import org.json.JSONObject + +/** + * Event RenewedSubscription use [data] of event and [userData] + */ +class RenewedSubscriptionEvent( + data: JSONObject, + userData: String? = null +) : BaseSubscriptionEvent(data, userData) { + + /** + * Type of event + */ + override val type = SubscriptionParameters.AFFISE_SUBSCRIPTION_RENEWAL + + /** + * Subtype of event + */ + override val subtype = SubscriptionParameters.AFFISE_SUB_RENEWED_SUBSCRIPTION +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/subscription/SubscriptionRenewalFromBillingRetry.kt b/attribution/src/main/java/com/affise/attribution/events/subscription/SubscriptionRenewalFromBillingRetry.kt new file mode 100644 index 0000000..a09136f --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/subscription/SubscriptionRenewalFromBillingRetry.kt @@ -0,0 +1,60 @@ +package com.affise.attribution.events.subscription + +import org.json.JSONObject + +/** + * Event ConvertedTrialFromRetry use [data] of event and [userData] + */ +class ConvertedTrialFromRetryEvent( + data: JSONObject, + userData: String? = null +) : BaseSubscriptionEvent(data, userData) { + + /** + * Type of event + */ + override val type = SubscriptionParameters.AFFISE_SUBSCRIPTION_RENEWAL_FROM_BILLING_RETRY + + /** + * Subtype of event + */ + override val subtype = SubscriptionParameters.AFFISE_SUB_CONVERTED_TRIAL_FROM_RETRY +} + +/** + * Event ConvertedOfferFromRetry use [data] of event and [userData] + */ +class ConvertedOfferFromRetryEvent( + data: JSONObject, + userData: String? = null +) : BaseSubscriptionEvent(data, userData) { + + /** + * Type of event + */ + override val type = SubscriptionParameters.AFFISE_SUBSCRIPTION_RENEWAL_FROM_BILLING_RETRY + + /** + * Subtype of event + */ + override val subtype = SubscriptionParameters.AFFISE_SUB_CONVERTED_OFFER_FROM_RETRY +} + +/** + * Event RenewedSubscriptionFromRetry use [data] of event and [userData] + */ +class RenewedSubscriptionFromRetryEvent( + data: JSONObject, + userData: String? = null +) : BaseSubscriptionEvent(data, userData) { + + /** + * Type of event + */ + override val type = SubscriptionParameters.AFFISE_SUBSCRIPTION_RENEWAL_FROM_BILLING_RETRY + + /** + * Subtype of event + */ + override val subtype = SubscriptionParameters.AFFISE_SUB_RENEWED_SUBSCRIPTION_FROM_RETRY +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/events/subscription/Unsubscription.kt b/attribution/src/main/java/com/affise/attribution/events/subscription/Unsubscription.kt new file mode 100644 index 0000000..34b6aea --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/events/subscription/Unsubscription.kt @@ -0,0 +1,22 @@ +package com.affise.attribution.events.subscription + +import org.json.JSONObject + +/** + * Event Unsubscription use [data] of event and [userData] + */ +class UnsubscriptionEvent( + data: JSONObject, + userData: String? = null +) : BaseSubscriptionEvent(data, userData) { + + /** + * Type of event + */ + override val type = SubscriptionParameters.AFFISE_UNSUBSCRIPTION + + /** + * Subtype of event + */ + override val subtype = SubscriptionParameters.AFFISE_SUB_UNSUBSCRIPTION +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/exceptions/CloudException.kt b/attribution/src/main/java/com/affise/attribution/exceptions/CloudException.kt new file mode 100644 index 0000000..d28a139 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/exceptions/CloudException.kt @@ -0,0 +1,15 @@ +package com.affise.attribution.exceptions + +/** + * Cloud exception contains network + * + * @property url the url where the request was made + * @property throwable the error per request + * @property attempts the number of attempts per request + */ +data class CloudException( + val url: String, + val throwable: Throwable, + val attempts: Int, + val retry: Boolean = false +) : Throwable() \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/exceptions/NetworkException.kt b/attribution/src/main/java/com/affise/attribution/exceptions/NetworkException.kt new file mode 100644 index 0000000..25024e6 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/exceptions/NetworkException.kt @@ -0,0 +1,11 @@ +package com.affise.attribution.exceptions + +import java.io.IOException + +/** + * Network exception + * + * @property status the request response status + * @property message the request response message + */ +class NetworkException(val status: Int, message: String) : IOException(message) \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/exceptions/TestApplicationCrashException.kt b/attribution/src/main/java/com/affise/attribution/exceptions/TestApplicationCrashException.kt new file mode 100644 index 0000000..81b43a1 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/exceptions/TestApplicationCrashException.kt @@ -0,0 +1,6 @@ +package com.affise.attribution.exceptions + +/** + * Exception used to test application crash + */ +internal class TestApplicationCrashException : IllegalStateException() \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/exceptions/UncaughtAffiseException.kt b/attribution/src/main/java/com/affise/attribution/exceptions/UncaughtAffiseException.kt new file mode 100644 index 0000000..a22a082 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/exceptions/UncaughtAffiseException.kt @@ -0,0 +1,11 @@ +package com.affise.attribution.exceptions + +/** + * Affise exception that is thrown to [Thread.UncaughtExceptionHandler] + * + * Helps to indicate that application is crashed by Affise library + */ +class UncaughtAffiseException( + message: String, + cause: Throwable +) : IllegalStateException(message, cause) \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/executors/ExecutorServiceProvider.kt b/attribution/src/main/java/com/affise/attribution/executors/ExecutorServiceProvider.kt new file mode 100644 index 0000000..c4a9e39 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/executors/ExecutorServiceProvider.kt @@ -0,0 +1,16 @@ +package com.affise.attribution.executors + +import java.util.concurrent.ExecutorService + +/** + * Executor service provider interface + */ +interface ExecutorServiceProvider { + + /** + * Provide executor service + * + * @return executor + */ + fun provideExecutorService(): ExecutorService +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/executors/ExecutorServiceProviderImpl.kt b/attribution/src/main/java/com/affise/attribution/executors/ExecutorServiceProviderImpl.kt new file mode 100644 index 0000000..a09be92 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/executors/ExecutorServiceProviderImpl.kt @@ -0,0 +1,26 @@ +package com.affise.attribution.executors + +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +/** + * Executor service provider + * + * @property threadName the name of use thread. + */ +class ExecutorServiceProviderImpl( + private val threadName: String +) : ExecutorServiceProvider { + + /** + * Executor + */ + private val executor = Executors.newSingleThreadExecutor { + Thread(it, threadName) + } + + /** + * Provide executor service + */ + override fun provideExecutorService(): ExecutorService = executor +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/init/AffiseFlag.kt b/attribution/src/main/java/com/affise/attribution/init/AffiseFlag.kt new file mode 100644 index 0000000..35a5b58 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/init/AffiseFlag.kt @@ -0,0 +1,5 @@ +package com.affise.attribution.init + +enum class AffiseFlag { + IOS_REQUEST_ADID +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/init/AffiseInitProperties.kt b/attribution/src/main/java/com/affise/attribution/init/AffiseInitProperties.kt new file mode 100644 index 0000000..a2bb9f1 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/init/AffiseInitProperties.kt @@ -0,0 +1,18 @@ +package com.affise.attribution.init + +import com.affise.attribution.events.autoCatchingClick.AutoCatchingType + +/** + * Model that holds properties required on library init + */ +data class AffiseInitProperties( + val affiseAppId: String?, + val isProduction: Boolean = true, + val partParamName: String? = null, + val partParamNameToken: String? = null, + val appToken: String? = null, + val secretId: String? = null, + val autoCatchingClickEvents: List? = null, + val enabledMetrics: Boolean = false, +// val flags: List? = null, +) diff --git a/attribution/src/main/java/com/affise/attribution/init/InitPropertiesStorage.kt b/attribution/src/main/java/com/affise/attribution/init/InitPropertiesStorage.kt new file mode 100644 index 0000000..8b375dc --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/init/InitPropertiesStorage.kt @@ -0,0 +1,24 @@ +package com.affise.attribution.init + +/** + * Storage for initiative sdk property + */ +interface InitPropertiesStorage { + + /** + * Get Affise init properties + * + * @return Affise init properties + */ + fun getProperties(): AffiseInitProperties? + + /** + * Set Affise init properties + */ + fun setProperties(model: AffiseInitProperties) + + /** + * Update secretId in Affise init properties + */ + fun updateSecretId(secretId: String) +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/init/InitPropertiesStorageImpl.kt b/attribution/src/main/java/com/affise/attribution/init/InitPropertiesStorageImpl.kt new file mode 100644 index 0000000..e49c9bb --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/init/InitPropertiesStorageImpl.kt @@ -0,0 +1,34 @@ +package com.affise.attribution.init + +/** + * Implementation for [InitPropertiesStorage] + * + */ +class InitPropertiesStorageImpl : InitPropertiesStorage { + + /** + * AffiseInitProperties cached value + */ + private var properties: AffiseInitProperties? = null + + /** + * Get Affise init properties + * + * @return Affise init properties + */ + override fun getProperties() = properties + + /** + * Set Affise init properties + */ + override fun setProperties(model: AffiseInitProperties) { + properties = model + } + + /** + * Update SecretId in Affise init properties + */ + override fun updateSecretId(secretId: String) { + properties = properties?.copy(secretId = secretId) + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/init/SetPropertiesWhenAppInitializedUseCase.kt b/attribution/src/main/java/com/affise/attribution/init/SetPropertiesWhenAppInitializedUseCase.kt new file mode 100644 index 0000000..13dc0ab --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/init/SetPropertiesWhenAppInitializedUseCase.kt @@ -0,0 +1,12 @@ +package com.affise.attribution.init + +/** + * Use case to set properties on library init + */ +interface SetPropertiesWhenAppInitializedUseCase { + + /** + * Init SetPropertiesWhenAppInitializedUseCase with [initProperties] + */ + fun init(initProperties: AffiseInitProperties) +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/init/SetPropertiesWhenAppInitializedUseCaseImpl.kt b/attribution/src/main/java/com/affise/attribution/init/SetPropertiesWhenAppInitializedUseCaseImpl.kt new file mode 100644 index 0000000..18c3c4e --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/init/SetPropertiesWhenAppInitializedUseCaseImpl.kt @@ -0,0 +1,18 @@ +package com.affise.attribution.init + +/** + * UseCase when user provided properties is stored to storage on app init. + * + * @property storage the storage for initiative sdk property. + */ +class SetPropertiesWhenAppInitializedUseCaseImpl( + private val storage: InitPropertiesStorage +) : SetPropertiesWhenAppInitializedUseCase { + + /** + * Init SetPropertiesWhenAppInitializedUseCase with [initProperties] + */ + override fun init(initProperties: AffiseInitProperties) { + storage.setProperties(initProperties) + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/internal/InternalEvent.kt b/attribution/src/main/java/com/affise/attribution/internal/InternalEvent.kt new file mode 100644 index 0000000..e14dc87 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/internal/InternalEvent.kt @@ -0,0 +1,29 @@ +package com.affise.attribution.internal + +import com.affise.attribution.utils.timestamp +import org.json.JSONObject + +/** + * Base internal event + */ +abstract class InternalEvent { + + /** + * Name of event + * + * @return name + */ + abstract fun getName(): String + + /** + * Event timestamp + */ + fun getTimestamp(): Long = timestamp() + + /** + * Serialize event to JSONObject + * + * @return JSONObject + */ + abstract fun serialize(): JSONObject +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/internal/InternalEventToSerializedEventConverter.kt b/attribution/src/main/java/com/affise/attribution/internal/InternalEventToSerializedEventConverter.kt new file mode 100644 index 0000000..c220cf7 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/internal/InternalEventToSerializedEventConverter.kt @@ -0,0 +1,38 @@ +package com.affise.attribution.internal + +import com.affise.attribution.converter.Converter +import com.affise.attribution.events.SerializedEvent +import com.affise.attribution.utils.generateUUID +import org.json.JSONObject + +/** + * Converter Internal Event to SerializedEvent + */ +class InternalEventToSerializedEventConverter : Converter { + + /** + * Convert [from] Event to SerializedEvent + */ + override fun convert(from: InternalEvent): SerializedEvent { + //Generate id + val id = generateUUID().toString() + + //Create JSONObject + val json = JSONObject().apply { + //Add Id + put(InternalParameters.AFFISE_INTERNAL_EVENT_ID, id) + + //Add name + put(InternalParameters.AFFISE_INTERNAL_EVENT_NAME, from.getName()) + + //Add timestamp + put(InternalParameters.AFFISE_INTERNAL_EVENT_TIMESTAMP, from.getTimestamp()) + + //Add event data + put(InternalParameters.AFFISE_INTERNAL_EVENT_DATA, from.serialize()) + } + + //Create SerializedEvent + return SerializedEvent(id, json) + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/internal/InternalEventsParams.kt b/attribution/src/main/java/com/affise/attribution/internal/InternalEventsParams.kt new file mode 100644 index 0000000..2288de9 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/internal/InternalEventsParams.kt @@ -0,0 +1,8 @@ +package com.affise.attribution.internal + +object InternalEventsParams { + + const val INTERNAL_EVENTS_DIR_NAME = "affise-internal-events" + const val INTERNAL_EVENTS_STORE_TIME = 7 * 24 * 60 * 60 * 1000 + const val INTERNAL_EVENTS_SEND_COUNT = 100 +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/internal/InternalEventsRepository.kt b/attribution/src/main/java/com/affise/attribution/internal/InternalEventsRepository.kt new file mode 100644 index 0000000..6b4d80f --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/internal/InternalEventsRepository.kt @@ -0,0 +1,34 @@ +package com.affise.attribution.internal + +import com.affise.attribution.events.SerializedEvent + +/** + * Internal Events repository interface + */ +internal interface InternalEventsRepository { + + /** + * Has save events by [url] or not + */ + fun hasEvents(url: String): Boolean + + /** + * Event recording for each url + */ + fun storeEvent(event: InternalEvent, urls: List) + + /** + * Get event in dir + */ + fun getEvents(url: String): List + + /** + * Delete events in dir + */ + fun deleteEvent(ids: List, url: String) + + /** + * Removes all events + */ + fun clear() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/internal/InternalEventsRepositoryImpl.kt b/attribution/src/main/java/com/affise/attribution/internal/InternalEventsRepositoryImpl.kt new file mode 100644 index 0000000..29fe132 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/internal/InternalEventsRepositoryImpl.kt @@ -0,0 +1,69 @@ +package com.affise.attribution.internal + +import com.affise.attribution.converter.Converter +import com.affise.attribution.events.SerializedEvent +import com.affise.attribution.logs.LogsManager +import com.affise.attribution.storages.InternalEventsStorage + +/** + * Internal events repository provide write, read and delete events. + * + * @property converterToBase64 convert string to encoding Base64 string + * @property converterToSerializedEvent to convert SdkEvent to SerializedEvent + * @property logsManager for error logging + * @property eventsStorage storage of events + */ +internal class InternalEventsRepositoryImpl( + private val converterToBase64: Converter, + private val converterToSerializedEvent: Converter, + private val logsManager: LogsManager, + private val eventsStorage: InternalEventsStorage +) : InternalEventsRepository { + + /** + * Has save events by [url] or not + */ + override fun hasEvents(url: String) = eventsStorage.hasEvents( + converterToBase64.convert(url) + ) + + /** + * Store [event] by [urls] + */ + override fun storeEvent(event: InternalEvent, urls: List) { + //For al urls + urls.forEach { + //Save event + eventsStorage.saveEvent( + converterToBase64.convert(it), + converterToSerializedEvent.convert(event) + ) + } + } + + /** + * Get serialized events by [url] + * + * @return list of serialized events + */ + override fun getEvents(url: String): List = eventsStorage.getEvents( + converterToBase64.convert(url) + ) + + /** + * Delete event for [url] by [ids] + */ + override fun deleteEvent(ids: List, url: String) { + eventsStorage.deleteEvent( + converterToBase64.convert(url), + ids + ) + } + + /** + * Removes all events + */ + override fun clear() { + eventsStorage.clear() + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/internal/InternalParameters.kt b/attribution/src/main/java/com/affise/attribution/internal/InternalParameters.kt new file mode 100644 index 0000000..36912c7 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/internal/InternalParameters.kt @@ -0,0 +1,8 @@ +package com.affise.attribution.internal + +object InternalParameters { + const val AFFISE_INTERNAL_EVENT_ID = "internal_event_id" + const val AFFISE_INTERNAL_EVENT_NAME = "internal_event_name" + const val AFFISE_INTERNAL_EVENT_TIMESTAMP = "internal_event_timestamp" + const val AFFISE_INTERNAL_EVENT_DATA = "internal_event_data" +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/internal/StoreInternalEventUseCase.kt b/attribution/src/main/java/com/affise/attribution/internal/StoreInternalEventUseCase.kt new file mode 100644 index 0000000..e0fd888 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/internal/StoreInternalEventUseCase.kt @@ -0,0 +1,13 @@ +package com.affise.attribution.internal + + +/** + * UseCase store InternalEvent interface + */ +internal interface StoreInternalEventUseCase { + + /** + * Store InternalEvent + */ + fun storeInternalEvent(event: InternalEvent) +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/internal/StoreInternalEventUseCaseImpl.kt b/attribution/src/main/java/com/affise/attribution/internal/StoreInternalEventUseCaseImpl.kt new file mode 100644 index 0000000..9a088ce --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/internal/StoreInternalEventUseCaseImpl.kt @@ -0,0 +1,27 @@ +package com.affise.attribution.internal + +import com.affise.attribution.executors.ExecutorServiceProvider +import com.affise.attribution.network.CloudConfig + +/** + * UseCase store internal events on device + * + * @property executorServiceProvider an Executor that provides methods to manage termination and methods + * @property repository the sdk events repository provide write, read and delete events. + */ +internal class StoreInternalEventUseCaseImpl( + private val executorServiceProvider: ExecutorServiceProvider, + private val repository: InternalEventsRepository +) : StoreInternalEventUseCase { + + /** + * Store [InternalEvent] + */ + override fun storeInternalEvent(event: InternalEvent) { + //Execute in executor service + executorServiceProvider.provideExecutorService().execute { + //Save event + repository.storeEvent(event, CloudConfig.getUrls()) + } + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/internal/predefined/SessionStartInternalEvent.kt b/attribution/src/main/java/com/affise/attribution/internal/predefined/SessionStartInternalEvent.kt new file mode 100644 index 0000000..ed22a07 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/internal/predefined/SessionStartInternalEvent.kt @@ -0,0 +1,34 @@ +package com.affise.attribution.internal.predefined + +import com.affise.attribution.parameters.Parameters +import com.affise.attribution.internal.InternalEvent +import org.json.JSONObject + +/** + * When session start. + * + * @property affiseSessionCount the count of all sessions. + * @property lifetimeSessionCount the total application work time milliseconds. + */ +internal class SessionStartInternalEvent( + private val affiseSessionCount: Long, + private val lifetimeSessionCount: Long, +) : InternalEvent() { + + /** + * Serialize SessionStartInternalEvent to JSONObject + * + * @return JSONObject of SessionStartInternalEvent + */ + override fun serialize() = JSONObject().apply { + put(Parameters.AFFISE_SESSION_COUNT, affiseSessionCount) + put(Parameters.LIFETIME_SESSION_COUNT, lifetimeSessionCount) + } + + /** + * Name of event + * + * @return name + */ + override fun getName() = "SessionStart" +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/logs/AffiseThreadUncaughtExceptionHandlerImpl.kt b/attribution/src/main/java/com/affise/attribution/logs/AffiseThreadUncaughtExceptionHandlerImpl.kt new file mode 100644 index 0000000..a852669 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/logs/AffiseThreadUncaughtExceptionHandlerImpl.kt @@ -0,0 +1,21 @@ +package com.affise.attribution.logs + +import com.affise.attribution.exceptions.UncaughtAffiseException + +/** + * Implementation of [Thread.UncaughtExceptionHandler] + */ +class AffiseThreadUncaughtExceptionHandlerImpl( + private val delegate: Thread.UncaughtExceptionHandler?, + private val logsManager: LogsManager +) : Thread.UncaughtExceptionHandler { + override fun uncaughtException(t: Thread, e: Throwable) { + val stackTrace = e.stackTraceToString() + if (stackTrace.contains("com.affise")) { + UncaughtAffiseException("Affise library uncaught exception on $t", e) + .also(logsManager::addSdkError) + } + + delegate?.uncaughtException(t, e) + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/logs/LogsManager.kt b/attribution/src/main/java/com/affise/attribution/logs/LogsManager.kt new file mode 100644 index 0000000..30d139e --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/logs/LogsManager.kt @@ -0,0 +1,32 @@ +package com.affise.attribution.logs + +/** + * Manager logs interface + */ +interface LogsManager { + + /** + * Add [throwable] of network + */ + fun addNetworkError(throwable: Throwable) + + /** + * Add [throwable] of device + */ + fun addDeviceError(throwable: Throwable) + + /** + * Add [throwable] of user + */ + fun addUserError(throwable: Throwable) + + /** + * Add [throwable] of sdk + */ + fun addSdkError(throwable: Throwable) + + /** + * Add [message] error of device + */ + fun addDeviceError(message: String) +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/logs/LogsManagerImpl.kt b/attribution/src/main/java/com/affise/attribution/logs/LogsManagerImpl.kt new file mode 100644 index 0000000..07b5c98 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/logs/LogsManagerImpl.kt @@ -0,0 +1,141 @@ +package com.affise.attribution.logs + +import com.affise.attribution.events.predefined.AffiseLog +import com.affise.attribution.exceptions.CloudException +import com.affise.attribution.exceptions.NetworkException +import org.json.JSONObject +import java.net.SocketTimeoutException +import java.net.UnknownHostException +import javax.net.ssl.SSLException + +/** + * Manager logs + * + * @property storeLogsUseCase the use case for store logs + */ +internal class LogsManagerImpl( + private val storeLogsUseCase: StoreLogsUseCase +) : LogsManager { + + /** + * Add network [throwable] to logs + */ + override fun addNetworkError(throwable: Throwable) { + when (throwable) { + is CloudException -> listOf(createCloudExceptionJson(throwable)) + else -> listOf( + JSONObject().apply { + put("network_error", throwable.stackTraceToString()) + } + ) + }.forEach { + //Store log + storeLog( + AffiseLog.NetworkLog(it) + ) + } + } + + /** + * Add device [throwable] to logs + */ + override fun addDeviceError(throwable: Throwable) { + storeLog( + //Create DevicedataLog + AffiseLog.DevicedataLog( + value = throwable.stackTraceToString() + ) + ) + } + + /** + * Add user [throwable] to logs + */ + override fun addUserError(throwable: Throwable) { + storeLog( + //Create UserdataLog + AffiseLog.UserdataLog( + value = throwable.stackTraceToString() + ) + ) + } + + /** + * Add sdk [throwable] to logs + */ + override fun addSdkError(throwable: Throwable) { + storeLog( + //Create SdkLog + AffiseLog.SdkLog( + value = throwable.stackTraceToString() + ) + ) + } + + /** + * Add device [message] to logs + */ + override fun addDeviceError(message: String) { + storeLog( + //Create DevicedataLog + AffiseLog.DevicedataLog(message) + ) + } + + /** + * Store [event] to logs + */ + private fun storeLog(event: AffiseLog) { + storeLogsUseCase.storeLog(event) + } + + /** + * Create exception json from [cloudException] + */ + private fun createCloudExceptionJson(cloudException: CloudException) = JSONObject().apply { + //Error data + val data: String + + //Error code + val code: Int? + + //Check throwable + when (cloudException.throwable) { + is NetworkException -> { + data = cloudException.throwable.message ?: "" + code = cloudException.throwable.status + } + is SocketTimeoutException -> { + data = "Timeout Exception" + code = 522 + } + is SSLException -> { + data = "SSL Exception" + code = 525 + } + is UnknownHostException -> { + data = "DNS Exception" + code = 434 + } + else -> { + data = "${cloudException.throwable.message}" + code = null + } + } + + //Add url + put("endpoint", cloudException.url) + + //Add code + put("code", code) + + //Add attempts count + put("attempts", cloudException.attempts) + + //Add is retry sending + put("retry", cloudException.retry) + + //Add message + put("message", data) + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/logs/LogsRepository.kt b/attribution/src/main/java/com/affise/attribution/logs/LogsRepository.kt new file mode 100644 index 0000000..15b8b6b --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/logs/LogsRepository.kt @@ -0,0 +1,33 @@ +package com.affise.attribution.logs + +import com.affise.attribution.events.predefined.AffiseLog + +/** + * Logs repository interface + */ +internal interface LogsRepository { + /** + * Has logs by [url] or not + */ + fun hasLogs(url: String): Boolean + + /** + * Store log for all [urls] + */ + fun storeLog(log: AffiseLog, urls: List) + + /** + * Get log in current [url] + */ + fun getLogs(url: String): List + + /** + * Delete logs with [ids] in current [url] + */ + fun deleteLogs(ids: List, url: String) + + /** + * Removes all logs + */ + fun clear() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/logs/LogsRepositoryImpl.kt b/attribution/src/main/java/com/affise/attribution/logs/LogsRepositoryImpl.kt new file mode 100644 index 0000000..29ce209 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/logs/LogsRepositoryImpl.kt @@ -0,0 +1,69 @@ +package com.affise.attribution.logs + +import com.affise.attribution.converter.Converter +import com.affise.attribution.events.predefined.AffiseLog +import com.affise.attribution.events.predefined.AffiseLogType +import com.affise.attribution.storages.LogsStorage + +/** + * Repository for logs models + * + * @property converterToBase64 convert string to encoding Base64 string + * @property converterToSerializedLog to convert Event to SerializedEvent + * @property logsStorage storage of logs + */ +internal class LogsRepositoryImpl( + private val converterToBase64: Converter, + private val converterToSerializedLog: Converter, + private val logsStorage: LogsStorage +) : LogsRepository { + + /** + * Has logs by [url] or not + */ + override fun hasLogs(url: String): Boolean = logsStorage.hasLogs( + converterToBase64.convert(url), + AffiseLogType.values().map { converterToBase64.convert(it.type) } + ) + + /** + * Store [log] + */ + @Synchronized + override fun storeLog(log: AffiseLog, urls: List) { + urls.forEach { url -> + logsStorage.saveLog( + converterToBase64.convert(url), + converterToBase64.convert(log.name.type), + converterToSerializedLog.convert(log) + ) + } + } + + /** + * Get logs by [url] + * @return logs + */ + override fun getLogs(url: String): List = logsStorage.getLogs( + converterToBase64.convert(url), + AffiseLogType.values().map { converterToBase64.convert(it.type) } + ) + + /** + * Removes all log by [ids] in [url] + */ + override fun deleteLogs(ids: List, url: String) { + logsStorage.deleteLogs( + converterToBase64.convert(url), + AffiseLogType.values().map { converterToBase64.convert(it.type) }, + ids + ) + } + + /** + * Removes all Logs + */ + override fun clear() { + logsStorage.clear() + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/logs/SerializedLog.kt b/attribution/src/main/java/com/affise/attribution/logs/SerializedLog.kt new file mode 100644 index 0000000..8092df1 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/logs/SerializedLog.kt @@ -0,0 +1,24 @@ +package com.affise.attribution.logs + +import org.json.JSONObject + +/** + * Serialized log contains [id] identification, [type] and log [data] + */ +data class SerializedLog( + + /** + * Log id + */ + val id: String, + + /** + * Log type + */ + val type: String, + + /** + * Log data + */ + val data: JSONObject +) \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/logs/StoreLogsUseCase.kt b/attribution/src/main/java/com/affise/attribution/logs/StoreLogsUseCase.kt new file mode 100644 index 0000000..1de3c4f --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/logs/StoreLogsUseCase.kt @@ -0,0 +1,14 @@ +package com.affise.attribution.logs + +import com.affise.attribution.events.predefined.AffiseLog + +/** + * UseCase store logs interface + */ +internal interface StoreLogsUseCase { + + /** + * Store log + */ + fun storeLog(log: AffiseLog) +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/logs/StoreLogsUseCaseImpl.kt b/attribution/src/main/java/com/affise/attribution/logs/StoreLogsUseCaseImpl.kt new file mode 100644 index 0000000..2b1dc33 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/logs/StoreLogsUseCaseImpl.kt @@ -0,0 +1,28 @@ +package com.affise.attribution.logs + +import com.affise.attribution.events.predefined.AffiseLog +import com.affise.attribution.executors.ExecutorServiceProvider +import com.affise.attribution.network.CloudConfig + +/** + * Logs use case for store logs on device + * + * @property executorServiceProvider an Executor that provides methods to manage termination and methods + * @property repository the logs repository provide write, read and delete logs. + */ +internal class StoreLogsUseCaseImpl( + private val executorServiceProvider: ExecutorServiceProvider, + private val repository: LogsRepository +) : StoreLogsUseCase { + + /** + * Store [log] + */ + override fun storeLog(log: AffiseLog) { + //Execute in executor service + executorServiceProvider.provideExecutorService().execute { + //Store log + repository.storeLog(log, CloudConfig.getUrls()) + } + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/metrics/MetricsData.kt b/attribution/src/main/java/com/affise/attribution/metrics/MetricsData.kt new file mode 100644 index 0000000..f7a58ed --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/metrics/MetricsData.kt @@ -0,0 +1,36 @@ +package com.affise.attribution.metrics + +/** + * Metrics data by activity + */ +internal class MetricsData { + /** + * Name of activity + */ + var activityName: String? = null + + /** + * All open time current activity + */ + var openTime: Long = 0 + + /** + * Data of clicks on activity + */ + var clicksData: MutableList? = null +} + +/** + * Metrics click data by views + */ +internal class MetricsClickData { + /** + * Name dat of click + */ + var name: String? = null + + /** + * Count of click current data + */ + var count: Int = 0 +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/metrics/MetricsEvent.kt b/attribution/src/main/java/com/affise/attribution/metrics/MetricsEvent.kt new file mode 100644 index 0000000..3f94c30 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/metrics/MetricsEvent.kt @@ -0,0 +1,64 @@ +package com.affise.attribution.metrics + +import com.affise.attribution.events.Event +import org.json.JSONArray +import org.json.JSONObject + +internal class MetricsEvent(val date: Long) : Event() { + + /** + * Event data + */ + var data: MutableList = mutableListOf() + + /** + * Serialize event to JSONObject + */ + override fun serialize(): JSONObject = JSONObject().apply { + put(KEY_DATE, date) + + val data = JSONArray().apply { + data.forEach { + val item = JSONObject().apply { + put(KEY_ACTIVITY_NAME, it.activityName) + put(KEY_OPEN_TIME, it.openTime) + + val clicksData = JSONArray().apply { + it.clicksData?.forEach { + val d = JSONObject().apply { + put(KEY_NAME, it.name) + put(KEY_COUNT, it.count) + } + + put(d) + } + } + + put(KEY_CLICKS_DATA, clicksData) + } + + put(item) + } + } + + put(KEY_DATA, data) + } + + override fun getName(): String = "MetricsEvent" + + override fun getUserData(): String = "Auto generate metrics" + + override fun getCategory(): String = "autoNative" + + companion object { + const val KEY_DATE = "begin_day_timestamp" + const val KEY_DATA = "data" + + const val KEY_ACTIVITY_NAME = "activity_mame" + const val KEY_OPEN_TIME = "open_time" + const val KEY_CLICKS_DATA = "clicks_data" + + const val KEY_NAME = "name" + const val KEY_COUNT = "count" + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/metrics/MetricsManager.kt b/attribution/src/main/java/com/affise/attribution/metrics/MetricsManager.kt new file mode 100644 index 0000000..9e9d2e5 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/metrics/MetricsManager.kt @@ -0,0 +1,5 @@ +package com.affise.attribution.metrics + +internal interface MetricsManager { + fun setEnabledMetrics(enabled: Boolean) +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/metrics/MetricsManagerImpl.kt b/attribution/src/main/java/com/affise/attribution/metrics/MetricsManagerImpl.kt new file mode 100644 index 0000000..0c9d8bf --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/metrics/MetricsManagerImpl.kt @@ -0,0 +1,160 @@ +package com.affise.attribution.metrics + +import android.app.Activity +import android.content.res.Resources +import android.view.View +import android.view.ViewGroup +import com.affise.attribution.converter.StringToSHA1Converter +import com.affise.attribution.utils.ActivityActionsManager +import com.affise.attribution.utils.timestamp + +/** + * MetricsManager + * + * @property activityActionsManager listeners for changes activity + * @property metricsUseCase usecase to generate metrics events + * @property converterToSHA1 convert String to SHA1 String + */ +internal class MetricsManagerImpl( + private val activityActionsManager: ActivityActionsManager, + private val metricsUseCase: MetricsUseCase, + private val converterToSHA1: StringToSHA1Converter +) : MetricsManager { + + /** + * HashMap of all open activities + */ + private var openActivities = HashMap() + + /** + * Flag of enabled metrics + */ + private var enabledMetrics: Boolean = false + + init { + //Add listeners to start activities + activityActionsManager.addOnActivityStartedListener { + openActivity(it) + } + + //Add listeners to stop activities + activityActionsManager.addOnActivityStoppedListener { + closeActivity(it) + } + + //Add listeners to clicks on activities + activityActionsManager.addOnActivityClickListener { activity, view -> + //Check if enabled metrics + if (enabledMetrics) { + clickOnActivity(activity, view) + } + } + } + + /** + * Enabled metrics + */ + override fun setEnabledMetrics(enabled: Boolean) { + enabledMetrics = enabled + } + + /** + * Handler of open [activity] + */ + private fun openActivity(activity: Activity) { + //Get name of activity + val activityName = activity.javaClass.simpleName + + //Check if activity is open + if (!openActivities.containsKey(activityName)) { + //Add activity to hash map with time of it opened + openActivities[activityName] = timestamp() + } + } + + /** + * Handler of close [activity] + */ + private fun closeActivity(activity: Activity) { + //Get name of activity + val activityName = activity.javaClass.simpleName + + //Check if activity is open + if (openActivities.containsKey(activityName)) { + //Remove activity from hash map of opened activities + openActivities.remove(activityName)?.let { startTime -> + //Check if enabled metrics + if (enabledMetrics) { + //Set data of open activity to usecase + metricsUseCase.addOpenActivityTime( + activity.javaClass.simpleName, + timestamp() - startTime + ) + } + } + } + } + + /** + * Handler of click on [view] in [activity] + */ + private fun clickOnActivity(activity: Activity, view: View) { + //Get name of activity + val activityName = activity.javaClass.simpleName + + //Generate data of click + val data = "AutoCatchingClickEvent_${getDataKey(activityName, view)}" + + //Set data of click on activity to usecase + metricsUseCase.addClickOnActivity(activityName, data) + } + + /** + * Generate data of click with [activityName] and [view] + */ + private fun getDataKey(activityName: String, view: View) = converterToSHA1 + .convert( + getDataView(view, true) + .fold(activityName) { acc, value -> + acc + value + } + ) + + /** + * Get data from [view], and flag if it [isRoot] view by click + */ + private fun getDataView(view: View, isRoot: Boolean = true): List = when (view) { + //Check view if it is ViewGroup + is ViewGroup -> { + mutableListOf().apply { + if (isRoot) { + add(getViewIdName(view)) + } + + addAll( + (0 until view.childCount).flatMap { + //Get data from all child views + getDataView(view.getChildAt(it), false) + } + ) + } + } + else -> listOf(getViewIdName(view)) + } + + /** + * Get view id, or empty value if view doesn't have id + */ + private fun getViewIdName(view: View) = view.id + .takeIf { it != View.NO_ID } + ?.let { id -> + view.resources?.let { resources -> + try { + resources.getResourceEntryName(id) + } catch (e: Resources.NotFoundException) { + null + } + } + } + ?: "" +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/metrics/MetricsRepository.kt b/attribution/src/main/java/com/affise/attribution/metrics/MetricsRepository.kt new file mode 100644 index 0000000..05c1154 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/metrics/MetricsRepository.kt @@ -0,0 +1,11 @@ +package com.affise.attribution.metrics + +import com.affise.attribution.events.SerializedEvent + +internal interface MetricsRepository { + fun hasMetrics(url: String): Boolean + fun getMetrics(url: String): List + fun addMetricsData(metricsData: MetricsData, urls: List) + fun deleteMetrics(url: String) + fun clear() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/metrics/MetricsRepositoryImpl.kt b/attribution/src/main/java/com/affise/attribution/metrics/MetricsRepositoryImpl.kt new file mode 100644 index 0000000..d9d9645 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/metrics/MetricsRepositoryImpl.kt @@ -0,0 +1,142 @@ +package com.affise.attribution.metrics + +import com.affise.attribution.converter.Converter +import com.affise.attribution.events.Event +import com.affise.attribution.events.SerializedEvent +import com.affise.attribution.storages.MetricsStorage +import java.util.Calendar + +/** + * Metrics repository provide write, read and delete metrics event. + * + * @property converterToBase64 convert string to encoding Base64 string + * @property converterToSerializedEvent convert metrics event to SerializedEvent + * @property metricsStorage storage of metrics events + */ +internal class MetricsRepositoryImpl( + private val converterToBase64: Converter, + private val converterToSerializedEvent: Converter, + private val metricsStorage: MetricsStorage +) : MetricsRepository { + + /** + * Has metrics by [url] or not + */ + override fun hasMetrics(url: String) = metricsStorage.hasMetrics( + converterToBase64.convert(url), + getCurrentDayName() + ) + + /** + * Get old metrics event from [url] + * @return list of metrics SerializedEvent + */ + override fun getMetrics(url: String): List = metricsStorage.getMetricsEvents( + converterToBase64.convert(url), + getCurrentDayName() + ).map { + //Convert to SerializedEvent + converterToSerializedEvent.convert(it) + } + + /** + * Add [metricsData] to current day for all [urls] + */ + override fun addMetricsData(metricsData: MetricsData, urls: List) { + //For all urls + urls.forEach { url -> + //Get metrics event by current day + val actualEvent = metricsStorage.getMetricsEvent( + converterToBase64.convert(url), + getCurrentDayName() + )?.also { currentEvent -> + currentEvent.data + .find { + //Find events by activity + it.activityName == metricsData.activityName + } + ?.also { currentData -> + //Add open time to activity + currentData.openTime += metricsData.openTime + + //Current data of clicks + val currentClicksData = currentData.clicksData ?: mutableListOf() + + //New data of clicks + val newClicksData = metricsData.clicksData ?: mutableListOf() + + if (currentClicksData.isEmpty()) { + currentClicksData.addAll(newClicksData) + } else { + newClicksData.forEach { clickData -> + currentClicksData + .find { + it.name == clickData.name + } + ?.let { + it.count += clickData.count + } + ?: run { + currentClicksData.add(clickData) + } + } + } + } + ?: run { + currentEvent.data.add(metricsData) + } + } ?: MetricsEvent(getCurrentDay()).also { event -> + event.data = mutableListOf(metricsData) + } + + //Save metrics event + saveMetricsEvent(actualEvent, url) + } + } + + /** + * Delete old metrics by [url] + */ + override fun deleteMetrics(url: String) { + metricsStorage.deleteMetrics( + converterToBase64.convert(url), + getCurrentDayName() + ) + } + + /** + * Removes all metrics events + */ + override fun clear() { + metricsStorage.clear() + } + + /** + * Save metrics [event] by [url] on current day + */ + private fun saveMetricsEvent(event: MetricsEvent, url: String) { + metricsStorage.saveMetricsEvent( + converterToBase64.convert(url), + getCurrentDayName(), + event + ) + } + + /** + * Get current day + * @return current day in millis + */ + private fun getCurrentDay() = Calendar.getInstance().apply { + set(Calendar.HOUR_OF_DAY, 0) + set(Calendar.MINUTE, 0) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + }.timeInMillis + + /** + * Get current day dir name + */ + private fun getCurrentDayName() = getCurrentDay().toString().let { + converterToBase64.convert(it) + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/metrics/MetricsUseCase.kt b/attribution/src/main/java/com/affise/attribution/metrics/MetricsUseCase.kt new file mode 100644 index 0000000..04b5c99 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/metrics/MetricsUseCase.kt @@ -0,0 +1,6 @@ +package com.affise.attribution.metrics + +internal interface MetricsUseCase { + fun addOpenActivityTime(activityName: String, openTime: Long) + fun addClickOnActivity(activityName: String, data: String) +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/metrics/MetricsUseCaseImpl.kt b/attribution/src/main/java/com/affise/attribution/metrics/MetricsUseCaseImpl.kt new file mode 100644 index 0000000..869d1fe --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/metrics/MetricsUseCaseImpl.kt @@ -0,0 +1,54 @@ +package com.affise.attribution.metrics + +import com.affise.attribution.executors.ExecutorServiceProvider +import com.affise.attribution.network.CloudConfig + +/** + * Metrics use case for store metrics events on device + * + * @property executorServiceProvider an Executor that provides methods to manage termination and methods + * @property metricsRepository the metrics events repository provide write, read and delete events. + */ +internal class MetricsUseCaseImpl( + private val executorServiceProvider: ExecutorServiceProvider, + private val metricsRepository: MetricsRepository +) : MetricsUseCase { + + /** + * Add [openTime] to current activity with name [activityName] + */ + override fun addOpenActivityTime(activityName: String, openTime: Long) { + executorServiceProvider.provideExecutorService().execute { + //Create MetricsData by activityName and openTime + val metricsData = MetricsData().also { + it.activityName = activityName + it.openTime = openTime + } + + //Add MetricsData to repository + metricsRepository.addMetricsData(metricsData, CloudConfig.getUrls()) + } + } + + /** + * Add click [data] with [activityName] + */ + override fun addClickOnActivity(activityName: String, data: String) { + executorServiceProvider.provideExecutorService().execute { + //Create MetricsClickData by ClickData + val clickData = MetricsClickData().also { + it.name = data + it.count = 1 + } + + //Create MetricsData by activityName and ClickData + val metricsData = MetricsData().also { + it.activityName = activityName + it.clicksData = mutableListOf(clickData) + } + + //Add MetricsData to repository + metricsRepository.addMetricsData(metricsData, CloudConfig.getUrls()) + } + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/modules/AffiseModule.kt b/attribution/src/main/java/com/affise/attribution/modules/AffiseModule.kt new file mode 100644 index 0000000..21ff57a --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/modules/AffiseModule.kt @@ -0,0 +1,25 @@ +package com.affise.attribution.modules + +import android.app.Application +import com.affise.attribution.parameters.base.PropertyProvider + +abstract class AffiseModule { + + protected var application: Application? = null + var dependencies: List? = null + + fun init(application: Application, logsManager: com.affise.attribution.logs.LogsManager, dependencies: List) { + this.dependencies = dependencies + this.application = application + init(logsManager) + } + + abstract fun init(logsManager: com.affise.attribution.logs.LogsManager) + + abstract fun providers(): List> + + inline fun get(): T? { + return dependencies?.firstOrNull { it is T } as? T + } +} + diff --git a/attribution/src/main/java/com/affise/attribution/modules/AffiseModuleManager.kt b/attribution/src/main/java/com/affise/attribution/modules/AffiseModuleManager.kt new file mode 100644 index 0000000..9e27da5 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/modules/AffiseModuleManager.kt @@ -0,0 +1,28 @@ +package com.affise.attribution.modules + +import android.app.Application +import com.affise.attribution.logs.LogsManager +import com.affise.attribution.parameters.factory.PostBackModelFactory + + +internal class AffiseModuleManager( + private val application: Application, + private val logsManager: LogsManager, + private val postBackModelFactory: PostBackModelFactory, +) { + + fun init(dependencies: List) { + AffiseModules.modules.forEach { module -> + getClass(module)?.let { + it.init(application, logsManager, dependencies) + postBackModelFactory.addProviders(it.providers()) + } + } + } + + private fun getClass(className: String): AffiseModule? = try { + Class.forName(className).newInstance() as? AffiseModule + } catch (_: Exception) { + null + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/modules/AffiseModules.kt b/attribution/src/main/java/com/affise/attribution/modules/AffiseModules.kt new file mode 100644 index 0000000..ef4b3ce --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/modules/AffiseModules.kt @@ -0,0 +1,9 @@ +package com.affise.attribution.modules + +internal object AffiseModules { + val modules = listOf( + "com.affise.attribution.module.advertising.AdvertisingModule", + "com.affise.attribution.module.network.NetworkModule", + "com.affise.attribution.module.phone.PhoneModule", + ) +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/network/CloudConfig.kt b/attribution/src/main/java/com/affise/attribution/network/CloudConfig.kt new file mode 100644 index 0000000..f4854ce --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/network/CloudConfig.kt @@ -0,0 +1,13 @@ +package com.affise.attribution.network + +object CloudConfig { + + /** + * Urls for send data + */ + private val urls: List = listOf( + "https://tracking.affattr.com/postback" + ) + + fun getUrls() = urls +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/network/CloudRepository.kt b/attribution/src/main/java/com/affise/attribution/network/CloudRepository.kt new file mode 100644 index 0000000..72e0011 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/network/CloudRepository.kt @@ -0,0 +1,14 @@ +package com.affise.attribution.network + +import com.affise.attribution.network.entity.PostBackModel + +/** + * Cloud repository interface + */ +interface CloudRepository { + + /** + * Send [data] to current [url] + */ + fun send(data: List, url: String) +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/network/CloudRepositoryImpl.kt b/attribution/src/main/java/com/affise/attribution/network/CloudRepositoryImpl.kt new file mode 100644 index 0000000..46b0d03 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/network/CloudRepositoryImpl.kt @@ -0,0 +1,68 @@ +package com.affise.attribution.network + +import com.affise.attribution.converter.Converter +import com.affise.attribution.exceptions.CloudException +import com.affise.attribution.network.entity.PostBackModel +import com.affise.attribution.parameters.UserAgentProvider +import java.net.URL + +internal class CloudRepositoryImpl( + private val httpClient: HttpClient, + private val userAgentProvider: UserAgentProvider?, + private val converter: Converter<@JvmSuppressWildcards List, @JvmSuppressWildcards String> +) : CloudRepository { + + /** + * Send [data] for [url] if [url] + * @throws CloudException contains throwable for url + */ + override fun send(data: List, url: String) { + //attempts to send + var attempts = ATTEMPTS_TO_SEND + + //Send or not + var send = false + + //While has attempts and not send + while (attempts != 0 && !send) { + try { + //Create request + createRequest(url, data) + + //Send is ok + send = true + } catch (throwable: Throwable) { + //Check attempts + if (--attempts == 0) { + //Add throwable + throw CloudException(url, throwable, ATTEMPTS_TO_SEND, true) + } + } + } + } + + /** + * Send [data] to [url] + */ + private fun createRequest(url: String, data: List) { + //Create request + httpClient.executeRequest( + URL(url), + HttpClient.Method.POST, + converter.convert(data), + createHeaders() + ) + } + + /** + * Create headers + */ + private fun createHeaders() = mapOf( + "User-Agent" to (userAgentProvider?.provideWithDefault() ?: ""), + "Content-Type" to "application/json; charset=utf-8" + ) + + companion object { + private const val ATTEMPTS_TO_SEND = 3 + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/network/HttpClient.kt b/attribution/src/main/java/com/affise/attribution/network/HttpClient.kt new file mode 100644 index 0000000..02360d9 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/network/HttpClient.kt @@ -0,0 +1,27 @@ +package com.affise.attribution.network + +import java.net.URL + +interface HttpClient { + + /** + * Method of connection + */ + enum class Method { + GET, + POST + } + + /** + * Create request and execute result + * Executes [method] on url [httpsUrl] with body of [data] and [headers] + * + * @return string representation of request + */ + fun executeRequest( + httpsUrl: URL, + method: Method, + data: String, + headers: Map + ): String? +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/network/HttpClientImpl.kt b/attribution/src/main/java/com/affise/attribution/network/HttpClientImpl.kt new file mode 100644 index 0000000..635fefd --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/network/HttpClientImpl.kt @@ -0,0 +1,69 @@ +package com.affise.attribution.network + +import com.affise.attribution.exceptions.NetworkException +import java.io.BufferedReader +import java.io.InputStreamReader +import java.net.URL +import javax.net.ssl.HttpsURLConnection + +class HttpClientImpl : HttpClient { + + /** + * Create request and execute result + * Executes [method] on url [httpsUrl] with body of [data] and [headers] + * + * @return string representation of request + */ + override fun executeRequest( + httpsUrl: URL, + method: HttpClient.Method, + data: String, + headers: Map + ): String? { + var connection: HttpsURLConnection? = null + var response: String? = null + try { + //Create data bytes + val postDataBytes = data.toByteArray(charset("UTF-8")) + + //Create connection + connection = httpsUrl.openConnection() as HttpsURLConnection + connection.doOutput = true + connection.doInput = true + connection.requestMethod = method.name + connection.readTimeout = 15000 + connection.connectTimeout = 15000 + + //Add headers + headers.forEach { + connection.setRequestProperty(it.key, it.value) + } + connection.useCaches = false + + //Send data + connection.outputStream.use { it.write(postDataBytes) } + + //Get response code + val responseCode = connection.responseCode + + //Check response code + if (responseCode == HttpsURLConnection.HTTP_OK) { + var line: String + //Create BufferedReader + val br = BufferedReader(InputStreamReader(connection.inputStream, "UTF-8")) + + //Read response + while (br.readLine().also { line = it ?: "" } != null) { + response = response?.let { it + line } ?: line + } + } else { + throw NetworkException(responseCode, connection.responseMessage) + } + } finally { + //Disconnect + connection?.disconnect() + } + + return response + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/network/entity/PostBackModel.kt b/attribution/src/main/java/com/affise/attribution/network/entity/PostBackModel.kt new file mode 100644 index 0000000..92c774a --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/network/entity/PostBackModel.kt @@ -0,0 +1,12 @@ +package com.affise.attribution.network.entity + +import com.affise.attribution.events.SerializedEvent +import com.affise.attribution.logs.SerializedLog + +data class PostBackModel( + val parameters: Map = emptyMap(), + val events: List? = null, + val logs: List? = null, + val metrics: List? = null, + val internalEvents: List? = null, +) diff --git a/attribution/src/main/java/com/affise/attribution/parameters/AffAppTokenPropertyProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/AffAppTokenPropertyProvider.kt new file mode 100644 index 0000000..7e960f2 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/AffAppTokenPropertyProvider.kt @@ -0,0 +1,25 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.converter.Converter +import com.affise.attribution.init.InitPropertiesStorage +import com.affise.attribution.parameters.base.StringWithParamPropertyProvider + +/** + * Provider for property [Parameters.AFFISE_APP_TOKEN] + * + * @property initProperties to retrieve appToken + */ +class AffAppTokenPropertyProvider( + private val initProperties: InitPropertiesStorage, + private val stringToSHA256Converter: Converter +) : StringWithParamPropertyProvider() { + + override val order: Float = 61.0f + override val key: String = Parameters.AFFISE_APP_TOKEN + + override fun provideWithParam(param: String): String = stringToSHA256Converter.convert( + initProperties.getProperties()?.affiseAppId + + param + + initProperties.getProperties()?.secretId + ) +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/AffPartParamNamePropertyProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/AffPartParamNamePropertyProvider.kt new file mode 100644 index 0000000..494581e --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/AffPartParamNamePropertyProvider.kt @@ -0,0 +1,20 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.init.InitPropertiesStorage +import com.affise.attribution.parameters.base.StringPropertyProvider + +/** + * Provider for property [Parameters.AFFISE_PART_PARAM_NAME] + * + * @property initProperties to retrieve part param name + */ +class AffPartParamNamePropertyProvider( + private val initProperties: InitPropertiesStorage +) : StringPropertyProvider() { + + override val order: Float = 59.0f + override val key: String = Parameters.AFFISE_PART_PARAM_NAME + + override fun provide(): String? = initProperties.getProperties()?.partParamName +} + diff --git a/attribution/src/main/java/com/affise/attribution/parameters/AffPartParamNameTokenPropertyProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/AffPartParamNameTokenPropertyProvider.kt new file mode 100644 index 0000000..d458259 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/AffPartParamNameTokenPropertyProvider.kt @@ -0,0 +1,19 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.init.InitPropertiesStorage +import com.affise.attribution.parameters.base.StringPropertyProvider + +/** + * Provider for property [Parameters.AFFISE_PART_PARAM_NAME_TOKEN] + * + * @property initProperties to retrieve part param name token + */ +class AffPartParamNameTokenPropertyProvider( + private val initProperties: InitPropertiesStorage +) : StringPropertyProvider() { + + override val order: Float = 60.0f + override val key: String = Parameters.AFFISE_PART_PARAM_NAME_TOKEN + + override fun provide(): String? = initProperties.getProperties()?.partParamNameToken +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/AffSDKSecretIdProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/AffSDKSecretIdProvider.kt new file mode 100644 index 0000000..831e94b --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/AffSDKSecretIdProvider.kt @@ -0,0 +1,19 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.init.InitPropertiesStorage +import com.affise.attribution.parameters.base.StringPropertyProvider + +/** + * Provider for parameter [Parameters.AFFISE_SDK_SECRET_ID] + * + * @property initProperties to retrieve id from + */ +internal class AffSDKSecretIdProvider( + private val initProperties: InitPropertiesStorage +) : StringPropertyProvider() { + + override val order: Float = 63.0f + override val key: String = Parameters.AFFISE_SDK_SECRET_ID + + override fun provide(): String? = initProperties.getProperties()?.secretId +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/AffSDKVersionProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/AffSDKVersionProvider.kt new file mode 100644 index 0000000..e394e83 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/AffSDKVersionProvider.kt @@ -0,0 +1,15 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.BuildConfig +import com.affise.attribution.parameters.base.StringPropertyProvider + +/** + * Provider for parameter [Parameters.AFFISE_SDK_VERSION] + */ +class AffSDKVersionProvider : StringPropertyProvider() { + + override val order: Float = 47.0f + override val key: String = Parameters.AFFISE_SDK_VERSION + + override fun provide(): String = BuildConfig.VERSION_NAME +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/AffiseAltDeviceIdProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/AffiseAltDeviceIdProvider.kt new file mode 100644 index 0000000..74d0b9f --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/AffiseAltDeviceIdProvider.kt @@ -0,0 +1,18 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.StringPropertyProvider +import com.affise.attribution.usecase.FirstAppOpenUseCase + +/** + * Provider for parameter [Parameters.AFFISE_ALT_DEVICE_ID] + * + * @property useCase to retrieve affise alt device id + */ +internal class AffiseAltDeviceIdProvider( + private val useCase: FirstAppOpenUseCase +) : StringPropertyProvider() { + override val order: Float = 28.0f + override val key: String = Parameters.AFFISE_ALT_DEVICE_ID + + override fun provide(): String? = useCase.getAffiseAltDeviseId() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/AffiseAppIdProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/AffiseAppIdProvider.kt new file mode 100644 index 0000000..49ee9b6 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/AffiseAppIdProvider.kt @@ -0,0 +1,19 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.init.InitPropertiesStorage +import com.affise.attribution.parameters.base.StringPropertyProvider + +/** + * Provider for parameter [Parameters.AFFISE_APP_ID] + * + * @property storage to retrieve affise app id + */ +class AffiseAppIdProvider( + private val storage: InitPropertiesStorage +) : StringPropertyProvider() { + + override val order: Float = 1.0f + override val key: String = Parameters.AFFISE_APP_ID + + override fun provide(): String? = storage.getProperties()?.affiseAppId +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/AffiseDeviceIdProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/AffiseDeviceIdProvider.kt new file mode 100644 index 0000000..4953e9f --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/AffiseDeviceIdProvider.kt @@ -0,0 +1,19 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.StringPropertyProvider +import com.affise.attribution.usecase.FirstAppOpenUseCase + +/** + * Provider for parameter [Parameters.AFFISE_DEVICE_ID] + * + * @property useCase to retrieve affise device id + */ +internal class AffiseDeviceIdProvider( + private val useCase: FirstAppOpenUseCase +) : StringPropertyProvider() { + + override val order: Float = 27.0f + override val key: String = Parameters.AFFISE_DEVICE_ID + + override fun provide(): String? = useCase.getAffiseDeviseId() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/AffisePackageAppNameProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/AffisePackageAppNameProvider.kt new file mode 100644 index 0000000..5d948a6 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/AffisePackageAppNameProvider.kt @@ -0,0 +1,19 @@ +package com.affise.attribution.parameters + +import android.content.Context +import com.affise.attribution.parameters.base.StringPropertyProvider + +/** + * Provider for parameter [Parameters.AFFISE_PKG_APP_NAME] + * + * @property context to retrieve package name from + */ +class AffisePackageAppNameProvider( + private val context: Context +) : StringPropertyProvider() { + + override val order: Float = 2.0f + override val key: String = Parameters.AFFISE_PKG_APP_NAME + + override fun provide(): String? = context.packageName +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/AffiseSessionCountProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/AffiseSessionCountProvider.kt new file mode 100644 index 0000000..2eaaef6 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/AffiseSessionCountProvider.kt @@ -0,0 +1,20 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.LongPropertyProvider +import com.affise.attribution.session.SessionManager + +/** + * Provider for parameter [Parameters.AFFISE_SESSION_COUNT] + * + * @property sessionManager to retrieve session count + */ +internal class AffiseSessionCountProvider( + private val sessionManager: SessionManager +) : LongPropertyProvider() { + + override val order: Float = 56.0f + override val key: String = Parameters.AFFISE_SESSION_COUNT + + override fun provide(): Long = sessionManager.getSessionCount() + .let { if (it == 0L) 1L else it } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/AndroidIdMD5Provider.kt b/attribution/src/main/java/com/affise/attribution/parameters/AndroidIdMD5Provider.kt new file mode 100644 index 0000000..191f098 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/AndroidIdMD5Provider.kt @@ -0,0 +1,21 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.converter.Converter +import com.affise.attribution.parameters.base.StringPropertyProvider + +/** + * Provider for parameter [Parameters.ANDROID_ID_MD5] + * + * @property androidIdProvider to retrieve android id + * @property strToMd5Converter to convert android id to md5 + */ +class AndroidIdMD5Provider( + private val androidIdProvider: StringPropertyProvider, + private val strToMd5Converter: Converter<@JvmSuppressWildcards String, @JvmSuppressWildcards String> +) : StringPropertyProvider() { + + override val order: Float = 31.0f + override val key: String = Parameters.ANDROID_ID_MD5 + + override fun provide(): String? = androidIdProvider.provide()?.let(strToMd5Converter::convert) +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/AndroidIdProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/AndroidIdProvider.kt new file mode 100644 index 0000000..33bc445 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/AndroidIdProvider.kt @@ -0,0 +1,25 @@ +package com.affise.attribution.parameters + +import android.annotation.SuppressLint +import android.app.Application +import android.provider.Settings +import com.affise.attribution.parameters.base.StringPropertyProvider + +/** + * Provider for parameter [Parameters.ANDROID_ID] + * + * @property app to retrieve contentResolver + */ +class AndroidIdProvider( + private val app: Application +) : StringPropertyProvider() { + + override val order: Float = 30.0f + override val key: String = Parameters.ANDROID_ID + + @SuppressLint("HardwareIds") + override fun provide(): String? = Settings.Secure.getString( + app.contentResolver, + Settings.Secure.ANDROID_ID + ) +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/ApiLevelOSProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/ApiLevelOSProvider.kt new file mode 100644 index 0000000..de647c7 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/ApiLevelOSProvider.kt @@ -0,0 +1,21 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.build.BuildConfigPropertiesProvider +import com.affise.attribution.parameters.base.StringPropertyProvider + +/** + * Provider for parameter [Parameters.API_LEVEL_OS] + * + * @property buildConfigPropertiesProvider to retrieve sdk version + */ +class ApiLevelOSProvider( + private val buildConfigPropertiesProvider: BuildConfigPropertiesProvider +) : StringPropertyProvider() { + + override val order: Float = 46.0f + override val key: String = Parameters.API_LEVEL_OS + + override fun provide(): String = buildConfigPropertiesProvider + .getSDKVersion() + .toString() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/AppVersionProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/AppVersionProvider.kt new file mode 100644 index 0000000..f650e92 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/AppVersionProvider.kt @@ -0,0 +1,32 @@ +package com.affise.attribution.parameters + +import android.content.Context +import com.affise.attribution.logs.LogsManager +import com.affise.attribution.parameters.base.StringPropertyProvider + +/** + * Provider for parameter [Parameters.APP_VERSION] + * App version number (Android) + * + * @property context to retrieve app version name + * @property logsManager for error logging + */ +class AppVersionProvider( + private val context: Context, + private val logsManager: LogsManager +) : StringPropertyProvider() { + + override val order: Float = 3.0f + override val key: String = Parameters.APP_VERSION + + override fun provide(): String? = try { + context.packageManager + .getPackageInfo(context.packageName, 0) + ?.versionName + } catch (e: Exception) { + //log error + logsManager.addDeviceError(e) + + null + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/AppVersionRawProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/AppVersionRawProvider.kt new file mode 100644 index 0000000..40a4209 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/AppVersionRawProvider.kt @@ -0,0 +1,40 @@ +package com.affise.attribution.parameters + +import android.content.Context +import android.os.Build +import com.affise.attribution.logs.LogsManager +import com.affise.attribution.parameters.base.StringPropertyProvider + +/** + * App version number (Android) [Parameters.APP_VERSION_RAW] + * + * @property context context to retrieve app version from + * @property logsManager for error logging + */ +class AppVersionRawProvider( + private val context: Context, + private val logsManager: LogsManager +) : StringPropertyProvider() { + + override val order: Float = 4.0f + override val key: String = Parameters.APP_VERSION_RAW + + @Suppress("DEPRECATION") + override fun provide(): String? = try { + context + .packageManager + .getPackageInfo(context.packageName, 0) + ?.let { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + it.longVersionCode + } else { + it.versionCode + } + } + ?.toString() + } catch (e: Exception) { + //log error + logsManager.addDeviceError(e) + null + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/CountryProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/CountryProvider.kt new file mode 100644 index 0000000..768b4d6 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/CountryProvider.kt @@ -0,0 +1,15 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.StringPropertyProvider +import java.util.Locale + +/** + * Provider for parameter [Parameters.COUNTRY] + */ +class CountryProvider : StringPropertyProvider() { + + override val order: Float = 39.0f + override val key: String = Parameters.COUNTRY + + override fun provide(): String? = Locale.getDefault().country +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/CpuTypeProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/CpuTypeProvider.kt new file mode 100644 index 0000000..7128a73 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/CpuTypeProvider.kt @@ -0,0 +1,19 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.build.BuildConfigPropertiesProvider +import com.affise.attribution.parameters.base.StringPropertyProvider + +/** + * Provider for parameter [Parameters.CPU_TYPE] + * + * @property buildPropertiesProvider to retrieve supported ABIs + */ +class CpuTypeProvider( + private val buildPropertiesProvider: BuildConfigPropertiesProvider +) : StringPropertyProvider() { + + override val order: Float = 22.0f + override val key: String = Parameters.CPU_TYPE + + override fun provide(): String = buildPropertiesProvider.getSupportedABIs().joinToString() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/CreatedTimeHourProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/CreatedTimeHourProvider.kt new file mode 100644 index 0000000..7e0cdc1 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/CreatedTimeHourProvider.kt @@ -0,0 +1,24 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.LongPropertyProvider +import java.util.Calendar + +/** + * Provider for parameter [Parameters.CREATED_TIME_HOUR] + */ +class CreatedTimeHourProvider : LongPropertyProvider() { + + override val order: Float = 20.0f + override val key: String = Parameters.CREATED_TIME_HOUR + + override fun provide(): Long = Calendar.getInstance().apply { + //Remove millisecond + set(Calendar.MILLISECOND, 0) + + //Remove second + set(Calendar.SECOND, 0) + + //Remove minute + set(Calendar.MINUTE, 0) + }.timeInMillis +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/CreatedTimeMilliProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/CreatedTimeMilliProvider.kt new file mode 100644 index 0000000..d99052a --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/CreatedTimeMilliProvider.kt @@ -0,0 +1,15 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.LongPropertyProvider +import com.affise.attribution.utils.timestamp + +/** + * Provider for parameter [Parameters.CREATED_TIME_MILLI] + */ +class CreatedTimeMilliProvider : LongPropertyProvider() { + + override val order: Float = 19.0f + override val key: String = Parameters.CREATED_TIME_MILLI + + override fun provide(): Long = timestamp() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/CreatedTimeProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/CreatedTimeProvider.kt new file mode 100644 index 0000000..fdc39e2 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/CreatedTimeProvider.kt @@ -0,0 +1,18 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.LongPropertyProvider +import java.util.Calendar + +/** + * Provider for parameter [Parameters.CREATED_TIME] + */ +class CreatedTimeProvider : LongPropertyProvider() { + + override val order: Float = 18.0f + override val key: String = Parameters.CREATED_TIME + + override fun provide(): Long = Calendar.getInstance().apply { + //Remove millisecond + set(Calendar.MILLISECOND, 0) + }.timeInMillis +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/CustomLongProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/CustomLongProvider.kt new file mode 100644 index 0000000..e305a17 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/CustomLongProvider.kt @@ -0,0 +1,12 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.LongPropertyProvider + +class CustomLongProvider( + override val key: String?, + override val order: Float, + private val provide: (() -> Long?)? = null + ) : LongPropertyProvider() { + + override fun provide(): Long? = provide?.invoke() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/DeeplinkClickPropertyProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/DeeplinkClickPropertyProvider.kt new file mode 100644 index 0000000..a5f4625 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/DeeplinkClickPropertyProvider.kt @@ -0,0 +1,19 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.deeplink.DeeplinkClickRepository +import com.affise.attribution.parameters.base.BooleanPropertyProvider + +/** + * Provider for property [Parameters.DEEPLINK_CLICK] + * + * @property deeplinkClickRepository to retrieve network is deeplink + */ +class DeeplinkClickPropertyProvider( + private val deeplinkClickRepository: DeeplinkClickRepository +) : BooleanPropertyProvider() { + + override val order: Float = 25.0f + override val key: String = Parameters.DEEPLINK_CLICK + + override fun provide(): Boolean = deeplinkClickRepository.isDeeplinkClick() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/DeeplinkProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/DeeplinkProvider.kt new file mode 100644 index 0000000..3bb46da --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/DeeplinkProvider.kt @@ -0,0 +1,19 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.deeplink.DeeplinkClickRepository +import com.affise.attribution.parameters.base.StringPropertyProvider + +/** + * Provider for parameter [Parameters.AFFISE_DEEPLINK] + * + * @property deeplinkClickRepository to retrieve deeplink + */ +class DeeplinkProvider( + private val deeplinkClickRepository: DeeplinkClickRepository +) : StringPropertyProvider() { + + override val order: Float = 58.0f + override val key: String = Parameters.AFFISE_DEEPLINK + + override fun provide(): String = deeplinkClickRepository.getDeeplink() ?: defaultValue +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/DeviceManufacturerProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/DeviceManufacturerProvider.kt new file mode 100644 index 0000000..28230b2 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/DeviceManufacturerProvider.kt @@ -0,0 +1,19 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.build.BuildConfigPropertiesProvider +import com.affise.attribution.parameters.base.StringPropertyProvider + +/** + * Provider for parameter [Parameters.DEVICE_MANUFACTURER] + * + * @property propertiesProvider provider for Build properties + */ +class DeviceManufacturerProvider( + private val propertiesProvider: BuildConfigPropertiesProvider +) : StringPropertyProvider() { + + override val order: Float = 24.0f + override val key: String = Parameters.DEVICE_MANUFACTURER + + override fun provide(): String? = propertiesProvider.getManufacturer() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/DeviceNameProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/DeviceNameProvider.kt new file mode 100644 index 0000000..7a0eb46 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/DeviceNameProvider.kt @@ -0,0 +1,21 @@ +package com.affise.attribution.parameters + +import android.app.Application +import android.provider.Settings +import com.affise.attribution.parameters.base.StringPropertyProvider + +/** + * Provider for parameter [Parameters.DEVICE_NAME] + * + * @property app to retrieve global settings + */ +class DeviceNameProvider( + private val app: Application +) : StringPropertyProvider() { + + override val order: Float = 41.0f + override val key: String = Parameters.DEVICE_NAME + + override fun provide(): String? = Settings.Global + .getString(app.contentResolver, "device_name") +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/DeviceTypeProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/DeviceTypeProvider.kt new file mode 100644 index 0000000..f53e146 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/DeviceTypeProvider.kt @@ -0,0 +1,47 @@ +package com.affise.attribution.parameters + +import android.app.Application +import android.app.UiModeManager +import android.content.Context +import android.content.res.Configuration +import com.affise.attribution.parameters.base.StringPropertyProvider + +/** + * Provider for parameter [Parameters.DEVICE_TYPE] + * + * @property app to retrieve system service and configuration + */ +class DeviceTypeProvider( + private val app: Application +) : StringPropertyProvider() { + + override val order: Float = 42.0f + override val key: String = Parameters.DEVICE_TYPE + + override fun provide() = detectDeviceTypeByUIMode() + ?: if (isTablet()) "tablet" else "smartphone" + + /** + * Check configuration if is tablet + * @return is tablet or not + */ + private fun isTablet(): Boolean { + val size = (app.resources.configuration.screenLayout + and Configuration.SCREENLAYOUT_SIZE_MASK) + return (size >= Configuration.SCREENLAYOUT_SIZE_LARGE) + } + + /** + * Check mode typ if is television or car + * @return mode type name + */ + private fun detectDeviceTypeByUIMode(): String? { + val manager = app.getSystemService(Context.UI_MODE_SERVICE) as? UiModeManager ?: return null + + return when (manager.currentModeType) { + Configuration.UI_MODE_TYPE_TELEVISION -> "tv" + Configuration.UI_MODE_TYPE_CAR -> "car" + else -> null + } + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/EmptyStringProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/EmptyStringProvider.kt new file mode 100644 index 0000000..ccbef3a --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/EmptyStringProvider.kt @@ -0,0 +1,10 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.StringPropertyProvider + +class EmptyStringProvider( + override val key: String?, + override val order: Float, +) : StringPropertyProvider() { + override fun provide(): String = "" +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/FirstOpenHourProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/FirstOpenHourProvider.kt new file mode 100644 index 0000000..cd9f0b8 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/FirstOpenHourProvider.kt @@ -0,0 +1,39 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.LongPropertyProvider +import com.affise.attribution.usecase.FirstAppOpenUseCase +import java.util.Calendar +import java.util.Date + +/** + * Provider for parameter [Parameters.FIRST_OPEN_HOUR] + * + * @property useCase to retrieve first open time from + */ +internal class FirstOpenHourProvider( + private val useCase: FirstAppOpenUseCase +) : LongPropertyProvider() { + + override val order: Float = 9.0f + override val key: String = Parameters.FIRST_OPEN_HOUR + + override fun provide(): Long? = useCase.getFirstOpenDate() + ?.time + ?.takeIf { it != 0L } + ?.let { + Calendar.getInstance().apply { + //Set date of first open + time = Date(it) + + //Remove millisecond + set(Calendar.MILLISECOND, 0) + + //Remove second + set(Calendar.SECOND, 0) + + //Remove minute + set(Calendar.MINUTE, 0) + } + } + ?.timeInMillis +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/FirstOpenTimeProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/FirstOpenTimeProvider.kt new file mode 100644 index 0000000..6fc329b --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/FirstOpenTimeProvider.kt @@ -0,0 +1,19 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.LongPropertyProvider +import com.affise.attribution.usecase.FirstAppOpenUseCase + +/** + * Provider for parameter [Parameters.FIRST_OPEN_TIME] + * + * @property useCase to retrieve first open time from + */ +internal class FirstOpenTimeProvider( + private val useCase: FirstAppOpenUseCase +) : LongPropertyProvider() { + + override val order: Float = 7.0f + override val key: String = Parameters.FIRST_OPEN_TIME + + override fun provide(): Long? = useCase.getFirstOpenDate()?.time +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/HardwareNameProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/HardwareNameProvider.kt new file mode 100644 index 0000000..67727d2 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/HardwareNameProvider.kt @@ -0,0 +1,19 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.build.BuildConfigPropertiesProvider +import com.affise.attribution.parameters.base.StringPropertyProvider + +/** + * Provider for parameter [Parameters.HARDWARE_NAME] + * + * @property propertiesProvider provider for Build properties + */ +class HardwareNameProvider( + private val propertiesProvider: BuildConfigPropertiesProvider +) : StringPropertyProvider() { + + override val order: Float = 23.0f + override val key: String = Parameters.HARDWARE_NAME + + override fun provide(): String? = propertiesProvider.getHardware() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/InstallBeginTimeProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/InstallBeginTimeProvider.kt new file mode 100644 index 0000000..d4b3fd0 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/InstallBeginTimeProvider.kt @@ -0,0 +1,21 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.LongPropertyProvider +import com.affise.attribution.usecase.RetrieveInstallReferrerUseCase + +/** + * Provider for parameter [Parameters.INSTALL_BEGIN_TIME] + * + * @property useCase usecase to retrieve install timestamp from + */ +class InstallBeginTimeProvider( + private val useCase: RetrieveInstallReferrerUseCase +) : LongPropertyProvider() { + + override val order: Float = 11.0f + override val key: String = Parameters.INSTALL_BEGIN_TIME + + override fun provide(): Long? = useCase.getInstallReferrer() + ?.installBeginTimestampSeconds + ?.takeIf { it != 0L } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/InstallFinishTimeProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/InstallFinishTimeProvider.kt new file mode 100644 index 0000000..7e33a36 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/InstallFinishTimeProvider.kt @@ -0,0 +1,21 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.LongPropertyProvider +import com.affise.attribution.usecase.FirstAppOpenUseCase + +/** + * Provider for parameter [Parameters.INSTALL_FINISH_TIME] + * + * @property useCase usecase to retrieve install timestamp from + */ +internal class InstallFinishTimeProvider( + private val useCase: FirstAppOpenUseCase +) : LongPropertyProvider() { + + override val order: Float = 12.0f + override val key: String = Parameters.INSTALL_FINISH_TIME + + override fun provide(): Long? = useCase.getFirstOpenDate() + ?.time + ?.takeIf { it != 0L } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/InstallFirstEventProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/InstallFirstEventProvider.kt new file mode 100644 index 0000000..179ebc8 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/InstallFirstEventProvider.kt @@ -0,0 +1,20 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.BooleanPropertyProvider +import com.affise.attribution.usecase.FirstAppOpenUseCase + + +/** + * Provider for parameter [Parameters.INSTALL_FIRST_EVENT] + * + * @property useCase to retrieve is first open from + */ +internal class InstallFirstEventProvider( + private val useCase: FirstAppOpenUseCase +) : BooleanPropertyProvider() { + + override val order: Float = 10.0f + override val key: String = Parameters.INSTALL_FIRST_EVENT + + override fun provide(): Boolean? = useCase.isFirstOpen() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/InstallReferrerProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/InstallReferrerProvider.kt new file mode 100644 index 0000000..ef61c49 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/InstallReferrerProvider.kt @@ -0,0 +1,58 @@ +package com.affise.attribution.parameters + +import android.app.Application +import com.affise.attribution.parameters.base.StringPropertyProvider +import com.affise.attribution.usecase.RetrieveInstallReferrerUseCase +import java.io.BufferedReader +import java.io.InputStreamReader + +/** + * Provider for parameter [Parameters.REFERRER] + * + * @property app to get partner_key in assets + * @property referrerUseCase usecase to retrieve install begin time + */ +class InstallReferrerProvider( + private val app: Application, + private val referrerUseCase: RetrieveInstallReferrerUseCase +) : StringPropertyProvider() { + + override val order: Float = 34.0f + override val key: String = Parameters.REFERRER + + override fun provide(): String? { + //Check referrer in partner_key + val referrer = try { + //Create InputStream + app.assets.open("partner_key").use { inputStream -> + //Create BufferedReader + BufferedReader(InputStreamReader(inputStream)).use { bufferedReader -> + //Create StringBuilder + val builder = StringBuilder() + //Create temp line + var line: String? + + //Read file + do { + line = bufferedReader.readLine()?.also { + builder.append(it) + } + } while (line != null) + + //Crete String result + builder.toString() + } + } + } catch (throwable: Throwable) { + //logsManager.addDeviceError(throwable) + null + } + + //if partner_key is empty or null use installReferrer + return if (referrer.isNullOrEmpty()) { + referrerUseCase.getInstallReferrer()?.installReferrer + } else { + referrer + } + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/InstalledHourProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/InstalledHourProvider.kt new file mode 100644 index 0000000..a48a78c --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/InstalledHourProvider.kt @@ -0,0 +1,39 @@ +package com.affise.attribution.parameters + +import android.content.Context +import com.affise.attribution.parameters.base.LongPropertyProvider +import java.util.Calendar +import java.util.Date + +/** + * Provider for parameter [Parameters.INSTALLED_HOUR] + * + * @property context to retrieve package manager from + */ +class InstalledHourProvider( + private val context: Context +) : LongPropertyProvider() { + + override val order: Float = 8.0f + override val key: String = Parameters.INSTALLED_HOUR + + override fun provide(): Long? = context + .packageManager + .getPackageInfo(context.packageName, 0) + ?.firstInstallTime + ?.stripTimestampToHours() + + private fun Long.stripTimestampToHours() = Calendar.getInstance().apply { + //Set first install time + time = Date(this@stripTimestampToHours) + + //Remove millisecond + set(Calendar.MILLISECOND, 0) + + //Remove second + set(Calendar.SECOND, 0) + + //Remove minute + set(Calendar.MINUTE, 0) + }.timeInMillis +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/InstalledTimeProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/InstalledTimeProvider.kt new file mode 100644 index 0000000..ef41bae --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/InstalledTimeProvider.kt @@ -0,0 +1,32 @@ +package com.affise.attribution.parameters + +import android.content.Context +import com.affise.attribution.logs.LogsManager +import com.affise.attribution.parameters.base.LongPropertyProvider + +/** + * Provider for parameter [Parameters.INSTALLED_TIME] + * + * @property context to retrieve first install time from + * @property logsManager for error logging + */ +class InstalledTimeProvider( + private val context: Context, + private val logsManager: LogsManager +) : LongPropertyProvider() { + + override val order: Float = 6.0f + override val key: String = Parameters.INSTALLED_TIME + + override fun provide(): Long? = try { + context + .packageManager + .getPackageInfo(context.packageName, 0) + ?.firstInstallTime + } catch (e: Exception) { + //log error + logsManager.addDeviceError(e) + + null + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/IsProductionPropertyProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/IsProductionPropertyProvider.kt new file mode 100644 index 0000000..8bac281 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/IsProductionPropertyProvider.kt @@ -0,0 +1,28 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.init.InitPropertiesStorage +import com.affise.attribution.parameters.base.StringPropertyProvider + +/** + * Provider for property [Parameters.AFFISE_SDK_POS] + * + * @property initProperties to retrieve is production + */ +class IsProductionPropertyProvider( + private val initProperties: InitPropertiesStorage +) : StringPropertyProvider() { + + override val order: Float = 50.0f + override val key: String = Parameters.AFFISE_SDK_POS + + override fun provide(): String = if (initProperties.getProperties()?.isProduction == true) { + TYPE_PRODUCTION + } else { + TYPE_SANDBOX + } + + companion object { + const val TYPE_SANDBOX = "Sandbox" + const val TYPE_PRODUCTION = "Production" + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/LanguageProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/LanguageProvider.kt new file mode 100644 index 0000000..9046a7c --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/LanguageProvider.kt @@ -0,0 +1,15 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.StringPropertyProvider +import java.util.Locale + +/** + * Provider for parameter [Parameters.LANGUAGE] + */ +class LanguageProvider : StringPropertyProvider() { + + override val order: Float = 40.0f + override val key: String = Parameters.LANGUAGE + + override fun provide(): String? = Locale.getDefault().toLanguageTag() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/LastSessionTimeProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/LastSessionTimeProvider.kt new file mode 100644 index 0000000..ac20704 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/LastSessionTimeProvider.kt @@ -0,0 +1,19 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.LongPropertyProvider +import com.affise.attribution.session.SessionManager + +/** + * Provider for parameter [Parameters.LAST_SESSION_TIME] + * + * @property sessionManager to retrieve last interaction time + */ +class LastSessionTimeProvider( + private val sessionManager: SessionManager +) : LongPropertyProvider() { + + override val order: Float = 21.0f + override val key: String = Parameters.LAST_SESSION_TIME + + override fun provide(): Long? = sessionManager.getLastInteractionTime() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/LifetimeSessionCountProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/LifetimeSessionCountProvider.kt new file mode 100644 index 0000000..27eda21 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/LifetimeSessionCountProvider.kt @@ -0,0 +1,19 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.LongPropertyProvider +import com.affise.attribution.session.SessionManager + +/** + * Provider for parameter [Parameters.LIFETIME_SESSION_COUNT] + * + * @property sessionManager to retrieve lifetime session time + */ +class LifetimeSessionCountProvider( + private val sessionManager: SessionManager +) : LongPropertyProvider() { + + override val order: Float = 57.0f + override val key: String = Parameters.LIFETIME_SESSION_COUNT + + override fun provide(): Long = sessionManager.getLifetimeSessionTime() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/MCCProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/MCCProvider.kt new file mode 100644 index 0000000..a741eeb --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/MCCProvider.kt @@ -0,0 +1,19 @@ +package com.affise.attribution.parameters + +import android.app.Application +import com.affise.attribution.parameters.base.StringPropertyProvider + +/** + * Provider for parameter [Parameters.MCCODE] + * + * @property app manager to fetch resources from + */ +class MCCProvider( + private val app: Application +) : StringPropertyProvider() { + + override val order: Float = 36.0f + override val key: String = Parameters.MCCODE + + override fun provide(): String = app.resources.configuration.mcc.toString() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/MNCProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/MNCProvider.kt new file mode 100644 index 0000000..40969b1 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/MNCProvider.kt @@ -0,0 +1,19 @@ +package com.affise.attribution.parameters + +import android.app.Application +import com.affise.attribution.parameters.base.StringPropertyProvider + +/** + * Provider for parameter [Parameters.MNCODE] + * + * @property app manager to fetch resources from + */ +class MNCProvider( + private val app: Application +) : StringPropertyProvider() { + + override val order: Float = 37.0f + override val key: String = Parameters.MNCODE + + override fun provide(): String = app.resources.configuration.mnc.toString() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/OSVersionProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/OSVersionProvider.kt new file mode 100644 index 0000000..f6b6576 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/OSVersionProvider.kt @@ -0,0 +1,19 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.build.BuildConfigPropertiesProvider +import com.affise.attribution.parameters.base.StringPropertyProvider + +/** + * Provider for parameter [Parameters.OS_VERSION] + * + * @property buildConfigPropertiesProvider to retrieve release name + */ +class OSVersionProvider( + private val buildConfigPropertiesProvider: BuildConfigPropertiesProvider +) : StringPropertyProvider() { + + override val order: Float = 48.0f + override val key: String = Parameters.OS_VERSION + + override fun provide(): String? = buildConfigPropertiesProvider.getReleaseName() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/OsNameProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/OsNameProvider.kt new file mode 100644 index 0000000..1b24607 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/OsNameProvider.kt @@ -0,0 +1,61 @@ +package com.affise.attribution.parameters + +import android.os.Build +import com.affise.attribution.build.BuildConfigPropertiesProvider +import com.affise.attribution.parameters.base.StringPropertyProvider + +/** + * Provider for parameter [Parameters.OS_NAME] + * + * Os name is generating based on [build-numbers](https://source.android.com/source/build-numbers.html) + */ +class OsNameProvider( + private val buildConfigPropertiesProvider: BuildConfigPropertiesProvider +) : StringPropertyProvider() { + + override val order: Float = 43.0f + override val key: String = Parameters.OS_NAME + + /** + * Returns release name, like: honeycomb, kitkat + */ + override fun provide(): String? = buildConfigPropertiesProvider.getSDKVersion().toCodeName() + + /** + * Get code name from Build.VERSION_CODES + */ + private fun Int.toCodeName() = when (this) { + Build.VERSION_CODES.S -> "Android12" + Build.VERSION_CODES.R -> "Android11" + Build.VERSION_CODES.Q -> "Android10" + Build.VERSION_CODES.P -> "Pie" + Build.VERSION_CODES.O_MR1 -> "Oreo" + Build.VERSION_CODES.O -> "Oreo" + Build.VERSION_CODES.N_MR1 -> "Nougat" + Build.VERSION_CODES.N -> "Nougat" + Build.VERSION_CODES.M -> "Marshmallow" + Build.VERSION_CODES.LOLLIPOP_MR1 -> "Lollipop" + Build.VERSION_CODES.LOLLIPOP -> "Lollipop" + Build.VERSION_CODES.KITKAT_WATCH -> "Lollipop" + Build.VERSION_CODES.KITKAT -> "KitKat" + Build.VERSION_CODES.JELLY_BEAN_MR2 -> "Jelly Bean" + Build.VERSION_CODES.JELLY_BEAN_MR1 -> "Jelly Bean" + Build.VERSION_CODES.JELLY_BEAN -> "Jelly Bean" + Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1 -> "Ice Cream Sandwich" + Build.VERSION_CODES.ICE_CREAM_SANDWICH -> "Ice Cream Sandwich" + Build.VERSION_CODES.HONEYCOMB_MR2 -> "Honeycomb" + Build.VERSION_CODES.HONEYCOMB_MR1 -> "Honeycomb" + Build.VERSION_CODES.HONEYCOMB -> "Honeycomb" + Build.VERSION_CODES.GINGERBREAD_MR1 -> "Gingerbread" + Build.VERSION_CODES.GINGERBREAD -> "Gingerbread" + Build.VERSION_CODES.FROYO -> "Froyo" + Build.VERSION_CODES.ECLAIR_MR1 -> "Eclair" + Build.VERSION_CODES.ECLAIR_0_1 -> "Eclair" + Build.VERSION_CODES.ECLAIR -> "Eclair" + Build.VERSION_CODES.DONUT -> "Donut" + Build.VERSION_CODES.CUPCAKE -> "Cupcake" + Build.VERSION_CODES.BASE_1_1 -> "1.1" + Build.VERSION_CODES.BASE -> "1.0" + else -> null + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/Parameters.kt b/attribution/src/main/java/com/affise/attribution/parameters/Parameters.kt new file mode 100644 index 0000000..8e9d746 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/Parameters.kt @@ -0,0 +1,91 @@ +package com.affise.attribution.parameters + +object Parameters { + const val AFFISE_EVENT_ID = "affise_event_id" + const val AFFISE_EVENT_NAME = "affise_event_name" //todo hold + const val AFFISE_EVENT_CATEGORY = "affise_event_category" + const val AFFISE_EVENT_TIMESTAMP = "affise_event_timestamp" + const val AFFISE_EVENT_FIRST_FOR_USER = "affise_event_first_for_user" + const val AFFISE_EVENT_USER_DATA = "affise_event_user_data" + const val AFFISE_EVENT_DATA = "affise_event_data" + const val AFFISE_PARAMETERS = "affise_parameters" + + const val AFFISE_APP_ID = "affise_app_id" + const val AFFISE_PKG_APP_NAME = "affise_pkg_app_name" + const val AFF_APP_NAME_DASHBOARD = "affise_app_name_dashboard" //todo abort + const val APP_VERSION = "app_version" + const val APP_VERSION_RAW = "app_version_raw" + const val STORE = "store" + const val TRACKER_TOKEN = "tracker_token" //todo hold + const val TRACKER_NAME = "tracker_name" //todo hold + const val FIRST_TRACKER_TOKEN = "first_tracker_token" //todo hold + const val FIRST_TRACKER_NAME = "first_tracker_name" //todo hold + const val LAST_TRACKER_TOKEN = "last_tracker_token" //todo hold + const val LAST_TRACKER_NAME = "last_tracker_name" //todo hold + const val OUTDATED_TRACKER_TOKEN = "outdated_tracker_token" //todo hold + const val INSTALLED_TIME = "installed_time" + const val FIRST_OPEN_TIME = "first_open_time" + const val INSTALLED_HOUR = "installed_hour" + const val FIRST_OPEN_HOUR = "first_open_hour" + const val INSTALL_FIRST_EVENT = "install_first_event" + const val INSTALL_BEGIN_TIME = "install_begin_time" + const val INSTALL_FINISH_TIME = "install_finish_time" + const val REFERRER_INSTALL_VERSION = "referrer_install_version" + const val REFERRAL_TIME = "referral_time" + const val REFERRER_CLICK_TIME = "referrer_click_time" + const val REFERRER_CLICK_TIME_SERVER = "referrer_click_time_server" + const val REFERRER_GOOGLE_PLAY_INSTANT = "referrer_google_play_instant" + const val CREATED_TIME = "created_time" + const val CREATED_TIME_MILLI = "created_time_milli" + const val CREATED_TIME_HOUR = "created_time_hour" + const val UNINSTALL_TIME = "uninstall_time" //todo research + const val REINSTALL_TIME = "reinstall_time" //todo research + const val LAST_SESSION_TIME = "last_session_time" + const val CPU_TYPE = "cpu_type" + const val HARDWARE_NAME = "hardware_name" + const val DEVICE_MANUFACTURER = "device_manufacturer" + const val DEEPLINK_CLICK = "deeplink_click" // todo + const val DEVICE_ATLAS_ID = "device_atlas_id" // todo + const val AFFISE_DEVICE_ID = "affise_device_id" + const val AFFISE_ALT_DEVICE_ID = "affise_alt_device_id" + const val ANDROID_ID = "android_id" + const val ANDROID_ID_MD5 = "android_id_md5" + const val REFTOKEN = "reftoken" + const val REFTOKENS = "reftokens" + const val REFERRER = "referrer" + const val USER_AGENT = "user_agent" + const val MCCODE = "mccode" + const val MNCODE = "mncode" + const val REGION = "region" + const val COUNTRY = "country" + const val LANGUAGE = "language" + const val DEVICE_NAME = "device_name" + const val DEVICE_TYPE = "device_type" + const val OS_NAME = "os_name" + const val PLATFORM = "platform" + const val SDK_PLATFORM = "sdk_platform" + const val API_LEVEL_OS = "api_level_os" + const val AFFISE_SDK_VERSION = "affise_sdk_version" + const val OS_VERSION = "os_version" + const val RANDOM_USER_ID = "random_user_id" + const val AFFISE_SDK_POS = "affise_sdk_pos" + const val TIMEZONE_DEV = "timezone_dev" + const val AFFISE_EVENT_TOKEN = "affise_event_token" //todo hold + const val LAST_TIME_SESSION = "last_time_session" + const val TIME_SESSION = "time_session" + const val AFFISE_SESSION_COUNT = "affise_session_count" + const val LIFETIME_SESSION_COUNT = "lifetime_session_count" //todo + const val AFFISE_DEEPLINK = "affise_deeplink" + const val AFFISE_PART_PARAM_NAME = "affise_part_param_name" + const val AFFISE_PART_PARAM_NAME_TOKEN = "affise_part_param_name_token" + const val AFFISE_APP_TOKEN = "affise_app_token" + const val LABEL = "label" //todo hold + const val AFFISE_SDK_SECRET_ID = "affise_sdk_secret_id" + const val UUID = "uuid" + const val AFFISE_APP_OPENED = "affise_app_opened" + const val PUSHTOKEN = "pushtoken" + const val AFFISE_EVENTS_COUNT = "affise_events_count" + const val AFFISE_SDK_EVENTS_COUNT = "affise_sdk_events_count" + const val AFFISE_METRICS_EVENTS_COUNT = "affise_metrics_events_count" + const val AFFISE_INTERNAL_EVENTS_COUNT = "affise_internal_events_count" +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/PlatformNameProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/PlatformNameProvider.kt new file mode 100644 index 0000000..76dd3e5 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/PlatformNameProvider.kt @@ -0,0 +1,15 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.platform.SdkPlatform +import com.affise.attribution.parameters.base.StringPropertyProvider + +/** + * Provider for parameter [Parameters.PLATFORM] + */ +class PlatformNameProvider : StringPropertyProvider() { + + override val order: Float = 44.0f + override val key: String = Parameters.PLATFORM + + override fun provide(): String = SdkPlatform.ANDROID +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/PushTokenProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/PushTokenProvider.kt new file mode 100644 index 0000000..5dbd062 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/PushTokenProvider.kt @@ -0,0 +1,23 @@ +package com.affise.attribution.parameters + +import android.content.SharedPreferences +import com.affise.attribution.parameters.base.StringPropertyProvider + +/** + * Provider for parameter [Parameters.PUSHTOKEN] + * + * @property preferences to retrieve push token + */ +class PushTokenProvider( + private val preferences: SharedPreferences +) : StringPropertyProvider() { + + override val order: Float = 65.0f + override val key: String = Parameters.PUSHTOKEN + + override fun provide(): String? = preferences.getString(KEY_APP_PUSHTOKEN, null) + + companion object { + const val KEY_APP_PUSHTOKEN = "com.affise.attribution.init.PUSHTOKEN" + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/RandomUserIdProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/RandomUserIdProvider.kt new file mode 100644 index 0000000..9b3099d --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/RandomUserIdProvider.kt @@ -0,0 +1,19 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.StringPropertyProvider +import com.affise.attribution.usecase.FirstAppOpenUseCase + +/** + * Provider for parameter [Parameters.RANDOM_USER_ID] + * + * @property useCase to retrieve random user id + */ +internal class RandomUserIdProvider( + private val useCase: FirstAppOpenUseCase +) : StringPropertyProvider() { + + override val order: Float = 49.0f + override val key: String = Parameters.RANDOM_USER_ID + + override fun provide(): String? = useCase.getRandomUserId() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/RefTokenProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/RefTokenProvider.kt new file mode 100644 index 0000000..d5216aa --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/RefTokenProvider.kt @@ -0,0 +1,44 @@ +package com.affise.attribution.parameters + +import android.annotation.SuppressLint +import android.content.SharedPreferences +import com.affise.attribution.parameters.base.StringPropertyProvider +import com.affise.attribution.utils.generateUUID + +/** + * Provider for parameter [Parameters.REFTOKEN] + * + * @property preferences to retrieve reftoken + */ +class RefTokenProvider( + private val preferences: SharedPreferences +) : StringPropertyProvider() { + + override val order: Float = 32.0f + override val key: String = Parameters.REFTOKEN + + @SuppressLint("ApplySharedPref") + override fun provide(): String { + //Get token + val token = preferences.getString(KEY, null) + + //Check token + return if (token == null) { + //If token is empty generate new token + val newToken = generateUUID().toString() + + //Save token to preferences + preferences.edit().apply { + putString(KEY, newToken) + }.commit() + + newToken + } else { + token + } + } + + companion object { + private const val KEY = "com.affise.attribution.parameters.REFTOKEN" + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/RefTokensProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/RefTokensProvider.kt new file mode 100644 index 0000000..98da4ee --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/RefTokensProvider.kt @@ -0,0 +1,44 @@ +package com.affise.attribution.parameters + +import android.annotation.SuppressLint +import android.content.SharedPreferences +import com.affise.attribution.parameters.base.StringPropertyProvider +import com.affise.attribution.utils.generateUUID + +/** + * Provider for parameter [Parameters.REFTOKENS] + * + * @property preferences to retrieve reftoken + */ +class RefTokensProvider( + private val preferences: SharedPreferences +) : StringPropertyProvider() { + + override val order: Float = 33.0f + override val key: String = Parameters.REFTOKENS + + @SuppressLint("ApplySharedPref") + override fun provide(): String { + //Get token + val token = preferences.getString(KEY, null) + + //Check token + return if (token == null) { + //If token is empty generate new token + val newToken = generateUUID().toString() + + //Save token to preferences + preferences.edit().apply { + putString(KEY, newToken) + }.commit() + + newToken + } else { + token + } + } + + companion object { + private const val KEY = "com.affise.attribution.parameters.REFTOKENS" + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/ReferralTimeProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/ReferralTimeProvider.kt new file mode 100644 index 0000000..036e285 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/ReferralTimeProvider.kt @@ -0,0 +1,20 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.LongPropertyProvider +import com.affise.attribution.usecase.RetrieveInstallReferrerUseCase + +/** + * Provider for parameter [Parameters.REFERRAL_TIME] + * + * @property referrerUseCase usecase to retrieve install begin time + */ +class ReferralTimeProvider( + private val referrerUseCase: RetrieveInstallReferrerUseCase +) : LongPropertyProvider() { + + override val order: Float = 14.0f + override val key: String = Parameters.REFERRAL_TIME + + override fun provide(): Long? = referrerUseCase.getInstallReferrer() + ?.installBeginTimestampServerSeconds +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/ReferrerClickTimestampProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/ReferrerClickTimestampProvider.kt new file mode 100644 index 0000000..c68980b --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/ReferrerClickTimestampProvider.kt @@ -0,0 +1,22 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.LongPropertyProvider +import com.affise.attribution.usecase.RetrieveInstallReferrerUseCase + + +/** + * Provider for parameter [Parameters.REFERRER_CLICK_TIME] + * + * @property useCase usecase to retrieve client-side timestamp, in seconds, when the referrer click happened. + */ +class ReferrerClickTimestampProvider( + private val useCase: RetrieveInstallReferrerUseCase +) : LongPropertyProvider() { + + override val order: Float = 15.0f + override val key: String = Parameters.REFERRER_CLICK_TIME + + override fun provide(): Long? = useCase.getInstallReferrer() + ?.referrerClickTimestampSeconds + ?.takeIf { it != 0L } +} diff --git a/attribution/src/main/java/com/affise/attribution/parameters/ReferrerClickTimestampServerProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/ReferrerClickTimestampServerProvider.kt new file mode 100644 index 0000000..6650fd2 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/ReferrerClickTimestampServerProvider.kt @@ -0,0 +1,23 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.LongPropertyProvider +import com.affise.attribution.usecase.RetrieveInstallReferrerUseCase + + +/** + * Provider for parameter [Parameters.REFERRER_CLICK_TIME_SERVER] + * + * @property useCase usecase to retrieve server-side timestamp, in seconds, when the referrer click happened. + */ +class ReferrerClickTimestampServerProvider( + private val useCase: RetrieveInstallReferrerUseCase +) : LongPropertyProvider() { + + override val order: Float = 16.0f + override val key: String = Parameters.REFERRER_CLICK_TIME_SERVER + + override fun provide(): Long? = useCase.getInstallReferrer() + ?.referrerClickTimestampServerSeconds + ?.takeIf { it != 0L } +} + diff --git a/attribution/src/main/java/com/affise/attribution/parameters/ReferrerGooglePlayInstantProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/ReferrerGooglePlayInstantProvider.kt new file mode 100644 index 0000000..1a02365 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/ReferrerGooglePlayInstantProvider.kt @@ -0,0 +1,21 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.BooleanPropertyProvider +import com.affise.attribution.usecase.RetrieveInstallReferrerUseCase + + +/** + * Provider for parameter [Parameters.REFERRER_GOOGLE_PLAY_INSTANT] + * + * @property referrerUseCase usecase to retrieve your app's instant experience was launched within the past 7 days. + */ +class ReferrerGooglePlayInstantProvider( + private val referrerUseCase: RetrieveInstallReferrerUseCase +) : BooleanPropertyProvider() { + + override val order: Float = 17.0f + override val key: String = Parameters.REFERRER_GOOGLE_PLAY_INSTANT + + override fun provide(): Boolean? = referrerUseCase.getInstallReferrer() + ?.googlePlayInstantParam +} diff --git a/attribution/src/main/java/com/affise/attribution/parameters/ReferrerInstallVersionProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/ReferrerInstallVersionProvider.kt new file mode 100644 index 0000000..d3fdc03 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/ReferrerInstallVersionProvider.kt @@ -0,0 +1,19 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.StringPropertyProvider +import com.affise.attribution.usecase.RetrieveInstallReferrerUseCase + +/** + * Provider for parameter [Parameters.REFERRER_INSTALL_VERSION] + * + * @property useCase usecase to retrieve install version from + */ +class ReferrerInstallVersionProvider( + private val useCase: RetrieveInstallReferrerUseCase +) : StringPropertyProvider() { + + override val order: Float = 13.0f + override val key: String = Parameters.REFERRER_INSTALL_VERSION + + override fun provide(): String? = useCase.getInstallReferrer()?.installVersion +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/RegionProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/RegionProvider.kt new file mode 100644 index 0000000..baf4ab2 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/RegionProvider.kt @@ -0,0 +1,15 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.StringPropertyProvider +import java.util.Locale + +/** + * Provider for parameter [Parameters.REGION] + */ +class RegionProvider : StringPropertyProvider() { + + override val order: Float = 38.0f + override val key: String = Parameters.REGION + + override fun provide(): String? = Locale.getDefault().country +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/SdkPlatformNameProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/SdkPlatformNameProvider.kt new file mode 100644 index 0000000..77beb52 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/SdkPlatformNameProvider.kt @@ -0,0 +1,15 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.platform.SdkPlatform +import com.affise.attribution.parameters.base.StringPropertyProvider + +/** + * Provider for parameter [Parameters.SDK_PLATFORM] + */ +class SdkPlatformNameProvider : StringPropertyProvider() { + + override val order: Float = 45.0f + override val key: String = Parameters.SDK_PLATFORM + + override fun provide(): String = SdkPlatform.info +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/StoreProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/StoreProvider.kt new file mode 100644 index 0000000..1cff947 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/StoreProvider.kt @@ -0,0 +1,75 @@ +package com.affise.attribution.parameters + +import android.app.Application +import android.os.Build +import com.affise.attribution.logs.LogsManager +import com.affise.attribution.parameters.base.StringPropertyProvider +import com.affise.attribution.utils.SystemAppChecker + +/** + * Provider for parameter [Parameters.STORE] + * + * @property app to retrieve package manager + * @property logsManager for error logging + * @property systemAppChecker to check system for preinstall + */ +class StoreProvider( + private val app: Application, + private val logsManager: LogsManager, + private val systemAppChecker: SystemAppChecker +) : StringPropertyProvider() { + + override val order: Float = 5.0f + override val key: String = Parameters.STORE + + /** + * Installer name + */ + private val installerName by lazy { + when { + !systemAppChecker.getSystemProperty(PREINSTALL_NAME).isNullOrEmpty() -> PREINSTALL + systemAppChecker.isPreinstallApp() -> PREINSTALL + else -> getInitiatingPackageName().let { + when (it) { + PACKAGE_GOOGLE -> GOOGLE + PACKAGE_HUAWEI -> HUAWEI + PACKAGE_AMAZON -> AMAZON + else -> APK + } + } + } + } + + override fun provide(): String = installerName + + /** + * Get initiating app package name + */ + @Suppress("DEPRECATION") + private fun getInitiatingPackageName() = try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + app.packageManager.getInstallSourceInfo(app.packageName).initiatingPackageName + } else { + app.packageManager.getInstallerPackageName(app.packageName) + } + } catch (throwable: Throwable) { + //log error + logsManager.addDeviceError(throwable) + + null + } + + companion object { + private const val PREINSTALL_NAME = "affise_part_param_name" + + private const val PACKAGE_GOOGLE = "com.android.vending" + private const val PACKAGE_HUAWEI = "com.huawei.appmarket" + private const val PACKAGE_AMAZON = "com.amazon.venezia" + + private const val GOOGLE = "GooglePlay" + private const val HUAWEI = "AppGalery" + private const val AMAZON = "Amazon" + private const val PREINSTALL = "Preinstall" + private const val APK = "Apk" + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/TimeSessionProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/TimeSessionProvider.kt new file mode 100644 index 0000000..6e9a2d2 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/TimeSessionProvider.kt @@ -0,0 +1,19 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.LongPropertyProvider +import com.affise.attribution.session.SessionManager + +/** + * Provider for parameter [Parameters.TIME_SESSION] + * + * @property sessionManager to retrieve session time + */ +class TimeSessionProvider( + private val sessionManager: SessionManager +) : LongPropertyProvider() { + + override val order: Float = 55.0f + override val key: String = Parameters.TIME_SESSION + + override fun provide(): Long = sessionManager.getSessionTime() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/TimezoneDeviceProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/TimezoneDeviceProvider.kt new file mode 100644 index 0000000..b7e9eb0 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/TimezoneDeviceProvider.kt @@ -0,0 +1,50 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.StringPropertyProvider +import java.util.Calendar +import java.util.concurrent.TimeUnit +import kotlin.math.absoluteValue + +/** + * Provider for parameter [Parameters.TIMEZONE_DEV] + */ +class TimezoneDeviceProvider : StringPropertyProvider() { + + override val order: Float = 51.0f + override val key: String = Parameters.TIMEZONE_DEV + + /** + * Returns timezone formatted in UTC template, for ex: UTC+0200 + */ + override fun provide(): String = Calendar.getInstance().let { + it.timeZone.getOffset(it.timeInMillis).toOffsetStr() + } + + /** + * Get offset string from int value + * @return offset string + */ + private fun Int.toOffsetStr(): String { + //Convert to minutes + val inMinutes = TimeUnit.MILLISECONDS.toMinutes(this.toLong()) + + //Get sign + val sign = if (inMinutes < 0) "-" else "+" + + //Get hours + val hours = inMinutes.div(60).format() + + //Get minutes + val minutes = inMinutes.rem(60).format() + + //Generate offset string + return "UTC$sign$hours$minutes" + } + + /** + * Formatting value to length = 2 + */ + private fun Long.format() = absoluteValue + .toString() + .padStart(2, '0') +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/UserAgentProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/UserAgentProvider.kt new file mode 100644 index 0000000..cbe7a55 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/UserAgentProvider.kt @@ -0,0 +1,14 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.StringPropertyProvider + +/** + * Provider for parameter [Parameters.USER_AGENT] + */ +class UserAgentProvider : StringPropertyProvider() { + + override val order: Float = 35.0f + override val key: String = Parameters.USER_AGENT + + override fun provide(): String? = System.getProperty("http.agent") +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/UuidProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/UuidProvider.kt new file mode 100644 index 0000000..ecbb6b7 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/UuidProvider.kt @@ -0,0 +1,15 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.parameters.base.StringPropertyProvider +import com.affise.attribution.utils.generateUUID + +/** + * Provider for parameter [Parameters.UUID] + */ +class UuidProvider : StringPropertyProvider() { + + override val order: Float = 64.0f + override val key: String = Parameters.UUID + + override fun provide(): String? = generateUUID().toString() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/base/PropertiesProviderFactory.kt b/attribution/src/main/java/com/affise/attribution/parameters/base/PropertiesProviderFactory.kt new file mode 100644 index 0000000..146e6f4 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/base/PropertiesProviderFactory.kt @@ -0,0 +1,116 @@ +package com.affise.attribution.parameters.base + +import android.app.Application +import android.content.SharedPreferences +import com.affise.attribution.build.BuildConfigPropertiesProvider +import com.affise.attribution.converter.Converter +import com.affise.attribution.deeplink.DeeplinkClickRepository +import com.affise.attribution.init.InitPropertiesStorage +import com.affise.attribution.logs.LogsManager +import com.affise.attribution.parameters.CustomLongProvider +import com.affise.attribution.parameters.EmptyStringProvider +import com.affise.attribution.parameters.* +import com.affise.attribution.parameters.factory.PostBackModelFactory +import com.affise.attribution.session.SessionManager +import com.affise.attribution.usecase.FirstAppOpenUseCase +import com.affise.attribution.usecase.RetrieveInstallReferrerUseCase +import com.affise.attribution.utils.SystemAppChecker + +/** + * Factory for [PostBackModelFactory] + */ +internal class PropertiesProviderFactory( + private val buildConfigPropertiesProvider: BuildConfigPropertiesProvider, + private val app: Application, + private val firstAppOpenUseCase: FirstAppOpenUseCase, + private val retrieveInstallReferrerUseCase: RetrieveInstallReferrerUseCase, + private val sessionManager: SessionManager, + private val sharedPreferences: SharedPreferences, + private val initPropertiesStorage: InitPropertiesStorage, + private val stringToMd5Converter: Converter, + private val stringToSha1Converter: Converter, + private val stringToSha256Converter: Converter, + private val logsManager: LogsManager, + private val deeplinkClickRepository: DeeplinkClickRepository, + private val installReferrerProvider: InstallReferrerProvider +) { + + fun create(): PostBackModelFactory { + val androidIdProvider = AndroidIdProvider(app) + val firstOpenTimeProvider = FirstOpenTimeProvider(firstAppOpenUseCase) + val lastSessionTimeProvider = LastSessionTimeProvider(sessionManager) + + return PostBackModelFactory( + providers = listOf( + UuidProvider(), + AffiseAppIdProvider(initPropertiesStorage), + AffisePackageAppNameProvider(app), + AppVersionProvider(app, logsManager), + AppVersionRawProvider(app, logsManager), + StoreProvider(app, logsManager, SystemAppChecker(app)), + InstalledTimeProvider(app, logsManager), + firstOpenTimeProvider, + InstalledHourProvider(app), + FirstOpenHourProvider(firstAppOpenUseCase), + InstallFirstEventProvider(firstAppOpenUseCase), + InstallBeginTimeProvider(retrieveInstallReferrerUseCase), + InstallFinishTimeProvider(firstAppOpenUseCase), + ReferrerInstallVersionProvider(retrieveInstallReferrerUseCase), + ReferralTimeProvider(retrieveInstallReferrerUseCase), + ReferrerClickTimestampProvider(retrieveInstallReferrerUseCase), + ReferrerClickTimestampServerProvider(retrieveInstallReferrerUseCase), + ReferrerGooglePlayInstantProvider(retrieveInstallReferrerUseCase), + CreatedTimeProvider(), + CreatedTimeMilliProvider(), + CreatedTimeHourProvider(), + CustomLongProvider(Parameters.LAST_TIME_SESSION, 54.0f) { + lastSessionTimeProvider.provide() + ?.takeIf { it > 0 } + ?: firstOpenTimeProvider.provideWithDefault() + }, + CpuTypeProvider(buildConfigPropertiesProvider), + HardwareNameProvider(buildConfigPropertiesProvider), + DeviceManufacturerProvider(buildConfigPropertiesProvider), + DeeplinkClickPropertyProvider(deeplinkClickRepository), + EmptyStringProvider(Parameters.DEVICE_ATLAS_ID, 26.0f), + AffiseDeviceIdProvider(firstAppOpenUseCase), + AffiseAltDeviceIdProvider(firstAppOpenUseCase), + androidIdProvider, + AndroidIdMD5Provider(androidIdProvider, stringToMd5Converter), + RefTokenProvider(sharedPreferences), + RefTokensProvider(sharedPreferences), + installReferrerProvider, + UserAgentProvider(), + MCCProvider(app), + MNCProvider(app), + RegionProvider(), + CountryProvider(), + LanguageProvider(), + DeviceNameProvider(app), + DeviceTypeProvider(app), + OsNameProvider(buildConfigPropertiesProvider), + PlatformNameProvider(), + SdkPlatformNameProvider(), + ApiLevelOSProvider(buildConfigPropertiesProvider), + AffSDKVersionProvider(), + OSVersionProvider(buildConfigPropertiesProvider), + RandomUserIdProvider(firstAppOpenUseCase), + IsProductionPropertyProvider(initPropertiesStorage), + TimezoneDeviceProvider(), + EmptyStringProvider(Parameters.AFFISE_EVENT_TOKEN, 52.0f), + EmptyStringProvider(Parameters.AFFISE_EVENT_NAME, 53.0f), + lastSessionTimeProvider, + TimeSessionProvider(sessionManager), + AffiseSessionCountProvider(sessionManager), + LifetimeSessionCountProvider(sessionManager), + DeeplinkProvider(deeplinkClickRepository), + AffPartParamNamePropertyProvider(initPropertiesStorage), + AffPartParamNameTokenPropertyProvider(initPropertiesStorage), + AffAppTokenPropertyProvider(initPropertiesStorage, stringToSha256Converter), + EmptyStringProvider(Parameters.LABEL, 62.0f), +// AffSDKSecretIdProvider(initPropertiesStorage), + PushTokenProvider(sharedPreferences), + ) + ) + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/base/PropertyProvider.kt b/attribution/src/main/java/com/affise/attribution/parameters/base/PropertyProvider.kt new file mode 100644 index 0000000..a2d2471 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/base/PropertyProvider.kt @@ -0,0 +1,76 @@ +package com.affise.attribution.parameters.base + +/** + * Base property provider + */ +abstract class PropertyProvider : Provider { + + /** + * Default value of provider + */ + abstract val defaultValue: T + + /** + * Provide data + */ + abstract fun provide(): T? + + /** + * Provide data with default value + */ + fun provideWithDefault(): T = provide() ?: defaultValue +} + +/** + * Base string property provider + */ +abstract class StringPropertyProvider : PropertyProvider() { + + /** + * Default value of provider + */ + override val defaultValue = "" +} + +/** + * Base boolean property provider + */ +abstract class BooleanPropertyProvider : PropertyProvider() { + + /** + * Default value of provider + */ + override val defaultValue = false +} + +/** + * Base long property provider + */ +abstract class LongPropertyProvider : PropertyProvider() { + + /** + * Default value of provider + */ + override val defaultValue = 0L +} + +/** + * Base string property provider with param + */ +abstract class StringWithParamPropertyProvider : Provider { + + /** + * Default value of provider + */ + private val defaultValue = "" + + /** + * Provide data with param + */ + abstract fun provideWithParam(param: String): String? + + /** + * Provide data with param and default value + */ + fun provideWithParamAndDefault(param: String): String = provideWithParam(param) ?: defaultValue +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/parameters/base/Provider.kt b/attribution/src/main/java/com/affise/attribution/parameters/base/Provider.kt new file mode 100644 index 0000000..f49aad1 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/base/Provider.kt @@ -0,0 +1,6 @@ +package com.affise.attribution.parameters.base + +interface Provider { + val order: Float + val key: String? +} diff --git a/attribution/src/main/java/com/affise/attribution/parameters/factory/PostBackModelFactory.kt b/attribution/src/main/java/com/affise/attribution/parameters/factory/PostBackModelFactory.kt new file mode 100644 index 0000000..71847f3 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/parameters/factory/PostBackModelFactory.kt @@ -0,0 +1,64 @@ +package com.affise.attribution.parameters.factory + +import com.affise.attribution.parameters.base.PropertyProvider +import com.affise.attribution.parameters.base.Provider +import com.affise.attribution.parameters.base.StringWithParamPropertyProvider +import com.affise.attribution.events.SerializedEvent +import com.affise.attribution.logs.SerializedLog +import com.affise.attribution.network.entity.PostBackModel +import com.affise.attribution.parameters.AffAppTokenPropertyProvider +import com.affise.attribution.parameters.CreatedTimeProvider + +internal class PostBackModelFactory( + providers: List +) { + private val allProviders: MutableList = providers.toMutableList() + + private fun mapProviders(): Map { + val createdTime = getProvider()?.provideWithDefault() + val sorted = allProviders.sortedBy { it.order }.filter { it.key != null } + return sorted.mapNotNull { provider -> + provider.key?.let { + it to when (provider) { + is CreatedTimeProvider -> createdTime + is PropertyProvider<*> -> provider.provideWithDefault() + is StringWithParamPropertyProvider -> { + when(provider) { + is AffAppTokenPropertyProvider -> provider.provideWithParamAndDefault(createdTime?.toString() ?: "") + else -> null + } + } + else -> null + } + } + }.toMap() + } + + /** + * Create PostBackModel with [events] and [logs] + * + * @return PostBackModel + */ + fun create( + events: List = emptyList(), + logs: List = emptyList(), + metrics: List = emptyList(), + internalEvents: List = emptyList(), + ): PostBackModel { + return PostBackModel( + parameters = mapProviders(), + events = events, + logs = logs, + metrics = metrics, + internalEvents = internalEvents, + ) + } + + inline fun getProvider(): T? { + return allProviders.firstOrNull { it is T } as? T + } + + fun addProviders(list: List) { + allProviders.addAll(list) + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/platform/SdkPlatform.kt b/attribution/src/main/java/com/affise/attribution/platform/SdkPlatform.kt new file mode 100644 index 0000000..a8695d8 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/platform/SdkPlatform.kt @@ -0,0 +1,25 @@ +package com.affise.attribution.platform + +internal object SdkPlatform { + const val ANDROID = "android" + private const val REACT = "react" + private const val FLUTTER = "flutter" + private const val UNITY = "unity" + + private var data = ANDROID + + internal val info: String + get() = data + + fun react() { + data = "$REACT $ANDROID" + } + + fun flutter() { + data = "$FLUTTER $ANDROID" + } + + fun unity() { + data = "$UNITY $ANDROID" + } +} diff --git a/attribution/src/main/java/com/affise/attribution/preferences/ApplicationLifecyclePreferencesRepository.kt b/attribution/src/main/java/com/affise/attribution/preferences/ApplicationLifecyclePreferencesRepository.kt new file mode 100644 index 0000000..d24ae4f --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/preferences/ApplicationLifecyclePreferencesRepository.kt @@ -0,0 +1,14 @@ +package com.affise.attribution.preferences + +import com.affise.attribution.preferences.models.ApplicationLifecyclePreferences + +/** + * Repository to store [ApplicationLifecyclePreferences] + */ +internal interface ApplicationLifecyclePreferencesRepository { + + /** + * property to access to model stored by repository + */ + var preferences: ApplicationLifecyclePreferences +} diff --git a/attribution/src/main/java/com/affise/attribution/preferences/ApplicationLifecyclePreferencesRepositoryImpl.kt b/attribution/src/main/java/com/affise/attribution/preferences/ApplicationLifecyclePreferencesRepositoryImpl.kt new file mode 100644 index 0000000..883c343 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/preferences/ApplicationLifecyclePreferencesRepositoryImpl.kt @@ -0,0 +1,12 @@ +package com.affise.attribution.preferences + +import com.affise.attribution.preferences.models.ApplicationLifecyclePreferences + +/** + * Implementation of [ApplicationLifecyclePreferencesRepository] + */ +internal class ApplicationLifecyclePreferencesRepositoryImpl : + ApplicationLifecyclePreferencesRepository { + override var preferences = ApplicationLifecyclePreferences() +} + diff --git a/attribution/src/main/java/com/affise/attribution/preferences/ApplicationLifetimePreferencesRepository.kt b/attribution/src/main/java/com/affise/attribution/preferences/ApplicationLifetimePreferencesRepository.kt new file mode 100644 index 0000000..10b100c --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/preferences/ApplicationLifetimePreferencesRepository.kt @@ -0,0 +1,14 @@ +package com.affise.attribution.preferences + +import com.affise.attribution.preferences.models.ApplicationLifetimePreferences + +/** + * Repository for [ApplicationLifetimePreferences] model + */ +internal interface ApplicationLifetimePreferencesRepository { + + /** + * property to access to model stored by repository + */ + var preferences: ApplicationLifetimePreferences +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/preferences/ApplicationLifetimePreferencesRepositoryImpl.kt b/attribution/src/main/java/com/affise/attribution/preferences/ApplicationLifetimePreferencesRepositoryImpl.kt new file mode 100644 index 0000000..05d55d4 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/preferences/ApplicationLifetimePreferencesRepositoryImpl.kt @@ -0,0 +1,32 @@ +package com.affise.attribution.preferences + +import android.content.SharedPreferences +import com.affise.attribution.preferences.models.ApplicationLifetimePreferences + +/** + * Implementation of [ApplicationLifetimePreferencesRepository] + * + * @property sharedPreferences to store [ApplicationLifetimePreferences] model + */ +internal class ApplicationLifetimePreferencesRepositoryImpl( + private val sharedPreferences: SharedPreferences +) : ApplicationLifetimePreferencesRepository { + override var preferences: ApplicationLifetimePreferences + get() { + return ApplicationLifetimePreferences( + trackingEnabled = sharedPreferences.getBoolean(PREFERENCE_TRACKING, true) + ) + } + set(value) { + sharedPreferences.edit() + .apply { + putBoolean(PREFERENCE_TRACKING, value.trackingEnabled) + } + .apply() + } + + companion object { + private const val PREFERENCE_TRACKING = + "com.affise.attribution.preferences.PREFERENCE_TRACKING" + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/preferences/models/ApplicationLifecyclePreferences.kt b/attribution/src/main/java/com/affise/attribution/preferences/models/ApplicationLifecyclePreferences.kt new file mode 100644 index 0000000..647d69a --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/preferences/models/ApplicationLifecyclePreferences.kt @@ -0,0 +1,12 @@ +package com.affise.attribution.preferences.models + +/** + * Model describes preferences which persist until application restart + * + * @property offlineMode when enabled, no network activity should be triggered by library + * @property backgroundTracking when disabled, library should not generate any tracking events while in background + */ +internal data class ApplicationLifecyclePreferences( + val offlineMode: Boolean = false, + val backgroundTracking: Boolean = true +) \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/preferences/models/ApplicationLifetimePreferences.kt b/attribution/src/main/java/com/affise/attribution/preferences/models/ApplicationLifetimePreferences.kt new file mode 100644 index 0000000..6aedff7 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/preferences/models/ApplicationLifetimePreferences.kt @@ -0,0 +1,10 @@ +package com.affise.attribution.preferences.models + +/** + * Model describes preferences which persist until application reinstall + * + * @property trackingEnabled when disabled, library should not generate any tracking events + */ +internal data class ApplicationLifetimePreferences( + val trackingEnabled: Boolean = true +) \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/preferences/models/BackgroundTrackingDisabledException.kt b/attribution/src/main/java/com/affise/attribution/preferences/models/BackgroundTrackingDisabledException.kt new file mode 100644 index 0000000..18bf1b0 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/preferences/models/BackgroundTrackingDisabledException.kt @@ -0,0 +1,7 @@ +package com.affise.attribution.preferences.models + +/** + * Exception describes that background tracking is disabled by library user + * Triggered when background tracking activity is not possible by user preference + */ +internal class BackgroundTrackingDisabledException: IllegalStateException() \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/preferences/models/OfflineModeEnabledException.kt b/attribution/src/main/java/com/affise/attribution/preferences/models/OfflineModeEnabledException.kt new file mode 100644 index 0000000..5fcc835 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/preferences/models/OfflineModeEnabledException.kt @@ -0,0 +1,8 @@ +package com.affise.attribution.preferences.models + +/** + * Exception describes that offline mode is set by library user + * Triggered when network activity is not possible by user preference + */ +internal class OfflineModeEnabledException: IllegalStateException() + diff --git a/attribution/src/main/java/com/affise/attribution/preferences/models/TrackingDisabledException.kt b/attribution/src/main/java/com/affise/attribution/preferences/models/TrackingDisabledException.kt new file mode 100644 index 0000000..7586604 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/preferences/models/TrackingDisabledException.kt @@ -0,0 +1,8 @@ +package com.affise.attribution.preferences.models + +/** + * Exception describes that tracking mode is disabled by library user + * Triggered when not possible to store event + */ +internal class TrackingDisabledException: IllegalStateException() + diff --git a/attribution/src/main/java/com/affise/attribution/referrer/AffiseReferrerData.kt b/attribution/src/main/java/com/affise/attribution/referrer/AffiseReferrerData.kt new file mode 100644 index 0000000..51e70c1 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/referrer/AffiseReferrerData.kt @@ -0,0 +1,33 @@ +package com.affise.attribution.referrer + +/** + * Model to store install referrer data + * + * @property installReferrer The referrer URL of the installed package. + * @property referrerClickTimestampSeconds The client-side timestamp, in seconds, when the referrer click happened. + * @property installBeginTimestampSeconds The client-side timestamp, in seconds, when app installation began. + * @property referrerClickTimestampServerSeconds The server-side timestamp, in seconds, when the referrer click happened. + * @property installBeginTimestampServerSeconds The server-side timestamp, in seconds, when app installation began. + * @property installVersion The app's version at the time when the app was first installed. + * @property googlePlayInstantParam Indicates whether your app's instant experience was launched within the past 7 days. + * + */ +data class AffiseReferrerData( + val installReferrer: String, + val referrerClickTimestampSeconds: Long, + val installBeginTimestampSeconds: Long, + val referrerClickTimestampServerSeconds: Long, + val installBeginTimestampServerSeconds: Long, + val installVersion: String, + val googlePlayInstantParam: Boolean, +) { + object KEYS { + const val installReferrer = "installReferrer" + const val referrerClickTimestampSeconds = "referrerClickTimestampSeconds" + const val installBeginTimestampSeconds = "installBeginTimestampSeconds" + const val referrerClickTimestampServerSeconds = "referrerClickTimestampServerSeconds" + const val installBeginTimestampServerSeconds = "installBeginTimestampServerSeconds" + const val installVersion = "installVersion" + const val googlePlayInstantParam = "googlePlayInstantParam" + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/referrer/AffiseReferrerDataToStringConverter.kt b/attribution/src/main/java/com/affise/attribution/referrer/AffiseReferrerDataToStringConverter.kt new file mode 100644 index 0000000..83b028c --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/referrer/AffiseReferrerDataToStringConverter.kt @@ -0,0 +1,48 @@ +package com.affise.attribution.referrer + +import com.affise.attribution.converter.Converter +import org.json.JSONObject + +/** + * Converter from [AffiseReferrerData] to [String] + */ +class AffiseReferrerDataToStringConverter : Converter { + + /** + * Convert [from] AffiseReferrerData to String + * + * @return AffiseReferrerData of string + */ + override fun convert(from: AffiseReferrerData) = mapOf( + Pair( + AffiseReferrerData.KEYS.installReferrer, + from.installReferrer + ), + Pair( + AffiseReferrerData.KEYS.referrerClickTimestampSeconds, + from.referrerClickTimestampSeconds + ), + Pair( + AffiseReferrerData.KEYS.installBeginTimestampSeconds, + from.installBeginTimestampSeconds + ), + Pair( + AffiseReferrerData.KEYS.referrerClickTimestampServerSeconds, + from.referrerClickTimestampServerSeconds + ), + Pair( + AffiseReferrerData.KEYS.installBeginTimestampServerSeconds, + from.installBeginTimestampServerSeconds + ), + Pair( + AffiseReferrerData.KEYS.installVersion, + from.installVersion + ), + Pair( + AffiseReferrerData.KEYS.googlePlayInstantParam, + from.googlePlayInstantParam + ), + ) + .let(::JSONObject) + .toString() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/referrer/OnReferrerCallback.kt b/attribution/src/main/java/com/affise/attribution/referrer/OnReferrerCallback.kt new file mode 100644 index 0000000..5f52f75 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/referrer/OnReferrerCallback.kt @@ -0,0 +1,12 @@ +package com.affise.attribution.referrer + + +/** + * Interface describing callback that is going to be triggered when Referrer is received by application + */ +fun interface OnReferrerCallback { + /** + * Triggered when new Referrer is received by application + */ + fun handleReferrer(value: String?) +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/referrer/ReferrerKey.kt b/attribution/src/main/java/com/affise/attribution/referrer/ReferrerKey.kt new file mode 100644 index 0000000..0985cf4 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/referrer/ReferrerKey.kt @@ -0,0 +1,37 @@ +package com.affise.attribution.referrer + +/** + * Type of referrer affise key + */ +enum class ReferrerKey(val type: String) { + AD_ID("ad_id"), + CAMPAIGN_ID("campaign_id"), + CLICK_ID("clickid"), + AFFISE_AD("affise_ad"), + AFFISE_AD_ID("affise_ad_id"), + AFFISE_AD_TYPE("affise_ad_type"), + AFFISE_ADSET("affise_adset"), + AFFISE_ADSET_ID("affise_adset_id"), + AFFISE_AFFC_ID("affise_affc_id"), + AFFISE_CHANNEL("affise_channel"), + AFFISE_CLICK_LOOK_BACK("affise_click_lookback"), + AFFISE_COST_CURRENCY("affise_cost_currency"), + AFFISE_COST_MODEL("affise_cost_model"), + AFFISE_COST_VALUE("affise_cost_value"), + AFFISE_DEEPLINK("affise_deeplink"), + AFFISE_KEYWORDS("affise_keywords"), + AFFISE_MEDIA_TYPE("affise_media_type"), + AFFISE_MODEL("affise_model"), + AFFISE_OS("affise_os"), + AFFISE_PARTNER("affise_partner"), + AFFISE_REF("affise_ref"), + AFFISE_SITE_ID("affise_siteid"), + AFFISE_SUB_SITE_ID("affise_sub_siteid"), + AFFC("affc"), + PID("pid"), + SUB_1("sub1"), + SUB_2("sub2"), + SUB_3("sub3"), + SUB_4("sub4"), + SUB_5("sub5") +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/session/CurrentActiveActivityCountProvider.kt b/attribution/src/main/java/com/affise/attribution/session/CurrentActiveActivityCountProvider.kt new file mode 100644 index 0000000..c82a314 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/session/CurrentActiveActivityCountProvider.kt @@ -0,0 +1,22 @@ +package com.affise.attribution.session + +/** + * Active activity count provider interface + */ +interface CurrentActiveActivityCountProvider { + + /** + * Start provider + */ + fun init() + + /** + * Add [listener] to subscribe opened activity count + */ + fun addActivityCountListener(listener: ((count: Long) -> Unit)) + + /** + * @return current foreground activity count + */ + fun getActivityCount(): Long +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/session/CurrentActiveActivityCountProviderImpl.kt b/attribution/src/main/java/com/affise/attribution/session/CurrentActiveActivityCountProviderImpl.kt new file mode 100644 index 0000000..1681b57 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/session/CurrentActiveActivityCountProviderImpl.kt @@ -0,0 +1,79 @@ +package com.affise.attribution.session + +import com.affise.attribution.utils.ActivityActionsManager +import com.affise.attribution.utils.ActivityLifecycleCallback + +/** + * Provider for change activities count + */ +internal class CurrentActiveActivityCountProviderImpl( + private val activityActionsManager: ActivityActionsManager +) : CurrentActiveActivityCountProvider { + /** + * Count of open activity + */ + private var activityCount: Long = 0 + + /** + * Listener of change count of open activity + */ + private var activityCountListener: MutableList<((count: Long) -> Unit)> = mutableListOf() + + /** + * Listener of start activity + */ + private var onStartedSubscription: ActivityLifecycleCallback? = null + + /** + * Listener of stop activity + */ + private var onStoppedSubscription: ActivityLifecycleCallback? = null + + /** + * Start provider + */ + @Synchronized + override fun init() { + if (onStartedSubscription == null) { + onStartedSubscription = ActivityLifecycleCallback { _ -> + //Update open activity count + activityCount += 1 + + //Notify new count + activityCountListener.forEach { + it.invoke(activityCount) + } + }.apply { + activityActionsManager.addOnActivityStartedListener(this) + } + } + + if (onStoppedSubscription == null) { + onStoppedSubscription = ActivityLifecycleCallback { _ -> + //Update open activity count + if (activityCount > 0) { + activityCount -= 1 + } + + //Notify new count + activityCountListener.forEach { + it.invoke(activityCount) + } + }.apply { + activityActionsManager.addOnActivityStoppedListener(this) + } + } + } + + /** + * Add [listener] for change activity count + */ + override fun addActivityCountListener(listener: (count: Long) -> Unit) { + activityCountListener.add(listener) + } + + /** + * Get current open activities count + */ + override fun getActivityCount(): Long = activityCount +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/session/SessionManager.kt b/attribution/src/main/java/com/affise/attribution/session/SessionManager.kt new file mode 100644 index 0000000..ff0a1b3 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/session/SessionManager.kt @@ -0,0 +1,49 @@ +package com.affise.attribution.session + +/** + * Manager Session interface + */ +interface SessionManager { + + /** + * Init Manager + */ + fun init() + + /** + * Get session active status + * + * @return session is active or not + */ + fun isSessionActive(): Boolean + + /** + * Get last interaction time + * + * @return Last interaction time + */ + fun getLastInteractionTime(): Long? + + /** + * Get session time + * + * @return session time + */ + fun getSessionTime(): Long + + /** + * Get lifetime session time + * + * @return lifetime session time + */ + fun getLifetimeSessionTime(): Long + + /** + * Get session count + * + * @return session count + */ + fun getSessionCount(): Long + + fun sessionStart() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/session/SessionManagerImpl.kt b/attribution/src/main/java/com/affise/attribution/session/SessionManagerImpl.kt new file mode 100644 index 0000000..ab9000f --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/session/SessionManagerImpl.kt @@ -0,0 +1,225 @@ +package com.affise.attribution.session + +import android.annotation.SuppressLint +import android.content.SharedPreferences +import android.os.SystemClock +import com.affise.attribution.parameters.Parameters +import com.affise.attribution.internal.StoreInternalEventUseCase +import com.affise.attribution.internal.predefined.SessionStartInternalEvent +import com.affise.attribution.utils.delayRun +import com.affise.attribution.utils.timestamp +import java.util.* + +data class SessionData( + val lifetimeSessionCount: Long = 0, + val affiseSessionCount: Long = 0 +) + +internal class SessionManagerImpl( + private val preferences: SharedPreferences, + private val activityCountProvider: CurrentActiveActivityCountProvider, + private val internalEventUseCase: StoreInternalEventUseCase +) : SessionManager { + + private var sessionData: SessionData = SessionData( + preferences.getLong(Parameters.LIFETIME_SESSION_COUNT, 0L), + preferences.getLong(Parameters.AFFISE_SESSION_COUNT, 0L) + ) + + /** + * Time of start session + */ + private var openAppTime: Long? = null + + /** + * Last date time of user active + */ + private var closeAppDateTime: Long? = null + + /** + * Session active status + */ + private var sessionActive: Boolean = false + + /** + * Open app status + */ + private var isOpenApp: Boolean = false + + /** + * Start manager + */ + override fun init() = subscribeToActivityEvents() + + /** + * Subscribe to change open activity count + */ + private fun subscribeToActivityEvents() { + activityCountProvider.addActivityCountListener { count -> + //Check open activity count + if (count > 0) { + sessionStart() + } else { + //Update session status if need + checkSessionToStart() + + //Save date time of user quit or hide app + closeAppDateTime = timestamp() + + //App is close + isOpenApp = false + + //Drop session status + sessionActive = false + + //Save sessionTime + saveSessionTime() + + //Drop open app time + openAppTime = null + } + } + } + + override fun sessionStart() { + //App is open + isOpenApp = true + + /** + * Check create open app time + */ + if (openAppTime == null) { + //open app time + openAppTime = SystemClock.elapsedRealtime() + } + + delayRun(TIME_TO_START_SESSION) { + if (sessionTime() == 0L) return@delayRun + //Send sdk events + internalEventUseCase.storeInternalEvent( + SessionStartInternalEvent( + affiseSessionCount = getSessionCount(), + lifetimeSessionCount = getLifetimeSessionTime() + ) + ) + } + } + + /** + * Getting the last time the user was in the application + * @return time + */ + override fun getLastInteractionTime() = when { + //Current time if app is open + isOpenApp -> timestamp() + //lastInteractionTime is session is active + else -> closeAppDateTime + } + + /** + * Get session active status + * + * @return session status + */ + override fun isSessionActive(): Boolean { + //Check session status + checkSessionToStart() + + //Return session status + return sessionActive + } + + /** + * Check time of start app and start session + */ + private fun checkSessionToStart() { + if (sessionActive) return + + //if session started + if (sessionTime() > 0) { + sessionActive = true + + //Save new session + addNewSession() + } + } + + private fun sessionTime(): Long { + if (sessionActive) return 0 + + //Check open app time + openAppTime?.let { startTime -> + //Time current session + val time = SystemClock.elapsedRealtime() - startTime - TIME_TO_START_SESSION + + //if session started + if (time > 0) { + return time + } + } + return 0 + } + + /** + * Save session time + */ + @SuppressLint("ApplySharedPref") + private fun saveSessionTime() { + val lifetimeSessionTime = getLifetimeSessionTime() + + sessionData = sessionData.copy(lifetimeSessionCount = lifetimeSessionTime) + + preferences + .edit() + .putLong(Parameters.LIFETIME_SESSION_COUNT, lifetimeSessionTime) + .commit() + } + + /** + * Get all old sessions time + */ + private fun getSaveSessionsTime() = sessionData.lifetimeSessionCount + + /** + * Save new session count + */ + @SuppressLint("ApplySharedPref") + private fun addNewSession() { + val count = sessionData.affiseSessionCount + 1 + + sessionData = sessionData.copy(affiseSessionCount = count) + + preferences + .edit() + .putLong(Parameters.AFFISE_SESSION_COUNT, count) + .commit() + } + + /** + * Get session cont + */ + override fun getSessionCount(): Long { + //Check session status + if (!sessionActive) { + checkSessionToStart() + } + + return sessionData.affiseSessionCount + } + + /** + * Get current session time + */ + override fun getSessionTime() = openAppTime?.let { startTime -> + SystemClock.elapsedRealtime() - startTime + } ?: 0 + + /** + * Get all sessions time + */ + override fun getLifetimeSessionTime() = getSaveSessionsTime() + getSessionTime() + + companion object { + private const val TIME_TO_START_SESSION = 15 * 1000L + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/storages/EventsStorage.kt b/attribution/src/main/java/com/affise/attribution/storages/EventsStorage.kt new file mode 100644 index 0000000..343e9cf --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/storages/EventsStorage.kt @@ -0,0 +1,11 @@ +package com.affise.attribution.storages + +import com.affise.attribution.events.SerializedEvent + +internal interface EventsStorage { + fun hasEvents(key: String): Boolean + fun saveEvent(key: String, event: SerializedEvent) + fun getEvents(key: String?): List + fun deleteEvent(key: String?, ids: List) + fun clear() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/storages/EventsStorageImpl.kt b/attribution/src/main/java/com/affise/attribution/storages/EventsStorageImpl.kt new file mode 100644 index 0000000..397b4bc --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/storages/EventsStorageImpl.kt @@ -0,0 +1,135 @@ +package com.affise.attribution.storages + +import android.annotation.SuppressLint +import android.content.Context +import com.affise.attribution.events.EventsParams +import com.affise.attribution.events.SerializedEvent +import com.affise.attribution.logs.LogsManager +import com.affise.attribution.utils.timestamp +import org.json.JSONObject +import java.io.File +import java.io.FileReader +import java.io.FileWriter + +/** + * Storage of events + * + * @property context to retrieve app dir + * @property logsManager for error logging + */ +internal class EventsStorageImpl( + private val context: Context, + private val logsManager: LogsManager, +) : EventsStorage { + + /** + * Has save events by [key] or not + */ + override fun hasEvents(key: String) = getEventsDirectory(key) + .listFiles() + ?.isNotEmpty() + ?: false + + /** + * Store [event] by [key] + */ + @SuppressLint("ApplySharedPref") + override fun saveEvent(key: String, event: SerializedEvent) { + //Create file for event + val file = File(getEventsDirectory(key), event.id) + + //Write event to file + FileWriter(file).use { + it.write(event.data.toString()) + } + } + + /** + * Get serialized events by [key] + * + * @return list of serialized events + */ + override fun getEvents(key: String?): List = getEventsDirectory(key) + .listFiles() + ?.asSequence() + ?.filter { it.isFile } + ?.filter { file -> + //Filter old files + (file.lastModified() > timestamp() - EventsParams.EVENTS_STORE_TIME).also { isActual -> + if (!isActual) { + //Delete old files + file.runCatching { delete() } + } + } + } + ?.take(EventsParams.EVENTS_SEND_COUNT) + ?.mapNotNull { file -> + try { + //Get data from file + val data = FileReader(file).use { + JSONObject(it.readText()) + } + //Create serializedEvent + SerializedEvent(file.name, data) + } catch (e: Exception) { + //Remove file if not create event + file.runCatching { delete() } + + logsManager.addSdkError(e) + null + } + } + ?.toList() + ?: emptyList() + + /** + * Delete event for [key] by [ids] + */ + override fun deleteEvent(key: String?, ids: List) { + //Delete event + getEventsDirectory(key) + .listFiles { _, name -> + //Get all files be name + name in ids + } + ?.forEach { + //Remove file + it.runCatching { delete() } + } + } + + /** + * Removes all events + */ + override fun clear() { + context.getDir(EventsParams.EVENTS_DIR_NAME, Context.MODE_PRIVATE).deleteRecursively() + } + + /** + * Get events directory by [key] (subDir) + * @return dir for events + */ + private fun getEventsDirectory(key: String? = null): File { + //Get root events dir + val eventDir = context.getDir(EventsParams.EVENTS_DIR_NAME, Context.MODE_PRIVATE) + .apply { + //Create eventDir if doesn't exists + if (!exists()) mkdir() + } + + return key + ?.takeIf { + //Check key + it.isNotEmpty() + } + ?.let { + //Get subdirectory by key + File(eventDir, it) + } + ?.apply { + //Create subdirectory if doesn't exists + if (!exists()) mkdir() + } + ?: eventDir + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/storages/InternalEventsStorage.kt b/attribution/src/main/java/com/affise/attribution/storages/InternalEventsStorage.kt new file mode 100644 index 0000000..95f8b86 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/storages/InternalEventsStorage.kt @@ -0,0 +1,11 @@ +package com.affise.attribution.storages + +import com.affise.attribution.events.SerializedEvent + +internal interface InternalEventsStorage { + fun hasEvents(key: String): Boolean + fun saveEvent(key: String, event: SerializedEvent) + fun getEvents(key: String?): List + fun deleteEvent(key: String?, ids: List) + fun clear() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/storages/InternalEventsStorageImpl.kt b/attribution/src/main/java/com/affise/attribution/storages/InternalEventsStorageImpl.kt new file mode 100644 index 0000000..094788c --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/storages/InternalEventsStorageImpl.kt @@ -0,0 +1,135 @@ +package com.affise.attribution.storages + +import android.annotation.SuppressLint +import android.content.Context +import com.affise.attribution.events.SerializedEvent +import com.affise.attribution.logs.LogsManager +import com.affise.attribution.internal.InternalEventsParams +import com.affise.attribution.utils.timestamp +import org.json.JSONObject +import java.io.File +import java.io.FileReader +import java.io.FileWriter + +/** + * Storage of internal events + * + * @property context to retrieve app dir + * @property logsManager for error logging + */ +internal class InternalEventsStorageImpl( + private val context: Context, + private val logsManager: LogsManager, +) : InternalEventsStorage { + + /** + * Has save events by [key] or not + */ + override fun hasEvents(key: String) = getEventsDirectory(key) + .listFiles() + ?.isNotEmpty() + ?: false + + /** + * Store [event] by [key] + */ + @SuppressLint("ApplySharedPref") + override fun saveEvent(key: String, event: SerializedEvent) { + //Create file for event + val file = File(getEventsDirectory(key), event.id) + + //Write event to file + FileWriter(file).use { + it.write(event.data.toString()) + } + } + + /** + * Get serialized events by [key] + * + * @return list of serialized events + */ + override fun getEvents(key: String?): List = getEventsDirectory(key) + .listFiles() + ?.asSequence() + ?.filter { it.isFile } + ?.filter { file -> + //Filter old files + (file.lastModified() > timestamp() - InternalEventsParams.INTERNAL_EVENTS_STORE_TIME).also { isActual -> + if (!isActual) { + //Delete old files + file.runCatching { delete() } + } + } + } + ?.take(InternalEventsParams.INTERNAL_EVENTS_SEND_COUNT) + ?.mapNotNull { file -> + try { + //Get data from file + val data = FileReader(file).use { + JSONObject(it.readText()) + } + //Create serializedEvent + SerializedEvent(file.name, data) + } catch (e: Exception) { + //Remove file if not create event + file.runCatching { delete() } + + logsManager.addSdkError(e) + null + } + } + ?.toList() + ?: emptyList() + + /** + * Delete event for [key] by [ids] + */ + override fun deleteEvent(key: String?, ids: List) { + //Delete event + getEventsDirectory(key) + .listFiles { _, name -> + //Get all files be name + name in ids + } + ?.forEach { + //Remove file + it.runCatching { delete() } + } + } + + /** + * Removes all events + */ + override fun clear() { + context.getDir(InternalEventsParams.INTERNAL_EVENTS_DIR_NAME, Context.MODE_PRIVATE).deleteRecursively() + } + + /** + * Get events directory by [key] (subDir) + * @return dir for events + */ + private fun getEventsDirectory(key: String? = null): File { + //Get root events dir + val eventDir = context.getDir(InternalEventsParams.INTERNAL_EVENTS_DIR_NAME, Context.MODE_PRIVATE) + .apply { + //Create eventDir if doesn't exists + if (!exists()) mkdir() + } + + return key + ?.takeIf { + //Check key + it.isNotEmpty() + } + ?.let { + //Get subdirectory by key + File(eventDir, it) + } + ?.apply { + //Create subdirectory if doesn't exists + if (!exists()) mkdir() + } + ?: eventDir + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/storages/IsFirstForUserStorage.kt b/attribution/src/main/java/com/affise/attribution/storages/IsFirstForUserStorage.kt new file mode 100644 index 0000000..34c5596 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/storages/IsFirstForUserStorage.kt @@ -0,0 +1,7 @@ +package com.affise.attribution.storages + + +interface IsFirstForUserStorage { + fun add(eventClass: String) + fun getEventsNames(): List +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/storages/IsFirstForUserStorageImpl.kt b/attribution/src/main/java/com/affise/attribution/storages/IsFirstForUserStorageImpl.kt new file mode 100644 index 0000000..c1a691b --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/storages/IsFirstForUserStorageImpl.kt @@ -0,0 +1,56 @@ +package com.affise.attribution.storages + +import android.content.Context +import com.affise.attribution.events.EventsParams +import com.affise.attribution.logs.LogsManager +import java.io.File + +/** + * Storage of already send events + * + * @property context to retrieve app dir + * @property logsManager for error logging + */ +internal class IsFirstForUserStorageImpl( + private val context: Context, + private val logsManager: LogsManager, +) : IsFirstForUserStorage { + + + override fun add(eventClass: String) { + getEventsFile().appendText("${eventClass}\n") + } + + override fun getEventsNames(): List { + try { + return getEventsFile().readLines().map { it.trim() } + } catch (e: Exception) { + logsManager.addSdkError(e) + } + + return emptyList() + } + + /** + * Get events sent file + * @return file for events sent + */ + private fun getEventsFile(): File { + //Get root events dir + val eventDir = context.getDir(EventsParams.EVENTS_DIR_NAME, Context.MODE_PRIVATE) + .apply { + //Create eventDir if doesn't exists + if (!exists()) mkdir() + } + + val file = File(eventDir, NAME).apply { + if (!exists()) createNewFile() + } + + return file + } + + companion object { + private const val NAME = "first-for-user" + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/storages/LogsStorage.kt b/attribution/src/main/java/com/affise/attribution/storages/LogsStorage.kt new file mode 100644 index 0000000..43abe61 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/storages/LogsStorage.kt @@ -0,0 +1,11 @@ +package com.affise.attribution.storages + +import com.affise.attribution.logs.SerializedLog + +internal interface LogsStorage { + fun hasLogs(key: String, subKeys: List): Boolean + fun saveLog(key: String, subKey: String, log: SerializedLog) + fun getLogs(key: String, subKeys: List): List + fun deleteLogs(key: String, subKeys: List, ids: List) + fun clear() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/storages/LogsStorageImpl.kt b/attribution/src/main/java/com/affise/attribution/storages/LogsStorageImpl.kt new file mode 100644 index 0000000..24c6f82 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/storages/LogsStorageImpl.kt @@ -0,0 +1,150 @@ +package com.affise.attribution.storages + +import android.content.Context +import com.affise.attribution.logs.SerializedLog +import org.json.JSONObject +import java.io.File +import java.io.FileReader +import java.io.FileWriter + +/** + * Storage of logs + * + * @property context to retrieve app dir + */ +internal class LogsStorageImpl( + private val context: Context +) : LogsStorage { + + /** + * Has logs by [url] or not + */ + override fun hasLogs(key: String, subKeys: List): Boolean = subKeys + .any { subKey -> + getLogsDirectory(key, subKey) + .listFiles() + ?.isNotEmpty() + ?: false + } + + /** + * Store not send [log] by [key] and [subKey] + */ + override fun saveLog(key: String, subKey: String, log: SerializedLog) { + //Create log dir + val logsDir = getLogsDirectory(key, subKey) + + //Delete old logs + logsDir.listFiles() + ?.sortedBy(File::lastModified) + ?.dropLast(LOGS_MAX_COUNT - 1) + ?.forEach { + it.runCatching { delete() } + } + + //Save logs + //Create file for log + val file = File(logsDir, log.id) + + //Write log to file + FileWriter(file).use { + it.write(log.data.toString()) + } + } + + /** + * Get logs by [key] and [subKeys] + */ + override fun getLogs(key: String, subKeys: List): List = + subKeys.flatMap { subKey -> + getLogsDirectory(key, subKey) + .listFiles() + ?.asSequence() + ?.filter { + //Get only file + it.isFile + } + ?.mapNotNull { file -> + try { + //Get data from file + val data = FileReader(file).use { + JSONObject(it.readText()) + } + //Create serializedLog + SerializedLog(file.name, subKey, data) + } catch (e: Exception) { + //Remove file if not create log + file.runCatching { delete() } + + null + } + } + ?.toList() + ?: emptyList() + } + + /** + * Delete logs by [key] and [subKeys] only in [ids] + */ + override fun deleteLogs(key: String, subKeys: List, ids: List) { + subKeys.map { subKey -> + getLogsDirectory(key, subKey) + } + .forEach { + it.listFiles { _, name -> name in ids } + ?.forEach { file -> + //Remove file + file.runCatching { delete() } + } + } + } + + /** + * Removes all logs + */ + override fun clear() { + context.getDir(LOGS_DIR_NAME, Context.MODE_PRIVATE).deleteRecursively() + } + + /** + * Get logs directory be [key] and [subKey] + */ + private fun getLogsDirectory(key: String, subKey: String? = null): File { + val logDir = context.getDir(LOGS_DIR_NAME, Context.MODE_PRIVATE) + .apply { + //Create logDir if doesn't exists + if (!exists()) mkdir() + } + + val logNameDir = key + .takeIf { + it.isNotEmpty() + } + ?.let { + File(logDir, key) + } + ?.apply { + //Create logNameDir if doesn't exists + if (!exists()) mkdir() + } + ?: logDir + + return subKey + .takeIf { + !it.isNullOrEmpty() + } + ?.let { + File(logNameDir, it) + } + ?.apply { + //Create keyDir if doesn't exists + if (!exists()) mkdir() + } + ?: logNameDir + } + + companion object { + private const val LOGS_DIR_NAME = "affise-logs" + private const val LOGS_MAX_COUNT = 5 + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/storages/MetricsStorage.kt b/attribution/src/main/java/com/affise/attribution/storages/MetricsStorage.kt new file mode 100644 index 0000000..a181f67 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/storages/MetricsStorage.kt @@ -0,0 +1,12 @@ +package com.affise.attribution.storages + +import com.affise.attribution.metrics.MetricsEvent + +internal interface MetricsStorage { + fun hasMetrics(key: String, ignoreSubKey: String): Boolean + fun getMetricsEvents(key: String, ignoreSubKey: String): List + fun getMetricsEvent(key: String, subKey: String): MetricsEvent? + fun saveMetricsEvent(key: String, subKey: String, event: MetricsEvent) + fun deleteMetrics(key: String, ignoreSubKey: String) + fun clear() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/storages/MetricsStorageImpl.kt b/attribution/src/main/java/com/affise/attribution/storages/MetricsStorageImpl.kt new file mode 100644 index 0000000..6cc77a3 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/storages/MetricsStorageImpl.kt @@ -0,0 +1,178 @@ +package com.affise.attribution.storages + +import android.content.Context +import com.affise.attribution.converter.JsonObjectToMetricsEventConverter +import com.affise.attribution.metrics.MetricsEvent +import com.affise.attribution.utils.generateUUID +import org.json.JSONObject +import java.io.File +import java.io.FileReader +import java.io.FileWriter + +/** + * Storage of metrics + * + * @property context use for getting access storage on device + */ +internal class MetricsStorageImpl( + private val context: Context, + private val converter: JsonObjectToMetricsEventConverter +) : MetricsStorage { + + /** + * Has metrics by [key] or not + */ + override fun hasMetrics(key: String, ignoreSubKey: String) = getMetricsDirectory(key) + .listFiles() + ?.filter { file -> + //Don't get events in current day + file.name != ignoreSubKey + }?.any { + it.listFiles()?.isNotEmpty() ?: false + } ?: false + + /** + * Get metrics events by [key] excluding [ignoreSubKey] + */ + override fun getMetricsEvents( + key: String, + ignoreSubKey: String + ): List = getMetricsDirectory(key) + .listFiles() + ?.filter { file -> + //Don't get events in current day + file.name != ignoreSubKey + } + ?.flatMap { + it.listFiles()?.toList() ?: emptyList() + } + ?.mapNotNull { file -> + try { + //Read file + FileReader(file).use { + JSONObject(it.readText()) + }.let { + //Convert to metrics event + converter.convert(it) + } + } catch (e: Throwable) { + //Remove file if not create metrics event + file.runCatching { delete() } + + null + } + } + ?: emptyList() + + /** + * Get metrics event by [key] and [subKey] + */ + override fun getMetricsEvent( + key: String, + subKey: String + ): MetricsEvent? = getMetricsDirectory(key, subKey) + .listFiles() + ?.firstOrNull() + ?.let { file -> + try { + //Read file + val saveData = FileReader(file).use { + JSONObject(it.readText()) + } + + //Delete file + file.runCatching { delete() } + + converter.convert(saveData) + } catch (e: Exception) { + //Remove file if not create log + if (file.exists()) { + file.runCatching { delete() } + } + + null + } + } + + /** + * Save metrics [event] by [key] and [subKey] + */ + override fun saveMetricsEvent(key: String, subKey: String, event: MetricsEvent) { + //Get metrics event dir for current day + val dir = getMetricsDirectory(key, subKey) + + //Delete old metrics event + dir.listFiles() + ?.sortedBy(File::lastModified) + ?.dropLast(METRICS_MAX_COUNT - 1) + ?.forEach { + it.runCatching { deleteRecursively() } + } + + //Generate new file + val file = File(dir, generateUUID().toString()) + + //Save metrics event to file + FileWriter(file).use { + it.write(event.serialize().toString()) + } + } + + /** + * Delete metrics event by [key] excluding [ignoreSubKey] + */ + override fun deleteMetrics(key: String, ignoreSubKey: String) { + getMetricsDirectory(key) + .listFiles() + ?.filter { file -> + //Don't get events in current day + file.name != ignoreSubKey + } + ?.forEach { file -> + //Delete + file.runCatching { deleteRecursively() } + } + } + + /** + * Removes all metrics events + */ + override fun clear() { + context.getDir(METRICS_DIR_NAME, Context.MODE_PRIVATE).deleteRecursively() + } + + /** + * Get logs directory by [key] and [subKey] + */ + private fun getMetricsDirectory(key: String, subKey: String? = null): File { + //Get or create metricsDir + val metricsDir = context.getDir(METRICS_DIR_NAME, Context.MODE_PRIVATE) + .apply { + if (!exists()) mkdir() + } + + //Get or create metricsUrlDir + val metricsUrlDir = File(metricsDir, key) + .apply { + if (!exists()) mkdir() + } + + //Get dir by url or key + return subKey + ?.takeIf { + it.isNotEmpty() + } + ?.let { + File(metricsUrlDir, it) + } + ?.apply { + if (!exists()) mkdir() + } + ?: metricsUrlDir + } + + companion object { + private const val METRICS_DIR_NAME = "affise-metrics" + private const val METRICS_MAX_COUNT = 30 + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/test/CrashApplicationUseCase.kt b/attribution/src/main/java/com/affise/attribution/test/CrashApplicationUseCase.kt new file mode 100644 index 0000000..d0de447 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/test/CrashApplicationUseCase.kt @@ -0,0 +1,16 @@ +package com.affise.attribution.test + +import com.affise.attribution.exceptions.TestApplicationCrashException + +/** + * Use case to help app testing + */ +interface CrashApplicationUseCase { + + /** + * Throws [TestApplicationCrashException] + */ + fun crash() +} + + diff --git a/attribution/src/main/java/com/affise/attribution/test/CrashApplicationUseCaseImpl.kt b/attribution/src/main/java/com/affise/attribution/test/CrashApplicationUseCaseImpl.kt new file mode 100644 index 0000000..6ec847d --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/test/CrashApplicationUseCaseImpl.kt @@ -0,0 +1,12 @@ +package com.affise.attribution.test + +import com.affise.attribution.exceptions.TestApplicationCrashException + +/** + * Implementation of [CrashApplicationUseCase] + */ +internal class CrashApplicationUseCaseImpl : CrashApplicationUseCase { + override fun crash() { + throw TestApplicationCrashException() + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/usecase/EraseUserDataUseCase.kt b/attribution/src/main/java/com/affise/attribution/usecase/EraseUserDataUseCase.kt new file mode 100644 index 0000000..ad9f277 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/usecase/EraseUserDataUseCase.kt @@ -0,0 +1,12 @@ +package com.affise.attribution.usecase + +/** + * Use case to completle remove user data from device + */ +interface EraseUserDataUseCase { + + /** + * Performs user data deletion + */ + fun eraseUserData() +} diff --git a/attribution/src/main/java/com/affise/attribution/usecase/EraseUserDataUseCaseImpl.kt b/attribution/src/main/java/com/affise/attribution/usecase/EraseUserDataUseCaseImpl.kt new file mode 100644 index 0000000..5287193 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/usecase/EraseUserDataUseCaseImpl.kt @@ -0,0 +1,20 @@ +package com.affise.attribution.usecase + +import com.affise.attribution.events.EventsRepository +import com.affise.attribution.events.GDPREventRepository + +/** + * Implementation of [EraseUserDataUseCase] + * + * @property eventsRepository to access user events + * @property gdprRepository to access user GDPR event + */ +internal class EraseUserDataUseCaseImpl( + private val eventsRepository: EventsRepository, + private val gdprRepository: GDPREventRepository +): EraseUserDataUseCase { + override fun eraseUserData() { + eventsRepository.clear() + gdprRepository.clear() + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/usecase/FirstAppOpenUseCase.kt b/attribution/src/main/java/com/affise/attribution/usecase/FirstAppOpenUseCase.kt new file mode 100644 index 0000000..74e33c6 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/usecase/FirstAppOpenUseCase.kt @@ -0,0 +1,112 @@ +package com.affise.attribution.usecase + +import android.content.SharedPreferences +import com.affise.attribution.parameters.Parameters +import com.affise.attribution.session.CurrentActiveActivityCountProvider +import com.affise.attribution.utils.* +import com.affise.attribution.utils.generateUUID +import com.affise.attribution.utils.timestamp +import com.affise.attribution.utils.saveBoolean +import com.affise.attribution.utils.saveLong +import java.util.* + +internal class FirstAppOpenUseCase( + private val preferences: SharedPreferences, + private val activityCountProvider: CurrentActiveActivityCountProvider +) { + + /** + * Check preferences for have first opened date and generate properties if no data + */ + fun onAppCreated() { + if (preferences.getLong(FIRST_OPENED_DATE_KEY, 0) == 0L) { + onAppFirstOpen() + } + checkSaveUUIDs() + + //init session observer + activityCountProvider.init() + } + + /** + * Generate properties on app first open + */ + private fun onAppFirstOpen() { + //Create first open date + val firstOpenDate = timestamp() + + checkSaveUUIDs() + + //Save properties + preferences.saveBoolean(FIRST_OPENED, true) + preferences.saveLong(FIRST_OPENED_DATE_KEY, firstOpenDate) + } + + private fun checkSaveUUIDs() { + preferences.apply { + //Create affDevId + checkSaveString(AFF_DEVICE_ID) { + generateUUID().toString() + } + //Create affAltDevId + checkSaveString(AFF_ALT_DEVICE_ID) { + generateUUID().toString() + } + //Create randomUserId + checkSaveString(Parameters.RANDOM_USER_ID) { + generateUUID().toString() + } + } + } + + /** + * Get first open + * @return is first open + */ + fun isFirstOpen() = preferences + .getBoolean(FIRST_OPENED, true) + .let { + if (it) { + //Save properties + preferences.edit().apply { + putBoolean(FIRST_OPENED, false) + }.apply() + true + } else { + false + } + } + + /** + * Get first open date + * @return first open date + */ + fun getFirstOpenDate() = preferences + .getLong(FIRST_OPENED_DATE_KEY, 0) + .let { if (it == 0L) null else Date(it) } + + /** + * Get devise id + * @return devise id + */ + fun getAffiseDeviseId() = preferences.getString(AFF_DEVICE_ID, "") + + /** + * Get alt devise id + * @return alt devise id + */ + fun getAffiseAltDeviseId() = preferences.getString(AFF_ALT_DEVICE_ID, "") + + /** + * Get random user id + * @return random user id + */ + fun getRandomUserId() = preferences.getString(Parameters.RANDOM_USER_ID, "") + + companion object { + private const val FIRST_OPENED = "FIRST_OPENED" + private const val FIRST_OPENED_DATE_KEY = "FIRST_OPENED_DATE_KEY" + private const val AFF_DEVICE_ID = "AFF_DEVICE_ID" + private const val AFF_ALT_DEVICE_ID = "AFF_ALT_DEVICE_ID" + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/usecase/PreferencesUseCase.kt b/attribution/src/main/java/com/affise/attribution/usecase/PreferencesUseCase.kt new file mode 100644 index 0000000..8debc85 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/usecase/PreferencesUseCase.kt @@ -0,0 +1,37 @@ +package com.affise.attribution.usecase + +/** + * Use case to set library preferences + */ +interface PreferencesUseCase { + + /** + * Sets Offline mode to [enabled] state + */ + fun setOfflineModeEnabled(enabled: Boolean) + + /** + * Returns state of Offline mode + */ + fun isOfflineModeEnabled(): Boolean + + /** + * Sets Background Tracking to [enabled] state + */ + fun setBackgroundTrackingEnabled(enabled: Boolean) + + /** + * Returns state of Background Tracking + */ + fun isBackgroundTrackingEnabled(): Boolean + + /** + * Sets Tracking to [enabled] state + */ + fun setTrackingEnabled(enabled: Boolean) + + /** + * Returns state of Tracking + */ + fun isTrackingEnabled(): Boolean +} diff --git a/attribution/src/main/java/com/affise/attribution/usecase/PreferencesUseCaseImpl.kt b/attribution/src/main/java/com/affise/attribution/usecase/PreferencesUseCaseImpl.kt new file mode 100644 index 0000000..fc0a3c1 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/usecase/PreferencesUseCaseImpl.kt @@ -0,0 +1,35 @@ +package com.affise.attribution.usecase + +import com.affise.attribution.preferences.ApplicationLifecyclePreferencesRepositoryImpl +import com.affise.attribution.preferences.ApplicationLifetimePreferencesRepositoryImpl + +/** + * Implementation of [PreferencesUseCase] + * + * @property repository to store preferences that is persisted until app restart + * @property lifetimeRepository to store preferences that is persisted until app reinstall + */ +internal class PreferencesUseCaseImpl( + private val repository: ApplicationLifecyclePreferencesRepositoryImpl, + private val lifetimeRepository: ApplicationLifetimePreferencesRepositoryImpl +) : PreferencesUseCase { + + override fun setOfflineModeEnabled(enabled: Boolean) { + repository.preferences = repository.preferences.copy(offlineMode = enabled) + } + + override fun isOfflineModeEnabled(): Boolean = repository.preferences.offlineMode + + override fun setBackgroundTrackingEnabled(enabled: Boolean) { + repository.preferences = repository.preferences.copy(backgroundTracking = enabled) + } + + override fun isBackgroundTrackingEnabled() = repository.preferences.backgroundTracking + + override fun setTrackingEnabled(enabled: Boolean) { + lifetimeRepository.preferences = + lifetimeRepository.preferences.copy(trackingEnabled = enabled) + } + + override fun isTrackingEnabled(): Boolean = lifetimeRepository.preferences.trackingEnabled +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/usecase/RetrieveInstallReferrerUseCase.kt b/attribution/src/main/java/com/affise/attribution/usecase/RetrieveInstallReferrerUseCase.kt new file mode 100644 index 0000000..53ad330 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/usecase/RetrieveInstallReferrerUseCase.kt @@ -0,0 +1,162 @@ +package com.affise.attribution.usecase + +import android.annotation.SuppressLint +import android.app.Application +import android.content.SharedPreferences +import android.net.Uri +import com.affise.attribution.converter.Converter +import com.affise.attribution.converter.StringToAffiseReferrerDataConverter +import com.affise.attribution.deeplink.DeeplinkManager +import com.affise.attribution.referrer.ReferrerKey +import com.affise.attribution.logs.LogsManager +import com.affise.attribution.referrer.AffiseReferrerData +import com.affise.attribution.referrer.AffiseReferrerDataToStringConverter +import com.affise.attribution.referrer.OnReferrerCallback +import com.android.installreferrer.api.InstallReferrerClient +import com.android.installreferrer.api.InstallReferrerStateListener +import com.android.installreferrer.api.ReferrerDetails + +class RetrieveInstallReferrerUseCase( + private val preferences: SharedPreferences, + private val toStringConverter: AffiseReferrerDataToStringConverter, + private val toAffiseReferrerDataConverter: StringToAffiseReferrerDataConverter, + private val app: Application, + private val deeplinkManager: DeeplinkManager, + private val logsManager: LogsManager, + private val installReferrerToDeeplinkUriConverter: Converter +) { + + /** + * Referrer client + */ + private var referrerClient: InstallReferrerClient? = null + + private var onReferrerFinished: (() -> Unit)? = null + + fun startInstallReferrerRetrieve(onFinished: (() -> Unit)? = null) { + //Create referrer client + referrerClient = InstallReferrerClient.newBuilder(app) + .build() + + //Start connection + referrerClient?.startConnection(object : InstallReferrerStateListener { + override fun onInstallReferrerSetupFinished(responseCode: Int) { + when (responseCode) { + InstallReferrerClient.InstallReferrerResponse.OK -> { + // Connection established. + try { + val data = referrerClient?.installReferrer ?: return + + //Processing referrer details + processReferrerDetails(data) + } catch (throwable: Throwable) { + logsManager.addSdkError( + RuntimeException("Error read ReferrerClient") + ) + } + } + InstallReferrerClient.InstallReferrerResponse.FEATURE_NOT_SUPPORTED -> { + // API not available on the current Play Store app. + } + InstallReferrerClient.InstallReferrerResponse.SERVICE_UNAVAILABLE -> { + // Connection couldn't be established. + } + } + onFinished?.invoke() + onReferrerFinished?.invoke() + } + + override fun onInstallReferrerServiceDisconnected() { + // Try to restart the connection on the next request to + // Google Play by calling the startConnection() method. + } + }) + } + + /** + * Get referrer uri value by key + */ + fun getReferrerValue(key: ReferrerKey, callback: OnReferrerCallback?) { + getInstallReferrer()?.let { + callback?.handleReferrer(getReferrerValue(key)) + return + } + + onReferrerFinished = { + callback?.handleReferrer(getReferrerValue(key)) + } + } + + private fun getReferrerValue(key: ReferrerKey): String? { + return getInstallReferrer()?.installReferrer?.let { + val uri = Uri.parse("https://referrer/?$it") + uri.getQueryParameter(key.type) + } + } + + /** + * Get install referrer + * @return install referrer + */ + fun getInstallReferrer() = preferences + .getString(REFERRER_KEY, null) + ?.let(toAffiseReferrerDataConverter::convert) + + /** + * Processing referrer details + */ + fun processReferrerDetails(data: ReferrerDetails) { + if (!isDelayedDeeplinkProcessed()) { + print("referrer ") + println(data.installReferrer) + data.installReferrer + ?.let(installReferrerToDeeplinkUriConverter::convert) + ?.also { + deeplinkManager.handleDeeplink(it) + } + setDelayedDeeplinkProcessed() + } + + //Generate referrer data + AffiseReferrerData( + installReferrer = data.installReferrer ?: "", + referrerClickTimestampSeconds = data.referrerClickTimestampSeconds, + installBeginTimestampSeconds = data.installBeginTimestampSeconds, + referrerClickTimestampServerSeconds = data.referrerClickTimestampServerSeconds, + installBeginTimestampServerSeconds = data.installBeginTimestampServerSeconds, + installVersion = data.installVersion ?: "", + googlePlayInstantParam = data.googlePlayInstantParam, + ) + .let(toStringConverter::convert) + .let(::storeToSharedPreferences) + + referrerClient?.endConnection() + } + + /** + * Save referrer data + */ + @SuppressLint("ApplySharedPref") + private fun storeToSharedPreferences(s: String) { + val e = preferences.edit() + e.putString(REFERRER_KEY, s) + e.commit() + } + + private fun isDelayedDeeplinkProcessed(): Boolean = + preferences.getBoolean(DELAYED_DEEPLINK_PROCESSED_KEY, false) + + @SuppressLint("ApplySharedPref") + private fun setDelayedDeeplinkProcessed() { + preferences.edit() + .apply { + putBoolean(DELAYED_DEEPLINK_PROCESSED_KEY, true) + } + .commit() + } + + companion object { + private const val REFERRER_KEY = "referrer_data" + private const val DELAYED_DEEPLINK_PROCESSED_KEY = "DELAYED_DEEPLINK_PROCESSED_KEY" + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/usecase/SendDataToServerUseCase.kt b/attribution/src/main/java/com/affise/attribution/usecase/SendDataToServerUseCase.kt new file mode 100644 index 0000000..adc3ac5 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/usecase/SendDataToServerUseCase.kt @@ -0,0 +1,9 @@ +package com.affise.attribution.usecase + +interface SendDataToServerUseCase { + + /** + * Send + */ + fun send(withDelay: Boolean) +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/usecase/SendDataToServerUseCaseImpl.kt b/attribution/src/main/java/com/affise/attribution/usecase/SendDataToServerUseCaseImpl.kt new file mode 100644 index 0000000..ca53955 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/usecase/SendDataToServerUseCaseImpl.kt @@ -0,0 +1,117 @@ +package com.affise.attribution.usecase + +import com.affise.attribution.events.EventsParams +import com.affise.attribution.events.EventsRepository +import com.affise.attribution.executors.ExecutorServiceProvider +import com.affise.attribution.logs.LogsManager +import com.affise.attribution.logs.LogsRepository +import com.affise.attribution.metrics.MetricsRepository +import com.affise.attribution.network.CloudConfig +import com.affise.attribution.network.CloudRepository +import com.affise.attribution.parameters.factory.PostBackModelFactory +import com.affise.attribution.preferences.models.OfflineModeEnabledException +import com.affise.attribution.internal.InternalEventsRepository + +internal class SendDataToServerUseCaseImpl( + private val postBackModelFactory: PostBackModelFactory, + private val cloudRepository: CloudRepository, + private val eventsRepository: EventsRepository, + private val internalEventsRepository: InternalEventsRepository, + private val sendServiceProvider: ExecutorServiceProvider, + private val logsRepository: LogsRepository, + private val metricsRepository: MetricsRepository, + private val logsManager: LogsManager, + private val preferencesUseCase: PreferencesUseCase +) : SendDataToServerUseCase { + + /** + * Flags to status is sending from current url + */ + private val isSend = CloudConfig.getUrls() + .map { it to false } + .toMap(HashMap()) + + /** + * Send data + */ + @Synchronized + override fun send(withDelay: Boolean) { + if (preferencesUseCase.isOfflineModeEnabled()) { + logsManager.addSdkError(OfflineModeEnabledException()) + return + } + //For all urls + CloudConfig.getUrls().forEach { + //Check if sending on this url + if (isSend[it] == false) { + //Lock sending to this url + isSend[it] = true + + sendServiceProvider.provideExecutorService().execute { + if (withDelay) { + Thread.sleep(TIME_DELAY_SENDING) + } + + //Send to this url + try { + send(it) + } catch (throwable: Throwable) { + //Log error + logsManager.addSdkError(throwable) + } + + //Unlock sending to this url + isSend[it] = false + } + } + } + } + + /** + * Sending for url + */ + private fun send(url: String) { + do { + //Get events + val events = eventsRepository.getEvents(url) + + //Get logs + val logs = logsRepository.getLogs(url) + + //Get metrics + val metrics = metricsRepository.getMetrics(url) + + //Get internal events + val internalEvents = internalEventsRepository.getEvents(url) + + //Generate data + val data = listOf(postBackModelFactory.create(events, logs, metrics, internalEvents)) + + try { + //Send data for single url + cloudRepository.send(data, url) + + //Remove sent events + eventsRepository.deleteEvent(events.map { it.id }, url) + + //Remove sent logs + logsRepository.deleteLogs(logs.map { it.id }, url) + + //Remove sent metrics + metricsRepository.deleteMetrics(url) + + //Remove sent internal events + internalEventsRepository.deleteEvent(internalEvents.map { it.id }, url) + } catch (cloudException: Throwable) { + //Log error + logsManager.addNetworkError(cloudException) + + return + } + } while (events.size == EventsParams.EVENTS_SEND_COUNT) + } + + companion object { + private const val TIME_DELAY_SENDING: Long = 15000L + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/usecase/SendGDPREventUseCaseImpl.kt b/attribution/src/main/java/com/affise/attribution/usecase/SendGDPREventUseCaseImpl.kt new file mode 100644 index 0000000..3bc6f03 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/usecase/SendGDPREventUseCaseImpl.kt @@ -0,0 +1,49 @@ +package com.affise.attribution.usecase + +import com.affise.attribution.events.GDPREventRepository +import com.affise.attribution.events.predefined.GDPREvent +import com.affise.attribution.executors.ExecutorServiceProvider +import com.affise.attribution.network.CloudConfig +import com.affise.attribution.network.CloudRepository +import com.affise.attribution.parameters.factory.PostBackModelFactory + +internal class SendGDPREventUseCaseImpl( + private val repository: GDPREventRepository, + private val serviceProvider: ExecutorServiceProvider, + private val cloudRepository: CloudRepository, + private val postBackModelFactory: PostBackModelFactory, + private val eraseUserDataUseCase: EraseUserDataUseCaseImpl + +) { + fun registerForgetMeEvent(userData: String) { + serviceProvider.provideExecutorService().execute { + repository.runCatching { setEvent(GDPREvent(userData)) } + .getOrNull() + ?.also { sendForgetMeEvent() } + } + } + + // For each url in CloudConfig send postback with GDPREvent + // if atleast one is succeed, erase user data + fun sendForgetMeEvent() { + serviceProvider.provideExecutorService().execute { + val serializedEvent = repository.getEvent() ?: return@execute + + var isSucceed = false + + CloudConfig.getUrls().forEach { + val postBackModel = postBackModelFactory.create(listOf(serializedEvent)) + try { + cloudRepository.send(listOf(postBackModel), it) + isSucceed = true + } catch (e: Throwable) { + // do nothing if failed to sent + } + } + + if (isSucceed) { + eraseUserDataUseCase.eraseUserData() + } + } + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/utils/ActivityActionsManager.kt b/attribution/src/main/java/com/affise/attribution/utils/ActivityActionsManager.kt new file mode 100644 index 0000000..f048b7b --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/utils/ActivityActionsManager.kt @@ -0,0 +1,50 @@ +package com.affise.attribution.utils + +import android.app.Activity +import android.view.View + +/** + * An interface describing a callback that will be fired when activity has a lifecycle event + */ +internal fun interface ActivityLifecycleCallback { + /** + * Triggered when a new [Activity] has received an lifecycle event + */ + fun handle(activity: Activity) +} + +/** + * An interface describing a callback that will be fired when a click event occurs on the Activity + */ +internal fun interface ActivityClickCallback { + /** + * Triggered when a new [Activity] has received a click event + */ + fun handle(activity: Activity, view: View) +} + +/** + * Manager for handling events occurring on the activity + */ +internal interface ActivityActionsManager { + + /** + * Add [listener] for start activities + */ + fun addOnActivityStartedListener(listener: ActivityLifecycleCallback) + + /** + * Add [listener] for resume activities + */ + fun addOnActivityResumedListener(listener: ActivityLifecycleCallback) + + /** + * Add [listener] for stop activities + */ + fun addOnActivityStoppedListener(listener: ActivityLifecycleCallback) + + /** + * Add [listener] for clicks on activities + */ + fun addOnActivityClickListener(listener: ActivityClickCallback) +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/utils/ActivityActionsManagerImpl.kt b/attribution/src/main/java/com/affise/attribution/utils/ActivityActionsManagerImpl.kt new file mode 100644 index 0000000..26992fb --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/utils/ActivityActionsManagerImpl.kt @@ -0,0 +1,195 @@ +package com.affise.attribution.utils + +import android.annotation.SuppressLint +import android.app.Activity +import android.app.Application +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import com.affise.attribution.logs.LogsManager + +/** + * Manager for handling events occurring on the activity + * + * @property app application on which they are listening Activities + * @property logsManager for error logging + */ +internal class ActivityActionsManagerImpl( + private val app: Application, + private val logsManager: LogsManager +) : ActivityActionsManager { + /** + * Listeners for start activities + */ + private val onActivityStartedListeners: MutableList = mutableListOf() + + /** + * Listeners for resume activities + */ + private val onActivityResumedListeners: MutableList = mutableListOf() + + /** + * Listeners for stop activities + */ + private val onActivityStoppedListeners: MutableList = mutableListOf() + + /** + * Listeners clicks on activities + */ + private val onActivityClickListeners: MutableList = mutableListOf() + + /** + * ActivityLifecycleCallbacks + */ + private val callback = object : Application.ActivityLifecycleCallbacks { + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + } + + override fun onActivityStarted(activity: Activity) { + //Invoke all listeners for start activities + onActivityStartedListeners.forEach { + it.handle(activity) + } + + //If enabled auto click event collector + (activity.window.decorView as ViewGroup) + .also { + //For all child View add listeners + addListeners(activity, it) + + it.viewTreeObserver.addOnGlobalLayoutListener { + //For all child View add listeners + addListeners(activity, it) + } + } + } + + override fun onActivityResumed(activity: Activity) { + //Invoke all listeners for resume activities + onActivityResumedListeners.forEach { + it.handle(activity) + } + } + + override fun onActivityPaused(activity: Activity) { + } + + override fun onActivityStopped(activity: Activity) { + //Invoke all listeners for stop activities + onActivityStoppedListeners.forEach { + it.handle(activity) + } + } + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { + } + + override fun onActivityDestroyed(activity: Activity) { + } + } + + init { + //Register callbacks + app.registerActivityLifecycleCallbacks(callback) + } + + /** + * Add [listener] for start activities + */ + override fun addOnActivityStartedListener(listener: ActivityLifecycleCallback) { + onActivityStartedListeners.add(listener) + } + + /** + * Add [listener] for resume activities + */ + override fun addOnActivityResumedListener(listener: ActivityLifecycleCallback) { + onActivityResumedListeners.add(listener) + } + + /** + * Add [listener] for stop activities + */ + override fun addOnActivityStoppedListener(listener: ActivityLifecycleCallback) { + onActivityStoppedListeners.add(listener) + } + + /** + * Add [listener] for clicks on activities + */ + override fun addOnActivityClickListener(listener: ActivityClickCallback) { + onActivityClickListeners.add(listener) + } + + /** + * Add listeners to views in [viewGroup] + */ + private fun addListeners(activity: Activity, viewGroup: ViewGroup) { + //For all child Views + (0 until viewGroup.childCount).forEach { + //Add listener + addListener(activity, viewGroup.getChildAt(it)) + + //If child view is ViewGroup + (viewGroup.getChildAt(it) as? ViewGroup) + ?.let { childView -> + //Add listeners to views + addListeners(activity, childView) + } + } + } + + /** + * Add listeners to [view] + */ + @SuppressLint("DiscouragedPrivateApi", "PrivateApi") + private fun addListener(activity: Activity, view: View) { + try { + //Get listenerInfo on view + val listenerInfo = View::class.java + .getDeclaredMethod(GET_LISTENER_INFO_DECLARED_METHOD_NAME) + .let { + it.isAccessible = true + it.invoke(view) + } + + //Get current onClickListener on view + val onClickListener = Class.forName(LISTENER_INFO_CLASS_NAME) + .getDeclaredField(ON_CLICK_LISTENER_DECLARED_FIELD_NAME) + .also { + it.isAccessible = true + } + + onClickListener.get(listenerInfo) + ?.takeIf { + //Take only original View.OnClickListener + it is View.OnClickListener && it !is AutoCatchingOnClickListener + } + ?.let { + it as? View.OnClickListener + } + ?.also { + //Set new onClickListener with sending event + onClickListener.set( + listenerInfo, + AutoCatchingOnClickListener(it) { view -> + //Invoke all listeners for clicks on activities + onActivityClickListeners.forEach { + it.handle(activity, view) + } + } + ) + } + } catch (throwable: Throwable) { + //Add throwable into logs + logsManager.addSdkError(throwable) + } + } + + companion object { + private const val GET_LISTENER_INFO_DECLARED_METHOD_NAME = "getListenerInfo" + private const val LISTENER_INFO_CLASS_NAME = "android.view.View\$ListenerInfo" + private const val ON_CLICK_LISTENER_DECLARED_FIELD_NAME = "mOnClickListener" + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/utils/AutoCatchingOnClickListener.kt b/attribution/src/main/java/com/affise/attribution/utils/AutoCatchingOnClickListener.kt new file mode 100644 index 0000000..e2aa75b --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/utils/AutoCatchingOnClickListener.kt @@ -0,0 +1,25 @@ +package com.affise.attribution.utils + +import android.view.View + +/** + * AutoCatching uses its click listeners to call the callback when the view is clicked. + * + * @property oldClickListener original click listeners + * @property afterClickListenerAction action after calling the original click listeners + */ +internal class AutoCatchingOnClickListener( + private val oldClickListener: View.OnClickListener?, + private val afterClickListenerAction: (View) -> Unit +) : View.OnClickListener { + + override fun onClick(view: View?) { + //Call original clickListener + oldClickListener?.onClick(view) + + //Get send data from view + view?.also { + afterClickListenerAction.invoke(it) + } + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/utils/DelayRun.kt b/attribution/src/main/java/com/affise/attribution/utils/DelayRun.kt new file mode 100644 index 0000000..b3b8a5e --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/utils/DelayRun.kt @@ -0,0 +1,18 @@ +package com.affise.attribution.utils + +import java.util.* + + +internal fun delayRun(delay: Long, run: () -> Unit) { + Timer().let { it -> + it.schedule(object : TimerTask() { + override fun run() { + //Delay execution + run.invoke() + + //Stop timer + it.cancel() + } + }, delay) + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/utils/EncryptedSharedPreferences.kt b/attribution/src/main/java/com/affise/attribution/utils/EncryptedSharedPreferences.kt new file mode 100644 index 0000000..752d062 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/utils/EncryptedSharedPreferences.kt @@ -0,0 +1,265 @@ +package com.affise.attribution.utils + +import android.content.Context +import android.content.SharedPreferences +import android.os.Build +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import android.util.Base64 +import java.security.KeyStore +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.spec.IvParameterSpec + +class EncryptedSharedPreferences(context: Context, preferencesName: String) : SharedPreferences { + + private val preferences: SharedPreferences = context.getSharedPreferences( + preferencesName, Context.MODE_PRIVATE + ) + + private val secretKey: SecretKey = getOrCreateSecretKey() + + /** + * Get SecretKey or generate new if aliases in KeyStore not contains affise alias + */ + private fun getOrCreateSecretKey(): SecretKey { + //Get KeyStore + val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { + load(null) + } + + //Check version + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + //Get all aliases in KeyStore + val alias = keyStore.aliases().toList() + + //Check affise alias in KeyStore aliases + if (!alias.contains(ALIAS_NAME)) { + //Create KeyGenerator + val keyGenerator = KeyGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_AES, + "AndroidKeyStore" + ) + + //Init KeyGenerator parameters + keyGenerator.init( + KeyGenParameterSpec.Builder( + ALIAS_NAME, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ) + .setBlockModes(KeyProperties.BLOCK_MODE_CBC) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) + .setRandomizedEncryptionRequired(false) + .build() + ) + + //Generate new key + keyGenerator.generateKey() + } + } else { + throw RuntimeException("EncryptedSharedPreferences support only Android 6.0+") + } + + return keyStore.getKey(ALIAS_NAME, null) as SecretKey + } + + override fun getAll(): MutableMap { + //Get all preferences values + val encryptedMap = preferences.all + + //Create map of decrypted values + val decryptedMap = HashMap(encryptedMap.size) + + //For all data in preferences values + for ((key, cipherText) in encryptedMap) { + try { + //Get decrypted StringSet or null if value is not StringSet + val stringSet = getDecryptedStringSet(cipherText) + + if (stringSet != null) { + //Add StringSet to map of decrypted values + decryptedMap[key] = stringSet + } else { + //Add string to map of decrypted values + decryptedMap[key] = decrypt(cipherText.toString()) + } + } catch (e: Exception) { + //Add value to map of decrypted values + decryptedMap[key] = cipherText.toString() + } + } + + return decryptedMap + } + + /** + * Generate decrypted StringSet from [cipherText] + */ + private fun getDecryptedStringSet(cipherText: Any?): Set? { + //Check for null + cipherText ?: return null + + //Check for Set + val encryptedSet = cipherText as? Set<*>? ?: return null + + //Create decrypted StringSet + val decryptedSet = java.util.HashSet() + + //Check and decrypt all values + for (value in encryptedSet) { + (value as? String) + ?.let { + decryptedSet.add(decrypt(it)) + } + ?: return null + } + + return decryptedSet + } + + override fun getString( + key: String?, defValue: String? + ): String? = decrypt(preferences.getString(key, defValue)) + + override fun getStringSet( + key: String?, defValues: MutableSet? + ): MutableSet? = preferences.getStringSet(key, defValues) + ?.let { encryptSet -> + //Decrypt value from preferences + HashSet(encryptSet.size).apply { + for (value in encryptSet) { + decrypt(value)?.let { + add(it) + } + } + } + } + + override fun getInt(key: String?, defValue: Int): Int = decrypt( + //Decrypt value from preferences + preferences.getString(key, null) + )?.toIntOrNull() ?: defValue + + override fun getLong(key: String?, defValue: Long): Long = decrypt( + //Decrypt value from preferences + preferences.getString(key, null) + )?.toLongOrNull() ?: defValue + + override fun getFloat(key: String?, defValue: Float): Float = decrypt( + //Decrypt value from preferences + preferences.getString(key, null) + )?.toFloatOrNull() ?: defValue + + override fun getBoolean(key: String?, defValue: Boolean): Boolean = decrypt( + //Decrypt value from preferences + preferences.getString(key, null) + )?.toBooleanStrictOrNull() ?: defValue + + override fun contains(key: String?): Boolean = false + + override fun edit(): SharedPreferences.Editor = Editor() + + override fun registerOnSharedPreferenceChangeListener( + listener: SharedPreferences.OnSharedPreferenceChangeListener? + ) { + } + + override fun unregisterOnSharedPreferenceChangeListener( + listener: SharedPreferences.OnSharedPreferenceChangeListener? + ) { + } + + /** + * Generate Cipher object with [encryptMode] + */ + private fun getCipher( + encryptMode: Int + ) = Cipher.getInstance("AES/CBC/PKCS7Padding").apply { + init(encryptMode, secretKey, IvParameterSpec(ByteArray(16))) + } + + /** + * Encrypted [value] + */ + private fun encrypt(value: String?): String? = try { + Base64.encodeToString( + getCipher(Cipher.ENCRYPT_MODE).doFinal(value?.toByteArray()), + Base64.NO_WRAP + ) + } catch (throwable: Throwable) { + null + } + + /** + * Decrypted [value] + */ + private fun decrypt(value: String?): String? = try { + String( + getCipher(Cipher.DECRYPT_MODE).doFinal(Base64.decode(value, Base64.NO_WRAP)), + charset("UTF-8") + ) + } catch (throwable: Throwable) { + null + } + + inner class Editor internal constructor() : SharedPreferences.Editor { + + private var editor: SharedPreferences.Editor = preferences.edit() + + override fun putString(s: String, s1: String?): SharedPreferences.Editor = this.apply { + //Encrypted and put value to preferences + editor.putString(s, encrypt(s1)).apply() + } + + override fun putStringSet( + s: String, set: Set? + ): SharedPreferences.Editor = this.apply { + //Encrypted and put value to preferences + set?.map { encrypt(it) } + ?.toSet() + ?.let { + editor.putStringSet(s, it) + } + } + + override fun putInt(s: String, i: Int): SharedPreferences.Editor = this.apply { + //Encrypted and put value to preferences + editor.putString(s, encrypt(i.toString())).apply() + } + + override fun putLong(s: String, l: Long): SharedPreferences.Editor = this.apply { + //Encrypted and put value to preferences + editor.putString(s, encrypt(l.toString())).apply() + } + + override fun putFloat(s: String, v: Float): SharedPreferences.Editor = this.apply { + //Encrypted and put value to preferences + editor.putString(s, encrypt(v.toString())).apply() + } + + override fun putBoolean(s: String, b: Boolean): SharedPreferences.Editor = this.apply { + //Encrypted and put value to preferences + val encryptedData = encrypt(b.toString()) + editor.putString(s, encryptedData).apply() + } + + override fun remove(s: String): SharedPreferences.Editor = this.apply { + editor.remove(s) + } + + override fun clear(): SharedPreferences.Editor = this.apply { + editor.clear() + } + + override fun commit(): Boolean = editor.commit() + + override fun apply() { + editor.apply() + } + } + + companion object { + private const val ALIAS_NAME = "_affise_preferences_key_name_" + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/utils/PrefUtils.kt b/attribution/src/main/java/com/affise/attribution/utils/PrefUtils.kt new file mode 100644 index 0000000..31cedde --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/utils/PrefUtils.kt @@ -0,0 +1,38 @@ +package com.affise.attribution.utils + +import android.content.SharedPreferences + +private const val SAVE_REPEAT = 3 + +internal fun SharedPreferences.saveString(key:String, value:String?, count:Int = SAVE_REPEAT) : Boolean { + val saved = this.edit().apply { + putString(key, value) + }.commit() + if (saved) return true + if (count <= 0) return false + return saveString(key, value, count-1) +} + +internal fun SharedPreferences.saveBoolean(key:String, value:Boolean, count:Int = SAVE_REPEAT) : Boolean { + val saved = this.edit().apply { + putBoolean(key, value) + }.commit() + if (saved) return true + if (count <= 0) return false + return saveBoolean(key, value, count-1) +} + +internal fun SharedPreferences.saveLong(key:String, value:Long, count:Int = SAVE_REPEAT) : Boolean { + val saved = this.edit().apply { + putLong(key, value) + }.commit() + if (saved) return true + if (count <= 0) return false + return saveLong(key, value, count-1) +} + +internal fun SharedPreferences.checkSaveString(key:String, func:()->String) { + if (getString(key, null).isNullOrEmpty()) { + saveString(key, func()) + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/utils/SystemAppChecker.kt b/attribution/src/main/java/com/affise/attribution/utils/SystemAppChecker.kt new file mode 100644 index 0000000..7ae45e4 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/utils/SystemAppChecker.kt @@ -0,0 +1,57 @@ +package com.affise.attribution.utils + +import android.annotation.SuppressLint +import android.app.Application +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager + +class SystemAppChecker(private val app: Application) { + + /** + * Check app if it system app + * + * @return is app is system + */ + fun isPreinstallApp() = isSystemAppByFLAG() || isSystemPreloaded() + + /** + * Check if application is preloaded. + * + * @return `true` if package is preloaded. + */ + private fun isSystemPreloaded() = try { + app.packageManager.getApplicationInfo(app.packageName, 0).let { + it.sourceDir.startsWith("/system/app/") || it.sourceDir.startsWith("/system/priv-app/") + } + } catch (e: PackageManager.NameNotFoundException) { + false + } + + /** + * Check if application is installed in the device's system image + *. + * @return is app is system + */ + private fun isSystemAppByFLAG() = try { + val applicationFlags = app.packageManager.getApplicationInfo(app.packageName, 0).flags + val systemFlags = ApplicationInfo.FLAG_SYSTEM or ApplicationInfo.FLAG_UPDATED_SYSTEM_APP + + // Check if FLAG_SYSTEM or FLAG_UPDATED_SYSTEM_APP are set. + (applicationFlags and systemFlags) != 0 + } catch (e: PackageManager.NameNotFoundException) { + false + } + + /** + * Check if app has flag in SystemProperties + */ + @SuppressLint("PrivateApi") + fun getSystemProperty(key: String) = try { + Class.forName("android.os.SystemProperties") + ?.getDeclaredMethod("get", String::class.java) + ?.invoke(null, key) + ?.toString() + } catch (throwable: Throwable) { + null + } +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/utils/Timestamp.kt b/attribution/src/main/java/com/affise/attribution/utils/Timestamp.kt new file mode 100644 index 0000000..6636208 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/utils/Timestamp.kt @@ -0,0 +1,3 @@ +package com.affise.attribution.utils + +fun timestamp(): Long = System.currentTimeMillis() \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/utils/UUID.kt b/attribution/src/main/java/com/affise/attribution/utils/UUID.kt new file mode 100644 index 0000000..5292ecd --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/utils/UUID.kt @@ -0,0 +1,30 @@ +package com.affise.attribution.utils + +import java.util.* + + +private fun get64LeastSignificantBitsForVersion1(): Long { + val random = Random() + val random63BitLong = random.nextLong() and 0x3FFFFFFFFFFFFFFFL + val variant3BitFlag = Long.MIN_VALUE + return random63BitLong or variant3BitFlag +} + +private fun get64MostSignificantBitsForVersion1(): Long { + val currentTimeMillis = System.currentTimeMillis() + val timeLow = currentTimeMillis and 0x00000000FFFFFFFFL shl 32 + val timeMid = currentTimeMillis shr 32 and 0xFFFFL shl 16 + val version = (1 shl 12).toLong() + val timeHi = currentTimeMillis shr 48 and 0x0FFFL + return timeLow or timeMid or version or timeHi +} + +private fun generateType1UUID(): UUID { + val most64SigBits = get64MostSignificantBitsForVersion1() + val least64SigBits = get64LeastSignificantBitsForVersion1() + return UUID(most64SigBits, least64SigBits) +} + +fun generateUUID() : UUID { + return generateType1UUID() +} \ No newline at end of file diff --git a/attribution/src/main/java/com/affise/attribution/webBridge/WebBridgeManager.kt b/attribution/src/main/java/com/affise/attribution/webBridge/WebBridgeManager.kt new file mode 100644 index 0000000..af904d6 --- /dev/null +++ b/attribution/src/main/java/com/affise/attribution/webBridge/WebBridgeManager.kt @@ -0,0 +1,54 @@ +package com.affise.attribution.webBridge + +import android.annotation.SuppressLint +import android.webkit.JavascriptInterface +import android.webkit.WebView +import com.affise.attribution.events.StoreEventUseCase + +@SuppressLint("JavascriptInterface") +internal class WebBridgeManager( + private val storeEventUseCase: StoreEventUseCase +) { + + /** + * WebView + */ + private var webView: WebView? = null + + /** + * Register [webView] to WebBridgeManager + */ + fun registerWebView(webView: WebView) { + //Save webView + this.webView = webView + + //Add JavascriptInterface to webView + webView.addJavascriptInterface(this, WEB_BRIDGE_JAVASCRIPT_INTERFACE_NAME) + } + + /** + * Unregister [webView] on WebBridgeManager + */ + fun unregisterWebView() { + //Remove JavascriptInterface on webView + webView?.removeJavascriptInterface(WEB_BRIDGE_JAVASCRIPT_INTERFACE_NAME) + + //Remove webView + webView = null + } + + /** + * Send [event] from webView + */ + @JavascriptInterface + fun sendEvent(event: String?) { + event?.let { + //Store event + storeEventUseCase.storeWebEvent(it) + } + } + + companion object { + const val WEB_BRIDGE_JAVASCRIPT_INTERFACE_NAME = "AffiseBridge" + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/converter/ConverterToBase64Test.kt b/attribution/src/test/java/com/affise/attribution/converter/ConverterToBase64Test.kt new file mode 100644 index 0000000..d2331f9 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/converter/ConverterToBase64Test.kt @@ -0,0 +1,46 @@ +package com.affise.attribution.converter + +import android.util.Base64 +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockkStatic +import io.mockk.unmockkStatic +import io.mockk.verifyAll +import org.junit.After +import org.junit.Before +import org.junit.Test + +/** + * Test for [ConverterToBase64] + */ +class ConverterToBase64Test { + + private val value = "value" + private val valueByteArray = value.toByteArray() + private val valueBase64 = "valueBase64" + + @Before + fun setup() { + mockkStatic(Base64::class) + every { + Base64.encodeToString(valueByteArray, Base64.NO_WRAP) + } returns valueBase64 + } + + @After + fun tearDown() { + unmockkStatic(Base64::class) + } + + @Test + fun convert() { + val converter = ConverterToBase64() + val result = converter.convert(value) + + Truth.assertThat(result).isEqualTo(valueBase64) + + verifyAll { + Base64.encodeToString(valueByteArray, Base64.NO_WRAP) + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/converter/JsonObjectToMetricsEventConverterTest.kt b/attribution/src/test/java/com/affise/attribution/converter/JsonObjectToMetricsEventConverterTest.kt new file mode 100644 index 0000000..d189ef4 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/converter/JsonObjectToMetricsEventConverterTest.kt @@ -0,0 +1,64 @@ +package com.affise.attribution.converter + +import com.google.common.truth.Truth +import org.json.JSONObject +import org.junit.Test + +/** + * Test for [JsonObjectToMetricsEventConverter] + */ +class JsonObjectToMetricsEventConverterTest { + + private val testBeginDayTimestamp = 1650499200000 + private val testActivityName = "MainActivity" + private val testOpenTime = 74678 + private val clicksData1Name = "AutoCatchingClickEvent_cad64327358b49a71dd7248913a51e5190bb54c5" + private val clicksData1Count = 22 + private val clicksData2Name = "AutoCatchingClickEvent_4b2c39f023b1da54dd115f71692febb4565bf5fe" + private val clicksData2Count = 3 + + private val jsonString = "{\n" + + " \"begin_day_timestamp\": $testBeginDayTimestamp,\n" + + " \"data\": [\n" + + " {\n" + + " \"activity_mame\": \"$testActivityName\",\n" + + " \"open_time\": $testOpenTime,\n" + + " \"clicks_data\": [\n" + + " {\n" + + " \"name\": \"$clicksData1Name\",\n" + + " \"count\": $clicksData1Count\n" + + " },\n" + + " {\n" + + " \"name\": \"$clicksData2Name\",\n" + + " \"count\": $clicksData2Count\n" + + " },\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }" + + @Test + fun convert() { + val converter = JsonObjectToMetricsEventConverter() + val event = converter.convert(JSONObject(jsonString)) + + Truth.assertThat(event.date).isEqualTo(testBeginDayTimestamp) + Truth.assertThat(event.data.size).isEqualTo(1) + + val data = event.data.first() + + Truth.assertThat(data.activityName).isEqualTo(testActivityName) + Truth.assertThat(data.openTime).isEqualTo(testOpenTime) + Truth.assertThat(data.clicksData?.size).isEqualTo(2) + + val clickData1 = data.clicksData?.get(0) + + Truth.assertThat(clickData1?.name).isEqualTo(clicksData1Name) + Truth.assertThat(clickData1?.count).isEqualTo(clicksData1Count) + + val clickData2 = data.clicksData?.get(1) + + Truth.assertThat(clickData2?.name).isEqualTo(clicksData2Name) + Truth.assertThat(clickData2?.count).isEqualTo(clicksData2Count) + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/converter/PostBackModelToJsonStringConverterTest.kt b/attribution/src/test/java/com/affise/attribution/converter/PostBackModelToJsonStringConverterTest.kt new file mode 100644 index 0000000..30ab6aa --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/converter/PostBackModelToJsonStringConverterTest.kt @@ -0,0 +1,299 @@ +package com.affise.attribution.converter + +import com.affise.attribution.events.SerializedEvent +import com.affise.attribution.logs.SerializedLog +import com.affise.attribution.network.entity.PostBackModel +import com.google.common.truth.Truth +import io.mockk.* +import org.json.JSONArray +import org.json.JSONObject +import org.junit.After +import org.junit.Before +import org.junit.Test + +class PostBackModelToJsonStringConverterTest { + + private val model: PostBackModel = spyk( + PostBackModel() + ) + + @Before + fun config() { + mockkConstructor(JSONArray::class) + mockkConstructor(JSONObject::class) + + every { + constructedWith().put(any(), any()) + } returns JSONObject() + + every { + constructedWith().put(any()) + } returns JSONArray() + } + + @After + fun tearDown() { + unmockkConstructor(JSONArray::class) + unmockkConstructor(JSONObject::class) + } + + @Test + fun convertEmpty() { + val testEmptyData = "[{}]" + + every { + constructedWith().toString() + } returns testEmptyData + + val models = listOf(model) + val converter = PostBackModelToJsonStringConverter() + + val data = converter.convert(models) + + Truth.assertThat(data).isEqualTo(testEmptyData) + } + + @Test + fun convert() { + val testData = """ + [ + { + "country": "test", + "hardware_name": "test", + "app_version": "test", + "reftoken": "test", + "isp": "test", + "aff_device_id": "test", + "affise_sdk_pos": "test", + "mac_sha1": "test", + "language": "test", + "device_type": "test", + "fireos_adid": "test", + "uuid": "test", + "aff_session_count": "test", + "oaid_md5": "test", + "android_id_md5": "test", + "cpu_type": "test", + "affsdk_secret_id": "test", + "affpart_param_name": "test", + "affise_pkg_app_name": "test", + "aff_deeplink": "test", + "lifetime_session_count": "test", + "app_version_raw": "test", + "reftokens": "test", + "mccode": "test", + "last_time_session": "test", + "affise_app_token": "test", + "mac_md5": "test", + "aff_event_name": "test", + "deeplink_click": "test", + "mncode": "test", + "region": "test", + "installed_time": "test", + "gaid_adid_md5": "test", + "installed_hour": "test", + "first_open_time": "test", + "affalt_device_id": "test", + "aff_event_token": "test", + "random_user_id": "test", + "affise_app_id": "test", + "device_manufacturer": "test", + "platform": "test", + "sdk_platform": "test", + "api_level_os": "test", + "device_name": "test", + "adid": "test", + "time_session": "test", + "aff_sdk_version": "test", + "device_atlas_id": "test", + "gaid_adid": "test", + "oaid": "test", + "user_agent": "test", + "install_finish_time": "test", + "connection_type": "test", + "proxy_ip_address": "test", + "altstr_adid": "test", + "os_version": "test", + "last_session_time": "test", + "timezone_dev": "test", + "affpart_param_name_token": "test", + "store": "test", + "label": "test", + "referral_time": "test", + "referrer": "test", + "coloros_adid": "test", + "install_begin_time": "test", + "first_open_hour": "test", + "os_name": "test", + "network_type": "test", + "android_id": "test" + } + ] + """.trimIndent() + + every { + constructedWith().toString() + } returns testData + + val models = listOf(model) + val converter = PostBackModelToJsonStringConverter() + + val data = converter.convert(models) + + Truth.assertThat(data).isEqualTo(testData) + } + + @Test + fun convertStringWithSlash() { + val testDataWithStringSlash = "[{\"user_agent\":\"Dalvik\\/2.1.0\"}]" + val testDataWithSlash = "[{\"user_agent\":\"Dalvik/2.1.0\"}]" + + every { + constructedWith().toString() + } returns testDataWithStringSlash + + val models = listOf(model) + val converter = PostBackModelToJsonStringConverter() + + val data = converter.convert(models) + + Truth.assertThat(data).isEqualTo(testDataWithSlash) + } + + @Test + fun `verify events exists after serialization`() { + unmockkConstructor(JSONArray::class) + unmockkConstructor(JSONObject::class) + + val converter = PostBackModelToJsonStringConverter() + val eventData = JSONObject() + val event = SerializedEvent("id", eventData) + + every { model.events } returns listOf(event) + + val result = converter.convert(listOf(model)) + .let(::JSONArray) + + val eventDeserialized = try { + result + .getJSONObject(0) + .getJSONArray("events") + .getJSONObject(0) + } catch (e: Exception) { + null + } + + Truth.assertThat(eventDeserialized).isNotNull() + } + + @Test + fun `verify logs exists after serialization`() { + unmockkConstructor(JSONArray::class) + unmockkConstructor(JSONObject::class) + + val converter = PostBackModelToJsonStringConverter() + val logData = JSONObject() + val log = SerializedLog("id", "type", logData) + + every { model.logs } returns listOf(log) + + val result = converter.convert(listOf(model)) + .let(::JSONArray) + + val logsDeserialized = try { + result + .getJSONObject(0) + .getJSONArray("sdk_events") + } catch (e: Exception) { + null + } + + val logsCount = try { + result + .getJSONObject(0) + .getInt("affise_sdk_events_count") + } catch (e: Exception) { + null + } + + Truth.assertThat(logsDeserialized).isNotNull() + Truth.assertThat(logsDeserialized?.length()).isEqualTo(1) + Truth.assertThat(logsCount).isEqualTo(1) + Truth.assertThat(logsDeserialized?.getJSONObject(0)).isNotNull() + } + + @Test + fun `verify events, metric and logs exists if empty`() { + unmockkConstructor(JSONArray::class) + unmockkConstructor(JSONObject::class) + + val converter = PostBackModelToJsonStringConverter() + + every { model.events } returns listOf() + every { model.logs } returns listOf() + + val result = converter.convert(listOf(model)) + .let(::JSONArray) + + val eventDeserialized = try { + result + .getJSONObject(0) + .getJSONArray("events") + } catch (e: Exception) { + null + } + + val eventCount = try { + result + .getJSONObject(0) + .getInt("affise_events_count") + } catch (e: Exception) { + null + } + + val logsDeserialized = try { + result + .getJSONObject(0) + .getJSONArray("sdk_events") + } catch (e: Exception) { + null + } + + val logsCount = try { + result + .getJSONObject(0) + .getInt("affise_sdk_events_count") + } catch (e: Exception) { + null + } + + val metricsDeserialized = try { + result + .getJSONObject(0) + .getJSONArray("metrics_events") + } catch (e: Exception) { + null + } + + val metricsCount = try { + result + .getJSONObject(0) + .getInt("affise_metrics_events_count") + } catch (e: Exception) { + null + } + + Truth.assertThat(eventDeserialized).isNotNull() + Truth.assertThat(eventCount).isEqualTo(0) + Truth.assertThat(eventDeserialized?.length()).isEqualTo(0) + + Truth.assertThat(logsDeserialized).isNotNull() + Truth.assertThat(logsCount).isEqualTo(0) + Truth.assertThat(logsDeserialized?.length()).isEqualTo(0) + + Truth.assertThat(metricsDeserialized).isNotNull() + Truth.assertThat(metricsCount).isEqualTo(0) + Truth.assertThat(metricsDeserialized?.length()).isEqualTo(0) + } + +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/converter/StringToMD5ConverterTest.kt b/attribution/src/test/java/com/affise/attribution/converter/StringToMD5ConverterTest.kt new file mode 100644 index 0000000..9fd2672 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/converter/StringToMD5ConverterTest.kt @@ -0,0 +1,59 @@ +package com.affise.attribution.converter + +import com.affise.attribution.logs.LogsManager +import com.google.common.truth.Truth +import io.mockk.* +import org.junit.Test +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException + +/** + * Test for [StringToMD5Converter] + */ +class StringToMD5ConverterTest { + + @Test + fun `verify convert returns valid md5 string is message digest is present`() { + val logsManager: LogsManager = mockk() + val converter = StringToMD5Converter(logsManager) + + val actual = converter.convert(STRING_RAW) + Truth.assertThat(actual).isEqualTo(STRING_MD5) + + } + + @Test + fun `verify convert returns empty string is message digest is thows exception`() { + val exeption = NoSuchAlgorithmException() + + val logsManager: LogsManager = mockk { + every { + addSdkError(exeption) + } just Runs + } + + mockkStatic(MessageDigest::class) { + every { + MessageDigest.getInstance(ALGORITHM_NAME) + } throws exeption + + val converter = StringToMD5Converter(logsManager) + + val actual = converter.convert(STRING_RAW) + Truth.assertThat(actual).isEqualTo(STRING_MD5_UNKNOWN_DIGEST) + + verifyAll { + MessageDigest.getInstance(ALGORITHM_NAME) + logsManager.addSdkError(exeption) + } + } + } + + companion object { + private const val ALGORITHM_NAME = "MD5" + private const val STRING_RAW = "raw string to encode" + private const val STRING_MD5 = "950e61ca1a9e8f6d08de8234ac45bbd0" + private const val STRING_MD5_UNKNOWN_DIGEST = "00000000000000000000000000000000" + } + +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/deeplink/DeeplinkManagerImplTest.kt b/attribution/src/test/java/com/affise/attribution/deeplink/DeeplinkManagerImplTest.kt new file mode 100644 index 0000000..2849e3c --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/deeplink/DeeplinkManagerImplTest.kt @@ -0,0 +1,108 @@ +package com.affise.attribution.deeplink + +import android.app.Activity +import android.net.Uri +import com.affise.attribution.init.InitPropertiesStorage +import com.affise.attribution.utils.ActivityActionsManager +import com.affise.attribution.utils.ActivityLifecycleCallback +import com.google.common.truth.Truth +import io.mockk.* +import org.junit.Test + +/** + * Test for [DeeplinkManagerImpl] + */ +class DeeplinkManagerImplTest { + + @Test + fun `verify onInit activity lifecycle callback is not added if init is called multiple times`() { + val initProperties: InitPropertiesStorage = mockk() + val isDeeplinkRepository: DeeplinkClickRepository = mockk() + + val listeners = slot() + val activityActionsManager: ActivityActionsManager = mockk { + every { + addOnActivityResumedListener(capture(listeners)) + } just Runs + } + + val manager = DeeplinkManagerImpl( + initProperties, isDeeplinkRepository, activityActionsManager + ) + + manager.init() + manager.init() + + Truth.assertThat(listeners.isCaptured).isTrue() + + verifyAll { + activityActionsManager.addOnActivityResumedListener(capture(listeners)) + } + } + + @Test + fun `verify onInit lifecycle callback invokes deeplink callback on activity resume`() { + val initProperties: InitPropertiesStorage = mockk { + every { + getProperties()?.affiseAppId + } returns "appid" + } + val isDeeplinkRepository: DeeplinkClickRepository = mockk { + every { + setDeeplinkClick(true) + } just Runs + + every { + setDeeplink("/appid") + } just Runs + } + + val uri: Uri = mockk { + every { + path + } returns "/appid" + every { + this@mockk.toString() + } returns "/appid" + } + val deeplinkCallback: OnDeeplinkCallback = mockk { + every { + handleDeeplink(uri) + } returns true + } + + val listeners = slot() + val activityActionsManager: ActivityActionsManager = mockk { + every { + addOnActivityResumedListener(capture(listeners)) + } just Runs + } + + val manager = DeeplinkManagerImpl( + initProperties, isDeeplinkRepository, activityActionsManager + ) + + manager.init() + manager.setDeeplinkCallback(deeplinkCallback) + + val activity: Activity = mockk { + every { + intent + } returns mockk { + every { + data + } returns uri + + every { + setData(null) + } returns this + } + } + + listeners.captured.handle(activity) + + verifyAll { + deeplinkCallback.handleDeeplink(uri) + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/deeplink/InstallReferrerToDeeplinkUriConverterTest.kt b/attribution/src/test/java/com/affise/attribution/deeplink/InstallReferrerToDeeplinkUriConverterTest.kt new file mode 100644 index 0000000..9cf2b22 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/deeplink/InstallReferrerToDeeplinkUriConverterTest.kt @@ -0,0 +1,91 @@ +package com.affise.attribution.deeplink + +import android.net.Uri +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkConstructor +import io.mockk.mockkStatic +import io.mockk.unmockkConstructor +import io.mockk.unmockkStatic +import io.mockk.verifyAll +import org.junit.After +import org.junit.Before +import org.junit.Test + +/** + * Test for [InstallReferrerToDeeplinkUriConverter] + */ +class InstallReferrerToDeeplinkUriConverterTest { + + @Before + fun setUp() { + mockkStatic(Uri::class) + mockkConstructor(Uri.Builder::class) + } + + @After + fun tearDown() { + unmockkStatic(Uri::class) + unmockkConstructor(Uri.Builder::class) + } + + @Test + fun `verify relative url`() { + val innerUri: Uri = mockk() + val innerUriString = "deeplink://app" + val uri: Uri = mockk { + every { getQueryParameter("deeplink") } returns innerUriString + } + val uriStr = "deeplink=test" + + every { + Uri.parse(uriStr) + } returns uri + + every { + Uri.parse(innerUriString) + } returns innerUri + + every { + anyConstructed().encodedQuery(uriStr).build() + } returns uri + + val converter = InstallReferrerToDeeplinkUriConverter() + val result = converter.convert(uriStr) + + Truth.assertThat(result).isEqualTo(innerUri) + + verifyAll { + uri.getQueryParameter("deeplink") + } + } + + @Test + fun `verify absolute url`() { + val innerUri: Uri = mockk() + val innerUriString = "deeplink://app" + + val uri: Uri = mockk { + every { getQueryParameter("deeplink") } returns innerUriString + } + val uriStr = "https://test?deeplink=test" + + every { + Uri.parse(uriStr) + } returns uri + + every { + Uri.parse(innerUriString) + } returns innerUri + + val converter = InstallReferrerToDeeplinkUriConverter() + val result = converter.convert(uriStr) + + Truth.assertThat(result).isEqualTo(innerUri) + + verifyAll { + uri.getQueryParameter("deeplink") + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/events/EventToSerializedEventConverterTest.kt b/attribution/src/test/java/com/affise/attribution/events/EventToSerializedEventConverterTest.kt new file mode 100644 index 0000000..23f650b --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/events/EventToSerializedEventConverterTest.kt @@ -0,0 +1,61 @@ +package com.affise.attribution.events + +import com.affise.attribution.utils.generateUUID +import com.affise.attribution.utils.timestamp +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic +import org.json.JSONObject +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.* + +/** + * Test for [EventToSerializedEventConverter] + */ +class EventToSerializedEventConverterTest { + @Before + fun setUp() { + mockkStatic(::timestamp) + mockkStatic(::generateUUID) + } + + @After + fun tearDown() { + unmockkStatic(::timestamp) + unmockkStatic(::generateUUID) + } + + @Test + fun `verify convert`() { + val uuid = UUID.fromString("be07d122-3f3c-11ec-9bbc-0242ac130002") + every { + generateUUID() + } returns uuid + + every { + timestamp() + } returns 1636229513985 + + val converter = EventToSerializedEventConverter() + + val event: Event = mockk { + every { serialize() } returns JSONObject() + every { getName() } returns "name" + every { getCategory() } returns "category" + every { isFirstForUser() } returns false + every { getUserData() } returns "user-data" + every { getPredefinedParameters() } returns mapOf() + } + val actual = converter.convert(event) + + val expected = javaClass.classLoader.getResourceAsStream("serialized_event.json").use { + it?.bufferedReader()?.readText() + } + Truth.assertThat(actual.id).isEqualTo(uuid.toString()) + Truth.assertThat(actual.data.toString()).isEqualTo(expected) + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/events/EventsRepositoryImplTest.kt b/attribution/src/test/java/com/affise/attribution/events/EventsRepositoryImplTest.kt new file mode 100644 index 0000000..9fcadd8 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/events/EventsRepositoryImplTest.kt @@ -0,0 +1,163 @@ +package com.affise.attribution.events + +import com.affise.attribution.converter.Converter +import com.affise.attribution.logs.LogsManager +import com.affise.attribution.storages.EventsStorage +import com.google.common.truth.Truth +import io.mockk.* +import org.json.JSONObject +import org.junit.Test + +/** + * Test for [EventsRepositoryImpl] + */ +class EventsRepositoryImplTest { + + private val url1 = "url1" + private val url2 = "url2" + + private val url1Base64 = "url1Base64" + private val url2Base64 = "url2Base64" + + private val test1EventId = "id1" + private val test1EventJson = JSONObject("{\"test_key\":\"test_value\"}") + private val test1Event: Event = mockk { + every { serialize() } returns test1EventJson + } + private val serializedEvent1 = SerializedEvent(test1EventId, test1EventJson) + + private val webEvent = "{\"affise_event_id\":\"1\",\"test_key\":\"test_value\"}" + private val webSerializedEventJson = "{\"affise_event_id\":\"1\",\"test_key\":\"test_value\"}" + + private val converter: Converter = mockk { + every { convert(test1Event) } returns serializedEvent1 + } + + private val converterBase64: Converter = mockk { + every { convert(url1) } returns url1Base64 + every { convert(url2) } returns url2Base64 + } + + private val storage: EventsStorage = mockk { + justRun { saveEvent(url1Base64, serializedEvent1) } + justRun { saveEvent(url2Base64, serializedEvent1) } + every { getEvents(url1Base64) } returns listOf(serializedEvent1) + justRun { deleteEvent(url1Base64, listOf(test1EventId)) } + justRun { clear() } + } + + private val logsManager: LogsManager = mockk() + + private val repository: EventsRepositoryImpl by lazy { + EventsRepositoryImpl(converterBase64, converter, logsManager, storage) + } + + @Test + fun `store event with 1 url`() { + repository.storeEvent(test1Event, listOf(url1)) + + verifyAll { + converter.convert(test1Event) + converterBase64.convert(url1) + storage.saveEvent(url1Base64, serializedEvent1) + logsManager wasNot Called + } + } + + @Test + fun `store event with some url`() { + repository.storeEvent(test1Event, listOf(url1, url2)) + + verifyAll { + converter.convert(test1Event) + converterBase64.convert(url1) + converterBase64.convert(url2) + storage.saveEvent(url1Base64, serializedEvent1) + storage.saveEvent(url2Base64, serializedEvent1) + logsManager wasNot Called + } + } + + @Test + fun `store web event with 1 url`() { + val slot = slot() + justRun { storage.saveEvent(url1Base64, capture(slot)) } + + repository.storeWebEvent(webEvent, listOf(url1)) + + Truth.assertThat(slot.isCaptured).isTrue() + Truth.assertThat(slot.captured.id).isEqualTo("1") + Truth.assertThat(slot.captured.data.toString()).isEqualTo(webSerializedEventJson) + + verifyAll { + converter wasNot Called + converterBase64.convert(url1) + storage.saveEvent(url1Base64, capture(slot)) + logsManager wasNot Called + } + } + + @Test + fun `store web event with some url`() { + val slot = mutableListOf() + + justRun { storage.saveEvent(url1Base64, capture(slot)) } + justRun { storage.saveEvent(url2Base64, capture(slot)) } + + repository.storeWebEvent(webEvent, listOf(url1, url2)) + + Truth.assertThat(slot.size).isEqualTo(2) + Truth.assertThat(slot[0].id).isEqualTo("1") + Truth.assertThat(slot[0].data.toString()).isEqualTo(webSerializedEventJson) + Truth.assertThat(slot[1].id).isEqualTo("1") + Truth.assertThat(slot[1].data.toString()).isEqualTo(webSerializedEventJson) + + verifyAll { + converter wasNot Called + converterBase64.convert(url1) + converterBase64.convert(url2) + storage.saveEvent(url1Base64, capture(slot)) + storage.saveEvent(url2Base64, capture(slot)) + logsManager wasNot Called + } + } + + @Test + fun `get events`() { + val events = repository.getEvents(url1) + + Truth.assertThat(events.size).isEqualTo(1) + Truth.assertThat(events.first()).isEqualTo(serializedEvent1) + + verifyAll { + converter wasNot Called + converterBase64.convert(url1) + storage.getEvents(url1Base64) + logsManager wasNot Called + } + } + + @Test + fun `delete events`() { + repository.deleteEvent(listOf(test1EventId), url1) + + verifyAll { + converter wasNot Called + converterBase64.convert(url1) + storage.deleteEvent(url1Base64, listOf(test1EventId)) + logsManager wasNot Called + } + } + + @Test + fun `clear events`() { + repository.clear() + + verifyAll { + converter wasNot Called + converterBase64 wasNot Called + storage.clear() + logsManager wasNot Called + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/init/InitPropertiesStorageImplTest.kt b/attribution/src/test/java/com/affise/attribution/init/InitPropertiesStorageImplTest.kt new file mode 100644 index 0000000..0d1a340 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/init/InitPropertiesStorageImplTest.kt @@ -0,0 +1,32 @@ +package com.affise.attribution.init + +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [InitPropertiesStorageImpl] + */ +class InitPropertiesStorageImplTest { + + @Test + fun `verify setProperties stores data in shared preferences`() { + val affiseAppIdMock = "id" + + val model: AffiseInitProperties = mockk { + every { + affiseAppId + } returns affiseAppIdMock + } + val storage = InitPropertiesStorageImpl() + storage.setProperties(model) + + Truth.assertThat(storage.getProperties()?.affiseAppId).isEqualTo("id") + + verifyAll { + model.affiseAppId + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/init/SetPropertiesWhenAppInitializedUseCaseImplTest.kt b/attribution/src/test/java/com/affise/attribution/init/SetPropertiesWhenAppInitializedUseCaseImplTest.kt new file mode 100644 index 0000000..916ae07 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/init/SetPropertiesWhenAppInitializedUseCaseImplTest.kt @@ -0,0 +1,31 @@ +package com.affise.attribution.init + +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [SetPropertiesWhenAppInitializedUseCaseImpl] + */ +class SetPropertiesWhenAppInitializedUseCaseImplTest { + + @Test + fun init() { + val model: AffiseInitProperties = mockk() + val storage: InitPropertiesStorage = mockk { + every { + setProperties(model) + } just Runs + } + val useCase = SetPropertiesWhenAppInitializedUseCaseImpl(storage) + useCase.init(model) + + verifyAll { + storage.setProperties(model) + } + + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/logs/LogToSerializedLogConverterTest.kt b/attribution/src/test/java/com/affise/attribution/logs/LogToSerializedLogConverterTest.kt new file mode 100644 index 0000000..46505ba --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/logs/LogToSerializedLogConverterTest.kt @@ -0,0 +1,72 @@ +package com.affise.attribution.logs + +import com.affise.attribution.converter.LogToSerializedLogConverter +import com.affise.attribution.events.predefined.AffiseLog +import com.affise.attribution.events.predefined.AffiseLogType +import com.affise.attribution.utils.generateUUID +import com.affise.attribution.utils.timestamp +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.verifyAll +import org.junit.Test +import java.util.UUID + +class LogToSerializedLogConverterTest { + + @Test + fun convert() { + val typeValue = "typeValue" + val logValue = "logValue" + + val logName: AffiseLogType = mockk { + every { type } returns typeValue + } + + val log: AffiseLog = mockk { + every { name } returns logName + every { value } returns logValue + } + + val converter = LogToSerializedLogConverter() + + mockkStatic(::generateUUID) { + every { + generateUUID() + } returns UUID(0, 0) + + mockkStatic(::timestamp) { + every { + timestamp() + } returns 1638265456848 + + val result = converter.convert(log) + + Truth.assertThat(result.id).isEqualTo(ID) + Truth.assertThat(result.data.toString()).isEqualTo(DATA) + + verifyAll { + logName.type + log.name + log.value + timestamp() + } + } + } + } + + companion object { + const val ID = "00000000-0000-0000-0000-000000000000" + const val DATA = + "{" + + "\"affise_sdkevent_name\":\"affise_event_sdklog\"," + + "\"affise_sdkevent_parameters\":{" + + "\"typeValue\":\"logValue\"" + + "}," + + "\"affise_sdkevent_timestamp\":1638265456848," + + "\"affise_sdkevent_id\":\"$ID\"" + + "}" + } + +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/logs/LogsManagerImplTest.kt b/attribution/src/test/java/com/affise/attribution/logs/LogsManagerImplTest.kt new file mode 100644 index 0000000..e065414 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/logs/LogsManagerImplTest.kt @@ -0,0 +1,62 @@ +package com.affise.attribution.logs + +import io.mockk.mockk +import io.mockk.every +import io.mockk.just +import io.mockk.verifyAll +import io.mockk.Runs +import org.junit.Test + +class LogsManagerImplTest { + + private val testThrowable = Throwable() + + private val storeLogsUseCase: StoreLogsUseCase = mockk { + every { + storeLog(any()) + } just Runs + } + + private val logsManager = LogsManagerImpl(storeLogsUseCase) + + @Test + fun addNetworkError() { + logsManager.addNetworkError(testThrowable) + + verifyAll { + storeLogsUseCase.storeLog(any()) + } + + } + + @Test + fun addDeviceError() { + logsManager.addDeviceError(testThrowable) + + verifyAll { + storeLogsUseCase.storeLog(any()) + } + + } + + @Test + fun addUserError() { + logsManager.addUserError(testThrowable) + + verifyAll { + storeLogsUseCase.storeLog(any()) + } + + } + + @Test + fun addSdkError() { + logsManager.addSdkError(testThrowable) + + verifyAll { + storeLogsUseCase.storeLog(any()) + } + + } + +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/logs/LogsRepositoryImplTest.kt b/attribution/src/test/java/com/affise/attribution/logs/LogsRepositoryImplTest.kt new file mode 100644 index 0000000..0d12354 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/logs/LogsRepositoryImplTest.kt @@ -0,0 +1,161 @@ +package com.affise.attribution.logs + +import com.affise.attribution.converter.Converter +import com.affise.attribution.events.predefined.AffiseLog +import com.affise.attribution.events.predefined.AffiseLogType +import com.affise.attribution.storages.LogsStorage +import com.google.common.truth.Truth +import io.mockk.* +import org.junit.Test + +/** + * Test for [LogsRepositoryImpl] + */ +class LogsRepositoryImplTest { + + private val url1 = "url1" + private val url2 = "url2" + + private val url1Base64 = "url1Base64" + private val url2Base64 = "url2Base64" + + private val testLogId = "testLogId" + private val testLogType = "testLogType" + private val testLogTypeBase64 = "testLogTypeBase64" + + private val logNetworkType = "affise_sdklog_network" + private val logDeviceType = "affise_sdklog_ddata" + private val logUserType = "affise_sdklog_udata" + private val logSdkType = "affise_sdklog_main" + + private val logNetworkTypeBase64 = "affise_sdklog_network_Base64" + private val logDeviceTypeBase64 = "affise_sdklog_ddata_Base64" + private val logUserTypeBase64 = "affise_sdklog_udata_Base64" + private val logSdkTypeBase64 = "affise_sdklog_main_Base64" + + private val allLogTypesBase64 = + listOf(logNetworkTypeBase64, logDeviceTypeBase64, logUserTypeBase64, logSdkTypeBase64) + + private val testLogName: AffiseLogType = mockk { + every { type } returns testLogType + } + + private val testLog: AffiseLog = mockk { + every { name } returns testLogName + } + + private val testSerializedLog: SerializedLog = mockk() + + private val converter: Converter = mockk { + every { convert(testLog) } returns testSerializedLog + } + + private val converterBase64: Converter = mockk { + every { convert(url1) } returns url1Base64 + every { convert(url2) } returns url2Base64 + every { convert(testLogType) } returns testLogTypeBase64 + every { convert(logNetworkType) } returns logNetworkTypeBase64 + every { convert(logDeviceType) } returns logDeviceTypeBase64 + every { convert(logUserType) } returns logUserTypeBase64 + every { convert(logSdkType) } returns logSdkTypeBase64 + } + + private val storage: LogsStorage = mockk { + justRun { saveLog(url1Base64, testLogTypeBase64, testSerializedLog) } + justRun { saveLog(url2Base64, testLogTypeBase64, testSerializedLog) } + every { getLogs(url1Base64, allLogTypesBase64) } returns listOf(testSerializedLog) + justRun { deleteLogs(url1Base64, allLogTypesBase64, listOf(testLogId)) } + justRun { clear() } + } + + private val logsManager: LogsManager = mockk() + + private val repository: LogsRepositoryImpl by lazy { + LogsRepositoryImpl(converterBase64, converter, storage) + } + + @Test + fun `store event with 1 url`() { + repository.storeLog(testLog, listOf(url1)) + + verifyAll { + testLogName.type + testLog.name + converterBase64.convert(testLogType) + converterBase64.convert(url1) + converter.convert(testLog) + storage.saveLog(url1Base64, testLogTypeBase64, testSerializedLog) + logsManager wasNot Called + } + } + + @Test + fun `store event with some url`() { + repository.storeLog(testLog, listOf(url1, url2)) + + verifyAll { + testLogName.type + testLog.name + converterBase64.convert(testLogType) + converterBase64.convert(url1) + converterBase64.convert(url2) + converter.convert(testLog) + storage.saveLog(url1Base64, testLogTypeBase64, testSerializedLog) + storage.saveLog(url2Base64, testLogTypeBase64, testSerializedLog) + logsManager wasNot Called + } + } + + @Test + fun `get events`() { + val events = repository.getLogs(url1) + + Truth.assertThat(events.size).isEqualTo(1) + Truth.assertThat(events.first()).isEqualTo(testSerializedLog) + + verifyAll { + testLogName wasNot Called + testLog wasNot Called + converter wasNot Called + converterBase64.convert(url1) + converterBase64.convert(logNetworkType) + converterBase64.convert(logDeviceType) + converterBase64.convert(logSdkType) + converterBase64.convert(logUserType) + storage.getLogs(url1Base64, allLogTypesBase64) + logsManager wasNot Called + } + } + + @Test + fun `delete events`() { + repository.deleteLogs(listOf(testLogId), url1) + + verifyAll { + testLogName wasNot Called + testLog wasNot Called + converter wasNot Called + converterBase64.convert(url1) + converterBase64.convert(logNetworkType) + converterBase64.convert(logDeviceType) + converterBase64.convert(logSdkType) + converterBase64.convert(logUserType) + storage.deleteLogs(url1Base64, allLogTypesBase64, listOf(testLogId)) + logsManager wasNot Called + } + } + + @Test + fun clear() { + repository.clear() + + verifyAll { + testLogName wasNot Called + testLog wasNot Called + converter wasNot Called + converterBase64 wasNot Called + storage.clear() + logsManager wasNot Called + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/logs/StoreLogsUseCaseImplTest.kt b/attribution/src/test/java/com/affise/attribution/logs/StoreLogsUseCaseImplTest.kt new file mode 100644 index 0000000..3b8dc32 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/logs/StoreLogsUseCaseImplTest.kt @@ -0,0 +1,50 @@ +package com.affise.attribution.logs + +import com.affise.attribution.events.predefined.AffiseLog +import com.affise.attribution.executors.ExecutorServiceProvider +import com.affise.attribution.network.CloudConfig +import com.google.common.util.concurrent.MoreExecutors +import io.mockk.* +import org.junit.After +import org.junit.Before +import org.junit.Test + +class StoreLogsUseCaseImplTest { + + private val urls: List = listOf("https://url1", "https://url2") + + private val log: AffiseLog = mockk() + + private val provider: ExecutorServiceProvider = mockk { + every { + provideExecutorService() + } returns MoreExecutors.newDirectExecutorService() + } + + private val repository: LogsRepository = mockk { + every { storeLog(log, urls) } just Runs + } + + private val useCase = StoreLogsUseCaseImpl(provider, repository) + + @Before + fun setup() { + mockkObject(CloudConfig) + every { CloudConfig.getUrls() } returns urls + } + + @After + fun tearDown() { + unmockkObject(CloudConfig) + } + + @Test + fun storeLog() { + useCase.storeLog(log) + + verifyAll { + provider.provideExecutorService() + repository.storeLog(log, urls) + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/metrics/MetricsManagerTest.kt b/attribution/src/test/java/com/affise/attribution/metrics/MetricsManagerTest.kt new file mode 100644 index 0000000..6ac8b5c --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/metrics/MetricsManagerTest.kt @@ -0,0 +1,253 @@ +package com.affise.attribution.metrics + +import android.app.Activity +import android.content.res.Resources +import android.view.View +import com.affise.attribution.converter.StringToSHA1Converter +import com.affise.attribution.utils.ActivityActionsManager +import com.affise.attribution.utils.ActivityClickCallback +import com.affise.attribution.utils.ActivityLifecycleCallback +import com.affise.attribution.utils.timestamp +import com.google.common.truth.Truth +import io.mockk.* +import org.junit.After +import org.junit.Before +import org.junit.Test + +/** + * Test for [MetricsManagerImpl] + */ +class MetricsManagerTest { + + private val activityName = "Activity" + + private val viewId = 1 + + private val viewResourceEntryName = "viewId" + + private val activityNameViewIdSha1 = "activityNameViewIdSha1" + + private val eventName = "AutoCatchingClickEvent_activityNameViewIdSha1" + + private val activity: Activity = mockk() + + private val resourcesMockk: Resources = mockk { + every { getResourceEntryName(viewId) } returns viewResourceEntryName + } + + private val view: View = mockk { + every { id } returns viewId + every { resources } returns resourcesMockk + } + + private val startedListeners = slot() + private val stoppedListeners = slot() + private val clickListeners = slot() + + private val activityActionsManager: ActivityActionsManager = mockk { + every { addOnActivityStartedListener(capture(startedListeners)) } just Runs + every { addOnActivityStoppedListener(capture(stoppedListeners)) } just Runs + every { addOnActivityClickListener(capture(clickListeners)) } just Runs + } + + private val metricsUseCase: MetricsUseCase = mockk { + every { addOpenActivityTime(activityName, any()) } just Runs + every { addClickOnActivity(activityName, any()) } just Runs + } + + private val converterToSHA1: StringToSHA1Converter = mockk { + every { convert(activityName + viewResourceEntryName) } returns activityNameViewIdSha1 + } + + private val metricsManager = MetricsManagerImpl(activityActionsManager, metricsUseCase, converterToSHA1) + + @Before + fun setUp() { + mockkConstructor(StringToSHA1Converter::class) + every { + constructedWith().convert(activityName + viewResourceEntryName) + } returns "eventName" + } + + @After + fun tearDown() { + unmockkConstructor(StringToSHA1Converter::class) + } + + @Test + fun `init object`() { + Truth.assertThat(startedListeners.isCaptured).isTrue() + Truth.assertThat(stoppedListeners.isCaptured).isTrue() + Truth.assertThat(clickListeners.isCaptured).isTrue() + + verifyAll { + activityActionsManager.addOnActivityStartedListener(capture(startedListeners)) + activityActionsManager.addOnActivityStoppedListener(capture(stoppedListeners)) + activityActionsManager.addOnActivityClickListener(capture(clickListeners)) + } + } + + @Test + fun `init true`() { + metricsManager.setEnabledMetrics(true) + + mockkStatic(::timestamp) { + every { + timestamp() + } returns 0 + + startedListeners.captured.handle(activity) + + every { + timestamp() + } returns 100 + + stoppedListeners.captured.handle(activity) + + verifyAll { + activityActionsManager.addOnActivityStartedListener(capture(startedListeners)) + activityActionsManager.addOnActivityStoppedListener(capture(stoppedListeners)) + activityActionsManager.addOnActivityClickListener(capture(clickListeners)) + metricsUseCase.addOpenActivityTime(activityName, 100) + } + } + } + + @Test + fun `init false`() { + metricsManager.setEnabledMetrics(false) + + mockkStatic(::timestamp) { + every { + timestamp() + } returns 0 + + startedListeners.captured.handle(activity) + + every { + timestamp() + } returns 100 + + stoppedListeners.captured.handle(activity) + + verifyAll { + activityActionsManager.addOnActivityStartedListener(capture(startedListeners)) + activityActionsManager.addOnActivityStoppedListener(capture(stoppedListeners)) + activityActionsManager.addOnActivityClickListener(capture(clickListeners)) + metricsUseCase wasNot Called + } + } + } + + @Test + fun `init true stop with out start`() { + metricsManager.setEnabledMetrics(true) + + mockkStatic(::timestamp) { + every { + timestamp() + } returns 100 + + stoppedListeners.captured.handle(activity) + + verifyAll { + activityActionsManager.addOnActivityStartedListener(capture(startedListeners)) + activityActionsManager.addOnActivityStoppedListener(capture(stoppedListeners)) + activityActionsManager.addOnActivityClickListener(capture(clickListeners)) + metricsUseCase wasNot Called + } + } + } + + @Test + fun `init true second stop with out second start`() { + metricsManager.setEnabledMetrics(true) + + mockkStatic(::timestamp) { + every { + timestamp() + } returns 0 + + startedListeners.captured.handle(activity) + + every { + timestamp() + } returns 100 + + stoppedListeners.captured.handle(activity) + + every { + timestamp() + } returns 1000 + + stoppedListeners.captured.handle(activity) + + verifyAll { + activityActionsManager.addOnActivityStartedListener(capture(startedListeners)) + activityActionsManager.addOnActivityStoppedListener(capture(stoppedListeners)) + activityActionsManager.addOnActivityClickListener(capture(clickListeners)) + metricsUseCase.addOpenActivityTime(activityName, 100) + } + } + } + + @Test + fun `init true click`() { + metricsManager.setEnabledMetrics(true) + + clickListeners.captured.handle(activity, view) + + verifyAll { + activityActionsManager.addOnActivityStartedListener(capture(startedListeners)) + activityActionsManager.addOnActivityStoppedListener(capture(stoppedListeners)) + activityActionsManager.addOnActivityClickListener(capture(clickListeners)) + metricsUseCase.addClickOnActivity(activityName, eventName) + } + } + + @Test + fun `init false click`() { + metricsManager.setEnabledMetrics(false) + + clickListeners.captured.handle(activity, view) + + verifyAll { + activityActionsManager.addOnActivityStartedListener(capture(startedListeners)) + activityActionsManager.addOnActivityStoppedListener(capture(stoppedListeners)) + activityActionsManager.addOnActivityClickListener(capture(clickListeners)) + metricsUseCase wasNot Called + } + } + + @Test + fun enabledMetrics() { + metricsManager.setEnabledMetrics(false) + + metricsManager.setEnabledMetrics(true) + + clickListeners.captured.handle(activity, view) + + verifyAll { + activityActionsManager.addOnActivityStartedListener(capture(startedListeners)) + activityActionsManager.addOnActivityStoppedListener(capture(stoppedListeners)) + activityActionsManager.addOnActivityClickListener(capture(clickListeners)) + metricsUseCase.addClickOnActivity(activityName, eventName) + } + } + + @Test + fun disabledMetrics() { + metricsManager.setEnabledMetrics(false) + + metricsManager.setEnabledMetrics(false) + + clickListeners.captured.handle(activity, view) + + verifyAll { + activityActionsManager.addOnActivityStartedListener(capture(startedListeners)) + activityActionsManager.addOnActivityStoppedListener(capture(stoppedListeners)) + activityActionsManager.addOnActivityClickListener(capture(clickListeners)) + metricsUseCase wasNot Called + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/metrics/MetricsRepositoryTest.kt b/attribution/src/test/java/com/affise/attribution/metrics/MetricsRepositoryTest.kt new file mode 100644 index 0000000..a0e6e38 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/metrics/MetricsRepositoryTest.kt @@ -0,0 +1,225 @@ +package com.affise.attribution.metrics + +import com.affise.attribution.converter.Converter +import com.affise.attribution.events.Event +import com.affise.attribution.events.SerializedEvent +import com.affise.attribution.storages.MetricsStorage +import com.google.common.truth.Truth +import io.mockk.* +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.* + +/** + * Test for [MetricsRepositoryImpl] + */ +class MetricsRepositoryTest { + + private val url1 = "url1" + private val url2 = "url2" + private val url1Base64 = "url1Base64" + private val url2Base64 = "url2Base64" + + private val currentDayTime = 1L + private val currentDayTimeString = "1" + private val currentDayNameDir = "currentDayDir" + + private val metricsDataTest1 = MetricsData().apply { + activityName = "activityName1" + openTime = 1 + clicksData = mutableListOf( + MetricsClickData().apply { + name = "name1" + count = 1 + } + ) + } + + private val metricsDataTest2 = MetricsData().apply { + activityName = "activityName1" + openTime = 1 + clicksData = mutableListOf( + MetricsClickData().apply { + name = "name2" + count = 1 + } + ) + } + + private val metricsDataTest3 = MetricsData().apply { + activityName = "activityName2" + openTime = 1 + clicksData = mutableListOf( + MetricsClickData().apply { + name = "name1" + count = 1 + } + ) + } + + private val testEvent: MetricsEvent = mockk { + every { data } returns mutableListOf() + } + private val testEvent2: MetricsEvent = mockk { + every { data } returns mutableListOf() + } + + private val testSerializedEvent: SerializedEvent = mockk() + + private val converterToBase64: Converter = mockk { + every { convert(url1) } returns url1Base64 + every { convert(url2) } returns url2Base64 + every { convert(currentDayTimeString) } returns currentDayNameDir + } + + private val converterToSerializedEvent: Converter = mockk { + every { convert(testEvent) } returns testSerializedEvent + } + + private val metricsStorage: MetricsStorage = mockk { + every { getMetricsEvents(url1Base64, currentDayNameDir) } returns listOf(testEvent) + every { getMetricsEvents(url2Base64, currentDayNameDir) } returns listOf(testEvent2) + every { getMetricsEvent(url1Base64, currentDayNameDir) } returns testEvent + every { getMetricsEvent(url2Base64, currentDayNameDir) } returns testEvent2 + justRun { saveMetricsEvent(url1Base64, currentDayNameDir, testEvent) } + justRun { saveMetricsEvent(url2Base64, currentDayNameDir, testEvent2) } + justRun { deleteMetrics(url1Base64, currentDayNameDir) } + justRun { clear() } + } + + private val repository = MetricsRepositoryImpl( + converterToBase64, converterToSerializedEvent, metricsStorage + ) + + @Before + fun setUp() { + mockkStatic(Calendar::class) + justRun { Calendar.getInstance().set(Calendar.MILLISECOND, 0) } + justRun { Calendar.getInstance().set(Calendar.SECOND, 0) } + justRun { Calendar.getInstance().set(Calendar.MINUTE, 0) } + justRun { Calendar.getInstance().set(Calendar.HOUR_OF_DAY, 0) } + every { Calendar.getInstance().timeInMillis } returns currentDayTime + } + + @After + fun tearDown() { + unmockkStatic(Calendar::class) + } + + @Test + fun getOldEvents() { + val events = repository.getMetrics(url1) + + Truth.assertThat(events.size).isEqualTo(1) + Truth.assertThat(events.first()).isEqualTo(testSerializedEvent) + + verifyAll { + testEvent wasNot Called + converterToBase64.convert(url1) + converterToBase64.convert(currentDayTimeString) + converterToSerializedEvent.convert(testEvent) + metricsStorage.getMetricsEvents(url1Base64, currentDayNameDir) + } + } + + @Test + fun `add metrics data`() { + repository.addMetricsData(metricsDataTest1, listOf(url1)) + + verifyAll { + testEvent.data + converterToBase64.convert(url1) + converterToBase64.convert(currentDayTimeString) + converterToSerializedEvent wasNot Called + metricsStorage.getMetricsEvent(url1Base64, currentDayNameDir) + metricsStorage.saveMetricsEvent(url1Base64, currentDayNameDir, testEvent) + } + } + + @Test + fun `add metrics data with many urls`() { + repository.addMetricsData(metricsDataTest1, listOf(url1, url2)) + + verifyAll { + testEvent.data + converterToBase64.convert(url1) + converterToBase64.convert(url2) + converterToBase64.convert(currentDayTimeString) + converterToSerializedEvent wasNot Called + metricsStorage.getMetricsEvent(url1Base64, currentDayNameDir) + metricsStorage.getMetricsEvent(url2Base64, currentDayNameDir) + metricsStorage.getMetricsEvent(url2Base64, currentDayNameDir) + metricsStorage.saveMetricsEvent(url1Base64, currentDayNameDir, testEvent) + metricsStorage.saveMetricsEvent(url2Base64, currentDayNameDir, testEvent2) + } + } + + @Test + fun `add metrics data with many events`() { + repository.addMetricsData(metricsDataTest1, listOf(url1)) + repository.addMetricsData(metricsDataTest1, listOf(url1)) + repository.addMetricsData(metricsDataTest2, listOf(url1)) + repository.addMetricsData(metricsDataTest3, listOf(url1)) + + val event = metricsStorage.getMetricsEvent(url1Base64, currentDayNameDir)!! + + val eventData = event.data + + Truth.assertThat(eventData.size).isEqualTo(2) + + val activities = eventData.map { it.activityName } + + val openTimes = eventData.map { it.openTime } + + val clicksData = eventData.mapNotNull { it.clicksData } + .flatten() + + Truth.assertThat(clicksData.size).isEqualTo(3) + + val clicksDataNames = clicksData.map { it.name } + + val clicksDataCounts = clicksData.map { it.count } + + Truth.assertThat(activities[0]) + .isEqualTo("activityName1")//metricsDataTest1 and metricsDataTest2 + Truth.assertThat(activities[1]).isEqualTo("activityName2")//metricsDataTest3 + Truth.assertThat(openTimes[0]) + .isEqualTo(3)//metricsDataTest1, metricsDataTest1 and metricsDataTest2 + Truth.assertThat(openTimes[1]).isEqualTo(1)//metricsDataTest3 + + Truth.assertThat(clicksData.size).isEqualTo(3) + Truth.assertThat(clicksDataNames[0]).isEqualTo("name1")//metricsDataTest1 + Truth.assertThat(clicksDataNames[1]).isEqualTo("name2")//metricsDataTest2 + Truth.assertThat(clicksDataNames[2]).isEqualTo("name1")//metricsDataTest3 + Truth.assertThat(clicksDataCounts[0]).isEqualTo(2)//metricsDataTest1 and metricsDataTest1 + Truth.assertThat(clicksDataCounts[1]).isEqualTo(1)//metricsDataTest2 + Truth.assertThat(clicksDataCounts[2]).isEqualTo(1)//metricsDataTest3 + + verifyAll { + converterToBase64.convert(url1) + converterToBase64.convert(currentDayTime.toString()) + } + } + + @Test + fun clear() { + repository.clear() + + verifyAll { + converterToBase64 wasNot Called + metricsStorage.clear() + } + } + + @Test + fun deleteMetrics() { + repository.deleteMetrics(url1) + + verifyAll { + converterToBase64.convert(url1) + converterToBase64.convert(currentDayTimeString) + metricsStorage.deleteMetrics(url1Base64, currentDayNameDir) + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/metrics/MetricsUseCaseTest.kt b/attribution/src/test/java/com/affise/attribution/metrics/MetricsUseCaseTest.kt new file mode 100644 index 0000000..fb96f72 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/metrics/MetricsUseCaseTest.kt @@ -0,0 +1,73 @@ +package com.affise.attribution.metrics + +import com.affise.attribution.executors.ExecutorServiceProvider +import com.google.common.truth.Truth +import com.google.common.util.concurrent.MoreExecutors +import io.mockk.* +import org.junit.Test + +class MetricsUseCaseTest { + + private val activityName = "activityName" + + private val openTime = 111L + + private val data = "data" + + private val slot = mutableListOf() + + private val executorServiceProvider: ExecutorServiceProvider = mockk { + every { + provideExecutorService() + } returns MoreExecutors.newDirectExecutorService() + } + + private val metricsRepository: MetricsRepository = mockk { + every { addMetricsData(capture(slot), any()) } just Runs + } + + private val metricsUseCase = MetricsUseCaseImpl( + executorServiceProvider, metricsRepository + ) + + @Test + fun addOpenActivityTime() { + metricsUseCase.addOpenActivityTime(activityName, openTime) + + Truth.assertThat(slot.size).isEqualTo(1) + + val event = slot.first() + + Truth.assertThat(event.activityName).isEqualTo(activityName) + Truth.assertThat(event.openTime).isEqualTo(openTime) + Truth.assertThat(event.clicksData).isNull() + + verifyAll { + executorServiceProvider.provideExecutorService() + metricsRepository.addMetricsData(capture(slot), any()) + } + } + + @Test + fun addClickOnActivity() { + metricsUseCase.addClickOnActivity(activityName, data) + + Truth.assertThat(slot.size).isEqualTo(1) + + val event = slot.first() + + Truth.assertThat(event.activityName).isEqualTo(activityName) + Truth.assertThat(event.openTime).isEqualTo(0) + Truth.assertThat(event.clicksData?.size).isEqualTo(1) + + val clicksData = event.clicksData!!.first() + + Truth.assertThat(clicksData.name).isEqualTo(data) + Truth.assertThat(clicksData.count).isEqualTo(1) + + verifyAll { + executorServiceProvider.provideExecutorService() + metricsRepository.addMetricsData(capture(slot), any()) + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/network/CloudRepositoryTest.kt b/attribution/src/test/java/com/affise/attribution/network/CloudRepositoryTest.kt new file mode 100644 index 0000000..041b276 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/network/CloudRepositoryTest.kt @@ -0,0 +1,188 @@ +package com.affise.attribution.network + +import com.affise.attribution.converter.Converter +import com.affise.attribution.exceptions.CloudException +import com.affise.attribution.exceptions.NetworkException +import com.affise.attribution.network.entity.PostBackModel +import com.affise.attribution.parameters.UserAgentProvider +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test +import javax.net.ssl.HttpsURLConnection + +class CloudRepositoryTest { + + private val data = "" + private val models = emptyList() + private val expectedResponse = "This is the expected response text!" + private val url = "https://url" + + private val client: HttpClient = mockk() + + private val provider: UserAgentProvider = mockk { + every { provideWithDefault() } returns "" + } + + private val converter: Converter, String> = mockk { + every { convert(models) } returns data + } + + private val repository: CloudRepositoryImpl = CloudRepositoryImpl(client, provider, converter) + + @Test + fun `send with good request`() { + every { + client.executeRequest(any(), HttpClient.Method.POST, data, any()) + } returns expectedResponse + + val sendWithOutError = try { + repository.send(models, url) + true + } catch (throwable: Throwable) { + false + } + + Truth.assertThat(sendWithOutError).isTrue() + + verifyAll { + client.executeRequest(any(), HttpClient.Method.POST, data, any()) + provider.provideWithDefault() + converter.convert(models) + } + } + + @Test + fun `send with second good request`() { + every { + client.executeRequest(any(), HttpClient.Method.POST, data, any()) + } throws NetworkException(HttpsURLConnection.HTTP_BAD_REQUEST, "") andThen expectedResponse + + val sendWithOutError = try { + repository.send(models, url) + true + } catch (throwable: Throwable) { + false + } + + Truth.assertThat(sendWithOutError).isTrue() + + verifyAll { + client.executeRequest(any(), HttpClient.Method.POST, data, any()) + provider.provideWithDefault() + converter.convert(models) + } + } + + @Test + fun `send with third good request`() { + every { + client.executeRequest(any(), HttpClient.Method.POST, data, any()) + } throws NetworkException(HttpsURLConnection.HTTP_BAD_REQUEST, "") andThenThrows + NetworkException(HttpsURLConnection.HTTP_BAD_REQUEST, "") andThen expectedResponse + + val sendWithOutError = try { + repository.send(models, url) + true + } catch (throwable: Throwable) { + false + } + + Truth.assertThat(sendWithOutError).isTrue() + + verifyAll { + client.executeRequest(any(), HttpClient.Method.POST, data, any()) + provider.provideWithDefault() + converter.convert(models) + } + + } + + @Test + fun `not send with fourth good request`() { + every { + client.executeRequest(any(), HttpClient.Method.POST, data, any()) + } throws NetworkException(HttpsURLConnection.HTTP_BAD_REQUEST, "") andThenThrows + NetworkException(HttpsURLConnection.HTTP_BAD_REQUEST, "") andThenThrows + NetworkException(HttpsURLConnection.HTTP_BAD_REQUEST, "") andThenThrows + NetworkException(HttpsURLConnection.HTTP_BAD_REQUEST, "") andThen expectedResponse + + val sendWithOutError = try { + repository.send(models, url) + true + } catch (throwable: Throwable) { + Truth.assertThat(throwable).isInstanceOf(CloudException::class.java) + throwable as CloudException + + Truth.assertThat(throwable.url).isEqualTo(url) + false + } + + Truth.assertThat(sendWithOutError).isFalse() + + verifyAll { + client.executeRequest(any(), HttpClient.Method.POST, data, any()) + provider.provideWithDefault() + converter.convert(models) + } + + } + + @Test + fun `not with third bad request`() { + every { + client.executeRequest(any(), HttpClient.Method.POST, data, any()) + } throws NetworkException(HttpsURLConnection.HTTP_BAD_REQUEST, "") andThenThrows + NetworkException(HttpsURLConnection.HTTP_BAD_REQUEST, "") andThenThrows + NetworkException(HttpsURLConnection.HTTP_BAD_REQUEST, "") andThen expectedResponse + + val sendWithOutError = try { + repository.send(models, url) + true + } catch (throwable: Throwable) { + Truth.assertThat(throwable).isInstanceOf(CloudException::class.java) + throwable as CloudException + + Truth.assertThat(throwable.url).isEqualTo(url) + false + } + + Truth.assertThat(sendWithOutError).isFalse() + + verifyAll { + client.executeRequest(any(), HttpClient.Method.POST, data, any()) + provider.provideWithDefault() + converter.convert(models) + } + } + + @Test + fun `send with bad request`() { + every { + client.executeRequest(any(), HttpClient.Method.POST, data, any()) + } throws NetworkException(HttpsURLConnection.HTTP_BAD_REQUEST, "") + + val sendWithOutError = try { + repository.send(models, url) + true + } catch (throwable: Throwable) { + Truth.assertThat(throwable).isInstanceOf(CloudException::class.java) + throwable as CloudException + + Truth.assertThat(throwable.url).isEqualTo(url) + + false + } + + Truth.assertThat(sendWithOutError).isFalse() + + verifyAll { + client.executeRequest(any(), HttpClient.Method.POST, data, any()) + provider.provideWithDefault() + converter.convert(models) + } + + } + +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/network/HttpClientTest.kt b/attribution/src/test/java/com/affise/attribution/network/HttpClientTest.kt new file mode 100644 index 0000000..56785a6 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/network/HttpClientTest.kt @@ -0,0 +1,124 @@ +package com.affise.attribution.network + +import com.affise.attribution.exceptions.NetworkException +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.io.OutputStream +import java.net.URL +import javax.net.ssl.HttpsURLConnection + +abstract class MockDataSource(url: URL) : HttpsURLConnection(url) + +class HttpClientTest { + + @Test + fun createGoodRequest() { + val expectedResponse = "This is the expected response text!" + + val data = "" + val method = HttpClient.Method.POST + val headers = emptyMap() + + val customInputStream: InputStream = ByteArrayInputStream(expectedResponse.toByteArray()) + val customOutputStream: OutputStream = ByteArrayOutputStream() + + val connection: MockDataSource = mockk { + every { doOutput = true } returns Unit + every { doInput = true } returns Unit + every { requestMethod = method.name } returns Unit + every { useCaches = false } returns Unit + every { readTimeout = 15000 } returns Unit + every { connectTimeout = 15000 } returns Unit + every { inputStream } returns customInputStream + every { responseCode } returns HttpsURLConnection.HTTP_OK + every { outputStream } returns customOutputStream + every { disconnect() } returns Unit + } + + val url: URL = mockk { + every { openConnection() } returns connection + } + + val client = HttpClientImpl() + val result = client.executeRequest(url, method, data, headers) + + Truth.assertThat(result).isEqualTo(expectedResponse) + + verifyAll { + connection.doOutput = true + connection.doInput = true + connection.requestMethod = method.name + connection.useCaches = false + connection.readTimeout = 15000 + connection.connectTimeout = 15000 + connection.inputStream + connection.responseCode + connection.outputStream + connection.disconnect() + + url.openConnection() + } + + } + + @Test + fun createBadRequest() { + val data = "" + val method = HttpClient.Method.POST + val headers = emptyMap() + + val customOutputStream: OutputStream = ByteArrayOutputStream() + + val connection: MockDataSource = mockk { + every { doOutput = true } returns Unit + every { doInput = true } returns Unit + every { requestMethod = method.name } returns Unit + every { useCaches = false } returns Unit + every { readTimeout = 15000 } returns Unit + every { connectTimeout = 15000 } returns Unit + every { responseCode } returns HttpsURLConnection.HTTP_BAD_REQUEST + every { responseMessage } returns "" + every { outputStream } returns customOutputStream + every { disconnect() } returns Unit + } + + val url: URL = mockk { + every { openConnection() } returns connection + } + + val client = HttpClientImpl() + + val networkExceptionThrown = try { + client.executeRequest(url, method, data, headers) + false + } catch (throwable: NetworkException) { + true + } catch (throwable: Throwable) { + false + } + + Truth.assertThat(networkExceptionThrown).isTrue() + + verifyAll { + connection.doOutput = true + connection.doInput = true + connection.requestMethod = method.name + connection.useCaches = false + connection.readTimeout = 15000 + connection.connectTimeout = 15000 + connection.responseCode + connection.responseMessage + connection.outputStream + connection.disconnect() + + url.openConnection() + } + + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/AffAppTokenPropertyProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/AffAppTokenPropertyProviderTest.kt new file mode 100644 index 0000000..478847b --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/AffAppTokenPropertyProviderTest.kt @@ -0,0 +1,44 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.converter.Converter +import com.affise.attribution.init.InitPropertiesStorage +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [AffAppTokenPropertyProvider] + */ +class AffAppTokenPropertyProviderTest { + + @Test + fun `verify when property is init`() { + val initProps: InitPropertiesStorage = mockk { + every { + getProperties()?.affiseAppId + } returns "affiseAppId" + every { + getProperties()?.secretId + } returns "secretId" + } + + val converter: Converter = mockk { + every { + convert("affiseAppId0secretId") + } returns "tokenSHA256" + } + + val provider = AffAppTokenPropertyProvider(initProps, converter) + val actual = provider.provideWithParam("0") + + Truth.assertThat(actual).isEqualTo("tokenSHA256") + + verifyAll { + initProps.getProperties()?.affiseAppId + initProps.getProperties()?.secretId + converter.convert("affiseAppId0secretId") + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/AffPartParamNamePropertyProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/AffPartParamNamePropertyProviderTest.kt new file mode 100644 index 0000000..a1efc91 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/AffPartParamNamePropertyProviderTest.kt @@ -0,0 +1,45 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.init.InitPropertiesStorage +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll + +import org.junit.Test + +/** + * Test for [AffPartParamNamePropertyProvider] + */ +class AffPartParamNamePropertyProviderTest { + + @Test + fun `verify when property is init`() { + val initProps: InitPropertiesStorage = mockk { + every { + getProperties()?.partParamName + } returns "token" + } + val provider = AffPartParamNamePropertyProvider(initProps) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo("token") + verifyAll { + initProps.getProperties()?.partParamName + } + } + + @Test + fun `verify when property is not init`() { + val initProps: InitPropertiesStorage = mockk { + every { + getProperties()?.partParamName + } returns null + } + val provider = AffPartParamNamePropertyProvider(initProps) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(null) + verifyAll { + initProps.getProperties()?.partParamName + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/AffPartParamNameTokenPropertyProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/AffPartParamNameTokenPropertyProviderTest.kt new file mode 100644 index 0000000..f82e83d --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/AffPartParamNameTokenPropertyProviderTest.kt @@ -0,0 +1,43 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.init.InitPropertiesStorage +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [AffPartParamNameTokenPropertyProvider] + */ +class AffPartParamNameTokenPropertyProviderTest { + @Test + fun `verify when property is init`() { + val initProps: InitPropertiesStorage = mockk { + every { + getProperties()?.partParamNameToken + } returns "token" + } + val provider = AffPartParamNameTokenPropertyProvider(initProps) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo("token") + verifyAll { + initProps.getProperties()?.partParamNameToken + } + } + + @Test + fun `verify when property is not init`() { + val initProps: InitPropertiesStorage = mockk { + every { + getProperties()?.partParamNameToken + } returns null + } + val provider = AffPartParamNameTokenPropertyProvider(initProps) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(null) + verifyAll { + initProps.getProperties()?.partParamNameToken + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/AffiseAltDeviceIdProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/AffiseAltDeviceIdProviderTest.kt new file mode 100644 index 0000000..d6cb7e0 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/AffiseAltDeviceIdProviderTest.kt @@ -0,0 +1,33 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.usecase.FirstAppOpenUseCase +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [AffiseAltDeviceIdProvider] + */ +class AffiseAltDeviceIdProviderTest { + + @Test + fun `verify provide`() { + val devId = "test" + val useCase: FirstAppOpenUseCase = mockk { + every { + getAffiseAltDeviseId() + } returns devId + } + val provider = AffiseAltDeviceIdProvider(useCase) + + val actual = provider.provide() + + Truth.assertThat(actual).isEqualTo(devId) + + verifyAll { + useCase.getAffiseAltDeviseId() + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/AffiseAppIdProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/AffiseAppIdProviderTest.kt new file mode 100644 index 0000000..94a067a --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/AffiseAppIdProviderTest.kt @@ -0,0 +1,52 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.init.AffiseInitProperties +import com.affise.attribution.init.InitPropertiesStorage +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [AffiseAppIdProvider]K + */ +class AffiseAppIdProviderTest { + + @Test + fun `verify provide`() { + val appId = "test" + val props: AffiseInitProperties = mockk { + every { + affiseAppId + } returns appId + } + val storage: InitPropertiesStorage = mockk { + every { + getProperties() + } returns props + } + val provider = AffiseAppIdProvider(storage) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(appId) + verifyAll { + storage.getProperties() + props.affiseAppId + } + } + + @Test + fun `verify provide when properties storage returns null`() { + val storage: InitPropertiesStorage = mockk { + every { + getProperties() + } returns null + } + val provider = AffiseAppIdProvider(storage) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(null) + verifyAll { + storage.getProperties() + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/AffiseDeviceIdProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/AffiseDeviceIdProviderTest.kt new file mode 100644 index 0000000..fac7f60 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/AffiseDeviceIdProviderTest.kt @@ -0,0 +1,32 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.usecase.FirstAppOpenUseCase +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [AffiseDeviceIdProvider] + */ +class AffiseDeviceIdProviderTest { + + @Test + fun `verify provide`() { + val devId = "test" + val useCase: FirstAppOpenUseCase = mockk { + every { + getAffiseDeviseId() + } returns devId + } + val provider = AffiseDeviceIdProvider(useCase) + + val actual = provider.provide() + + Truth.assertThat(actual).isEqualTo(devId) + verifyAll { + useCase.getAffiseDeviseId() + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/AffisePackageAppNameProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/AffisePackageAppNameProviderTest.kt new file mode 100644 index 0000000..7f316d4 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/AffisePackageAppNameProviderTest.kt @@ -0,0 +1,46 @@ +package com.affise.attribution.parameters + +import android.content.Context +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [AffisePackageAppNameProvider] + */ +class AffisePackageAppNameProviderTest { + + @Test + fun `verify provide`() { + val name = "com.targetApp.packageName" + + val context: Context = mockk { + every { + packageName + } returns name + } + val provider = AffisePackageAppNameProvider(context) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(name) + verifyAll { + context.packageName + } + } + + @Test + fun `verify provide when getPackageName returns null`() { + val context: Context = mockk { + every { + packageName + } returns null + } + val provider = AffisePackageAppNameProvider(context) + val actual = provider.provide() + Truth.assertThat(actual).isNull() + verifyAll { + context.packageName + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/AndroidIdMD5ProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/AndroidIdMD5ProviderTest.kt new file mode 100644 index 0000000..e2c0cf4 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/AndroidIdMD5ProviderTest.kt @@ -0,0 +1,60 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.converter.Converter +import com.affise.attribution.parameters.base.StringPropertyProvider +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [AndroidIdMD5Provider] + */ +class AndroidIdMD5ProviderTest { + + @Test + fun `verify android id hashed with md5 is provided when android id provider returns value`() { + val aId = "android id" + val aIdConverted = "android id converted" + val aIdProvider: StringPropertyProvider = mockk { + every { + provide() + } returns aId + } + val strToMd5Converter: Converter = mockk { + every { + convert(aId) + } returns aIdConverted + } + val provider = AndroidIdMD5Provider( + aIdProvider, + strToMd5Converter + ) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(aIdConverted) + verifyAll { + aIdProvider.provide() + strToMd5Converter.convert(aId) + } + } + + @Test + fun `verify returns null when android id is null`() { + val aIdProvider: StringPropertyProvider = mockk { + every { + provide() + } returns null + } + val strToMd5Converter: Converter = mockk() + val provider = AndroidIdMD5Provider( + aIdProvider, + strToMd5Converter + ) + val actual = provider.provide() + Truth.assertThat(actual).isNull() + verifyAll { + aIdProvider.provide() + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/AndroidIdProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/AndroidIdProviderTest.kt new file mode 100644 index 0000000..a83ed37 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/AndroidIdProviderTest.kt @@ -0,0 +1,43 @@ +package com.affise.attribution.parameters + +import android.app.Application +import android.content.ContentResolver +import android.provider.Settings +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [AndroidIdProvider] + */ +class AndroidIdProviderTest { + + @Test + fun `verify settings secure is called with android_id argument`() { + mockkStatic(Settings.Secure::class) { + val secureValue = "value" + val contentResolverMock: ContentResolver = mockk() + + every { + Settings.Secure.getString(contentResolverMock, "android_id") + } returns secureValue + val app: Application = mockk { + every { + contentResolver + } returns contentResolverMock + } + + val provider = AndroidIdProvider(app) + val actual = provider.provide() + + Truth.assertThat(actual).isEqualTo(secureValue) + + verifyAll { + app.contentResolver + } + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/ApiLevelOSProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/ApiLevelOSProviderTest.kt new file mode 100644 index 0000000..c4fd3a3 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/ApiLevelOSProviderTest.kt @@ -0,0 +1,28 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.build.BuildConfigPropertiesProvider +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import org.junit.Test + +/** + * Test for [ApiLevelOSProvider] + */ +class ApiLevelOSProviderTest { + + @Test + fun provide() { + val propertiesProvider: BuildConfigPropertiesProvider = mockk { + every { + getSDKVersion() + } returns 42 + } + + val provider = ApiLevelOSProvider(propertiesProvider) + + val actual = provider.provide() + + Truth.assertThat(actual).isEqualTo("42") + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/AppVersionProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/AppVersionProviderTest.kt new file mode 100644 index 0000000..cf4c9c5 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/AppVersionProviderTest.kt @@ -0,0 +1,118 @@ +package com.affise.attribution.parameters + +import android.content.Context +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import com.affise.attribution.logs.LogsManager +import com.google.common.truth.Truth +import io.mockk.* +import org.junit.Test + +/** + * Test for [AppVersionProvider] + */ +class AppVersionProviderTest { + + @Test + fun `verify provide returns version code from package manager`() { + val packageName = "com.myApp" + val versionNameMock = "2" + + val logsManager: LogsManager = mockk() + + val packageInfo: PackageInfo = PackageInfo().apply { + versionName = versionNameMock + } + val packageManagerMock: PackageManager = mockk { + every { + getPackageInfo(packageName, 0) + } returns packageInfo + } + val context: Context = mockk { + every { + packageManager + } returns packageManagerMock + + every { + getPackageName() + } returns packageName + } + + val provider = AppVersionProvider(context, logsManager) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(versionNameMock) + verifyAll { + context.packageManager + context.packageName + packageManagerMock.getPackageInfo(packageName, 0) + } + } + + @Test + fun `verify provide returns null when package manager throws exception`() { + val packageName = "com.myApp" + val exception = PackageManager.NameNotFoundException() + + val logsManager: LogsManager = mockk { + every { + addDeviceError(exception) + } just Runs + } + + val packageManagerMock: PackageManager = mockk { + every { + getPackageInfo(packageName, 0) + } throws exception + } + val context: Context = mockk { + every { + packageManager + } returns packageManagerMock + + every { + getPackageName() + } returns packageName + } + + val provider = AppVersionProvider(context, logsManager) + val actual = provider.provide() + Truth.assertThat(actual).isNull() + verifyAll { + context.packageManager + context.packageName + packageManagerMock.getPackageInfo(packageName, 0) + logsManager.addDeviceError(exception) + } + } + + @Test + fun `verify provide returns null when package manager returns null`() { + val packageName = "com.myApp" + + val logsManager: LogsManager = mockk() + + val packageManagerMock: PackageManager = mockk { + every { + getPackageInfo(packageName, 0) + } returns null + } + val context: Context = mockk { + every { + packageManager + } returns packageManagerMock + + every { + getPackageName() + } returns packageName + } + + val provider = AppVersionProvider(context, logsManager) + val actual = provider.provide() + Truth.assertThat(actual).isNull() + verifyAll { + context.packageManager + context.packageName + packageManagerMock.getPackageInfo(packageName, 0) + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/AppVersionRawProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/AppVersionRawProviderTest.kt new file mode 100644 index 0000000..9927e2f --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/AppVersionRawProviderTest.kt @@ -0,0 +1,119 @@ +package com.affise.attribution.parameters + +import android.content.Context +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import com.affise.attribution.logs.LogsManager +import com.google.common.truth.Truth +import io.mockk.* +import org.junit.Test + +/** + * Test for [AppVersionRawProvider] + */ +class AppVersionRawProviderTest { + + @Test + @Suppress("DEPRECATION") + fun `verify provide returns version code from package manager`() { + val packageName = "com.myApp" + val versionCodeMock = 2 + + val logsManager: LogsManager = mockk() + + val packageInfo: PackageInfo = PackageInfo().apply { + versionCode = versionCodeMock + } + val packageManagerMock: PackageManager = mockk { + every { + getPackageInfo(packageName, 0) + } returns packageInfo + } + val context: Context = mockk { + every { + packageManager + } returns packageManagerMock + + every { + getPackageName() + } returns packageName + } + + val provider = AppVersionRawProvider(context, logsManager) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(versionCodeMock.toString()) + verifyAll { + context.packageManager + context.packageName + packageManagerMock.getPackageInfo(packageName, 0) + } + } + + @Test + fun `verify provide returns null when package manager throws exception`() { + val packageName = "com.myApp" + val exception = PackageManager.NameNotFoundException() + + val logsManager: LogsManager = mockk { + every { + addDeviceError(exception) + } just Runs + } + + val packageManagerMock: PackageManager = mockk { + every { + getPackageInfo(packageName, 0) + } throws exception + } + val context: Context = mockk { + every { + packageManager + } returns packageManagerMock + + every { + getPackageName() + } returns packageName + } + + val provider = AppVersionRawProvider(context, logsManager) + val actual = provider.provide() + Truth.assertThat(actual).isNull() + verifyAll { + context.packageManager + context.packageName + packageManagerMock.getPackageInfo(packageName, 0) + logsManager.addDeviceError(exception) + } + } + + @Test + fun `verify provide returns null when package manager returns null`() { + val packageName = "com.myApp" + + val logsManager: LogsManager = mockk() + + val packageManagerMock: PackageManager = mockk { + every { + getPackageInfo(packageName, 0) + } returns null + } + val context: Context = mockk { + every { + packageManager + } returns packageManagerMock + + every { + getPackageName() + } returns packageName + } + + val provider = AppVersionRawProvider(context, logsManager) + val actual = provider.provide() + Truth.assertThat(actual).isNull() + verifyAll { + context.packageManager + context.packageName + packageManagerMock.getPackageInfo(packageName, 0) + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/CountryProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/CountryProviderTest.kt new file mode 100644 index 0000000..a7f8481 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/CountryProviderTest.kt @@ -0,0 +1,18 @@ +package com.affise.attribution.parameters + +import com.google.common.truth.Truth +import org.junit.Test +import java.util.Locale + +/** + * Test for [CountryProvider] + */ +class CountryProviderTest { + @Test + fun provide() { + Locale.setDefault(Locale("lang", "COUNTRY")) + val countryProvider = CountryProvider() + val actual = countryProvider.provide() + Truth.assertThat(actual).isEqualTo("COUNTRY") + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/CpuTypeProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/CpuTypeProviderTest.kt new file mode 100644 index 0000000..2db205f --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/CpuTypeProviderTest.kt @@ -0,0 +1,29 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.build.BuildConfigPropertiesProvider +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [CpuTypeProvider] + */ +class CpuTypeProviderTest { + + @Test + fun `verify provide`() { + val propsProvider: BuildConfigPropertiesProvider = mockk { + every { + getSupportedABIs() + } returns listOf("a", "b") + } + val provider = CpuTypeProvider(propsProvider) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo("a, b") + verifyAll { + propsProvider.getSupportedABIs() + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/CreatedTimeHourProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/CreatedTimeHourProviderTest.kt new file mode 100644 index 0000000..77ddae5 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/CreatedTimeHourProviderTest.kt @@ -0,0 +1,53 @@ +package com.affise.attribution.parameters + +import com.google.common.truth.Truth +import io.mockk.* +import org.junit.Test +import java.util.Calendar + +class CreatedTimeHourProviderTest { + + @Test + fun provide() { + val testTimeInSec = 1636722000000 + val testTimeHour = 1636722000000 + + val calendar: Calendar = mockk { + every { + set(Calendar.MILLISECOND, 0) + } returns Unit + + every { + set(Calendar.SECOND, 0) + } returns Unit + + every { + set(Calendar.MINUTE, 0) + } returns Unit + + every { + timeInMillis + } returns testTimeHour + } + + mockkStatic(Calendar::class) { + every { + Calendar.getInstance() + } returns calendar + + val provider = CreatedTimeHourProvider() + val result = provider.provide() + + Truth.assertThat(testTimeInSec).isEqualTo(result) + } + + verifyAll { + Calendar.getInstance() + calendar.set(Calendar.MILLISECOND, 0) + calendar.set(Calendar.SECOND, 0) + calendar.set(Calendar.MINUTE, 0) + calendar.timeInMillis + } + + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/CreatedTimeMilliProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/CreatedTimeMilliProviderTest.kt new file mode 100644 index 0000000..2db1e6f --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/CreatedTimeMilliProviderTest.kt @@ -0,0 +1,26 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.utils.timestamp +import com.google.common.truth.Truth +import io.mockk.* +import org.junit.Test + +class CreatedTimeMilliProviderTest { + + @Test + fun provide() { + val testTimeInSec = 1403568849259 + val testTimeInMillis = 1403568849259L + + mockkStatic(::timestamp) { + every { + timestamp() + } returns testTimeInMillis + + val provider = CreatedTimeMilliProvider() + val result = provider.provide() + + Truth.assertThat(testTimeInSec).isEqualTo(result) + } + } +} diff --git a/attribution/src/test/java/com/affise/attribution/parameters/CreatedTimeProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/CreatedTimeProviderTest.kt new file mode 100644 index 0000000..03fb2c6 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/CreatedTimeProviderTest.kt @@ -0,0 +1,43 @@ +package com.affise.attribution.parameters + +import com.google.common.truth.Truth +import io.mockk.* +import org.junit.Test +import java.util.Calendar + +class CreatedTimeProviderTest { + + @Test + fun provide() { + val testTimeInSec = 1403568849259L + val testTimeInMillis = 1403568849259L + + val calendar: Calendar = mockk { + every { + set(Calendar.MILLISECOND, 0) + } just Runs + + every { + timeInMillis + } returns testTimeInMillis + } + + mockkStatic(Calendar::class) { + every { + Calendar.getInstance() + } returns calendar + + val provider = CreatedTimeProvider() + val result = provider.provide() + + Truth.assertThat(testTimeInSec).isEqualTo(result) + } + + verifyAll { + Calendar.getInstance() + calendar.set(Calendar.MILLISECOND, 0) + calendar.timeInMillis + } + } + +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/DeeplinkClickProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/DeeplinkClickProviderTest.kt new file mode 100644 index 0000000..9528c0c --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/DeeplinkClickProviderTest.kt @@ -0,0 +1,45 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.deeplink.DeeplinkClickRepository +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll + +import org.junit.Test + +/** + * Test for [DeeplinkClickPropertyProvider] + */ +class DeeplinkClickProviderTest { + + @Test + fun `verify when isDeeplink is false`() { + val repository: DeeplinkClickRepository = mockk { + every { + isDeeplinkClick() + } returns false + } + val provider = DeeplinkClickPropertyProvider(repository) + val actual = provider.provide() + Truth.assertThat(actual).isFalse() + verifyAll { + repository.isDeeplinkClick() + } + } + + @Test + fun `verify when isDeeplink is true`() { + val repository: DeeplinkClickRepository = mockk { + every { + isDeeplinkClick() + } returns true + } + val provider = DeeplinkClickPropertyProvider(repository) + val actual = provider.provide() + Truth.assertThat(actual).isTrue() + verifyAll { + repository.isDeeplinkClick() + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/DeeplinkProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/DeeplinkProviderTest.kt new file mode 100644 index 0000000..0315b21 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/DeeplinkProviderTest.kt @@ -0,0 +1,40 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.deeplink.DeeplinkClickRepository +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [DeeplinkProvider] + */ +class DeeplinkProviderTest { + + @Test + fun `verify provide returns empty string when repository returns null`() { + val repository: DeeplinkClickRepository = mockk { + every { getDeeplink() } returns null + } + val provider = DeeplinkProvider(repository) + Truth.assertThat(provider.provide()).isEqualTo("") + + verifyAll { + repository.getDeeplink() + } + } + + @Test + fun `verify provide returns string when repository returns string`() { + val repository: DeeplinkClickRepository = mockk { + every { getDeeplink() } returns "string" + } + val provider = DeeplinkProvider(repository) + Truth.assertThat(provider.provide()).isEqualTo( "string") + + verifyAll { + repository.getDeeplink() + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/DeviceManufacturerProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/DeviceManufacturerProviderTest.kt new file mode 100644 index 0000000..f9196d2 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/DeviceManufacturerProviderTest.kt @@ -0,0 +1,47 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.build.BuildConfigPropertiesProvider +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [DeviceManufacturerProvider] + */ +class DeviceManufacturerProviderTest { + + @Test + fun `verify when manufacturer is returned from build config`() { + val manufacturer = "xiaomi" + val propertiesProvider: BuildConfigPropertiesProvider = mockk { + every { + getManufacturer() + } returns manufacturer + } + val provider = DeviceManufacturerProvider(propertiesProvider) + + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(manufacturer) + verifyAll { + propertiesProvider.getManufacturer() + } + } + + @Test + fun `verify when manufacturer returned from build config is null`() { + val propertiesProvider: BuildConfigPropertiesProvider = mockk { + every { + getManufacturer() + } returns null + } + val provider = DeviceManufacturerProvider(propertiesProvider) + + val actual = provider.provide() + Truth.assertThat(actual).isNull() + verifyAll { + propertiesProvider.getManufacturer() + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/DeviceTypeProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/DeviceTypeProviderTest.kt new file mode 100644 index 0000000..592cb62 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/DeviceTypeProviderTest.kt @@ -0,0 +1,141 @@ +package com.affise.attribution.parameters + +import android.app.Application +import android.app.UiModeManager +import android.content.res.Configuration +import android.content.res.Resources +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [DeviceTypeProvider] + */ +class DeviceTypeProviderTest { + + @Test + fun `provider returns tv if UiModeManager configuration is television`() { + val app: Application = mockk() + val provider = DeviceTypeProvider(app) + + val uiModeManager: UiModeManager = mockk { + every { + currentModeType + } returns Configuration.UI_MODE_TYPE_TELEVISION + } + every { + app.getSystemService(SYSTEM_SERVICE) + } returns uiModeManager + val actual = provider.provide() + + Truth.assertThat(actual).isEqualTo(TYPE_TV) + + verifyAll { + app.getSystemService(SYSTEM_SERVICE) + uiModeManager.currentModeType + } + } + + @Test + fun `provider returns car if UiModeManager configuration is car`() { + val app: Application = mockk() + val provider = DeviceTypeProvider(app) + + val uiModeManager: UiModeManager = mockk { + every { + currentModeType + } returns Configuration.UI_MODE_TYPE_CAR + } + every { + app.getSystemService(SYSTEM_SERVICE) + } returns uiModeManager + val actual = provider.provide() + + Truth.assertThat(actual).isEqualTo(TYPE_CAR) + + verifyAll { + app.getSystemService(SYSTEM_SERVICE) + uiModeManager.currentModeType + } + } + + @Test + fun `provider returns phone if UiModeManager is not present and screen size is SMALL`() { + `test device type provider when UiModeManager is not present`( + size = Configuration.SCREENLAYOUT_SIZE_SMALL, + expectedType = TYPE_SMART + ) + } + + @Test + fun `provider returns tablet if UiModeManager is not present and screen size is LARGE`() { + `test device type provider when UiModeManager is not present`( + size = Configuration.SCREENLAYOUT_SIZE_LARGE, + expectedType = TYPE_TABLET + ) + } + + @Test + fun `provider returns tablet if UiModeManager is not present and screen size is XLARGE`() { + `test device type provider when UiModeManager is not present`( + size = Configuration.SCREENLAYOUT_SIZE_XLARGE, + expectedType = TYPE_TABLET + ) + } + + @Test + fun `provider returns phone if UiModeManager is not present and screen size is NORMAL`() { + `test device type provider when UiModeManager is not present`( + size = Configuration.SCREENLAYOUT_SIZE_NORMAL, + expectedType = TYPE_SMART + ) + } + + @Test + fun `provider returns phone if UiModeManager is not present and screen size is UNDEFINED`() { + `test device type provider when UiModeManager is not present`( + size = Configuration.SCREENLAYOUT_SIZE_UNDEFINED, + expectedType = TYPE_SMART + ) + } + + private fun `test device type provider when UiModeManager is not present`(size: Int, expectedType: String) { + val app: Application = mockk() + val provider = DeviceTypeProvider(app) + + every { + app.getSystemService(SYSTEM_SERVICE) + } returns null + + val configurationMock: Configuration = Configuration().apply { + screenLayout = size + } + val resources: Resources = mockk { + every { + configuration + } returns configurationMock + } + every { + app.resources + } returns resources + val actual = provider.provide() + + Truth.assertThat(actual).isEqualTo(expectedType) + + verifyAll { + app.getSystemService(SYSTEM_SERVICE) + app.resources + resources.configuration + } + } + + companion object { + const val SYSTEM_SERVICE = "uimode" + const val TYPE_SMART = "smartphone" + const val TYPE_TABLET = "tablet" + const val TYPE_TV = "tv" + const val TYPE_CAR = "car" + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/FirstOpenHourProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/FirstOpenHourProviderTest.kt new file mode 100644 index 0000000..1aca942 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/FirstOpenHourProviderTest.kt @@ -0,0 +1,62 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.usecase.FirstAppOpenUseCase +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test +import java.util.Date + +/** + * Test for [FirstOpenHourProvider] + */ +class FirstOpenHourProviderTest { + + @Test + fun `verify when usecase returns valid date it is stripped`() { + val date = Date(1635176812345) + val useCase: FirstAppOpenUseCase = mockk { + every { + getFirstOpenDate() + } returns date + } + val provider = FirstOpenHourProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(1635174000000) + verifyAll { + useCase.getFirstOpenDate() + } + } + + @Test + fun `verify when usecase returns null result is null`() { + val useCase: FirstAppOpenUseCase = mockk { + every { + getFirstOpenDate() + } returns null + } + val provider = FirstOpenHourProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(null) + verifyAll { + useCase.getFirstOpenDate() + } + } + + @Test + fun `verify when usecase returns invalid date result is null`() { + val date = Date(0) + val useCase: FirstAppOpenUseCase = mockk { + every { + getFirstOpenDate() + } returns date + } + val provider = FirstOpenHourProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(null) + verifyAll { + useCase.getFirstOpenDate() + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/FirstOpenTimeProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/FirstOpenTimeProviderTest.kt new file mode 100644 index 0000000..097c0e6 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/FirstOpenTimeProviderTest.kt @@ -0,0 +1,52 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.usecase.FirstAppOpenUseCase +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test +import java.util.Date + +/** + * Test for [FirstOpenTimeProvider] + */ +class FirstOpenTimeProviderTest { + + @Test + fun `verify provide when firstAppOpenUseCase returns date should return date string`() { + val time = 222L + val firstOpenDate: Date = mockk { + every { + getTime() + } returns time + } + val useCase: FirstAppOpenUseCase = mockk { + every { + getFirstOpenDate() + } returns firstOpenDate + } + val provider = FirstOpenTimeProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(time) + verifyAll { + useCase.getFirstOpenDate() + firstOpenDate.time + } + } + + @Test + fun `verify provide when firstAppOpenUseCase null should return null`() { + val useCase: FirstAppOpenUseCase = mockk { + every { + getFirstOpenDate() + } returns null + } + val provider = FirstOpenTimeProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(null) + verifyAll { + useCase.getFirstOpenDate() + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/HardwareNameProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/HardwareNameProviderTest.kt new file mode 100644 index 0000000..ec5c2ca --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/HardwareNameProviderTest.kt @@ -0,0 +1,30 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.build.BuildConfigPropertiesProvider +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [HardwareNameProvider] + */ +class HardwareNameProviderTest { + + @Test + fun `verify provide`() { + val hardware = "hw" + val propertiesProvider: BuildConfigPropertiesProvider = mockk { + every { + getHardware() + } returns hardware + } + val provider = HardwareNameProvider(propertiesProvider) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(hardware) + verifyAll { + propertiesProvider.getHardware() + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/InstallBeginTimeProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/InstallBeginTimeProviderTest.kt new file mode 100644 index 0000000..e9ba10b --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/InstallBeginTimeProviderTest.kt @@ -0,0 +1,76 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.referrer.AffiseReferrerData +import com.affise.attribution.usecase.RetrieveInstallReferrerUseCase +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [InstallBeginTimeProvider] + */ +class InstallBeginTimeProviderTest { + + @Test + fun `verify when usecase returns valid date`() { + val time = 1635176812345L + + val referrerData: AffiseReferrerData = mockk { + every { + installBeginTimestampSeconds + } returns time + } + val useCase: RetrieveInstallReferrerUseCase = mockk { + every { + getInstallReferrer() + } returns referrerData + } + val provider = InstallBeginTimeProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(time) + verifyAll { + useCase.getInstallReferrer() + referrerData.installBeginTimestampSeconds + } + } + + @Test + fun `verify when usecase returns null result is null`() { + val useCase: RetrieveInstallReferrerUseCase = mockk { + every { + getInstallReferrer() + } returns null + } + val provider = InstallBeginTimeProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(null) + verifyAll { + useCase.getInstallReferrer() + } + } + + @Test + fun `verify when usecase returns invalid object with date result is null`() { + val time = 0L + + val referrerData: AffiseReferrerData = mockk { + every { + installBeginTimestampSeconds + } returns time + } + val useCase: RetrieveInstallReferrerUseCase = mockk { + every { + getInstallReferrer() + } returns referrerData + } + val provider = InstallBeginTimeProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(null) + verifyAll { + useCase.getInstallReferrer() + referrerData.installBeginTimestampSeconds + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/InstallFinishTimeProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/InstallFinishTimeProviderTest.kt new file mode 100644 index 0000000..aca3e4c --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/InstallFinishTimeProviderTest.kt @@ -0,0 +1,64 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.usecase.FirstAppOpenUseCase +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Assert.* + +import org.junit.Test +import java.util.Date + +/** + * Test for [InstallFinishTimeProvider] + */ +class InstallFinishTimeProviderTest { + + @Test + fun `verify when usecase returns valid date`() { + val date = Date(1635176812345) + val useCase: FirstAppOpenUseCase = mockk { + every { + getFirstOpenDate() + } returns date + } + val provider = InstallFinishTimeProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(1635176812345) + verifyAll { + useCase.getFirstOpenDate() + } + } + + @Test + fun `verify when usecase returns null result is null`() { + val useCase: FirstAppOpenUseCase = mockk { + every { + getFirstOpenDate() + } returns null + } + val provider = InstallFinishTimeProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(null) + verifyAll { + useCase.getFirstOpenDate() + } + } + + @Test + fun `verify when usecase returns invalid date result is null`() { + val date = Date(0) + val useCase: FirstAppOpenUseCase = mockk { + every { + getFirstOpenDate() + } returns date + } + val provider = InstallFinishTimeProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(null) + verifyAll { + useCase.getFirstOpenDate() + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/InstallFirstEventProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/InstallFirstEventProviderTest.kt new file mode 100644 index 0000000..220524c --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/InstallFirstEventProviderTest.kt @@ -0,0 +1,47 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.usecase.FirstAppOpenUseCase +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + + +/** + * Test for [InstallFirstEventProvider] + */ +class InstallFirstEventProviderTest { + + @Test + fun `verify when usecase returns valid true`() { + val isFirstOpen = true + val useCase: FirstAppOpenUseCase = mockk { + every { + isFirstOpen() + } returns isFirstOpen + } + val provider = InstallFirstEventProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(isFirstOpen) + verifyAll { + useCase.isFirstOpen() + } + } + + @Test + fun `verify when usecase returns valid false`() { + val isFirstOpen = false + val useCase: FirstAppOpenUseCase = mockk { + every { + isFirstOpen() + } returns isFirstOpen + } + val provider = InstallFirstEventProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(isFirstOpen) + verifyAll { + useCase.isFirstOpen() + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/InstallReferrerProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/InstallReferrerProviderTest.kt new file mode 100644 index 0000000..46391cf --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/InstallReferrerProviderTest.kt @@ -0,0 +1,56 @@ +package com.affise.attribution.parameters + +import android.app.Application +import com.affise.attribution.referrer.AffiseReferrerData +import com.affise.attribution.usecase.RetrieveInstallReferrerUseCase +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [InstallReferrerProvider] + */ +class InstallReferrerProviderTest { + + private val app: Application = mockk() + + @Test + fun `verify when usecase returns null result is null`() { + val useCase: RetrieveInstallReferrerUseCase = mockk { + every { + getInstallReferrer() + } returns null + } + val provider = InstallReferrerProvider(app, useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(null) + verifyAll { + useCase.getInstallReferrer() + } + } + + @Test + fun `verify when usecase returns referrer`() { + val referrer = "organic" + + val referrerData: AffiseReferrerData = mockk { + every { + installReferrer + } returns referrer + } + val useCase: RetrieveInstallReferrerUseCase = mockk { + every { + getInstallReferrer() + } returns referrerData + } + val provider = InstallReferrerProvider(app, useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(referrer) + verifyAll { + useCase.getInstallReferrer() + referrerData.installReferrer + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/InstalledHourProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/InstalledHourProviderTest.kt new file mode 100644 index 0000000..4e1e479 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/InstalledHourProviderTest.kt @@ -0,0 +1,80 @@ +package com.affise.attribution.parameters + +import android.content.Context +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [InstalledHourProvider] + */ +class InstalledHourProviderTest { + @Test + fun `verify provide when first install time returns decimal timestamp is stripped`() { + val time = 1635176812345 + val packageInfo: PackageInfo = PackageInfo().apply { + firstInstallTime = time + } + val packageName = "com.my.app" + val packageManager: PackageManager = mockk { + every { + getPackageInfo(packageName, 0) + } returns packageInfo + } + val context: Context = mockk { + every { + getPackageManager() + } returns packageManager + + every { + getPackageName() + } returns packageName + } + val provider = InstalledHourProvider(context) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(1635174000000) + + verifyAll { + context.packageManager + context.packageName + packageManager.getPackageInfo(packageName, 0) + } + + } + + @Test + fun `verify provide when getPackageInfo returns null`() { + + val packageName = "com.my.app" + val packageManager: PackageManager = mockk { + every { + getPackageInfo(packageName, 0) + } returns null + } + val context: Context = mockk { + every { + getPackageManager() + } returns packageManager + + every { + getPackageName() + } returns packageName + } + val provider = InstalledHourProvider(context) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(null) + + verifyAll { + context.packageManager + context.packageName + packageManager.getPackageInfo(packageName, 0) + } + + } + + +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/InstalledTimeProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/InstalledTimeProviderTest.kt new file mode 100644 index 0000000..095c011 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/InstalledTimeProviderTest.kt @@ -0,0 +1,86 @@ +package com.affise.attribution.parameters + +import android.content.Context +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import com.affise.attribution.logs.LogsManager +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [InstalledTimeProvider] + */ +class InstalledTimeProviderTest { + + @Test + fun `verify provide when first install time returns decimal timestamp is stripped`() { + val time = 1635176812345 + + val logsManager: LogsManager = mockk() + + val packageInfo: PackageInfo = PackageInfo().apply { + firstInstallTime = time + } + val packageName = "com.my.app" + val packageManager: PackageManager = mockk { + every { + getPackageInfo(packageName, 0) + } returns packageInfo + } + val context: Context = mockk { + every { + getPackageManager() + } returns packageManager + + every { + getPackageName() + } returns packageName + } + val provider = InstalledTimeProvider(context, logsManager) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(time) + + verifyAll { + context.packageManager + context.packageName + packageManager.getPackageInfo(packageName, 0) + } + + } + + @Test + fun `verify provide when getPackageInfo returns null`() { + val packageName = "com.my.app" + + val logsManager: LogsManager = mockk() + + val packageManager: PackageManager = mockk { + every { + getPackageInfo(packageName, 0) + } returns null + } + val context: Context = mockk { + every { + getPackageManager() + } returns packageManager + + every { + getPackageName() + } returns packageName + } + val provider = InstalledTimeProvider(context, logsManager) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(null) + + verifyAll { + context.packageManager + context.packageName + packageManager.getPackageInfo(packageName, 0) + } + + } + +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/IsProductionPropertyProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/IsProductionPropertyProviderTest.kt new file mode 100644 index 0000000..9441b86 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/IsProductionPropertyProviderTest.kt @@ -0,0 +1,53 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.init.InitPropertiesStorage +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [IsProductionPropertyProvider] + */ +class IsProductionPropertyProviderTest { + + @Test + fun `verify when isProduction is false`() { + val initProps: InitPropertiesStorage = mockk { + every { + getProperties()?.isProduction + } returns false + } + val provider = IsProductionPropertyProvider(initProps) + val actual = provider.provide() + + Truth.assertThat(actual).isEqualTo(TYPE_SANDBOX) + + verifyAll { + initProps.getProperties()?.isProduction + } + } + + @Test + fun `verify when isProduction is true`() { + val initProps: InitPropertiesStorage = mockk { + every { + getProperties()?.isProduction + } returns true + } + val provider = IsProductionPropertyProvider(initProps) + val actual = provider.provide() + + Truth.assertThat(actual).isEqualTo(TYPE_PRODUCTION) + + verifyAll { + initProps.getProperties()?.isProduction + } + } + + companion object { + const val TYPE_SANDBOX = "Sandbox" + const val TYPE_PRODUCTION = "Production" + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/LanguageProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/LanguageProviderTest.kt new file mode 100644 index 0000000..619d23e --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/LanguageProviderTest.kt @@ -0,0 +1,19 @@ +package com.affise.attribution.parameters + +import com.google.common.truth.Truth +import org.junit.Test +import java.util.Locale + +/** + * Test for [LanguageProvider] + */ +class LanguageProviderTest { + + @Test + fun verify() { + Locale.setDefault(Locale.CHINA) + val provider = LanguageProvider() + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo("zh-CN") + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/LifetimeSessionCountProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/LifetimeSessionCountProviderTest.kt new file mode 100644 index 0000000..33f0813 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/LifetimeSessionCountProviderTest.kt @@ -0,0 +1,32 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.session.SessionManager +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +class LifetimeSessionCountProviderTest { + + @Test + fun provide() { + val lifetimeSessionTime = 10L + + val sessionManager: SessionManager = mockk { + every { + getLifetimeSessionTime() + } returns lifetimeSessionTime + } + + val provider = LifetimeSessionCountProvider(sessionManager) + val result = provider.provide() + + Truth.assertThat(result).isEqualTo(lifetimeSessionTime) + + verifyAll { + sessionManager.getLifetimeSessionTime() + } + + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/MCCProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/MCCProviderTest.kt new file mode 100644 index 0000000..b65a65e --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/MCCProviderTest.kt @@ -0,0 +1,41 @@ +package com.affise.attribution.parameters + +import android.app.Application +import android.content.res.Configuration +import android.content.res.Resources +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [MCCProvider] + */ +class MCCProviderTest { + + @Test + fun `verify provide`() { + val code = 100 + val configuration: Configuration = Configuration().apply { + mcc = code + } + val resources: Resources = mockk { + every { + getConfiguration() + } returns configuration + } + val app: Application = mockk { + every { + getResources() + } returns resources + } + val provider = MCCProvider(app) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(code.toString()) + verifyAll { + app.resources + resources.configuration + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/MNCProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/MNCProviderTest.kt new file mode 100644 index 0000000..3e19e7d --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/MNCProviderTest.kt @@ -0,0 +1,41 @@ +package com.affise.attribution.parameters + +import android.app.Application +import android.content.res.Configuration +import android.content.res.Resources +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [MNCProvider] + */ +class MNCProviderTest { + + @Test + fun `verify provide`() { + val code = 100 + val configuration: Configuration = Configuration().apply { + mnc = code + } + val resources: Resources = mockk { + every { + getConfiguration() + } returns configuration + } + val app: Application = mockk { + every { + getResources() + } returns resources + } + val provider = MNCProvider(app) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(code.toString()) + verifyAll { + app.resources + resources.configuration + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/OSVersionProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/OSVersionProviderTest.kt new file mode 100644 index 0000000..b6dcf35 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/OSVersionProviderTest.kt @@ -0,0 +1,54 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.build.BuildConfigPropertiesProvider +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [OSVersionProvider] + */ +class OSVersionProviderTest { + + @Test + fun `verify provider returns release name from build config`() { + val buildConfig: BuildConfigPropertiesProvider = mockk { + every { + getReleaseName() + } returns "2.2" + } + + val provider = OSVersionProvider(buildConfig) + + val actual = provider.provide() + + Truth.assertThat(actual).isEqualTo("2.2") + + verifyAll { + buildConfig.getReleaseName() + } + + } + + @Test + fun `verify provider returns null if release name from build config is null`() { + val buildConfig: BuildConfigPropertiesProvider = mockk { + every { + getReleaseName() + } returns null + } + + val provider = OSVersionProvider(buildConfig) + + val actual = provider.provide() + + Truth.assertThat(actual).isNull() + + verifyAll { + buildConfig.getReleaseName() + } + + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/OsNameProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/OsNameProviderTest.kt new file mode 100644 index 0000000..22d3f08 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/OsNameProviderTest.kt @@ -0,0 +1,78 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.build.BuildConfigPropertiesProvider +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +/** + * Test for [OsNameProvider] + */ +@RunWith(Parameterized::class) +class OsNameProviderTest( + private val apiLevel: Int, + private val expectedName: String +) { + + @Test + fun `verify code name is generated from api version`() { + val buildConfigPropertiesProvider: BuildConfigPropertiesProvider = mockk { + every { getSDKVersion() } returns apiLevel + } + val provider = OsNameProvider(buildConfigPropertiesProvider) + Truth.assertThat(provider.provide()).isEqualTo(expectedName.ifEmpty { null }) + verifyAll { + buildConfigPropertiesProvider.getSDKVersion() + } + } + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "when api level is {0} codename should be {1}") + fun data(): List> { + return arrayListOf( + createParams(apiLevel = 99, expectedName = ""), + createParams(apiLevel = 31, expectedName = "Android12"), + createParams(apiLevel = 30, expectedName = "Android11"), + createParams(apiLevel = 29, expectedName = "Android10"), + createParams(apiLevel = 28, expectedName = "Pie"), + createParams(apiLevel = 27, expectedName = "Oreo"), + createParams(apiLevel = 26, expectedName = "Oreo"), + createParams(apiLevel = 25, expectedName = "Nougat"), + createParams(apiLevel = 24, expectedName = "Nougat"), + createParams(apiLevel = 23, expectedName = "Marshmallow"), + createParams(apiLevel = 22, expectedName = "Lollipop"), + createParams(apiLevel = 21, expectedName = "Lollipop"), + createParams(apiLevel = 19, expectedName = "KitKat"), + createParams(apiLevel = 18, expectedName = "Jelly Bean"), + createParams(apiLevel = 17, expectedName = "Jelly Bean"), + createParams(apiLevel = 16, expectedName = "Jelly Bean"), + createParams(apiLevel = 15, expectedName = "Ice Cream Sandwich"), + createParams(apiLevel = 14, expectedName = "Ice Cream Sandwich"), + createParams(apiLevel = 13, expectedName = "Honeycomb"), + createParams(apiLevel = 12, expectedName = "Honeycomb"), + createParams(apiLevel = 11, expectedName = "Honeycomb"), + createParams(apiLevel = 10, expectedName = "Gingerbread"), + createParams(apiLevel = 9, expectedName = "Gingerbread"), + createParams(apiLevel = 8, expectedName = "Froyo"), + createParams(apiLevel = 7, expectedName = "Eclair"), + createParams(apiLevel = 6, expectedName = "Eclair"), + createParams(apiLevel = 5, expectedName = "Eclair"), + createParams(apiLevel = 4, expectedName = "Donut"), + createParams(apiLevel = 3, expectedName = "Cupcake"), + createParams(apiLevel = 2, expectedName = "1.1"), + createParams(apiLevel = 1, expectedName = "1.0"), + createParams(apiLevel = 0, expectedName = ""), + ).toList() + } + + private fun createParams( + apiLevel: Int, expectedName: String + ) = arrayOf(apiLevel, expectedName) + } + +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/PushTokenProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/PushTokenProviderTest.kt new file mode 100644 index 0000000..0c189ba --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/PushTokenProviderTest.kt @@ -0,0 +1,56 @@ +package com.affise.attribution.parameters + +import android.content.SharedPreferences +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +class PushTokenProviderTest { + + @Test + fun provideNull() { + val preferences: SharedPreferences = mockk { + every { + getString(KEY_APP_PUSHTOKEN, null) + } returns null + } + + val provider = PushTokenProvider(preferences) + val result = provider.provide() + + Truth.assertThat(result).isNull() + + verifyAll { + preferences.getString(KEY_APP_PUSHTOKEN, null) + } + + } + + @Test + fun provide() { + val pushtoken = "pushtoken" + + val preferences: SharedPreferences = mockk { + every { + getString(KEY_APP_PUSHTOKEN, null) + } returns pushtoken + } + + val provider = PushTokenProvider(preferences) + val result = provider.provide() + + Truth.assertThat(result).isEqualTo(pushtoken) + + verifyAll { + preferences.getString(KEY_APP_PUSHTOKEN, null) + } + + } + + companion object { + private const val KEY_APP_PUSHTOKEN = "com.affise.attribution.init.PUSHTOKEN" + } + +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/RefTokenProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/RefTokenProviderTest.kt new file mode 100644 index 0000000..2544fc4 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/RefTokenProviderTest.kt @@ -0,0 +1,88 @@ +package com.affise.attribution.parameters + +import android.content.SharedPreferences +import com.affise.attribution.utils.generateUUID +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic +import io.mockk.verifyAll +import org.junit.After +import org.junit.Test + +import org.junit.Before +import java.util.UUID + +/** + * Test for [RefTokenProvider] + */ +class RefTokenProviderTest { + + @Before + fun setUp() { + mockkStatic(::generateUUID) + } + + @After + fun tearDown() { + unmockkStatic(::generateUUID) + } + + @Test + fun `verify provide() when shared prefs is empty new UUID should be generated and stored to prefs`() { + val spKey = "com.affise.attribution.parameters.REFTOKEN" + val generatedUUID = "00000000-0000-0000-0000-000000000000" + every { + generateUUID() + } returns UUID(0, 0) + + val editor: SharedPreferences.Editor = mockk { + every { + putString(spKey, generatedUUID) + } returns this + every { + commit() + } returns true + } + val sp: SharedPreferences = mockk { + every { + getString(spKey, null) + } returns null + every { + edit() + } returns editor + } + + val provider = RefTokenProvider(sp) + + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(generatedUUID) + verifyAll { + sp.getString(spKey, null) + sp.edit() + editor.putString(spKey, generatedUUID) + editor.commit() + } + } + + @Test + fun `verify provide() when shared prefs contains UUID return UUID`() { + val spKey = "com.affise.attribution.parameters.REFTOKEN" + val generatedUUID = "00000000-0000-0000-0000-000000000000" + + val sp: SharedPreferences = mockk { + every { + getString(spKey, null) + } returns generatedUUID + } + + val provider = RefTokenProvider(sp) + + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(generatedUUID) + verifyAll { + sp.getString(spKey, null) + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/ReferralTimeProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/ReferralTimeProviderTest.kt new file mode 100644 index 0000000..78aae71 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/ReferralTimeProviderTest.kt @@ -0,0 +1,75 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.referrer.AffiseReferrerData +import com.affise.attribution.usecase.RetrieveInstallReferrerUseCase +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [ReferralTimeProvider] + */ +class ReferralTimeProviderTest { + @Test + fun `verify when usecase returns valid date`() { + val time = 1635176812345L + + val referrerData: AffiseReferrerData = mockk { + every { + installBeginTimestampServerSeconds + } returns time + } + val useCase: RetrieveInstallReferrerUseCase = mockk { + every { + getInstallReferrer() + } returns referrerData + } + val provider = ReferralTimeProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(time) + verifyAll { + useCase.getInstallReferrer() + referrerData.installBeginTimestampServerSeconds + } + } + + @Test + fun `verify when usecase returns null result is null`() { + val useCase: RetrieveInstallReferrerUseCase = mockk { + every { + getInstallReferrer() + } returns null + } + val provider = ReferralTimeProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(null) + verifyAll { + useCase.getInstallReferrer() + } + } + + @Test + fun `verify when usecase returns invalid object with date result is null`() { + val time = 0L + + val referrerData: AffiseReferrerData = mockk { + every { + installBeginTimestampServerSeconds + } returns time + } + val useCase: RetrieveInstallReferrerUseCase = mockk { + every { + getInstallReferrer() + } returns referrerData + } + val provider = ReferralTimeProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(time) + verifyAll { + useCase.getInstallReferrer() + referrerData.installBeginTimestampServerSeconds + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/ReferrerClickTimestampProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/ReferrerClickTimestampProviderTest.kt new file mode 100644 index 0000000..32a873d --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/ReferrerClickTimestampProviderTest.kt @@ -0,0 +1,75 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.referrer.AffiseReferrerData +import com.affise.attribution.usecase.RetrieveInstallReferrerUseCase +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [ReferrerClickTimestampProvider] + */ +class ReferrerClickTimestampProviderTest { + @Test + fun `verify when usecase returns valid date`() { + val time = 1635176812345L + + val referrerData: AffiseReferrerData = mockk { + every { + referrerClickTimestampSeconds + } returns time + } + val useCase: RetrieveInstallReferrerUseCase = mockk { + every { + getInstallReferrer() + } returns referrerData + } + val provider = ReferrerClickTimestampProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(time) + verifyAll { + useCase.getInstallReferrer() + referrerData.referrerClickTimestampSeconds + } + } + + @Test + fun `verify when usecase returns null result is null`() { + val useCase: RetrieveInstallReferrerUseCase = mockk { + every { + getInstallReferrer() + } returns null + } + val provider = ReferrerClickTimestampProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(null) + verifyAll { + useCase.getInstallReferrer() + } + } + + @Test + fun `verify when usecase returns invalid object with date result is null`() { + val time = 0L + + val referrerData: AffiseReferrerData = mockk { + every { + referrerClickTimestampSeconds + } returns time + } + val useCase: RetrieveInstallReferrerUseCase = mockk { + every { + getInstallReferrer() + } returns referrerData + } + val provider = ReferrerClickTimestampProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(null) + verifyAll { + useCase.getInstallReferrer() + referrerData.referrerClickTimestampSeconds + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/ReferrerClickTimestampServerProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/ReferrerClickTimestampServerProviderTest.kt new file mode 100644 index 0000000..54b08de --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/ReferrerClickTimestampServerProviderTest.kt @@ -0,0 +1,75 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.referrer.AffiseReferrerData +import com.affise.attribution.usecase.RetrieveInstallReferrerUseCase +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [ReferrerClickTimestampServerProvider] + */ +class ReferrerClickTimestampServerProviderTest { + @Test + fun `verify when usecase returns valid date`() { + val time = 1635176812345L + + val referrerData: AffiseReferrerData = mockk { + every { + referrerClickTimestampServerSeconds + } returns time + } + val useCase: RetrieveInstallReferrerUseCase = mockk { + every { + getInstallReferrer() + } returns referrerData + } + val provider = ReferrerClickTimestampServerProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(time) + verifyAll { + useCase.getInstallReferrer() + referrerData.referrerClickTimestampServerSeconds + } + } + + @Test + fun `verify when usecase returns null result is null`() { + val useCase: RetrieveInstallReferrerUseCase = mockk { + every { + getInstallReferrer() + } returns null + } + val provider = ReferrerClickTimestampServerProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(null) + verifyAll { + useCase.getInstallReferrer() + } + } + + @Test + fun `verify when usecase returns invalid object with date result is null`() { + val time = 0L + + val referrerData: AffiseReferrerData = mockk { + every { + referrerClickTimestampServerSeconds + } returns time + } + val useCase: RetrieveInstallReferrerUseCase = mockk { + every { + getInstallReferrer() + } returns referrerData + } + val provider = ReferrerClickTimestampServerProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(null) + verifyAll { + useCase.getInstallReferrer() + referrerData.referrerClickTimestampServerSeconds + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/ReferrerGooglePlayInstantProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/ReferrerGooglePlayInstantProviderTest.kt new file mode 100644 index 0000000..0f286d1 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/ReferrerGooglePlayInstantProviderTest.kt @@ -0,0 +1,75 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.referrer.AffiseReferrerData +import com.affise.attribution.usecase.RetrieveInstallReferrerUseCase +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [ReferrerGooglePlayInstantProvider] + */ +class ReferrerGooglePlayInstantProviderTest { + @Test + fun `verify when usecase returns valid date`() { + val googlePlayInstant = true + + val referrerData: AffiseReferrerData = mockk { + every { + googlePlayInstantParam + } returns googlePlayInstant + } + val useCase: RetrieveInstallReferrerUseCase = mockk { + every { + getInstallReferrer() + } returns referrerData + } + val provider = ReferrerGooglePlayInstantProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(googlePlayInstant) + verifyAll { + useCase.getInstallReferrer() + referrerData.googlePlayInstantParam + } + } + + @Test + fun `verify when usecase returns null result is null`() { + val useCase: RetrieveInstallReferrerUseCase = mockk { + every { + getInstallReferrer() + } returns null + } + val provider = ReferrerGooglePlayInstantProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(null) + verifyAll { + useCase.getInstallReferrer() + } + } + + @Test + fun `verify when usecase returns invalid object with date result is null`() { + val googlePlayInstant = false + + val referrerData: AffiseReferrerData = mockk { + every { + googlePlayInstantParam + } returns googlePlayInstant + } + val useCase: RetrieveInstallReferrerUseCase = mockk { + every { + getInstallReferrer() + } returns referrerData + } + val provider = ReferrerGooglePlayInstantProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(googlePlayInstant) + verifyAll { + useCase.getInstallReferrer() + referrerData.googlePlayInstantParam + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/ReferrerInstallVersionProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/ReferrerInstallVersionProviderTest.kt new file mode 100644 index 0000000..b5ee952 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/ReferrerInstallVersionProviderTest.kt @@ -0,0 +1,75 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.referrer.AffiseReferrerData +import com.affise.attribution.usecase.RetrieveInstallReferrerUseCase +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +/** + * Test for [ReferrerInstallVersionProvider] + */ +class ReferrerInstallVersionProviderTest { + @Test + fun `verify when usecase returns valid date`() { + val version = "test_version" + + val referrerData: AffiseReferrerData = mockk { + every { + installVersion + } returns version + } + val useCase: RetrieveInstallReferrerUseCase = mockk { + every { + getInstallReferrer() + } returns referrerData + } + val provider = ReferrerInstallVersionProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(version) + verifyAll { + useCase.getInstallReferrer() + referrerData.installVersion + } + } + + @Test + fun `verify when usecase returns null result is null`() { + val useCase: RetrieveInstallReferrerUseCase = mockk { + every { + getInstallReferrer() + } returns null + } + val provider = ReferrerInstallVersionProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(null) + verifyAll { + useCase.getInstallReferrer() + } + } + + @Test + fun `verify when usecase returns invalid object with date result is null`() { + val version = "" + + val referrerData: AffiseReferrerData = mockk { + every { + installVersion + } returns version + } + val useCase: RetrieveInstallReferrerUseCase = mockk { + every { + getInstallReferrer() + } returns referrerData + } + val provider = ReferrerInstallVersionProvider(useCase) + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo(version) + verifyAll { + useCase.getInstallReferrer() + referrerData.installVersion + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/SessionCountProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/SessionCountProviderTest.kt new file mode 100644 index 0000000..f9a9e78 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/SessionCountProviderTest.kt @@ -0,0 +1,55 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.session.SessionManager +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +class SessionCountProviderTest { + + @Test + fun provideDefault() { + val sessionCount = 0L + val defaultCount = 1 + + val useCase: SessionManager = mockk { + every { + getSessionCount() + } returns sessionCount + } + + val provider = AffiseSessionCountProvider(useCase) + val result = provider.provide() + + Truth.assertThat(result).isEqualTo(defaultCount) + + verifyAll { + useCase.getSessionCount() + } + + } + + @Test + fun provide() { + val sessionCount = 10L + + val useCase: SessionManager = mockk { + every { + getSessionCount() + } returns sessionCount + } + + val provider = AffiseSessionCountProvider(useCase) + val result = provider.provide() + + Truth.assertThat(result).isEqualTo(sessionCount) + + verifyAll { + useCase.getSessionCount() + } + + } + +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/StoreProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/StoreProviderTest.kt new file mode 100644 index 0000000..fc8f0c2 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/StoreProviderTest.kt @@ -0,0 +1,124 @@ +package com.affise.attribution.parameters + +import android.app.Application +import android.content.pm.InstallSourceInfo +import android.content.pm.PackageManager +import com.affise.attribution.logs.LogsManager +import com.affise.attribution.utils.SystemAppChecker +import com.google.common.truth.Truth +import io.mockk.* +import org.junit.Test + +/** + * Test fore [StoreProvider] + */ +@Suppress("DEPRECATION") +class StoreProviderTest { + + private val packageNameTest = "com.my.app" + private val testException = PackageManager.NameNotFoundException() + + private val packageManagerMockk: PackageManager = mockk { + every { getInstallerPackageName(packageNameTest) } throws testException + } + private val appMockk: Application = mockk { + every { packageManager } returns packageManagerMockk + every { packageName } returns packageNameTest + } + private val logsManagerMockk: LogsManager = mockk { + every { addDeviceError(testException) } just Runs + } + private val systemAppCheckerMockk: SystemAppChecker = mockk { + every { getSystemProperty(PREINSTALL_NAME) } returns "" + every { isPreinstallApp() } returns false + } + + private val provider = StoreProvider(appMockk, logsManagerMockk, systemAppCheckerMockk) + + @Test + fun `verify APK`() { + val actual = provider.provide() + + Truth.assertThat(actual).isEqualTo(APK) + + verifyAll { + systemAppCheckerMockk.getSystemProperty(PREINSTALL_NAME) + systemAppCheckerMockk.isPreinstallApp() + appMockk.packageName + appMockk.packageManager + packageManagerMockk.getInstallerPackageName(packageNameTest) + logsManagerMockk.addDeviceError(testException) + } + } + + @Test + fun `verify installer google`() { + val installSourceInfoMockk: InstallSourceInfo = mockk { + every { + initiatingPackageName + } returns packageNameTest + } + + every { + packageManagerMockk.getInstallerPackageName(packageNameTest) + } returns PACKAGE_GOOGLE + + every { + packageManagerMockk.getInstallSourceInfo(PACKAGE_GOOGLE) + } returns installSourceInfoMockk + + val actual = provider.provide() + + Truth.assertThat(actual).isEqualTo(GOOGLE) + + verifyAll { + systemAppCheckerMockk.getSystemProperty(PREINSTALL_NAME) + systemAppCheckerMockk.isPreinstallApp() + appMockk.packageName + appMockk.packageManager + packageManagerMockk.getInstallerPackageName(packageNameTest) + } + + } + + @Test + fun `verify preinstall`() { + every { + systemAppCheckerMockk.isPreinstallApp() + } returns true + + val actual = provider.provide() + + Truth.assertThat(actual).isEqualTo(PREINSTALL) + + verifyAll { + systemAppCheckerMockk.getSystemProperty(PREINSTALL_NAME) + systemAppCheckerMockk.isPreinstallApp() + } + } + + @Test + fun `verify has system property`() { + every { + systemAppCheckerMockk.getSystemProperty(PREINSTALL_NAME) + } returns "value" + + val actual = provider.provide() + + Truth.assertThat(actual).isEqualTo(PREINSTALL) + + verifyAll { + systemAppCheckerMockk.getSystemProperty(PREINSTALL_NAME) + } + } + + companion object { + private const val PREINSTALL_NAME = "affise_part_param_name" + private const val PACKAGE_GOOGLE = "com.android.vending" + + private const val GOOGLE = "GooglePlay" + private const val PREINSTALL = "Preinstall" + private const val APK = "Apk" + } + +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/TimeSessionProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/TimeSessionProviderTest.kt new file mode 100644 index 0000000..78850d1 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/TimeSessionProviderTest.kt @@ -0,0 +1,33 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.session.SessionManager +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Test + +class TimeSessionProviderTest { + + @Test + fun provide() { + val sessionTime = 10L + + val sessionManager: SessionManager = mockk { + every { + getSessionTime() + } returns sessionTime + } + + val provider = TimeSessionProvider(sessionManager) + val result = provider.provide() + + Truth.assertThat(result).isEqualTo(sessionTime) + + verifyAll { + sessionManager.getSessionTime() + } + + } + +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/TimezoneDeviceProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/TimezoneDeviceProviderTest.kt new file mode 100644 index 0000000..474331a --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/TimezoneDeviceProviderTest.kt @@ -0,0 +1,55 @@ +package com.affise.attribution.parameters + +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.verifyAll +import io.mockk.verifySequence +import org.junit.Test +import java.util.Calendar +import java.util.TimeZone + +/** + * Test for [TimezoneDeviceProvider] + */ +class TimezoneDeviceProviderTest { + + @Test + fun provide() { + mockkStatic(Calendar::class) { + val date = 1609448400L + val timeZoneMock: TimeZone = mockk { + every { + getOffset(date) + } returns 19800000 + } + val calendar: Calendar = mockk { + every { + timeZone + } returns timeZoneMock + + every { + timeInMillis + } returns date + } + + every { + Calendar.getInstance() + } returns calendar + + val provider = TimezoneDeviceProvider() + + val actual = provider.provide() + Truth.assertThat(actual).isEqualTo("UTC+0530") + + verifyAll { + Calendar.getInstance() + calendar.timeZone + calendar.timeInMillis + timeZoneMock.getOffset(date) + } + + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/parameters/UuidProviderTest.kt b/attribution/src/test/java/com/affise/attribution/parameters/UuidProviderTest.kt new file mode 100644 index 0000000..583a378 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/parameters/UuidProviderTest.kt @@ -0,0 +1,31 @@ +package com.affise.attribution.parameters + +import com.affise.attribution.utils.generateUUID +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockkStatic +import org.junit.Test +import java.util.* + +/** + * Test for [UuidProvider] + */ +class UuidProviderTest { + + @Test + fun provide() { + mockkStatic(::generateUUID) { + every { + generateUUID() + } returns UUID(0, 0) + + val actualUuid = UuidProvider().provide() + Truth.assertThat(defaultUuidString).isEqualTo(actualUuid) + } + } + + companion object { + const val defaultUuidString = "00000000-0000-0000-0000-000000000000" + } + +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/session/SessionManagerImplTest.kt b/attribution/src/test/java/com/affise/attribution/session/SessionManagerImplTest.kt new file mode 100644 index 0000000..d9d2c6c --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/session/SessionManagerImplTest.kt @@ -0,0 +1,456 @@ +package com.affise.attribution.session + +import android.content.SharedPreferences +import android.os.SystemClock +import com.affise.attribution.internal.StoreInternalEventUseCase +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.verifyAll +import org.junit.Test + +class SessionManagerImplTest { + + private val keyLifetimeSessionCount = "lifetime_session_count" + private val keyAffiseSessionCount = "affise_session_count" + + private val saveLifetimeSessionCount = 10000L + private val saveSessionCount = 10L + + private val timeStart = 0L + private val timeStartSession = 15001L + private val timeNotStartSession = 14999L + private val timeHideSession = 1000L + + private lateinit var listenerTest: (count: Long) -> Unit + + private val provider = object : CurrentActiveActivityCountProvider { + override fun init() { + } + + override fun addActivityCountListener(listener: (count: Long) -> Unit) { + listenerTest = listener + } + + override fun getActivityCount(): Long { + return 0 + } + } + + @Test + fun getSaveSessionTime() { + val provider: CurrentActiveActivityCountProvider = mockk() + val preferences: SharedPreferences = mockk { + every { + getLong(keyLifetimeSessionCount, 0L) + } returns saveLifetimeSessionCount + every { + getLong(keyAffiseSessionCount, 0L) + } returns 0 + } + val internalEventUseCase: StoreInternalEventUseCase = mockk() + val manager = SessionManagerImpl(preferences, provider, internalEventUseCase) + val lifetimeSessionTime = manager.getLifetimeSessionTime() + + Truth.assertThat(lifetimeSessionTime).isEqualTo(saveLifetimeSessionCount) + + verifyAll { + preferences.getLong(keyLifetimeSessionCount, 0L) + preferences.getLong(keyAffiseSessionCount, 0L) + } + } + + /** + * less 15 seconds return only save time + */ + @Test + fun getLifetimeSessionTimeNotStartSession() { + val preferences: SharedPreferences = mockk { + every { + getLong(keyLifetimeSessionCount, 0L) + } returns saveLifetimeSessionCount + every { + getLong(keyAffiseSessionCount, 0L) + } returns 0 + } + val internalEventUseCase: StoreInternalEventUseCase = mockk() + + mockkStatic(SystemClock::class) { + every { + SystemClock.elapsedRealtime() + } returns timeStart + + val manager = SessionManagerImpl(preferences, provider, internalEventUseCase).apply { init() } + + listenerTest(1) + + every { + SystemClock.elapsedRealtime() + } returns timeNotStartSession + + val lifetimeSessionTime = manager.getLifetimeSessionTime() + + Truth.assertThat(lifetimeSessionTime) + .isEqualTo(saveLifetimeSessionCount + timeNotStartSession) + + verifyAll { + preferences.getLong(keyLifetimeSessionCount, 0L) + preferences.getLong(keyAffiseSessionCount, 0L) + } + } + } + + /** + * more 15 seconds return save time and time current session + */ + @Test + fun getStartSessionTime() { + val preferences: SharedPreferences = mockk { + every { + getLong(keyLifetimeSessionCount, 0L) + } returns saveLifetimeSessionCount + every { + getLong(keyAffiseSessionCount, 0L) + } returns 0 + } + val internalEventUseCase: StoreInternalEventUseCase = mockk() + + mockkStatic(SystemClock::class) { + every { + SystemClock.elapsedRealtime() + } returns timeStart + + val manager = SessionManagerImpl(preferences, provider, internalEventUseCase).apply { init() } + + listenerTest(1) + + every { + SystemClock.elapsedRealtime() + } returns timeStartSession + + val lifetimeSessionTime = manager.getLifetimeSessionTime() + + Truth.assertThat(lifetimeSessionTime) + .isEqualTo(saveLifetimeSessionCount + timeStartSession) + + verifyAll { + preferences.getLong(keyLifetimeSessionCount, 0L) + preferences.getLong(keyAffiseSessionCount, 0L) + } + + } + } + + /** + * After hide app only save session + */ + @Test + fun getStartSessionAndHideTime() { + val preferences: SharedPreferences = mockk { + every { + getLong(keyLifetimeSessionCount, 0L) + } returns saveLifetimeSessionCount + + every { + getLong(keyAffiseSessionCount, 0L) + } returns 0 + + every { + edit().putLong(keyLifetimeSessionCount, any()).commit() + } returns true + + every { + edit().putLong(keyAffiseSessionCount, 1).commit() + } returns true + } + val internalEventUseCase: StoreInternalEventUseCase = mockk() + + mockkStatic(SystemClock::class) { + every { + SystemClock.elapsedRealtime() + } returns timeStart + + val manager = SessionManagerImpl(preferences, provider, internalEventUseCase).apply { init() } + + listenerTest(1) + + every { + SystemClock.elapsedRealtime() + } returns timeStartSession + + listenerTest(0) + + every { + SystemClock.elapsedRealtime() + } returns timeHideSession + + val lifetimeSessionTime = manager.getLifetimeSessionTime() + + Truth.assertThat(lifetimeSessionTime) + .isEqualTo(saveLifetimeSessionCount - timeStart + timeStartSession) + //.isEqualTo(saveLifetimeSessionCount) + + verifyAll { + preferences.getLong(keyLifetimeSessionCount, 0L) + preferences.getLong(keyAffiseSessionCount, 0L) + preferences.edit().putLong(keyLifetimeSessionCount, any()).commit() + preferences.edit().putLong(keyAffiseSessionCount, 1).commit() + } + + } + } + + //--SessionTime--- + + @Test + fun getSessionTimeNotOpenActive() { + val preferences: SharedPreferences = mockk { + every { + getLong(keyLifetimeSessionCount, 0L) + } returns saveLifetimeSessionCount + + every { + getLong(keyAffiseSessionCount, 0L) + } returns 0 + } + val internalEventUseCase: StoreInternalEventUseCase = mockk() + + val manager = SessionManagerImpl(preferences, provider, internalEventUseCase).apply { init() } + + val sessionTime = manager.getSessionTime() + + Truth.assertThat(sessionTime).isEqualTo(0L) + } + + @Test + fun getSessionTimeNotStartSession() { + val preferences: SharedPreferences = mockk { + every { + getLong(keyLifetimeSessionCount, 0L) + } returns saveLifetimeSessionCount + + every { + getLong(keyAffiseSessionCount, 0L) + } returns 0 + } + val internalEventUseCase: StoreInternalEventUseCase = mockk() + + mockkStatic(SystemClock::class) { + every { + SystemClock.elapsedRealtime() + } returns timeStart + + val manager = SessionManagerImpl(preferences, provider, internalEventUseCase).apply { init() } + + listenerTest(1) + + every { + SystemClock.elapsedRealtime() + } returns timeNotStartSession + + val sessionTime = manager.getSessionTime() + + Truth.assertThat(sessionTime).isEqualTo(timeNotStartSession) + + verifyAll { + preferences.getLong(keyLifetimeSessionCount, 0L) + preferences.getLong(keyAffiseSessionCount, 0L) + } + } + } + + @Test + fun getSessionTimeActive() { + val preferences: SharedPreferences = mockk { + every { + getLong(keyLifetimeSessionCount, 0L) + } returns saveLifetimeSessionCount + + every { + getLong(keyAffiseSessionCount, 0L) + } returns 0 + } + val internalEventUseCase: StoreInternalEventUseCase = mockk() + + mockkStatic(SystemClock::class) { + every { + SystemClock.elapsedRealtime() + } returns timeStart + + val manager = SessionManagerImpl(preferences, provider, internalEventUseCase).apply { init() } + + listenerTest(1) + + every { + SystemClock.elapsedRealtime() + } returns timeStartSession + + val sessionTime = manager.getSessionTime() + + Truth.assertThat(sessionTime).isEqualTo(timeStartSession) + + verifyAll { + preferences.getLong(keyLifetimeSessionCount, 0L) + preferences.getLong(keyAffiseSessionCount, 0L) + } + } + } + + @Test + fun isSessionActive() { + val preferences: SharedPreferences = mockk { + every { + getLong(keyAffiseSessionCount, 0L) + } returns saveSessionCount + + every { + getLong(keyLifetimeSessionCount, 0L) + } returns 0 + + every { + edit().putLong(keyAffiseSessionCount, any()).commit() + } returns true + } + val internalEventUseCase: StoreInternalEventUseCase = mockk() + + mockkStatic(SystemClock::class) { + every { + SystemClock.elapsedRealtime() + } returns timeStart + + val manager = SessionManagerImpl(preferences, provider, internalEventUseCase).apply { init() } + + listenerTest(1) + + every { + SystemClock.elapsedRealtime() + } returns timeStartSession + + val isSessionActive = manager.isSessionActive() + + Truth.assertThat(isSessionActive).isTrue() + + verifyAll { + preferences.getLong(keyAffiseSessionCount, 0L) + preferences.getLong(keyLifetimeSessionCount, 0L) + preferences.edit().putLong(keyAffiseSessionCount, any()).commit() + } + } + } + + @Test + fun isSessionNotActive() { + val preferences: SharedPreferences = mockk { + every { + getLong(keyLifetimeSessionCount, 0L) + } returns 0 + + every { + getLong(keyAffiseSessionCount, 0L) + } returns 0 + } + val internalEventUseCase: StoreInternalEventUseCase = mockk() + + mockkStatic(SystemClock::class) { + every { + SystemClock.elapsedRealtime() + } returns timeStart + + val manager = SessionManagerImpl(preferences, provider, internalEventUseCase).apply { init() } + + listenerTest(1) + + every { + SystemClock.elapsedRealtime() + } returns timeNotStartSession + + val isSessionActive = manager.isSessionActive() + + Truth.assertThat(isSessionActive).isFalse() + + verifyAll { + preferences.getLong(keyLifetimeSessionCount, 0L) + preferences.getLong(keyAffiseSessionCount, 0L) + } + } + } + + @Test + fun isSessionClose() { + mockkStatic(SystemClock::class) { + every { + SystemClock.elapsedRealtime() + } returns timeStart + + val preferences: SharedPreferences = mockk { + every { + getLong(keyLifetimeSessionCount, 0L) + } returns saveSessionCount + + every { + getLong(keyAffiseSessionCount, 0L) + } returns 0 + + every { + edit().putLong(keyLifetimeSessionCount, any()).commit() + } returns true + + every { + edit().putLong(keyAffiseSessionCount, 1).commit() + } returns true + } + val internalEventUseCase: StoreInternalEventUseCase = mockk() + + val manager = SessionManagerImpl(preferences, provider, internalEventUseCase).apply { init() } + + listenerTest(1) + + every { + SystemClock.elapsedRealtime() + } returns timeStartSession + + listenerTest(0) + + every { + SystemClock.elapsedRealtime() + } returns timeHideSession + + val isSessionActive = manager.isSessionActive() + + Truth.assertThat(isSessionActive).isFalse() + + verifyAll { + preferences.getLong(keyLifetimeSessionCount, 0L) + preferences.getLong(keyAffiseSessionCount, 0L) + preferences.edit().putLong(keyAffiseSessionCount, 1).commit() + preferences.edit().putLong(keyLifetimeSessionCount, any()).commit() + } + } + } + + @Test + fun getSessionCount() { + val preferences: SharedPreferences = mockk { + every { + getLong(keyLifetimeSessionCount, 0L) + } returns 0 + + every { + getLong(keyAffiseSessionCount, 0L) + } returns saveSessionCount + } + val internalEventUseCase: StoreInternalEventUseCase = mockk() + + val manager = SessionManagerImpl(preferences, provider, internalEventUseCase).apply { init() } + + val isSessionActive = manager.getSessionCount() + + Truth.assertThat(isSessionActive).isEqualTo(saveSessionCount) + + verifyAll { + preferences.getLong(keyAffiseSessionCount, 0L) + preferences.getLong(keyLifetimeSessionCount, 0L) + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/storages/EventsStorageImplTest.kt b/attribution/src/test/java/com/affise/attribution/storages/EventsStorageImplTest.kt new file mode 100644 index 0000000..68b8c36 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/storages/EventsStorageImplTest.kt @@ -0,0 +1,303 @@ +package com.affise.attribution.storages + +import android.content.Context +import android.content.SharedPreferences +import com.affise.attribution.events.SerializedEvent +import com.affise.attribution.logs.LogsManager +import com.affise.attribution.utils.timestamp +import com.google.common.truth.Truth +import io.mockk.* +import org.json.JSONObject +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File +import java.io.FileReader +import java.io.FileWriter + +/** + * Test for [EventsStorageImpl] + */ +class EventsStorageImplTest { + + @get:Rule + val rootEventFolderRule = TemporaryFolder() + private val rootEventFolder = "affise-events" + + private val key = "key" + + private val test1EventId = "test1EventId" + private val test2EventId = "test2EventId" + private val testEvent = "{\"key\":\"value\"}" + private val testSerializedEvent = SerializedEvent(test1EventId, JSONObject(testEvent)) + + private val editor: SharedPreferences.Editor = mockk { + every { commit() } returns true + } + + private val context: Context by lazy { + mockk { + every { getDir(rootEventFolder, Context.MODE_PRIVATE) } returns rootEventFolderRule.root + } + } + + private val logsManager: LogsManager = mockk() + + @Test + fun `verify storeEvent`() { + val storage = EventsStorageImpl(context, logsManager) + + storage.saveEvent(key, testSerializedEvent) + + val firstFileContent = rootEventFolderRule + .root + .listFiles { _, name -> + name == key + } + ?.firstOrNull() + ?.listFiles { _, name -> + name == test1EventId + } + ?.firstOrNull() + ?.getContent() + + Truth.assertThat(firstFileContent).isEqualTo(testEvent) + Truth.assertThat(rootEventFolderRule.root.listFiles()?.size).isEqualTo(1) + + verifyAll { + context.getDir(rootEventFolder, Context.MODE_PRIVATE) + editor wasNot Called + logsManager wasNot Called + } + } + + @Test + fun `verify get events`() { + val storage = EventsStorageImpl(context, logsManager) + val events = storage.getEvents(key) + + Truth.assertThat(events.size).isEqualTo(0) + + verifyAll { + context.getDir(rootEventFolder, Context.MODE_PRIVATE) + editor wasNot Called + logsManager wasNot Called + } + } + + @Test + fun `verify get events 1`() { + createTempFile(key, test1EventId, testEvent) + + val storage = EventsStorageImpl(context, logsManager) + val events = storage.getEvents(key) + + Truth.assertThat(events.size).isEqualTo(1) + Truth.assertThat(events.first().data.toString()).isEqualTo(testEvent) + + verifyAll { + context.getDir(rootEventFolder, Context.MODE_PRIVATE) + editor wasNot Called + logsManager wasNot Called + } + } + + @Test + fun `verify get events 1 invalid`() { + val invalidFileName = "invalidFileName" + val invalidFileContent = "{invalid json format}" + + every { logsManager.addSdkError(any()) } just Runs + + createTempFile(key, invalidFileName, invalidFileContent) + + val storage = EventsStorageImpl(context, logsManager) + val events = storage.getEvents(key) + + Truth.assertThat(events.size).isEqualTo(0) + + verifyAll { + context.getDir(rootEventFolder, Context.MODE_PRIVATE) + logsManager.addSdkError(any()) + editor wasNot Called + } + } + + @Test + fun `verify get event else key`() { + createTempFile(key, test1EventId, testEvent) + + val storage = EventsStorageImpl(context, logsManager) + val events = storage.getEvents("key2") + + Truth.assertThat(events.size).isEqualTo(0) + + verifyAll { + context.getDir(rootEventFolder, Context.MODE_PRIVATE) + editor wasNot Called + logsManager wasNot Called + } + } + + @Test + fun `verify get event in group old`() { + createTempFile(key, test1EventId, testEvent) + + mockkStatic(::timestamp) { + every { + timestamp() + } returns System.currentTimeMillis() + EVENTS_STORE_TIME + + val storage = EventsStorageImpl(context, logsManager) + val events = storage.getEvents(key) + + Truth.assertThat(events.size).isEqualTo(0) + + verifyAll { + context.getDir(rootEventFolder, Context.MODE_PRIVATE) + timestamp() + editor wasNot Called + logsManager wasNot Called + } + } + } + + @Test + fun `verify deleteEvent`() { + createTempFile(key, test1EventId, testEvent) + + var keyDirs = rootEventFolderRule.root.listFiles() + + Truth.assertThat(keyDirs?.size).isEqualTo(1) + Truth.assertThat(keyDirs?.first()?.isDirectory).isEqualTo(true) + + var files = keyDirs?.flatMap { it.listFiles()?.toList() ?: emptyList() } + + Truth.assertThat(files?.size).isEqualTo(1) + + val storage = EventsStorageImpl(context, logsManager) + storage.deleteEvent(key, listOf(test1EventId)) + + keyDirs = rootEventFolderRule.root.listFiles() + + Truth.assertThat(keyDirs?.size).isEqualTo(1) + Truth.assertThat(keyDirs?.first()?.isDirectory).isEqualTo(true) + + files = keyDirs?.flatMap { it.listFiles()?.toList() ?: emptyList() } + + Truth.assertThat(files?.size).isEqualTo(0) + + verifyAll { + context.getDir(rootEventFolder, Context.MODE_PRIVATE) + editor wasNot Called + logsManager wasNot Called + } + } + + @Test + fun `verify deleteEvent some`() { + createTempFile(key, test1EventId, testEvent) + createTempFile(key, test2EventId, testEvent) + + var keyDirs = rootEventFolderRule.root.listFiles() + + Truth.assertThat(keyDirs?.size).isEqualTo(1) + Truth.assertThat(keyDirs?.first()?.isDirectory).isEqualTo(true) + + var files = keyDirs?.flatMap { it.listFiles()?.toList() ?: emptyList() } + + Truth.assertThat(files?.size).isEqualTo(2) + + val storage = EventsStorageImpl(context, logsManager) + storage.deleteEvent(key, listOf(test1EventId, test2EventId)) + + keyDirs = rootEventFolderRule.root.listFiles() + + Truth.assertThat(keyDirs?.size).isEqualTo(1) + Truth.assertThat(keyDirs?.first()?.isDirectory).isEqualTo(true) + + files = keyDirs?.flatMap { it.listFiles()?.toList() ?: emptyList() } + + Truth.assertThat(files?.size).isEqualTo(0) + + verifyAll { + context.getDir(rootEventFolder, Context.MODE_PRIVATE) + editor wasNot Called + logsManager wasNot Called + } + } + + @Test + fun `verify deleteEvent 1 in some`() { + createTempFile(key, test1EventId, testEvent) + createTempFile("key2", test2EventId, testEvent) + + var keyDirs = rootEventFolderRule.root.listFiles() + + Truth.assertThat(keyDirs?.size).isEqualTo(2) + Truth.assertThat(keyDirs?.get(0)?.isDirectory).isEqualTo(true) + Truth.assertThat(keyDirs?.get(1)?.isDirectory).isEqualTo(true) + + var files = keyDirs?.flatMap { it.listFiles()?.toList() ?: emptyList() } + + Truth.assertThat(files?.size).isEqualTo(2) + + val storage = EventsStorageImpl(context, logsManager) + storage.deleteEvent(key, listOf(test1EventId, test2EventId)) + + keyDirs = rootEventFolderRule.root.listFiles() + + Truth.assertThat(keyDirs?.size).isEqualTo(2) + Truth.assertThat(keyDirs?.get(0)?.isDirectory).isEqualTo(true) + Truth.assertThat(keyDirs?.get(1)?.isDirectory).isEqualTo(true) + + files = keyDirs?.flatMap { it.listFiles()?.toList() ?: emptyList() } + + Truth.assertThat(files?.size).isEqualTo(1) + + verifyAll { + context.getDir(rootEventFolder, Context.MODE_PRIVATE) + editor wasNot Called + logsManager wasNot Called + } + } + + @Test + fun clear() { + createTempFile(key, test1EventId, testEvent) + createTempFile("key2", test2EventId, testEvent) + + Truth.assertThat(rootEventFolderRule.root.listFiles()).isNotEmpty() + + val storage = EventsStorageImpl(context, logsManager) + + storage.clear() + + Truth.assertThat(rootEventFolderRule.root.listFiles()).isNull() + + verifyAll { + context.getDir(rootEventFolder, Context.MODE_PRIVATE) + } + } + + private fun File.getContent(): String { + return FileReader(this).use { + it.readText() + } + } + + private fun createTempFile(dir: String, name: String, content: String) { + val folder = File(rootEventFolderRule.root, dir) + folder.mkdir() + + val file = File(folder, name) + + FileWriter(file).use { + file.writeText(content) + } + } + + companion object { + private const val EVENTS_STORE_TIME = 7 * 24 * 60 * 60 * 1000 + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/storages/LogsStorageImplTest.kt b/attribution/src/test/java/com/affise/attribution/storages/LogsStorageImplTest.kt new file mode 100644 index 0000000..ca9bb6d --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/storages/LogsStorageImplTest.kt @@ -0,0 +1,273 @@ +package com.affise.attribution.storages + +import android.content.Context +import com.affise.attribution.events.predefined.AffiseLog +import com.affise.attribution.logs.SerializedLog +import com.google.common.truth.Truth +import io.mockk.Called +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.json.JSONObject +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File +import java.io.FileWriter + +/** + * Test for [LogsStorageImpl] + */ +class LogsStorageImplTest { + + @get:Rule + val folderRule = TemporaryFolder() + + private val key1 = "key1" + private val key2 = "key2" + + private val context: Context by lazy { + mockk { + every { getDir(ROOT_DIR, Context.MODE_PRIVATE) } returns folderRule.root + } + } + + private val logType = LOG_NETWORK_DIR + private val serializedLogId = "serializedLogId" + private val serializedLogDataJson = "{\"test_key\":\"test_value\"}" + private val serializedLogData = JSONObject(serializedLogDataJson) + private val log: AffiseLog = mockk() + private val serializedLog: SerializedLog = mockk { + every { id } returns serializedLogId + every { data } returns serializedLogData + every { type } returns logType + } + + private val logNetworkType = "affise_sdklog_network" + private val logDeviceType = "affise_sdklog_ddata" + private val logUserType = "affise_sdklog_udata" + private val logSdkType = "affise_sdklog_main" + + private val allLogTypes = listOf(logNetworkType, logDeviceType, logUserType, logSdkType) + + @Test + fun storeLog() { + for (i in 0..10) { + createTemp(key1, i.toString()) + } + + val storage = LogsStorageImpl(context) + + storage.saveLog(key1, LOG_NETWORK_DIR, serializedLog) + + val files1 = folderRule.root.listFiles() + ?.flatMap { it.listFiles()?.toList() ?: emptyList() } + ?.filter { it.name.contains(LOG_NETWORK_DIR) } + ?.flatMap { it.listFiles()?.toList() ?: emptyList() } + ?.filter { it.isFile } + + Truth.assertThat(files1?.size).isEqualTo(MAX_FILES_SIZE) + + verifyAll { + log wasNot Called + serializedLog.id + serializedLog.data + context.getDir(ROOT_DIR, Context.MODE_PRIVATE) + } + } + + @Test + fun getLogsEmpty() { + val storage = LogsStorageImpl(context) + + val files = storage.getLogs(key1, allLogTypes) + + Truth.assertThat(files).isEmpty() + + verifyAll { + context.getDir(ROOT_DIR, Context.MODE_PRIVATE) + } + } + + @Test + fun `create dirs of key`() { + val storage = LogsStorageImpl(context) + storage.getLogs(key1, allLogTypes) + + val filesSize = folderRule.root.listFiles()?.size + + Truth.assertThat(filesSize).isEqualTo(1) + + verifyAll { + context.getDir(ROOT_DIR, Context.MODE_PRIVATE) + } + } + + @Test + fun `create dirs of types`() { + val storage = LogsStorageImpl(context) + storage.getLogs(key1, allLogTypes) + + val filesSize = folderRule.root.listFiles() + ?.flatMap { it.listFiles()?.toList() ?: emptyList() } + ?.size + + Truth.assertThat(filesSize).isEqualTo(4) + + verifyAll { + context.getDir(ROOT_DIR, Context.MODE_PRIVATE) + } + } + + @Test + fun getLogsNotEmpty() { + val serializedLogDataJson = "{\"test_key\":\"test_value\"}" + createTemp(key1, "fileName", serializedLogDataJson) + + val storage = LogsStorageImpl(context) + val files = storage.getLogs(key1, allLogTypes) + val filesSize = files.size + + Truth.assertThat(filesSize).isEqualTo(1) + + verifyAll { + context.getDir(ROOT_DIR, Context.MODE_PRIVATE) + } + } + + @Test + fun getLogsNotEmptyAndNotValid() { + val serializedLogDataJson = "{\"test_key\":\"test_value\"}" + createTemp(key1, "fileName", serializedLogDataJson) + createTemp(key2, "fileNameSecond", "not valid json") + + val storage = LogsStorageImpl(context) + val files = storage.getLogs(key1, allLogTypes) + val filesSize = files.size + + Truth.assertThat(filesSize).isEqualTo(1) + + verifyAll { + context.getDir(ROOT_DIR, Context.MODE_PRIVATE) + } + } + + @Test + fun deleteLog() { + val serializedLogDataJson = "{\"test_key\":\"test_value\"}" + createTemp(key1, "fileName", serializedLogDataJson) + + val storage = LogsStorageImpl(context) + storage.deleteLogs(key1, allLogTypes, listOf("fileName")) + + val typeDirs = folderRule.root.listFiles() + + val typeDirsNames = typeDirs?.map { it.name } + + Truth.assertThat(typeDirsNames).contains(key1) + + val subTypeDirs = typeDirs + ?.flatMap { it.listFiles()?.toList() ?: emptyList() } + + val subTypeDirsNames = subTypeDirs?.map { it.name } + + Truth.assertThat(subTypeDirs?.size).isEqualTo(4) + + Truth.assertThat(subTypeDirsNames).contains(LOG_NETWORK_DIR) + Truth.assertThat(subTypeDirsNames).contains(LOG_DEVICE_DIR) + Truth.assertThat(subTypeDirsNames).contains(LOG_USER_DIR) + Truth.assertThat(subTypeDirsNames).contains(LOG_SDK_DIR) + + val filesSize = subTypeDirs + ?.flatMap { it.listFiles()?.toList() ?: emptyList() } + + Truth.assertThat(filesSize).isEmpty() + + verifyAll { + context.getDir(ROOT_DIR, Context.MODE_PRIVATE) + } + } + + @Test + fun deleteLogNotEmpty() { + val serializedLogDataJson = "{\"test_key\":\"test_value\"}" + createTemp(key1, "fileName", serializedLogDataJson) + createTemp(key1, "fileNameSecond", serializedLogDataJson) + + val storage = LogsStorageImpl(context) + storage.deleteLogs(key1, allLogTypes, listOf("fileName")) + + val typeDirs = folderRule.root.listFiles() + + val typeDirsNames = typeDirs?.map { it.name } + + Truth.assertThat(typeDirsNames).contains(key1) + + val subTypeDirs = typeDirs + ?.flatMap { it.listFiles()?.toList() ?: emptyList() } + + val subTypeDirsNames = subTypeDirs?.map { it.name } + + Truth.assertThat(subTypeDirs?.size).isEqualTo(4) + + Truth.assertThat(subTypeDirsNames).contains(LOG_NETWORK_DIR) + Truth.assertThat(subTypeDirsNames).contains(LOG_DEVICE_DIR) + Truth.assertThat(subTypeDirsNames).contains(LOG_USER_DIR) + Truth.assertThat(subTypeDirsNames).contains(LOG_SDK_DIR) + + val filesSize = subTypeDirs + ?.flatMap { it.listFiles()?.toList() ?: emptyList() } + + Truth.assertThat(filesSize?.size).isEqualTo(1) + + verifyAll { + context.getDir(ROOT_DIR, Context.MODE_PRIVATE) + } + } + + @Test + fun clear() { + createTemp(key1, "fileName", serializedLogDataJson) + createTemp(key2, "fileNameSecond", serializedLogDataJson) + + Truth.assertThat(folderRule.root.listFiles()).isNotEmpty() + + val storage = LogsStorageImpl(context) + + storage.clear() + + Truth.assertThat(folderRule.root.listFiles()).isNull() + + verifyAll { + context.getDir(ROOT_DIR, Context.MODE_PRIVATE) + } + } + + private fun createTemp( + dir: String, + name: String, + content: String = "", + type: String = LOG_NETWORK_DIR, + ) { + val folderType = File(folderRule.root, dir) + .apply { if (!exists()) mkdir() } + + val folder = File(folderType, type) + .apply { if (!exists()) mkdir() } + + val file = File(folder, name) + + FileWriter(file).use { + file.writeText(content) + } + } + + companion object { + private const val ROOT_DIR = "affise-logs" + private const val LOG_NETWORK_DIR = "affise_sdklog_network" + private const val LOG_DEVICE_DIR = "affise_sdklog_ddata" + private const val LOG_USER_DIR = "affise_sdklog_udata" + private const val LOG_SDK_DIR = "affise_sdklog_main" + private const val MAX_FILES_SIZE = 5 + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/storages/MetricsStorageTest.kt b/attribution/src/test/java/com/affise/attribution/storages/MetricsStorageTest.kt new file mode 100644 index 0000000..23d84e0 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/storages/MetricsStorageTest.kt @@ -0,0 +1,279 @@ +package com.affise.attribution.storages + +import android.content.Context +import com.affise.attribution.converter.JsonObjectToMetricsEventConverter +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verifyAll +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File +import java.io.FileWriter + +/** + * Test for [MetricsStorageImpl] + */ +class MetricsStorageTest { + + @get:Rule + val folderRule = TemporaryFolder() + + private val key1 = "key" + private val key2 = "key2" + private val ignoreSubKey = "ignoreSubKey" + private val subKey1 = "subKey" + private val subKey2 = "subKey2" + private val eventJsonText = "{event:event}" + + private val context: Context by lazy { + mockk { + every { getDir(ROOT_DIR, Context.MODE_PRIVATE) } returns folderRule.root + } + } + + private val converter = JsonObjectToMetricsEventConverter() + + @Test + fun `get old events is empty`() { + val storage = MetricsStorageImpl(context, converter) + + val events = storage.getMetricsEvents(key1, ignoreSubKey) + + Truth.assertThat(events).isEmpty() + + verifyAll { + context.getDir(ROOT_DIR, Context.MODE_PRIVATE) + } + } + + @Test + fun `get old events is not empty`() { + createTemp(key1, subKey1, "fileName", eventJsonText) + + val storage = MetricsStorageImpl(context, converter) + + val events = storage.getMetricsEvents(key1, ignoreSubKey) + + Truth.assertThat(events.size).isEqualTo(1) + + verifyAll { + context.getDir(ROOT_DIR, Context.MODE_PRIVATE) + } + } + + @Test + fun `get old events is not empty with many days`() { + createTemp(key1, subKey1, "fileName", eventJsonText) + createTemp(key1, subKey2, "secondFileName", eventJsonText) + + val storage = MetricsStorageImpl(context, converter) + + val events = storage.getMetricsEvents(key1, ignoreSubKey) + + Truth.assertThat(events.size).isEqualTo(2) + + verifyAll { + context.getDir(ROOT_DIR, Context.MODE_PRIVATE) + } + } + + @Test + fun `get old events is not empty with many events`() { + createTemp(key1, subKey1, "fileName", eventJsonText) + createTemp(key1, subKey1, "secondFileName", eventJsonText) + + val storage = MetricsStorageImpl(context, converter) + + val events = storage.getMetricsEvents(key1, ignoreSubKey) + + Truth.assertThat(events.size).isEqualTo(2) + + verifyAll { + context.getDir(ROOT_DIR, Context.MODE_PRIVATE) + } + } + + @Test + fun `get old events is not empty with event in current day`() { + createTemp(key1, subKey1, "fileName", eventJsonText) + createTemp(key1, ignoreSubKey, "secondFileName", eventJsonText) + + val storage = MetricsStorageImpl(context, converter) + + val events = storage.getMetricsEvents(key1, ignoreSubKey) + + Truth.assertThat(events.size).isEqualTo(1) + + verifyAll { + context.getDir(ROOT_DIR, Context.MODE_PRIVATE) + } + } + + @Test + fun `get old events is empty with event in current day`() { + createTemp(key1, ignoreSubKey, "fileName", eventJsonText) + + val storage = MetricsStorageImpl(context, converter) + + val events = storage.getMetricsEvents(key1, ignoreSubKey) + + Truth.assertThat(events.size).isEqualTo(0) + + verifyAll { + context.getDir(ROOT_DIR, Context.MODE_PRIVATE) + } + } + + @Test + fun `delete metrics`() { + createTemp(key1, subKey1, "fileName", eventJsonText) + createTemp(key1, subKey2, "fileName", eventJsonText) + + val filesBeforeDelete = folderRule.root.listFiles() + ?.flatMap { it.listFiles()?.toList() ?: emptyList() } + ?.flatMap { it.listFiles()?.toList() ?: emptyList() } + + Truth.assertThat(filesBeforeDelete?.size).isEqualTo(2) + + val storage = MetricsStorageImpl(context, converter) + + storage.deleteMetrics(key1, ignoreSubKey) + + val filesAfterDelete = folderRule.root.listFiles() + ?.flatMap { it.listFiles()?.toList() ?: emptyList() } + ?.flatMap { it.listFiles()?.toList() ?: emptyList() } + + Truth.assertThat(filesAfterDelete?.size).isEqualTo(0) + + verifyAll { + context.getDir(ROOT_DIR, Context.MODE_PRIVATE) + } + } + + @Test + fun `not delete metrics in current day`() { + createTemp(key1, ignoreSubKey, "fileName", eventJsonText) + + val filesBeforeDelete = folderRule.root.listFiles() + ?.flatMap { it.listFiles()?.toList() ?: emptyList() } + ?.flatMap { it.listFiles()?.toList() ?: emptyList() } + + Truth.assertThat(filesBeforeDelete?.size).isEqualTo(1) + + val storage = MetricsStorageImpl(context, converter) + + storage.deleteMetrics(key1, ignoreSubKey) + + val filesAfterDelete = folderRule.root.listFiles() + ?.flatMap { it.listFiles()?.toList() ?: emptyList() } + ?.flatMap { it.listFiles()?.toList() ?: emptyList() } + + Truth.assertThat(filesAfterDelete?.size).isEqualTo(1) + + verifyAll { + context.getDir(ROOT_DIR, Context.MODE_PRIVATE) + } + } + + @Test + fun `not delete metrics another url`() { + createTemp(key1, subKey1, "fileName", eventJsonText) + + val filesBeforeDelete = folderRule.root.listFiles() + ?.flatMap { it.listFiles()?.toList() ?: emptyList() } + ?.flatMap { it.listFiles()?.toList() ?: emptyList() } + + Truth.assertThat(filesBeforeDelete?.size).isEqualTo(1) + + val storage = MetricsStorageImpl(context, converter) + + storage.deleteMetrics(key2, ignoreSubKey) + + val filesAfterDelete = folderRule.root.listFiles() + ?.flatMap { it.listFiles()?.toList() ?: emptyList() } + ?.flatMap { it.listFiles()?.toList() ?: emptyList() } + + Truth.assertThat(filesAfterDelete?.size).isEqualTo(1) + + verifyAll { + context.getDir(ROOT_DIR, Context.MODE_PRIVATE) + } + } + + @Test + fun `delete only old metrics current url with many url, many days and many events`() { + createTemp(key1, subKey1, "fileName1", eventJsonText) + createTemp(key2, subKey1, "fileName2", eventJsonText) + createTemp(key1, subKey2, "fileName3", eventJsonText) + createTemp(key2, subKey2, "fileName4", eventJsonText) + createTemp(key1, ignoreSubKey, "fileName5", eventJsonText) + createTemp(key2, ignoreSubKey, "fileName6", eventJsonText) + + val filesBeforeDelete = folderRule.root.listFiles() + ?.flatMap { it.listFiles()?.toList() ?: emptyList() } + ?.flatMap { it.listFiles()?.toList() ?: emptyList() } + + Truth.assertThat(filesBeforeDelete?.size).isEqualTo(6) + + val storage = MetricsStorageImpl(context, converter) + + storage.deleteMetrics(key1, ignoreSubKey) + + val filesAfterDelete = folderRule.root.listFiles() + ?.flatMap { it.listFiles()?.toList() ?: emptyList() } + ?.flatMap { it.listFiles()?.toList() ?: emptyList() } + + Truth.assertThat(filesAfterDelete?.size).isEqualTo(4) + + verifyAll { + context.getDir(ROOT_DIR, Context.MODE_PRIVATE) + } + } + + @Test + fun clear() { + createTemp(key1, subKey1, "fileName1", eventJsonText) + createTemp(key2, subKey1, "fileName2", eventJsonText) + createTemp(key1, subKey2, "fileName3", eventJsonText) + createTemp(key2, subKey2, "fileName4", eventJsonText) + createTemp(key1, ignoreSubKey, "fileName5", eventJsonText) + createTemp(key2, ignoreSubKey, "fileName6", eventJsonText) + + Truth.assertThat(folderRule.root.listFiles()).isNotEmpty() + + val storage = MetricsStorageImpl(context, converter) + + storage.clear() + + Truth.assertThat(folderRule.root.listFiles()).isNull() + + verifyAll { + context.getDir(ROOT_DIR, Context.MODE_PRIVATE) + } + } + + private fun createTemp( + urlDirName: String, + dateDirName: String, + fileName: String, + content: String + ) { + val urlDir = File(folderRule.root, urlDirName) + .apply { if (!exists()) mkdir() } + + val dateDir = File(urlDir, dateDirName) + .apply { if (!exists()) mkdir() } + + val file = File(dateDir, fileName) + + FileWriter(file).use { + file.writeText(content) + } + } + + companion object { + private const val ROOT_DIR = "affise-metrics" + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/usecase/EraseUserDataUseCase.kt b/attribution/src/test/java/com/affise/attribution/usecase/EraseUserDataUseCase.kt new file mode 100644 index 0000000..9444623 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/usecase/EraseUserDataUseCase.kt @@ -0,0 +1,5 @@ +package com.affise.attribution.usecase + +interface EraseUserDataUseCase { + +} diff --git a/attribution/src/test/java/com/affise/attribution/usecase/SendDataToServerUseCaseTest.kt b/attribution/src/test/java/com/affise/attribution/usecase/SendDataToServerUseCaseTest.kt new file mode 100644 index 0000000..024bb38 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/usecase/SendDataToServerUseCaseTest.kt @@ -0,0 +1,203 @@ +package com.affise.attribution.usecase + +import com.affise.attribution.events.EventsRepository +import com.affise.attribution.events.SerializedEvent +import com.affise.attribution.executors.ExecutorServiceProvider +import com.affise.attribution.internal.InternalEventsRepository +import com.affise.attribution.logs.LogsManager +import com.affise.attribution.logs.LogsRepository +import com.affise.attribution.logs.SerializedLog +import com.affise.attribution.metrics.MetricsRepository +import com.affise.attribution.network.CloudConfig +import com.affise.attribution.network.CloudRepository +import com.affise.attribution.network.entity.PostBackModel +import com.affise.attribution.parameters.factory.PostBackModelFactory +import com.google.common.truth.Truth +import com.google.common.util.concurrent.MoreExecutors +import io.mockk.Called +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkObject +import io.mockk.verifyAll +import org.junit.After +import org.junit.Before +import org.junit.Ignore +import org.junit.Test + +@Ignore("TODO: fix in https://affise.atlassian.net/browse/AA-154 ") +class SendDataToServerUseCaseTest { + + private val url1 = "https://url1" + private val url2 = "https://url2" + private val urls: List = listOf(url1, url2) + + private val slot = mutableListOf>() + + private val test1EventId = "idEvent1" + private val test1SerializedEvent = SerializedEvent(test1EventId, mockk()) + + private val test2EventId = "idEvent2" + private val test2SerializedEvent = SerializedEvent(test2EventId, mockk()) + + private val test1LogId = "idLog1" + private val test1SerializedLog = SerializedLog(test1LogId, "type1", mockk()) + + private val propertiesProvider: PostBackModelFactory = mockk { + every { create(any(), any()) } returns mockk() + } + + private val cloudRepository: CloudRepository = mockk() + + private val eventsRepository: EventsRepository = mockk() + + private val internalEventsRepository: InternalEventsRepository = mockk() + + private val metricsRepository: MetricsRepository = mockk() + + private val logRepository: LogsRepository = mockk { + every { getLogs("") } returns emptyList() + } + + private val logsManager: LogsManager = mockk() + + private val sendServiceProvider: ExecutorServiceProvider = mockk { + every { + provideExecutorService() + } returns MoreExecutors.newDirectExecutorService() + } + + private val preferencesUseCase: PreferencesUseCase = mockk { + every { isOfflineModeEnabled() } returns false + } + + private val useCase: SendDataToServerUseCaseImpl by lazy { + SendDataToServerUseCaseImpl( + propertiesProvider, + cloudRepository, + eventsRepository, + internalEventsRepository, + sendServiceProvider, + logRepository, + metricsRepository, + logsManager, + preferencesUseCase + ) + } + + @Before + fun setup() { + mockkObject(CloudConfig) + every { CloudConfig.getUrls() } returns urls + } + + @After + fun tearDown() { + unmockkObject(CloudConfig) + } + + @Test + fun `send technical log with empty events and logs`() { + justRun { cloudRepository.send(capture(slot), url1) } + justRun { cloudRepository.send(capture(slot), url2) } + + every { eventsRepository.getEvents(url1) } returns emptyList() + every { eventsRepository.getEvents(url2) } returns emptyList() + justRun { eventsRepository.deleteEvent(emptyList(), url1) } + justRun { eventsRepository.deleteEvent(emptyList(), url2) } + + every { logRepository.getLogs(url1) } returns emptyList() + every { logRepository.getLogs(url2) } returns emptyList() + justRun { logRepository.deleteLogs(emptyList(), url1) } + justRun { logRepository.deleteLogs(emptyList(), url2) } + every { preferencesUseCase.isOfflineModeEnabled() } returns false + + every { metricsRepository.getMetrics(url1) } returns emptyList() + every { metricsRepository.getMetrics(url2) } returns emptyList() + justRun { metricsRepository.deleteMetrics(url1) } + justRun { metricsRepository.deleteMetrics(url2) } + + useCase.send(false) + + Truth.assertThat(slot.size).isEqualTo(2) + + verifyAll { + cloudRepository.send(capture(slot), url1) + cloudRepository.send(capture(slot), url2) + + eventsRepository.getEvents(url1) + eventsRepository.getEvents(url2) + eventsRepository.deleteEvent(emptyList(), url1) + eventsRepository.deleteEvent(emptyList(), url2) + + logRepository.getLogs(url1) + logRepository.getLogs(url2) + logRepository.deleteLogs(emptyList(), url1) + logRepository.deleteLogs(emptyList(), url2) + + metricsRepository.getMetrics(url1) + metricsRepository.getMetrics(url2) + metricsRepository.deleteMetrics(url1) + metricsRepository.deleteMetrics(url2) + + propertiesProvider.create(any(), any()) + logsManager wasNot Called + } + } + + @Test + fun `send events and logs`() { + justRun { cloudRepository.send(capture(slot), url1) } + justRun { cloudRepository.send(capture(slot), url2) } + + every { + eventsRepository.getEvents(url1) + } returns + listOf(test1SerializedEvent) andThen + listOf(test2SerializedEvent) andThen + emptyList() + + every { eventsRepository.getEvents(url2) } returns emptyList() + justRun { eventsRepository.deleteEvent(any(), any()) } + + every { + logRepository.getLogs(url1) + } returns + listOf(test1SerializedLog) andThen + emptyList() + + every { logRepository.getLogs(url2) } returns emptyList() + justRun { logRepository.deleteLogs(any(), any()) } + + every { metricsRepository.getMetrics(url1) } returns emptyList() + every { metricsRepository.getMetrics(url2) } returns emptyList() + justRun { metricsRepository.deleteMetrics(url1) } + justRun { metricsRepository.deleteMetrics(url2) } + + useCase.send(false) + + Truth.assertThat(slot.size).isEqualTo(3) + + verifyAll { + cloudRepository.send(capture(slot), url1) + cloudRepository.send(capture(slot), url2) + + eventsRepository.getEvents(url1) + eventsRepository.getEvents(url2) + eventsRepository.deleteEvent(any(), any()) + + logRepository.getLogs(url1) + logRepository.getLogs(url2) + logRepository.deleteLogs(any(), any()) + + metricsRepository.getMetrics(url1) + metricsRepository.getMetrics(url2) + metricsRepository.deleteMetrics(url1) + metricsRepository.deleteMetrics(url2) + + propertiesProvider.create(any(), any()) + logsManager wasNot Called + } + } +} \ No newline at end of file diff --git a/attribution/src/test/java/com/affise/attribution/webBridge/WebBridgeManagerTest.kt b/attribution/src/test/java/com/affise/attribution/webBridge/WebBridgeManagerTest.kt new file mode 100644 index 0000000..7596198 --- /dev/null +++ b/attribution/src/test/java/com/affise/attribution/webBridge/WebBridgeManagerTest.kt @@ -0,0 +1,80 @@ +package com.affise.attribution.webBridge + +import android.webkit.WebView +import com.affise.attribution.events.StoreEventUseCase +import io.mockk.* +import org.junit.Test + +class WebBridgeManagerTest { + + private val testEvent = "testEvent" + + private val webView = mockk() + + private val storeEventUseCase = mockk() + + private val webBridgeManager = WebBridgeManager(storeEventUseCase) + + @Test + fun registerWebView() { + every { + webView.addJavascriptInterface(webBridgeManager, WEB_BRIDGE_INTERFACE) + } just Runs + + webBridgeManager.registerWebView(webView) + + verifyAll { + webView.addJavascriptInterface(webBridgeManager, WEB_BRIDGE_INTERFACE) + storeEventUseCase wasNot Called + } + } + + @Test + fun unregisterWithWebView() { + every { + webView.addJavascriptInterface(webBridgeManager, WEB_BRIDGE_INTERFACE) + } just Runs + + every { + webView.removeJavascriptInterface(WEB_BRIDGE_INTERFACE) + } just Runs + + webBridgeManager.registerWebView(webView) + webBridgeManager.unregisterWebView() + + verifyAll { + webView.addJavascriptInterface(webBridgeManager, WEB_BRIDGE_INTERFACE) + webView.removeJavascriptInterface(WEB_BRIDGE_INTERFACE) + storeEventUseCase wasNot Called + } + } + + @Test + fun unregisterWithOutWebView() { + webBridgeManager.unregisterWebView() + + verifyAll { + webView wasNot Called + storeEventUseCase wasNot Called + } + } + + @Test + fun sendEvent() { + every { + storeEventUseCase.storeWebEvent(testEvent) + } just Runs + + webBridgeManager.sendEvent(testEvent) + + verifyAll { + storeEventUseCase.storeWebEvent(testEvent) + webView wasNot Called + } + } + + companion object { + const val WEB_BRIDGE_INTERFACE = "AffiseBridge" + } + +} \ No newline at end of file diff --git a/attribution/src/test/resources/serialized_event.json b/attribution/src/test/resources/serialized_event.json new file mode 100644 index 0000000..b5056ba --- /dev/null +++ b/attribution/src/test/resources/serialized_event.json @@ -0,0 +1 @@ +{"affise_event_data":{},"affise_event_id":"be07d122-3f3c-11ec-9bbc-0242ac130002","affise_event_user_data":"user-data","affise_event_category":"category","affise_parameters":{},"affise_event_timestamp":1636229513985,"affise_event_name":"name","affise_event_first_for_user":false} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..565dd16 --- /dev/null +++ b/build.gradle @@ -0,0 +1,39 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + ext.kotlin_version = "1.6.0" + repositories { + google() + mavenCentral() + } + dependencies { + classpath "com.android.tools.build:gradle:7.0.4" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath 'com.google.gms:google-services:4.3.15' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + google() + mavenCentral() + maven { url 'https://jitpack.io' } + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} + +ext { + affiseVersion = "1.5.4" + + dokkaVersion = "1.7.20" + testJunit = "4.13.2" + testAndroidxJunit = "1.1.5" + testEspressoCore = "3.5.1" + testMockk = "1.12.0" + testTruth = "1.1.3" +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..98bed16 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f6b961f Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..cda73d1 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +#Thu Jan 27 12:52:15 MSK 2022 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..48b4912 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,3 @@ +include ':attribution' +include ':app' +rootProject.name = "AffiseAttributionLib"