diff --git a/CHANGELOG.md b/CHANGELOG.md index 479d3e889..da7ef1590 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ ## CHANGE LOG +### April 13, 2021 + +* [CleverTap Android SDK v4.1.0](https://github.com/CleverTap/clevertap-android-sdk/blob/master/docs/CTCORECHANGELOG.md) +* [CleverTap Geofence SDK v1.0.2](https://github.com/CleverTap/clevertap-android-sdk/blob/master/docs/CTGEOFENCECHANGELOG.md) +* [CleverTap Xiaomi Push SDK v1.0.1](https://github.com/CleverTap/clevertap-android-sdk/blob/master/docs/CTXIAOMIPUSHCHANGELOG.md) +* [CleverTap Huawei Push SDK v1.0.1](https://github.com/CleverTap/clevertap-android-sdk/blob/master/docs/CTHUAWEIPUSHCHANGELOG.md) + ### February 21, 2021 * [CleverTap Android SDK v4.0.3](https://github.com/CleverTap/clevertap-android-sdk/blob/master/docs/CTCORECHANGELOG.md) diff --git a/README.md b/README.md index fc46e10c0..b276a3452 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,10 @@

- +

# CleverTap Android SDKs [![Build Status](https://app.bitrise.io/app/09efc6b9404a6341/status.svg?token=TejL3E1NHyTiR5ajHKGJ6Q)](https://app.bitrise.io/app/09efc6b9404a6341) -[![codebeat badge](https://codebeat.co/badges/49b05fa0-4228-443c-9f1c-d11efa6d2ef8)](https://codebeat.co/projects/github-com-clevertap-clevertap-android-sdk-master) -[ ![Download](https://api.bintray.com/packages/clevertap/Maven/CleverTapAndroidSDK/images/download.svg) ](https://bintray.com/clevertap/Maven/CleverTapAndroidSDK/_latestVersion) - -## ⍗ Table of contents -* [Introduction](#-introduction) -* [Installation](#-installation) - * [Dependencies](#-dependencies) -* [Integration](#-integration) -* [Initialization](#-initialization) -* [Example Usage](#-example-usage) -* [CleverTap Geofence SDK](#-clevertap-geofence-sdk) -* [CleverTap Xiaomi Push SDK](#-clevertap-xiaomi-push-sdk) -* [CleverTap Huawei Push SDK](#-clevertap-huawei-push-sdk) -* [License](#-license) +[![Download](https://api.bintray.com/packages/clevertap/Maven/CleverTapAndroidSDK/images/download.svg) ](https://bintray.com/clevertap/Maven/CleverTapAndroidSDK/_latestVersion) ## 👋 Introduction [(Back to top)](#-table-of-contents) @@ -33,11 +20,11 @@ To get started, sign up [here](https://clevertap.com/live-product-demo/) ## 🎉 Installation [(Back to top)](#-table-of-contents) -We publish the SDK to `jcenter` and `mavenCentral` as an `AAR` file. Just declare it as dependency in your `build.gradle` file. +We publish the SDK to `mavenCentral` as an `AAR` file. Just declare it as dependency in your `build.gradle` file. ```groovy dependencies { - implementation "com.clevertap.android:clevertap-android-sdk:4.0.4" + implementation "com.clevertap.android:clevertap-android-sdk:4.1.0" } ``` @@ -45,7 +32,7 @@ Alternatively, you can download and add the AAR file included in this repo in yo ```groovy dependencies { - implementation (name: "clevertap-android-sdk-4.0.4", ext: 'aar') + implementation (name: "clevertap-android-sdk-4.1.0", ext: 'aar') } ``` @@ -57,7 +44,7 @@ Add the Firebase Messaging library and Android Support Library v4 as dependencie ```groovy dependencies { - implementation "com.clevertap.android:clevertap-android-sdk:4.0.4" + implementation "com.clevertap.android:clevertap-android-sdk:4.1.0" implementation "androidx.core:core:1.3.0" implementation "com.google.firebase:firebase-messaging:20.2.4" implementation "com.google.android.gms:play-services-ads:19.4.0" // Required only if you enable Google ADID collection in the SDK (turned off by default). @@ -73,7 +60,7 @@ Also be sure to include the `google-services.json` classpath in your Project lev buildscript { repositories { google() - jcenter() + mavenCentral() // if you are including the aar file manually in your Module libs directory add this: flatDir { @@ -104,7 +91,7 @@ Interstitial InApp Notification templates support Audio and Video with the help implementation "com.google.android.exoplayer:exoplayer-ui:2.11.5" ``` -Once you've updated your module `build.gradle` file, make sure you have specified `jcenter()` and `google()` as a repositories in your project `build.gradle` and then sync your project in File -> Sync Project with Gradle Files. +Once you've updated your module `build.gradle` file, make sure you have specified `mavenCentral()` and `google()` as a repositories in your project `build.gradle` and then sync your project in File -> Sync Project with Gradle Files. ## 🎉 Integration [(Back to top)](#-table-of-contents) @@ -195,7 +182,6 @@ CleverTap Xiaomi Push SDK provides an out of the box service to use the Xiaomi P CleverTap Huawei Push SDK provides an out of the box service to use the Huawei Messaging Service. Find the integration steps for the CleverTap Huawei Push SDK [here](https://github.com/CleverTap/clevertap-android-sdk/blob/master/docs/CTHUAWEIPUSH.md) - ## 📄 License [(Back to top)](#-table-of-contents) CleverTap Android SDK is MIT licensed, as found in the [LICENSE](https://github.com/CleverTap/clevertap-android-sdk/blob/master/LICENSE) file. \ No newline at end of file diff --git a/build.gradle b/build.gradle index 45b10e882..708dd8a59 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,5 @@ +apply plugin: 'org.sonarqube' + // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { // do not change order of apply @@ -6,25 +8,27 @@ buildscript { repositories { google()// Google's Maven repository - jcenter() - maven { url 'http://developer.huawei.com/repo/' } mavenCentral() + gradlePluginPortal() + maven { url 'http://developer.huawei.com/repo/' } + } dependencies { classpath "com.android.tools.build:gradle:$gradlePluginVersion" classpath "com.google.gms:google-services:$googleServicesPluginVersion"// Google Services plugin classpath "com.github.dcendents:android-maven-gradle-plugin:$mavenPluginVersion" - classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:$bintrayPluginVersion" classpath "com.huawei.agconnect:agcp:$huaweiPluginVersion"// Huawei Push Plugin classpath "org.jacoco:org.jacoco.core:$jacocoVersion" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.1.1" } } allprojects { repositories { - jcenter() google() + mavenCentral() + gradlePluginPortal() maven { url 'http://developer.huawei.com/repo/' } flatDir { dirs 'libs' @@ -32,6 +36,14 @@ allprojects { } } +sonarqube { + properties { + property "sonar.projectKey", "CleverTap_clevertap-android-sdk" + property "sonar.organization", "clevertap" + property "sonar.host.url", "https://sonarcloud.io" + } +} + task clean(type: Delete) { delete rootProject.buildDir } @@ -51,4 +63,4 @@ task copyTemplates { expand('ext': project.ext.properties) } } -} +} \ No newline at end of file diff --git a/clevertap-core/build.gradle b/clevertap-core/build.gradle index ea815be67..087df5c66 100644 --- a/clevertap-core/build.gradle +++ b/clevertap-core/build.gradle @@ -8,8 +8,6 @@ ext { licenseName = 'The Apache Software License, Version 2.0' licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' allLicenses = ["Apache-2.0"] - - minSdkVersionVal = 16 } apply from: "../gradle-scripts/commons.gradle" @@ -31,7 +29,14 @@ dependencies { // Unit testing dependencies testImplementation project(':test_shared') - testImplementation deps.androidXCoreKTX - testImplementation deps.kotlinStdlib testImplementation deps.firebaseMessaging } + +sonarqube { + properties { + property "sonar.projectKey", "CleverTap_clevertap-android-sdk" + property "sonar.organization", "clevertap" + property "sonar.host.url", "https://sonarcloud.io" + } +} + diff --git a/clevertap-core/src/main/AndroidManifest.xml b/clevertap-core/src/main/AndroidManifest.xml index 7fd5b9814..e32b0c7f8 100644 --- a/clevertap-core/src/main/AndroidManifest.xml +++ b/clevertap-core/src/main/AndroidManifest.xml @@ -10,7 +10,7 @@ android:theme="@android:style/Theme.Translucent.NoTitleBar" /> diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/ActivityLifeCycleManager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/ActivityLifeCycleManager.java new file mode 100644 index 000000000..f637be64b --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/ActivityLifeCycleManager.java @@ -0,0 +1,197 @@ +package com.clevertap.android.sdk; + +import android.app.Activity; +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import android.os.RemoteException; +import com.android.installreferrer.api.InstallReferrerClient; +import com.android.installreferrer.api.InstallReferrerStateListener; +import com.android.installreferrer.api.ReferrerDetails; +import com.clevertap.android.sdk.events.BaseEventQueueManager; +import com.clevertap.android.sdk.inapp.InAppController; +import com.clevertap.android.sdk.pushnotification.PushProviders; +import com.clevertap.android.sdk.task.CTExecutorFactory; +import com.clevertap.android.sdk.task.Task; +import java.util.concurrent.Callable; + +class ActivityLifeCycleManager { + + private final AnalyticsManager analyticsManager; + + private final BaseEventQueueManager baseEventQueueManager; + + private final BaseCallbackManager callbackManager; + + private final CleverTapInstanceConfig config; + + private final Context context; + + private final CoreMetaData coreMetaData; + + private final InAppController inAppController; + + private final PushProviders pushProviders; + + private final SessionManager sessionManager; + + ActivityLifeCycleManager(Context context, + CleverTapInstanceConfig config, + AnalyticsManager analyticsManager, + CoreMetaData coreMetaData, + SessionManager sessionManager, + PushProviders pushProviders, + BaseCallbackManager callbackManager, + InAppController inAppController, + BaseEventQueueManager baseEventQueueManager) { + this.context = context; + this.config = config; + this.analyticsManager = analyticsManager; + this.coreMetaData = coreMetaData; + this.sessionManager = sessionManager; + this.pushProviders = pushProviders; + this.callbackManager = callbackManager; + this.inAppController = inAppController; + this.baseEventQueueManager = baseEventQueueManager; + } + + //Lifecycle + public void activityPaused() { + CoreMetaData.setAppForeground(false); + sessionManager.setAppLastSeen(System.currentTimeMillis()); + config.getLogger().verbose(config.getAccountId(), "App in background"); + final int now = (int) (System.currentTimeMillis() / 1000); + if (coreMetaData.inCurrentSession()) { + try { + StorageHelper + .putInt(context, + StorageHelper.storageKeyWithSuffix(config, Constants.LAST_SESSION_EPOCH), + now); + config.getLogger().verbose(config.getAccountId(), "Updated session time: " + now); + } catch (Throwable t) { + config.getLogger() + .verbose(config.getAccountId(), "Failed to update session time time: " + t.getMessage()); + } + } + } + + //Lifecycle + public void activityResumed(Activity activity) { + config.getLogger().verbose(config.getAccountId(), "App in foreground"); + sessionManager.checkTimeoutSession(); + //Anything in this If block will run once per App Launch. + //Will not run for Apps which disable App Launched event + if (!coreMetaData.isAppLaunchPushed()) { + + analyticsManager.pushAppLaunchedEvent(); + analyticsManager.fetchFeatureFlags(); + pushProviders.onTokenRefresh(); + Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask(); + task.execute("HandlingInstallReferrer",new Callable() { + @Override + public Void call() { + if (!coreMetaData.isInstallReferrerDataSent() && coreMetaData + .isFirstSession()) { + handleInstallReferrerOnFirstInstall(); + } + return null; + } + }); + + try { + if (callbackManager.getGeofenceCallback() != null) { + callbackManager.getGeofenceCallback().triggerLocation(); + } + } catch (IllegalStateException e) { + config.getLogger().verbose(config.getAccountId(), e.getLocalizedMessage()); + } catch (Exception e) { + config.getLogger().verbose(config.getAccountId(), "Failed to trigger location"); + } + } + baseEventQueueManager.pushInitialEventsAsync(); + inAppController.checkExistingInAppNotifications(activity); + inAppController.checkPendingInAppNotifications(activity); + } + + public void onActivityCreated(final Bundle notification, final Uri deepLink) { + try { + boolean shouldProcess = config.isDefaultInstance(); + + if (shouldProcess) { + if (notification != null && !notification.isEmpty() && notification + .containsKey(Constants.NOTIFICATION_TAG)) { + analyticsManager.pushNotificationClickedEvent(notification); + } + + if (deepLink != null) { + try { + analyticsManager.pushDeepLink(deepLink, false); + } catch (Throwable t) { + // no-op + } + } + } + } catch (Throwable t) { + Logger.v("Throwable - " + t.getLocalizedMessage()); + } + } + + private void handleInstallReferrerOnFirstInstall() { + config.getLogger().verbose(config.getAccountId(), "Starting to handle install referrer"); + try { + final InstallReferrerClient referrerClient = InstallReferrerClient.newBuilder(context).build(); + referrerClient.startConnection(new InstallReferrerStateListener() { + @Override + public void onInstallReferrerServiceDisconnected() { + if (!coreMetaData.isInstallReferrerDataSent()) { + handleInstallReferrerOnFirstInstall(); + } + } + + @Override + public void onInstallReferrerSetupFinished(int responseCode) { + switch (responseCode) { + case InstallReferrerClient.InstallReferrerResponse.OK: + // Connection established. + ReferrerDetails response; + try { + response = referrerClient.getInstallReferrer(); + String referrerUrl = response.getInstallReferrer(); + coreMetaData + .setReferrerClickTime(response.getReferrerClickTimestampSeconds()); + coreMetaData + .setAppInstallTime(response.getInstallBeginTimestampSeconds()); + analyticsManager.pushInstallReferrer(referrerUrl); + coreMetaData.setInstallReferrerDataSent(true); + config.getLogger().debug(config.getAccountId(), + "Install Referrer data set [Referrer URL-" + referrerUrl + "]"); + } catch (RemoteException e) { + config.getLogger().debug(config.getAccountId(), + "Remote exception caused by Google Play Install Referrer library - " + e + .getMessage()); + referrerClient.endConnection(); + coreMetaData.setInstallReferrerDataSent(false); + } + referrerClient.endConnection(); + break; + case InstallReferrerClient.InstallReferrerResponse.FEATURE_NOT_SUPPORTED: + // API not available on the current Play Store app. + config.getLogger().debug(config.getAccountId(), + "Install Referrer data not set, API not supported by Play Store on device"); + break; + case InstallReferrerClient.InstallReferrerResponse.SERVICE_UNAVAILABLE: + // Connection couldn't be established. + config.getLogger().debug(config.getAccountId(), + "Install Referrer data not set, connection to Play Store unavailable"); + break; + } + } + }); + } catch (Throwable t) { + config.getLogger().verbose(config.getAccountId(), + "Google Play Install Referrer's InstallReferrerClient Class not found - " + t + .getLocalizedMessage() + + " \n Please add implementation 'com.android.installreferrer:installreferrer:2.1' to your build.gradle"); + } + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/ActivityLifecycleCallback.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/ActivityLifecycleCallback.java index 239c1e82b..b477e52c6 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/ActivityLifecycleCallback.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/ActivityLifecycleCallback.java @@ -11,7 +11,7 @@ @SuppressWarnings({"unused", "WeakerAccess"}) public final class ActivityLifecycleCallback { - static boolean registered = false; + public static boolean registered = false; /** * Enables lifecycle callbacks for Android devices diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/AnalyticsManager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/AnalyticsManager.java new file mode 100644 index 000000000..3d37d2c9e --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/AnalyticsManager.java @@ -0,0 +1,1303 @@ +package com.clevertap.android.sdk; + +import static com.clevertap.android.sdk.utils.CTJsonConverter.getErrorObject; +import static com.clevertap.android.sdk.utils.CTJsonConverter.getWzrkFields; + +import android.content.Context; +import android.location.Location; +import android.net.Uri; +import android.os.Bundle; +import com.clevertap.android.sdk.displayunits.model.CleverTapDisplayUnit; +import com.clevertap.android.sdk.events.BaseEventQueueManager; +import com.clevertap.android.sdk.inapp.CTInAppNotification; +import com.clevertap.android.sdk.inbox.CTInboxMessage; +import com.clevertap.android.sdk.response.CleverTapResponse; +import com.clevertap.android.sdk.response.CleverTapResponseHelper; +import com.clevertap.android.sdk.response.DisplayUnitResponse; +import com.clevertap.android.sdk.response.InAppResponse; +import com.clevertap.android.sdk.response.InboxResponse; +import com.clevertap.android.sdk.task.CTExecutorFactory; +import com.clevertap.android.sdk.task.MainLooperHandler; +import com.clevertap.android.sdk.task.Task; +import com.clevertap.android.sdk.utils.CTJsonConverter; +import com.clevertap.android.sdk.utils.UriHelper; +import com.clevertap.android.sdk.validation.ValidationResult; +import com.clevertap.android.sdk.validation.ValidationResultFactory; +import com.clevertap.android.sdk.validation.ValidationResultStack; +import com.clevertap.android.sdk.validation.Validator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +public class AnalyticsManager extends BaseAnalyticsManager { + + private final CTLockManager ctLockManager; + + private final HashMap installReferrerMap = new HashMap<>(8); + + private final BaseEventQueueManager baseEventQueueManager; + + private final BaseCallbackManager callbackManager; + + private final CleverTapInstanceConfig config; + + private final Context context; + + private final ControllerManager controllerManager; + + private final CoreMetaData coreMetaData; + + private final DeviceInfo deviceInfo; + + private final LocalDataStore localDataStore; + + private final MainLooperHandler mainLooperHandler; + + private final ValidationResultStack validationResultStack; + + private final Validator validator; + + private final HashMap notificationIdTagMap = new HashMap<>(); + + private final Object notificationMapLock = new Object(); + + private final HashMap notificationViewedIdTagMap = new HashMap<>(); + + AnalyticsManager(Context context, + CleverTapInstanceConfig config, + BaseEventQueueManager baseEventQueueManager, + Validator validator, + ValidationResultStack validationResultStack, + CoreMetaData coreMetaData, + LocalDataStore localDataStore, + DeviceInfo deviceInfo, + MainLooperHandler mainLooperHandler, + BaseCallbackManager callbackManager, ControllerManager controllerManager, + final CTLockManager ctLockManager) { + this.context = context; + this.config = config; + this.baseEventQueueManager = baseEventQueueManager; + this.validator = validator; + this.validationResultStack = validationResultStack; + this.coreMetaData = coreMetaData; + this.localDataStore = localDataStore; + this.deviceInfo = deviceInfo; + this.mainLooperHandler = mainLooperHandler; + this.callbackManager = callbackManager; + this.ctLockManager = ctLockManager; + this.controllerManager = controllerManager; + } + + @Override + public void addMultiValuesForKey(final String key, final ArrayList values) { + Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask(); + task.execute("addMultiValuesForKey", new Callable() { + @Override + public Void call() { + final String command = (localDataStore.getProfileValueForKey(key) != null) + ? Constants.COMMAND_ADD : Constants.COMMAND_SET; + _handleMultiValues(values, key, command); + return null; + } + }); + } + + /** + * This method is internal to the CleverTap SDK. + * Developers should not use this method manually + */ + @Override + public void fetchFeatureFlags() { + if (config.isAnalyticsOnly()) { + return; + } + JSONObject event = new JSONObject(); + JSONObject notif = new JSONObject(); + try { + notif.put("t", Constants.FETCH_TYPE_FF); + event.put("evtName", Constants.WZRK_FETCH); + event.put("evtData", notif); + } catch (JSONException e) { + e.printStackTrace(); + } + sendFetchEvent(event); + } + + //Event + @Override + public void forcePushAppLaunchedEvent() { + coreMetaData.setAppLaunchPushed(false); + pushAppLaunchedEvent(); + } + + @Override + public void pushAppLaunchedEvent() { + if (config.isDisableAppLaunchedEvent()) { + coreMetaData.setAppLaunchPushed(true); + config.getLogger() + .debug(config.getAccountId(), "App Launched Events disabled in the Android Manifest file"); + return; + } + if (coreMetaData.isAppLaunchPushed()) { + config.getLogger() + .verbose(config.getAccountId(), "App Launched has already been triggered. Will not trigger it "); + return; + } else { + config.getLogger().verbose(config.getAccountId(), "Firing App Launched event"); + } + coreMetaData.setAppLaunchPushed(true); + JSONObject event = new JSONObject(); + try { + event.put("evtName", Constants.APP_LAUNCHED_EVENT); + + event.put("evtData", deviceInfo.getAppLaunchedFields()); + } catch (Throwable t) { + // We won't get here + } + baseEventQueueManager.queueEvent(context, event, Constants.RAISED_EVENT); + } + + @Override + public void pushDisplayUnitClickedEventForID(String unitID) { + JSONObject event = new JSONObject(); + + try { + event.put("evtName", Constants.NOTIFICATION_CLICKED_EVENT_NAME); + + //wzrk fields + if (controllerManager.getCTDisplayUnitController() != null) { + CleverTapDisplayUnit displayUnit = controllerManager.getCTDisplayUnitController() + .getDisplayUnitForID(unitID); + if (displayUnit != null) { + JSONObject eventExtraData = displayUnit.getWZRKFields(); + if (eventExtraData != null) { + event.put("evtData", eventExtraData); + try { + coreMetaData.setWzrkParams(eventExtraData); + } catch (Throwable t) { + // no-op + } + } + } + } + + baseEventQueueManager.queueEvent(context, event, Constants.RAISED_EVENT); + } catch (Throwable t) { + // We won't get here + config.getLogger().verbose(config.getAccountId(), + Constants.FEATURE_DISPLAY_UNIT + "Failed to push Display Unit clicked event" + t); + } + } + + @Override + public void pushDisplayUnitViewedEventForID(String unitID) { + JSONObject event = new JSONObject(); + + try { + event.put("evtName", Constants.NOTIFICATION_VIEWED_EVENT_NAME); + + //wzrk fields + if (controllerManager.getCTDisplayUnitController() != null) { + CleverTapDisplayUnit displayUnit = controllerManager.getCTDisplayUnitController() + .getDisplayUnitForID(unitID); + if (displayUnit != null) { + JSONObject eventExtras = displayUnit.getWZRKFields(); + if (eventExtras != null) { + event.put("evtData", eventExtras); + } + } + } + + baseEventQueueManager.queueEvent(context, event, Constants.RAISED_EVENT); + } catch (Throwable t) { + // We won't get here + config.getLogger().verbose(config.getAccountId(), + Constants.FEATURE_DISPLAY_UNIT + "Failed to push Display Unit viewed event" + t); + } + } + + @Override + @SuppressWarnings({"unused"}) + public void pushError(final String errorMessage, final int errorCode) { + final HashMap props = new HashMap<>(); + props.put("Error Message", errorMessage); + props.put("Error Code", errorCode); + + try { + final String activityName = CoreMetaData.getCurrentActivityName(); + if (activityName != null) { + props.put("Location", activityName); + } else { + props.put("Location", "Unknown"); + } + } catch (Throwable t) { + // Ignore + props.put("Location", "Unknown"); + } + + pushEvent("Error Occurred", props); + } + + @Override + public void pushEvent(String eventName, Map eventActions) { + + if (eventName == null || eventName.equals("")) { + return; + } + + ValidationResult validationResult = validator.isRestrictedEventName(eventName); + // Check for a restricted event name + if (validationResult.getErrorCode() > 0) { + validationResultStack.pushValidationResult(validationResult); + return; + } + + ValidationResult discardedResult = validator.isEventDiscarded(eventName); + // Check for a discarded event name + if (discardedResult.getErrorCode() > 0) { + validationResultStack.pushValidationResult(discardedResult); + return; + } + + if (eventActions == null) { + eventActions = new HashMap<>(); + } + + JSONObject event = new JSONObject(); + try { + // Validate + ValidationResult vr = validator.cleanEventName(eventName); + + // Check for an error + if (vr.getErrorCode() != 0) { + event.put(Constants.ERROR_KEY, getErrorObject(vr)); + } + + eventName = vr.getObject().toString(); + JSONObject actions = new JSONObject(); + for (String key : eventActions.keySet()) { + Object value = eventActions.get(key); + vr = validator.cleanObjectKey(key); + key = vr.getObject().toString(); + // Check for an error + if (vr.getErrorCode() != 0) { + event.put(Constants.ERROR_KEY, getErrorObject(vr)); + } + try { + vr = validator.cleanObjectValue(value, Validator.ValidationContext.Event); + } catch (IllegalArgumentException e) { + // The object was neither a String, Boolean, or any number primitives + ValidationResult error = ValidationResultFactory + .create(512, Constants.PROP_VALUE_NOT_PRIMITIVE, eventName, key, + value != null ? value.toString() : ""); + config.getLogger().debug(config.getAccountId(), error.getErrorDesc()); + validationResultStack.pushValidationResult(error); + // Skip this record + continue; + } + value = vr.getObject(); + // Check for an error + if (vr.getErrorCode() != 0) { + event.put(Constants.ERROR_KEY, getErrorObject(vr)); + } + actions.put(key, value); + } + event.put("evtName", eventName); + event.put("evtData", actions); + baseEventQueueManager.queueEvent(context, event, Constants.RAISED_EVENT); + } catch (Throwable t) { + // We won't get here + } + } + + /** + * Raises the Notification Clicked event, if {@param clicked} is true, + * otherwise the Notification Viewed event, if {@param clicked} is false. + * + * @param clicked Whether or not this notification was clicked + * @param data The data to be attached as the event data + * @param customData Additional data such as form input to to be added to the event data + */ + @Override + @SuppressWarnings({"unused", "WeakerAccess"}) + public void pushInAppNotificationStateEvent(boolean clicked, CTInAppNotification data, Bundle customData) { + JSONObject event = new JSONObject(); + try { + JSONObject notif = getWzrkFields(data); + + if (customData != null) { + for (String x : customData.keySet()) { + + Object value = customData.get(x); + if (value != null) { + notif.put(x, value); + } + } + } + + if (clicked) { + try { + coreMetaData.setWzrkParams(notif); + } catch (Throwable t) { + // no-op + } + event.put("evtName", Constants.NOTIFICATION_CLICKED_EVENT_NAME); + } else { + event.put("evtName", Constants.NOTIFICATION_VIEWED_EVENT_NAME); + } + + event.put("evtData", notif); + baseEventQueueManager.queueEvent(context, event, Constants.RAISED_EVENT); + } catch (Throwable ignored) { + // We won't get here + } + } + + @Override + public void pushInstallReferrer(String url) { + try { + config.getLogger().verbose(config.getAccountId(), "Referrer received: " + url); + + if (url == null) { + return; + } + int now = (int) (System.currentTimeMillis() / 1000); + + //noinspection Constant Conditions + if (installReferrerMap.containsKey(url) && now - installReferrerMap.get(url) < 10) { + config.getLogger() + .verbose(config.getAccountId(), + "Skipping install referrer due to duplicate within 10 seconds"); + return; + } + + installReferrerMap.put(url, now); + + Uri uri = Uri.parse("wzrk://track?install=true&" + url); + + pushDeepLink(uri, true); + } catch (Throwable t) { + // no-op + } + } + + @Override + public synchronized void pushInstallReferrer(String source, String medium, String campaign) { + if (source == null && medium == null && campaign == null) { + return; + } + try { + // If already pushed, don't send it again + int status = StorageHelper.getInt(context, "app_install_status", 0); + if (status != 0) { + Logger.d("Install referrer has already been set. Will not override it"); + return; + } + StorageHelper.putInt(context, "app_install_status", 1); + + if (source != null) { + source = Uri.encode(source); + } + if (medium != null) { + medium = Uri.encode(medium); + } + if (campaign != null) { + campaign = Uri.encode(campaign); + } + + String uriStr = "wzrk://track?install=true"; + if (source != null) { + uriStr += "&utm_source=" + source; + } + if (medium != null) { + uriStr += "&utm_medium=" + medium; + } + if (campaign != null) { + uriStr += "&utm_campaign=" + campaign; + } + + Uri uri = Uri.parse(uriStr); + pushDeepLink(uri, true); + } catch (Throwable t) { + Logger.v("Failed to push install referrer", t); + } + } + + @Override + public void pushNotificationClickedEvent(final Bundle extras) { + + if (config.isAnalyticsOnly()) { + config.getLogger() + .debug(config.getAccountId(), + "is Analytics Only - will not process Notification Clicked event."); + return; + } + + if (extras == null || extras.isEmpty() || extras.get(Constants.NOTIFICATION_TAG) == null) { + config.getLogger().debug(config.getAccountId(), + "Push notification: " + (extras == null ? "NULL" : extras.toString()) + + " not from CleverTap - will not process Notification Clicked event."); + return; + } + + String accountId = null; + try { + accountId = extras.getString(Constants.WZRK_ACCT_ID_KEY); + } catch (Throwable t) { + // no-op + } + + boolean shouldProcess = (accountId == null && config.isDefaultInstance()) + || config.getAccountId() + .equals(accountId); + + if (!shouldProcess) { + config.getLogger().debug(config.getAccountId(), + "Push notification not targeted at this instance, not processing Notification Clicked Event"); + return; + } + + if (extras.containsKey(Constants.INAPP_PREVIEW_PUSH_PAYLOAD_KEY)) { + Runnable pendingInappRunnable = new Runnable() { + @Override + public void run() { + try { + Logger.v("Received in-app via push payload: " + extras + .getString(Constants.INAPP_PREVIEW_PUSH_PAYLOAD_KEY)); + JSONObject r = new JSONObject(); + JSONArray inappNotifs = new JSONArray(); + r.put(Constants.INAPP_JSON_RESPONSE_KEY, inappNotifs); + inappNotifs.put(new JSONObject(extras.getString(Constants.INAPP_PREVIEW_PUSH_PAYLOAD_KEY))); + CleverTapResponse cleverTapResponse = new CleverTapResponseHelper(); + cleverTapResponse = new InAppResponse(cleverTapResponse, config, + controllerManager, true); + cleverTapResponse.processResponse(r, null, context); + } catch (Throwable t) { + Logger.v("Failed to display inapp notification from push notification payload", t); + } + } + }; + mainLooperHandler.setPendingRunnable(pendingInappRunnable); + return; + } + + if (extras.containsKey(Constants.INBOX_PREVIEW_PUSH_PAYLOAD_KEY)) { + Runnable pendingInboxRunnable = new Runnable() { + @Override + public void run() { + try { + Logger.v("Received inbox via push payload: " + extras + .getString(Constants.INBOX_PREVIEW_PUSH_PAYLOAD_KEY)); + JSONObject r = new JSONObject(); + JSONArray inboxNotifs = new JSONArray(); + r.put(Constants.INBOX_JSON_RESPONSE_KEY, inboxNotifs); + JSONObject testPushObject = new JSONObject( + extras.getString(Constants.INBOX_PREVIEW_PUSH_PAYLOAD_KEY)); + testPushObject.put("_id", String.valueOf(System.currentTimeMillis() / 1000)); + inboxNotifs.put(testPushObject); + CleverTapResponse cleverTapResponse = new CleverTapResponseHelper(); + cleverTapResponse = new InboxResponse(cleverTapResponse, config, + ctLockManager, callbackManager, controllerManager); + + cleverTapResponse.processResponse(r, null, context); + } catch (Throwable t) { + Logger.v("Failed to process inbox message from push notification payload", t); + } + } + }; + mainLooperHandler.setPendingRunnable(pendingInboxRunnable); + return; + } + + if (extras.containsKey(Constants.DISPLAY_UNIT_PREVIEW_PUSH_PAYLOAD_KEY)) { + handleSendTestForDisplayUnits(extras); + return; + } + + if (!extras.containsKey(Constants.NOTIFICATION_ID_TAG) || (extras.getString(Constants.NOTIFICATION_ID_TAG) + == null)) { + config.getLogger().debug(config.getAccountId(), + "Push notification ID Tag is null, not processing Notification Clicked event for: " + extras + .toString()); + return; + } + + // Check for dupe notification views; if same notficationdId within specified time interval (5 secs) don't process + boolean isDuplicate = checkDuplicateNotificationIds(extras, notificationIdTagMap, + Constants.NOTIFICATION_ID_TAG_INTERVAL); + if (isDuplicate) { + config.getLogger().debug(config.getAccountId(), + "Already processed Notification Clicked event for " + extras.toString() + + ", dropping duplicate."); + return; + } + + JSONObject event = new JSONObject(); + JSONObject notif = new JSONObject(); + try { + for (String x : extras.keySet()) { + if (!x.startsWith(Constants.WZRK_PREFIX)) { + continue; + } + Object value = extras.get(x); + notif.put(x, value); + } + + event.put("evtName", Constants.NOTIFICATION_CLICKED_EVENT_NAME); + event.put("evtData", notif); + baseEventQueueManager.queueEvent(context, event, Constants.RAISED_EVENT); + + try { + coreMetaData.setWzrkParams(getWzrkFields(extras)); + } catch (Throwable t) { + // no-op + } + } catch (Throwable t) { + // We won't get here + } + if (callbackManager.getPushNotificationListener() != null) { + callbackManager.getPushNotificationListener() + .onNotificationClickedPayloadReceived(Utils.convertBundleObjectToHashMap(extras)); + } else { + Logger.d("CTPushNotificationListener is not set"); + } + } + + /** + * Pushes the Notification Viewed event to CleverTap. + * + * @param extras The {@link Bundle} object that contains the + * notification details + */ + @Override + @SuppressWarnings({"unused", "WeakerAccess"}) + public void pushNotificationViewedEvent(Bundle extras) { + + if (extras == null || extras.isEmpty() || extras.get(Constants.NOTIFICATION_TAG) == null) { + config.getLogger().debug(config.getAccountId(), + "Push notification: " + (extras == null ? "NULL" : extras.toString()) + + " not from CleverTap - will not process Notification Viewed event."); + return; + } + + if (!extras.containsKey(Constants.NOTIFICATION_ID_TAG) || (extras.getString(Constants.NOTIFICATION_ID_TAG) + == null)) { + config.getLogger().debug(config.getAccountId(), + "Push notification ID Tag is null, not processing Notification Viewed event for: " + extras + .toString()); + return; + } + + // Check for dupe notification views; if same notficationdId within specified time interval (2 secs) don't process + boolean isDuplicate = checkDuplicateNotificationIds(extras, notificationViewedIdTagMap, + Constants.NOTIFICATION_VIEWED_ID_TAG_INTERVAL); + if (isDuplicate) { + config.getLogger().debug(config.getAccountId(), + "Already processed Notification Viewed event for " + extras.toString() + ", dropping duplicate."); + return; + } + + config.getLogger().debug("Recording Notification Viewed event for notification: " + extras.toString()); + + JSONObject event = new JSONObject(); + try { + JSONObject notif = getWzrkFields(extras); + event.put("evtName", Constants.NOTIFICATION_VIEWED_EVENT_NAME); + event.put("evtData", notif); + } catch (Throwable ignored) { + //no-op + } + baseEventQueueManager.queueEvent(context, event, Constants.NV_EVENT); + } + + @Override + public void pushProfile(final Map profile) { + if (profile == null || profile.isEmpty()) { + return; + } + Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask(); + task.execute("profilePush",new Callable() { + @Override + public Void call() { + _push(profile); + return null; + } + }); + } + + @Override + public void removeMultiValuesForKey(final String key, final ArrayList values) { + Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask(); + task.execute("removeMultiValuesForKey", new Callable() { + @Override + public Void call() { + _handleMultiValues(values, key, Constants.COMMAND_REMOVE); + return null; + } + }); + } + + @Override + public void removeValueForKey(final String key) { + Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask(); + task.execute("removeValueForKey", new Callable() { + @Override + public Void call() { + _removeValueForKey(key); + return null; + } + }); + } + + @Override + public void sendDataEvent(final JSONObject event) { + baseEventQueueManager.queueEvent(context, event, Constants.DATA_EVENT); + } + + void _generateEmptyMultiValueError(String key) { + ValidationResult error = ValidationResultFactory.create(512, Constants.INVALID_MULTI_VALUE, key); + validationResultStack.pushValidationResult(error); + config.getLogger().debug(config.getAccountId(), error.getErrorDesc()); + } + + void pushChargedEvent(HashMap chargeDetails, + ArrayList> items) { + + if (chargeDetails == null || items == null) { + config.getLogger().debug(config.getAccountId(), "Invalid Charged event: details and or items is null"); + return; + } + + if (items.size() > 50) { + ValidationResult error = ValidationResultFactory.create(522); + config.getLogger().debug(config.getAccountId(), error.getErrorDesc()); + validationResultStack.pushValidationResult(error); + } + + JSONObject evtData = new JSONObject(); + JSONObject chargedEvent = new JSONObject(); + ValidationResult vr; + try { + for (String key : chargeDetails.keySet()) { + Object value = chargeDetails.get(key); + vr = validator.cleanObjectKey(key); + key = vr.getObject().toString(); + // Check for an error + if (vr.getErrorCode() != 0) { + chargedEvent.put(Constants.ERROR_KEY, getErrorObject(vr)); + } + + try { + vr = validator.cleanObjectValue(value, Validator.ValidationContext.Event); + } catch (IllegalArgumentException e) { + // The object was neither a String, Boolean, or any number primitives + ValidationResult error = ValidationResultFactory.create(511, + Constants.PROP_VALUE_NOT_PRIMITIVE, "Charged", key, + value != null ? value.toString() : ""); + validationResultStack.pushValidationResult(error); + config.getLogger().debug(config.getAccountId(), error.getErrorDesc()); + // Skip this property + continue; + } + value = vr.getObject(); + // Check for an error + if (vr.getErrorCode() != 0) { + chargedEvent.put(Constants.ERROR_KEY, getErrorObject(vr)); + } + + evtData.put(key, value); + } + + JSONArray jsonItemsArray = new JSONArray(); + for (HashMap map : items) { + JSONObject itemDetails = new JSONObject(); + for (String key : map.keySet()) { + Object value = map.get(key); + vr = validator.cleanObjectKey(key); + key = vr.getObject().toString(); + // Check for an error + if (vr.getErrorCode() != 0) { + chargedEvent.put(Constants.ERROR_KEY, getErrorObject(vr)); + } + + try { + vr = validator.cleanObjectValue(value, Validator.ValidationContext.Event); + } catch (IllegalArgumentException e) { + // The object was neither a String, Boolean, or any number primitives + ValidationResult error = ValidationResultFactory + .create(511, Constants.OBJECT_VALUE_NOT_PRIMITIVE, key, + value != null ? value.toString() : ""); + config.getLogger().debug(config.getAccountId(), error.getErrorDesc()); + validationResultStack.pushValidationResult(error); + // Skip this property + continue; + } + value = vr.getObject(); + // Check for an error + if (vr.getErrorCode() != 0) { + chargedEvent.put(Constants.ERROR_KEY, getErrorObject(vr)); + } + itemDetails.put(key, value); + } + jsonItemsArray.put(itemDetails); + } + evtData.put("Items", jsonItemsArray); + + chargedEvent.put("evtName", Constants.CHARGED_EVENT); + chargedEvent.put("evtData", evtData); + baseEventQueueManager.queueEvent(context, chargedEvent, Constants.RAISED_EVENT); + } catch (Throwable t) { + // We won't get here + } + } + + synchronized void pushDeepLink(Uri uri, boolean install) { + if (uri == null) { + return; + } + + try { + JSONObject referrer = UriHelper.getUrchinFromUri(uri); + if (referrer.has("us")) { + coreMetaData.setSource(referrer.get("us").toString()); + } + if (referrer.has("um")) { + coreMetaData.setMedium(referrer.get("um").toString()); + } + if (referrer.has("uc")) { + coreMetaData.setCampaign(referrer.get("uc").toString()); + } + + referrer.put("referrer", uri.toString()); + if (install) { + referrer.put("install", true); + } + recordPageEventWithExtras(referrer); + + } catch (Throwable t) { + config.getLogger().verbose(config.getAccountId(), "Failed to push deep link", t); + } + } + + Future raiseEventForGeofences(String eventName, JSONObject geofenceProperties) { + + Future future = null; + + JSONObject event = new JSONObject(); + try { + event.put("evtName", eventName); + event.put("evtData", geofenceProperties); + + Location location = new Location(""); + location.setLatitude(geofenceProperties.getDouble("triggered_lat")); + location.setLongitude(geofenceProperties.getDouble("triggered_lng")); + + geofenceProperties.remove("triggered_lat"); + geofenceProperties.remove("triggered_lng"); + + coreMetaData.setLocationFromUser(location); + + future = baseEventQueueManager.queueEvent(context, event, Constants.RAISED_EVENT); + } catch (JSONException e) { + config.getLogger().debug(config.getAccountId(), Constants.LOG_TAG_GEOFENCES + + "JSON Exception when raising GeoFence event " + + eventName + " - " + e.getLocalizedMessage()); + } + + return future; + } + + void recordPageEventWithExtras(JSONObject extras) { + try { + JSONObject jsonObject = new JSONObject(); + // Add the extras + if (extras != null && extras.length() > 0) { + Iterator keys = extras.keys(); + while (keys.hasNext()) { + try { + String key = (String) keys.next(); + jsonObject.put(key, extras.getString(key)); + } catch (ClassCastException ignore) { + // Really won't get here + } + } + } + baseEventQueueManager.queueEvent(context, jsonObject, Constants.PAGE_EVENT); + } catch (Throwable t) { + // We won't get here + } + } + + void setMultiValuesForKey(final String key, final ArrayList values) { + Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask(); + task.execute("setMultiValuesForKey", new Callable() { + @Override + public Void call() { + _handleMultiValues(values, key, Constants.COMMAND_SET); + return null; + } + }); + } + + private JSONArray _cleanMultiValues(ArrayList values, String key) { + + try { + if (values == null || key == null) { + return null; + } + + JSONArray cleanedValues = new JSONArray(); + ValidationResult vr; + + // loop through and clean the new values + for (String value : values) { + value = (value == null) ? "" : value; // so we will generate a validation error later on + + // validate value + vr = validator.cleanMultiValuePropertyValue(value); + + // Check for an error + if (vr.getErrorCode() != 0) { + validationResultStack.pushValidationResult(vr); + } + + // reset the value + Object _value = vr.getObject(); + value = (_value != null) ? vr.getObject().toString() : null; + + // if value is empty generate an error and return + if (value == null || value.isEmpty()) { + _generateEmptyMultiValueError(key); + // Abort + return null; + } + // add to the newValues to be merged + cleanedValues.put(value); + } + + return cleanedValues; + + } catch (Throwable t) { + config.getLogger().verbose(config.getAccountId(), "Error cleaning multi values for key " + key, t); + _generateEmptyMultiValueError(key); + return null; + } + } + + private JSONArray _constructExistingMultiValue(String key, String command) { + + boolean remove = command.equals(Constants.COMMAND_REMOVE); + boolean add = command.equals(Constants.COMMAND_ADD); + + // only relevant for add's and remove's; a set overrides the existing value, so return a new array + if (!remove && !add) { + return new JSONArray(); + } + + Object existing = _getProfilePropertyIgnorePersonalizationFlag(key); + + // if there is no existing value + if (existing == null) { + // if its a remove then return null to abort operation + // no point in running remove against a nonexistent value + if (remove) { + return null; + } + + // otherwise return an empty array + return new JSONArray(); + } + + // value exists + + // the value should only ever be a JSONArray or scalar (String really) + + // if its already a JSONArray return that + if (existing instanceof JSONArray) { + return (JSONArray) existing; + } + + // handle a scalar value as the existing value + /* + if its an add, our rule is to promote the scalar value to multi value and include the cleaned stringified + scalar value as the first element of the resulting array + + NOTE: the existing scalar value is currently limited to 120 bytes; when adding it to a multi value + it is subject to the current 40 byte limit + + if its a remove, our rule is to delete the key from the local copy + if the cleaned stringified existing value is equal to any of the cleaned values passed to the remove method + + if its an add, return an empty array as the default, + in the event the existing scalar value fails stringifying/cleaning + + returning null will signal that a remove operation should be aborted, + as there is no valid promoted multi value to remove against + */ + + JSONArray _default = (add) ? new JSONArray() : null; + + String stringified = _stringifyAndCleanScalarProfilePropValue(existing); + + return (stringified != null) ? new JSONArray().put(stringified) : _default; + } + + private void _generateInvalidMultiValueKeyError(String key) { + ValidationResult error = ValidationResultFactory.create(523, Constants.INVALID_MULTI_VALUE_KEY, key); + validationResultStack.pushValidationResult(error); + config.getLogger().debug(config.getAccountId(), + "Invalid multi-value property key " + key + " profile multi value operation aborted"); + } + + // use for internal profile getter doesn't do the personalization check + private Object _getProfilePropertyIgnorePersonalizationFlag(String key) { + return localDataStore.getProfileValueForKey(key); + } + + private void _handleMultiValues(ArrayList values, String key, String command) { + if (key == null) { + return; + } + + if (values == null || values.isEmpty()) { + _generateEmptyMultiValueError(key); + return; + } + + ValidationResult vr; + + // validate the key + vr = validator.cleanMultiValuePropertyKey(key); + + // Check for an error + if (vr.getErrorCode() != 0) { + validationResultStack.pushValidationResult(vr); + } + + // reset the key + Object _key = vr.getObject(); + String cleanKey = (_key != null) ? vr.getObject().toString() : null; + + // if key is empty generate an error and return + if (cleanKey == null || cleanKey.isEmpty()) { + _generateInvalidMultiValueKeyError(key); + return; + } + + key = cleanKey; + + try { + JSONArray currentValues = _constructExistingMultiValue(key, command); + JSONArray newValues = _cleanMultiValues(values, key); + _validateAndPushMultiValue(currentValues, newValues, values, key, command); + + } catch (Throwable t) { + config.getLogger() + .verbose(config.getAccountId(), "Error handling multi value operation for key " + key, t); + } + } + + private void _push(Map profile) { + if (profile == null || profile.isEmpty()) { + return; + } + + try { + ValidationResult vr; + JSONObject customProfile = new JSONObject(); + JSONObject fieldsToUpdateLocally = new JSONObject(); + for (String key : profile.keySet()) { + Object value = profile.get(key); + + vr = validator.cleanObjectKey(key); + key = vr.getObject().toString(); + // Check for an error + if (vr.getErrorCode() != 0) { + validationResultStack.pushValidationResult(vr); + } + + if (key.isEmpty()) { + ValidationResult keyError = ValidationResultFactory.create(512, Constants.PUSH_KEY_EMPTY); + validationResultStack.pushValidationResult(keyError); + config.getLogger().debug(config.getAccountId(), keyError.getErrorDesc()); + // Skip this property + continue; + } + + try { + vr = validator.cleanObjectValue(value, Validator.ValidationContext.Profile); + } catch (Throwable e) { + // The object was neither a String, Boolean, or any number primitives + ValidationResult error = ValidationResultFactory.create(512, + Constants.OBJECT_VALUE_NOT_PRIMITIVE_PROFILE, + value != null ? value.toString() : "", key); + validationResultStack.pushValidationResult(error); + config.getLogger().debug(config.getAccountId(), error.getErrorDesc()); + // Skip this property + continue; + } + value = vr.getObject(); + // Check for an error + if (vr.getErrorCode() != 0) { + validationResultStack.pushValidationResult(vr); + } + + // test Phone: if no device country code, test if phone starts with +, log but always send + if (key.equalsIgnoreCase("Phone")) { + try { + value = value.toString(); + String countryCode = deviceInfo.getCountryCode(); + if (countryCode == null || countryCode.isEmpty()) { + String _value = (String) value; + if (!_value.startsWith("+")) { + ValidationResult error = ValidationResultFactory + .create(512, Constants.INVALID_COUNTRY_CODE, _value); + validationResultStack.pushValidationResult(error); + config.getLogger().debug(config.getAccountId(), error.getErrorDesc()); + } + } + config.getLogger().verbose(config.getAccountId(), + "Profile phone is: " + value + " device country code is: " + ((countryCode != null) + ? countryCode : "null")); + } catch (Exception e) { + validationResultStack + .pushValidationResult(ValidationResultFactory.create(512, Constants.INVALID_PHONE)); + config.getLogger() + .debug(config.getAccountId(), "Invalid phone number: " + e.getLocalizedMessage()); + continue; + } + } + + // add to the local profile update object + fieldsToUpdateLocally.put(key, value); + customProfile.put(key, value); + } + + config.getLogger() + .verbose(config.getAccountId(), "Constructed custom profile: " + customProfile.toString()); + + // update local profile values + if (fieldsToUpdateLocally.length() > 0) { + localDataStore.setProfileFields(fieldsToUpdateLocally); + } + + baseEventQueueManager.pushBasicProfile(customProfile); + + } catch (Throwable t) { + // Will not happen + config.getLogger().verbose(config.getAccountId(), "Failed to push profile", t); + } + } + + private void _removeValueForKey(String key) { + try { + key = (key == null) ? "" : key; // so we will generate a validation error later on + + // validate the key + ValidationResult vr; + + vr = validator.cleanObjectKey(key); + key = vr.getObject().toString(); + + if (key.isEmpty()) { + ValidationResult error = ValidationResultFactory.create(512, Constants.KEY_EMPTY); + validationResultStack.pushValidationResult(error); + config.getLogger().debug(config.getAccountId(), error.getErrorDesc()); + // Abort + return; + } + // Check for an error + if (vr.getErrorCode() != 0) { + validationResultStack.pushValidationResult(vr); + } + + // remove from the local profile + localDataStore.removeProfileField(key); + + // send the delete command + JSONObject command = new JSONObject().put(Constants.COMMAND_DELETE, true); + JSONObject update = new JSONObject().put(key, command); + baseEventQueueManager.pushBasicProfile(update); + + config.getLogger() + .verbose(config.getAccountId(), "removing value for key " + key + " from user profile"); + + } catch (Throwable t) { + config.getLogger().verbose(config.getAccountId(), "Failed to remove profile value for key " + key, t); + } + } + + private String _stringifyAndCleanScalarProfilePropValue(Object value) { + String val = CTJsonConverter.toJsonString(value); + + if (val != null) { + ValidationResult vr = validator.cleanMultiValuePropertyValue(val); + + // Check for an error + if (vr.getErrorCode() != 0) { + validationResultStack.pushValidationResult(vr); + } + + Object _value = vr.getObject(); + val = (_value != null) ? vr.getObject().toString() : null; + } + + return val; + } + + private void _validateAndPushMultiValue(JSONArray currentValues, JSONArray newValues, + ArrayList originalValues, String key, String command) { + + try { + + // if any of these are null, indicates some problem along the way so abort operation + if (currentValues == null || newValues == null || originalValues == null || key == null + || command == null) { + return; + } + + String mergeOperation = command.equals(Constants.COMMAND_REMOVE) ? Validator.REMOVE_VALUES_OPERATION + : Validator.ADD_VALUES_OPERATION; + + // merge currentValues and newValues + ValidationResult vr = validator + .mergeMultiValuePropertyForKey(currentValues, newValues, mergeOperation, key); + + // Check for an error + if (vr.getErrorCode() != 0) { + validationResultStack.pushValidationResult(vr); + } + + // set the merged local values array + JSONArray localValues = (JSONArray) vr.getObject(); + + // update local profile + // remove an empty array + if (localValues == null || localValues.length() <= 0) { + localDataStore.removeProfileField(key); + } else { + // not empty so save to local profile + localDataStore.setProfileField(key, localValues); + } + + // push to server + JSONObject commandObj = new JSONObject(); + commandObj.put(command, new JSONArray(originalValues)); + + JSONObject fields = new JSONObject(); + fields.put(key, commandObj); + + baseEventQueueManager.pushBasicProfile(fields); + + config.getLogger() + .verbose(config.getAccountId(), "Constructed multi-value profile push: " + fields.toString()); + + } catch (Throwable t) { + config.getLogger().verbose(config.getAccountId(), "Error pushing multiValue for key " + key, t); + } + } + + private boolean checkDuplicateNotificationIds(Bundle extras, HashMap notificationTagMap, + int interval) { + synchronized (notificationMapLock) { + // default to false; only return true if we are sure we've seen this one before + boolean isDupe = false; + try { + String notificationIdTag = extras.getString(Constants.NOTIFICATION_ID_TAG); + long now = System.currentTimeMillis(); + if (notificationTagMap.containsKey(notificationIdTag)) { + long timestamp; + // noinspection ConstantConditions + timestamp = (Long) notificationTagMap.get(notificationIdTag); + // same notificationId within time internal treat as dupe + if (now - timestamp < interval) { + isDupe = true; + } + } + notificationTagMap.put(notificationIdTag, now); + } catch (Throwable ignored) { + // no-op + } + return isDupe; + } + } + + public void sendPingEvent(final JSONObject eventObject) { + baseEventQueueManager + .queueEvent(context, eventObject, Constants.PING_EVENT); + } + + public void sendFetchEvent(final JSONObject eventObject) { + baseEventQueueManager + .queueEvent(context, eventObject, Constants.FETCH_EVENT); + } + + /** + * This method handles send Test flow for Display Units + * + * @param extras - bundled data of notification payload + */ + private void handleSendTestForDisplayUnits(Bundle extras) { + try { + JSONObject r = CTJsonConverter.displayUnitFromExtras(extras); + + CleverTapResponse cleverTapResponse = new CleverTapResponseHelper(); + + cleverTapResponse = new DisplayUnitResponse(cleverTapResponse, config, + callbackManager, controllerManager); + + cleverTapResponse.processResponse(r, null, context); + + } catch (Throwable t) { + Logger.v("Failed to process Display Unit from push notification payload", t); + } + } + + /** + * Raises the Notification Clicked event, if {@param clicked} is true, + * otherwise the Notification Viewed event, if {@param clicked} is false. + * + * @param clicked Whether or not this notification was clicked + * @param data The data to be attached as the event data + * @param customData Additional data such as form input to to be added to the event data + */ + @SuppressWarnings({"unused", "WeakerAccess"}) + void pushInboxMessageStateEvent(boolean clicked, CTInboxMessage data, Bundle customData) { + JSONObject event = new JSONObject(); + try { + JSONObject notif = getWzrkFields(data); + + if (customData != null) { + for (String x : customData.keySet()) { + + Object value = customData.get(x); + if (value != null) { + notif.put(x, value); + } + } + } + + if (clicked) { + try { + coreMetaData.setWzrkParams(notif); + } catch (Throwable t) { + // no-op + } + event.put("evtName", Constants.NOTIFICATION_CLICKED_EVENT_NAME); + } else { + event.put("evtName", Constants.NOTIFICATION_VIEWED_EVENT_NAME); + } + + event.put("evtData", notif); + baseEventQueueManager.queueEvent(context, event, Constants.RAISED_EVENT); + } catch (Throwable ignored) { + // We won't get here + } + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/BaseAnalyticsManager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/BaseAnalyticsManager.java new file mode 100644 index 000000000..91aa1f26f --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/BaseAnalyticsManager.java @@ -0,0 +1,51 @@ +package com.clevertap.android.sdk; + +import android.os.Bundle; +import com.clevertap.android.sdk.inapp.CTInAppNotification; +import java.util.ArrayList; +import java.util.Map; +import org.json.JSONObject; + +public abstract class BaseAnalyticsManager { + + public abstract void addMultiValuesForKey(String key, ArrayList values); + + public abstract void fetchFeatureFlags(); + + //Event + public abstract void forcePushAppLaunchedEvent(); + + public abstract void pushAppLaunchedEvent(); + + public abstract void pushDisplayUnitClickedEventForID(String unitID); + + public abstract void pushDisplayUnitViewedEventForID(String unitID); + + @SuppressWarnings({"unused"}) + public abstract void pushError(String errorMessage, int errorCode); + + public abstract void pushEvent(String eventName, Map eventActions); + + @SuppressWarnings({"unused", "WeakerAccess"}) + public abstract void pushInAppNotificationStateEvent(boolean clicked, CTInAppNotification data, + Bundle customData); + + public abstract void pushInstallReferrer(String url); + + public abstract void pushInstallReferrer(String source, String medium, String campaign); + + public abstract void pushNotificationClickedEvent(Bundle extras); + + @SuppressWarnings({"unused", "WeakerAccess"}) + public abstract void pushNotificationViewedEvent(Bundle extras); + + public abstract void pushProfile(Map profile); + + public abstract void removeMultiValuesForKey(String key, ArrayList values); + + public abstract void removeValueForKey(String key); + + public abstract void sendDataEvent(JSONObject event); + + public abstract void sendFetchEvent(final JSONObject eventObject); +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/BaseCTApiListener.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/BaseCTApiListener.java deleted file mode 100644 index a77c07928..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/BaseCTApiListener.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.clevertap.android.sdk; - -import android.content.Context; - -public interface BaseCTApiListener { - - CleverTapInstanceConfig config(); - - Context context(); - - DeviceInfo deviceInfo(); - - ValidationResultStack remoteErrorLogger(); -} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/BaseCallbackManager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/BaseCallbackManager.java new file mode 100644 index 000000000..b34f349e6 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/BaseCallbackManager.java @@ -0,0 +1,67 @@ +package com.clevertap.android.sdk; + +import com.clevertap.android.sdk.displayunits.DisplayUnitListener; +import com.clevertap.android.sdk.displayunits.model.CleverTapDisplayUnit; +import com.clevertap.android.sdk.product_config.CTProductConfigListener; +import com.clevertap.android.sdk.pushnotification.CTPushNotificationListener; +import com.clevertap.android.sdk.pushnotification.amp.CTPushAmpListener; +import java.util.ArrayList; + +public abstract class BaseCallbackManager { + + abstract void _notifyInboxInitialized(); + + public abstract void _notifyInboxMessagesDidUpdate(); + + public abstract FailureFlushListener getFailureFlushListener(); + + public abstract CTFeatureFlagsListener getFeatureFlagListener(); + + public abstract GeofenceCallback getGeofenceCallback(); + + public abstract InAppNotificationButtonListener getInAppNotificationButtonListener(); + + public abstract InAppNotificationListener getInAppNotificationListener(); + + public abstract CTInboxListener getInboxListener(); + + public abstract CTProductConfigListener getProductConfigListener(); + + public abstract CTPushAmpListener getPushAmpListener(); + + public abstract CTPushNotificationListener getPushNotificationListener(); + + public abstract SyncListener getSyncListener(); + + public abstract void notifyDisplayUnitsLoaded(final ArrayList displayUnits); + + //Profile + public abstract void notifyUserProfileInitialized(String deviceID); + + abstract void notifyUserProfileInitialized(); + + public abstract void setDisplayUnitListener(DisplayUnitListener listener); + + public abstract void setFailureFlushListener(FailureFlushListener failureFlushListener); + + public abstract void setFeatureFlagListener(CTFeatureFlagsListener listener); + + public abstract void setGeofenceCallback(GeofenceCallback geofenceCallback); + + public abstract void setInAppNotificationButtonListener( + InAppNotificationButtonListener inAppNotificationButtonListener); + + public abstract void setInAppNotificationListener(InAppNotificationListener inAppNotificationListener); + + public abstract void setInboxListener(CTInboxListener inboxListener); + + public abstract void setProductConfigListener( + CTProductConfigListener productConfigListener); + + public abstract void setPushAmpListener(CTPushAmpListener pushAmpListener); + + public abstract void setPushNotificationListener( + CTPushNotificationListener pushNotificationListener); + + public abstract void setSyncListener(SyncListener syncListener); +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/BaseLocationManager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/BaseLocationManager.java new file mode 100644 index 000000000..78d12be7b --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/BaseLocationManager.java @@ -0,0 +1,11 @@ +package com.clevertap.android.sdk; + +import android.location.Location; +import java.util.concurrent.Future; + +abstract class BaseLocationManager { + + public abstract Location _getLocation(); + + abstract Future _setLocation(final Location location); +} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/BaseSessionManager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/BaseSessionManager.java new file mode 100644 index 000000000..bb3f8511e --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/BaseSessionManager.java @@ -0,0 +1,16 @@ +package com.clevertap.android.sdk; + +import android.content.Context; + +abstract class BaseSessionManager { + + /** + * Destroys the current session and resets firstSession flag, if first session lasts more than 20 minutes + *

For an app like Music Player
  • user installs an app and plays music and then moves to background. + *
  • User then re-launches an App after listening music in background for more than 20 minutes, in this case + * since an app is not yet killed due to background music app installed event must not be raised by SDK + */ + abstract void destroySession(); + + abstract void lazyCreateSession(Context context); +} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTExecutors.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/CTExecutors.java deleted file mode 100644 index 11b5d4497..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTExecutors.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.clevertap.android.sdk; - -import android.os.Handler; -import android.os.Looper; -import androidx.annotation.NonNull; -import androidx.annotation.RestrictTo; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; - -/** - * Global executor pools for the whole application. - *

    - * Grouping tasks like this avoids the effects of task starvation (e.g. disk reads don't wait behind - * webservice requests). - */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public class CTExecutors { - - private static class MainThreadExecutor implements Executor { - - private final Handler mainThreadHandler = new Handler(Looper.getMainLooper()); - - @Override - public void execute(@NonNull Runnable command) { - mainThreadHandler.post(command); - } - } - - private static CTExecutors sInstance; - - private final Executor diskIO; - - private final Executor mainThread; - - public static synchronized CTExecutors getInstance() { - if (sInstance == null) { - sInstance = new CTExecutors(Executors.newSingleThreadExecutor(), - new MainThreadExecutor()); - } - return sInstance; - } - - private CTExecutors(Executor diskIO, Executor mainThread) { - this.diskIO = diskIO; - this.mainThread = mainThread; - } - - public Executor diskIO() { - return diskIO; - } - - public Executor mainThread() { - return mainThread; - } -} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTExperimentsListener.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/CTExperimentsListener.java deleted file mode 100644 index e65c27f1d..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTExperimentsListener.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.clevertap.android.sdk; - -public interface CTExperimentsListener { - - void CTExperimentsUpdated(); -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxListener.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxListener.java index 0702f037a..e9eb9989d 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxListener.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxListener.java @@ -1,5 +1,7 @@ package com.clevertap.android.sdk; +import com.clevertap.android.sdk.inbox.CTInboxMessage; + public interface CTInboxListener { /** diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxStyleConfig.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxStyleConfig.java index 063d86de7..f11a306f0 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxStyleConfig.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxStyleConfig.java @@ -2,6 +2,7 @@ import android.os.Parcel; import android.os.Parcelable; +import com.clevertap.android.sdk.inbox.CTInboxActivity; import java.util.ArrayList; import java.util.Arrays; @@ -285,7 +286,7 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeString(noMessageViewTextColor); } - boolean isUsingTabs() { + public boolean isUsingTabs() { return (tabs != null && tabs.length > 0); } } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTLockManager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/CTLockManager.java new file mode 100644 index 000000000..12b50f9f6 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/CTLockManager.java @@ -0,0 +1,20 @@ +package com.clevertap.android.sdk; + +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; + +@RestrictTo(Scope.LIBRARY) +public class CTLockManager { + + private final Boolean eventLock = true; + + private final Object inboxControllerLock = new Object(); + + public Boolean getEventLock() { + return eventLock; + } + + public Object getInboxControllerLock() { + return inboxControllerLock; + } +} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTWebInterface.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/CTWebInterface.java index 9f25c5821..8fc984758 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTWebInterface.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/CTWebInterface.java @@ -12,7 +12,7 @@ @SuppressWarnings("WeakerAccess") public class CTWebInterface { - private WeakReference weakReference; + private final WeakReference weakReference; public CTWebInterface(CleverTapAPI instance) { this.weakReference = new WeakReference<>(instance); diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CallbackManager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/CallbackManager.java new file mode 100644 index 000000000..6391ad5af --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/CallbackManager.java @@ -0,0 +1,244 @@ +package com.clevertap.android.sdk; + +import static com.clevertap.android.sdk.Utils.runOnUiThread; + +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; +import com.clevertap.android.sdk.displayunits.DisplayUnitListener; +import com.clevertap.android.sdk.displayunits.model.CleverTapDisplayUnit; +import com.clevertap.android.sdk.product_config.CTProductConfigListener; +import com.clevertap.android.sdk.pushnotification.CTPushNotificationListener; +import com.clevertap.android.sdk.pushnotification.amp.CTPushAmpListener; +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +@RestrictTo(Scope.LIBRARY) +public class CallbackManager extends BaseCallbackManager { + + private WeakReference displayUnitListenerWeakReference; + + private GeofenceCallback geofenceCallback; + + private WeakReference inAppNotificationButtonListener; + + private InAppNotificationListener inAppNotificationListener; + + private CTInboxListener inboxListener; + + private final CleverTapInstanceConfig config; + + private final DeviceInfo deviceInfo; + + private FailureFlushListener failureFlushListener; + + private WeakReference featureFlagListenerWeakReference; + + private WeakReference productConfigListener; + + private CTPushAmpListener pushAmpListener = null; + + private CTPushNotificationListener pushNotificationListener = null; + + private SyncListener syncListener = null; + + public CallbackManager(CleverTapInstanceConfig config, DeviceInfo deviceInfo) { + this.config = config; + this.deviceInfo = deviceInfo; + } + + @Override + public void _notifyInboxMessagesDidUpdate() { + if (this.inboxListener != null) { + Utils.runOnUiThread(new Runnable() { + @Override + public void run() { + if (inboxListener != null) { + inboxListener.inboxMessagesDidUpdate(); + } + } + }); + } + } + + @Override + public FailureFlushListener getFailureFlushListener() { + return failureFlushListener; + } + + @Override + public void setFailureFlushListener(final FailureFlushListener failureFlushListener) { + this.failureFlushListener = failureFlushListener; + } + + @Override + public CTFeatureFlagsListener getFeatureFlagListener() { + if (featureFlagListenerWeakReference != null && featureFlagListenerWeakReference.get() != null) { + return featureFlagListenerWeakReference.get(); + } + return null; + } + + @Override + public void setFeatureFlagListener(final CTFeatureFlagsListener listener) { + this.featureFlagListenerWeakReference = new WeakReference<>(listener); + } + + @Override + public GeofenceCallback getGeofenceCallback() { + return geofenceCallback; + } + + @Override + public void setGeofenceCallback(final GeofenceCallback geofenceCallback) { + this.geofenceCallback = geofenceCallback; + } + + @Override + public InAppNotificationButtonListener getInAppNotificationButtonListener() { + if (inAppNotificationButtonListener != null && inAppNotificationButtonListener.get() != null) { + return inAppNotificationButtonListener.get(); + } + return null; + } + + @Override + public void setInAppNotificationButtonListener( + InAppNotificationButtonListener inAppNotificationButtonListener) { + this.inAppNotificationButtonListener = new WeakReference<>(inAppNotificationButtonListener); + } + + @Override + public InAppNotificationListener getInAppNotificationListener() { + return inAppNotificationListener; + } + + @Override + public void setInAppNotificationListener(final InAppNotificationListener inAppNotificationListener) { + this.inAppNotificationListener = inAppNotificationListener; + } + + @Override + public CTInboxListener getInboxListener() { + return inboxListener; + } + + @Override + public void setInboxListener(final CTInboxListener inboxListener) { + this.inboxListener = inboxListener; + } + + @Override + public CTProductConfigListener getProductConfigListener() { + if (productConfigListener != null && productConfigListener.get() != null) { + return productConfigListener.get(); + } + return null; + } + + @Override + public void setProductConfigListener(final CTProductConfigListener productConfigListener) { + if (productConfigListener != null) { + this.productConfigListener = new WeakReference<>(productConfigListener); + } + } + + @Override + public CTPushAmpListener getPushAmpListener() { + return pushAmpListener; + } + + @Override + public void setPushAmpListener(final CTPushAmpListener pushAmpListener) { + this.pushAmpListener = pushAmpListener; + } + + @Override + public CTPushNotificationListener getPushNotificationListener() { + return pushNotificationListener; + } + + @Override + public void setPushNotificationListener( + final CTPushNotificationListener pushNotificationListener) { + this.pushNotificationListener = pushNotificationListener; + } + + @Override + public SyncListener getSyncListener() { + return syncListener; + } + + @Override + public void setSyncListener(final SyncListener syncListener) { + this.syncListener = syncListener; + } + + //Profile + @Override + public void notifyUserProfileInitialized(String deviceID) { + deviceID = (deviceID != null) ? deviceID : deviceInfo.getDeviceID(); + + if (deviceID == null) { + return; + } + + final SyncListener sl; + try { + sl = getSyncListener(); + if (sl != null) { + sl.profileDidInitialize(deviceID); + } + } catch (Throwable t) { + // Ignore + } + } + + @Override + public void setDisplayUnitListener(DisplayUnitListener listener) { + if (listener != null) { + displayUnitListenerWeakReference = new WeakReference<>(listener); + } else { + config.getLogger().verbose(config.getAccountId(), + Constants.FEATURE_DISPLAY_UNIT + "Failed to set - DisplayUnitListener can't be null"); + } + } + + void _notifyInboxInitialized() { + if (this.inboxListener != null) { + this.inboxListener.inboxDidInitialize(); + } + } + + /** + * Notify the registered Display Unit listener about the running Display Unit campaigns + * + * @param displayUnits - Array of Display Units {@link CleverTapDisplayUnit} + */ + public void notifyDisplayUnitsLoaded(final ArrayList displayUnits) { + if (displayUnits != null && !displayUnits.isEmpty()) { + if (displayUnitListenerWeakReference != null && displayUnitListenerWeakReference.get() != null) { + runOnUiThread(new Runnable() { + @Override + public void run() { + //double check to ensure null safety + if (displayUnitListenerWeakReference != null + && displayUnitListenerWeakReference.get() != null) { + displayUnitListenerWeakReference.get().onDisplayUnitsLoaded(displayUnits); + } + } + }); + } else { + config.getLogger().verbose(config.getAccountId(), + Constants.FEATURE_DISPLAY_UNIT + "No registered listener, failed to notify"); + } + } else { + config.getLogger() + .verbose(config.getAccountId(), Constants.FEATURE_DISPLAY_UNIT + "No Display Units found"); + } + } + + void notifyUserProfileInitialized() { + notifyUserProfileInitialized(deviceInfo.getDeviceID()); + } + +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapAPI.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapAPI.java index c3dcf3dec..7205739d8 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapAPI.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapAPI.java @@ -1,109 +1,53 @@ package com.clevertap.android.sdk; -import static android.content.Context.JOB_SCHEDULER_SERVICE; import static android.content.Context.NOTIFICATION_SERVICE; -import static com.clevertap.android.sdk.CTJsonConverter.getErrorObject; -import static com.clevertap.android.sdk.CTJsonConverter.getRenderedTargetList; -import static com.clevertap.android.sdk.CTJsonConverter.getWzrkFields; -import static com.clevertap.android.sdk.Utils.runOnUiThread; -import android.annotation.SuppressLint; import android.app.Activity; -import android.app.AlarmManager; -import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.job.JobInfo; import android.app.job.JobParameters; -import android.app.job.JobScheduler; -import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.ServiceInfo; -import android.graphics.Bitmap; -import android.graphics.Color; import android.location.Location; -import android.location.LocationManager; import android.media.AudioAttributes; -import android.media.RingtoneManager; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; import android.net.Uri; import android.os.Build; -import android.os.Build.VERSION_CODES; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.os.RemoteException; -import android.os.SystemClock; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; -import androidx.core.app.NotificationCompat; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.fragment.app.FragmentTransaction; -import com.android.installreferrer.api.InstallReferrerClient; -import com.android.installreferrer.api.InstallReferrerStateListener; -import com.android.installreferrer.api.ReferrerDetails; -import com.clevertap.android.sdk.ab_testing.CTABTestController; -import com.clevertap.android.sdk.displayunits.CTDisplayUnitController; import com.clevertap.android.sdk.displayunits.DisplayUnitListener; import com.clevertap.android.sdk.displayunits.model.CleverTapDisplayUnit; +import com.clevertap.android.sdk.events.EventDetail; import com.clevertap.android.sdk.featureFlags.CTFeatureFlagsController; -import com.clevertap.android.sdk.login.IdentityRepo; -import com.clevertap.android.sdk.login.IdentityRepoFactory; -import com.clevertap.android.sdk.login.LoginInfoProvider; +import com.clevertap.android.sdk.inbox.CTInboxActivity; +import com.clevertap.android.sdk.inbox.CTInboxMessage; +import com.clevertap.android.sdk.inbox.CTMessageDAO; import com.clevertap.android.sdk.product_config.CTProductConfigController; import com.clevertap.android.sdk.product_config.CTProductConfigListener; -import com.clevertap.android.sdk.pushnotification.CTNotificationIntentService; import com.clevertap.android.sdk.pushnotification.CTPushNotificationListener; -import com.clevertap.android.sdk.pushnotification.CTPushNotificationReceiver; import com.clevertap.android.sdk.pushnotification.NotificationInfo; import com.clevertap.android.sdk.pushnotification.PushConstants; import com.clevertap.android.sdk.pushnotification.PushConstants.PushType; -import com.clevertap.android.sdk.pushnotification.PushProviders; -import com.clevertap.android.sdk.pushnotification.amp.CTBackgroundIntentService; -import com.clevertap.android.sdk.pushnotification.amp.CTBackgroundJobService; import com.clevertap.android.sdk.pushnotification.amp.CTPushAmpListener; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; +import com.clevertap.android.sdk.task.CTExecutorFactory; +import com.clevertap.android.sdk.task.Task; +import com.clevertap.android.sdk.utils.UriHelper; +import com.clevertap.android.sdk.validation.ManifestValidator; +import com.clevertap.android.sdk.validation.ValidationResult; import java.lang.ref.WeakReference; -import java.net.URL; -import java.net.URLDecoder; -import java.text.ParseException; -import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; import java.util.Collections; -import java.util.Date; import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.Random; -import java.util.TimeZone; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.concurrent.Callable; import java.util.concurrent.Future; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocketFactory; import org.json.JSONArray; -import org.json.JSONException; import org.json.JSONObject; @@ -111,35 +55,7 @@ *

    CleverTapAPI

    * This is the main CleverTapAPI class that manages the SDK instances */ -public class CleverTapAPI implements CleverTapAPIListener { - - //InApp - private final class NotificationPrepareRunnable implements Runnable { - - private final WeakReference cleverTapAPIWeakReference; - - private final JSONObject jsonObject; - - private final boolean videoSupport = haveVideoPlayerSupport; - - NotificationPrepareRunnable(CleverTapAPI cleverTapAPI, JSONObject jsonObject) { - this.cleverTapAPIWeakReference = new WeakReference<>(cleverTapAPI); - this.jsonObject = jsonObject; - } - - @Override - public void run() { - final CTInAppNotification inAppNotification = new CTInAppNotification() - .initWithJSON(jsonObject, videoSupport); - if (inAppNotification.getError() != null) { - getConfigLogger() - .debug(getAccountId(), "Unable to parse inapp notification " + inAppNotification.getError()); - return; - } - inAppNotification.listener = cleverTapAPIWeakReference.get(); - inAppNotification.prepareForDisplay(); - } - } +public class CleverTapAPI implements CTInboxActivity.InboxActivityListener { /** * Implement to get called back when the device push token is refreshed @@ -170,204 +86,24 @@ public int intValue() { } } - private enum EventGroup { - - REGULAR(""), - PUSH_NOTIFICATION_VIEWED("-spiky"); - - private final String httpResource; - - EventGroup(String httpResource) { - this.httpResource = httpResource; - } - } @SuppressWarnings("unused") public static final String NOTIFICATION_TAG = "wzrk_pn"; - static boolean haveVideoPlayerSupport; - private static int debugLevel = CleverTapAPI.LogLevel.INFO.intValue(); - private static CleverTapInstanceConfig defaultConfig; + static CleverTapInstanceConfig defaultConfig; private static HashMap instances; - private static boolean appForeground = false; - - private static int activityCount = 0; - - private static final List pendingNotifications = Collections - .synchronizedList(new ArrayList()); - - private static CTInAppNotification currentlyDisplayingInApp = null; - - private static WeakReference currentActivity; - - private static int initialAppEnteredForegroundTime = 0; - - private static SSLContext sslContext; - - private static SSLSocketFactory sslSocketFactory; - @SuppressWarnings({"FieldCanBeLocal", "unused"}) private static String sdkVersion; // For Google Play Store/Android Studio analytics - private static boolean isUIEditorEnabled = false; - - private long EXECUTOR_THREAD_ID = 0; - - private long NOTIFICATION_THREAD_ID = 0; - - private long appInstallTime = 0; - - private long appLastSeen = 0; - - private boolean appLaunchPushed = false; - - private final Object appLaunchPushedLock = new Object(); - - private String cachedGUID = null; - - private Runnable commsRunnable = null; - - private final CleverTapInstanceConfig config; - private final Context context; - private CTABTestController ctABTestController; - - private CTFeatureFlagsController ctFeatureFlagsController; - - private CTInboxController ctInboxController; - - private CTProductConfigController ctProductConfigController; - - private int currentRequestTimestamp = 0; - - private String currentScreenName = ""; - - private int currentSessionId = 0; - - private boolean currentUserOptedOut = false; - - private DBAdapter dbAdapter; - - private final DeviceInfo deviceInfo; - - private final Object displayUnitControllerLock = new Object(); - - private WeakReference displayUnitListenerWeakReference; - - private boolean enableNetworkInfoReporting = false; - - private final ExecutorService es; - - private final Boolean eventLock = true; - - private CTExperimentsListener experimentsListener = null; - - private WeakReference featureFlagsListener; - - private boolean firstRequestInSession = false; - - private boolean firstSession = false; - - private GeofenceCallback geofenceCallback; - - private int geofenceSDKVersion = 0; - - private final Handler handlerUsingMainLooper; - - private InAppFCManager inAppFCManager; - - private WeakReference inAppNotificationButtonListener; - - private InAppNotificationListener inAppNotificationListener; - - private HashSet inappActivityExclude = null; - - private final Object inboxControllerLock = new Object(); - - private CTInboxListener inboxListener; - private WeakReference inboxMessageButtonListener; - private boolean installReferrerDataSent = false; - - private final HashMap installReferrerMap = new HashMap<>(8); - - private boolean isBgPing = false; - - private boolean isLocationForGeofence = false; - - private boolean isProductConfigRequested; - - private int lastLocationPingTime = 0; - - private int lastLocationPingTimeForGeofence = 0; - - private int lastSessionLength = 0; - - private int lastVisitTime; - - private final LocalDataStore localDataStore; - - private Location locationFromUser = null; - - private CTDisplayUnitController mCTDisplayUnitController; - - private int mResponseFailureCount = 0; - - private final int maxDelayFrequency = 1000 * 60 * 10; - - private int minDelayFrequency = 0; - - private int networkRetryCount = 0; - - private final HashMap notificationIdTagMap = new HashMap<>(); - - private final Object notificationMapLock = new Object(); - - private final HashMap notificationViewedIdTagMap = new HashMap<>(); - - private final ExecutorService ns; - - private boolean offline = false; - - private final Object optOutFlagLock = new Object(); - - private Runnable pendingInappRunnable = null; - - private String processingUserLoginIdentifier = null; - - private final Boolean processingUserLoginLock = true; - - private WeakReference productConfigListener; - - private CTPushAmpListener pushAmpListener = null; - - private CTPushNotificationListener pushNotificationListener = null; - - private Runnable pushNotificationViewedRunnable = null; - - private final PushProviders pushProviders; - - private long referrerClickTime = 0; - - private String source = null, medium = null, campaign = null; - - private SyncListener syncListener = null; - - private final Object tokenLock = new Object(); - - private DevicePushTokenRefreshListener tokenRefreshListener; - - private final ValidationResultStack validationResultStack; - - private final Validator validator; - - private JSONObject wzrkParams = null; + private CoreState coreState; /** * This method is used to change the credentials of CleverTap account Id and token programmatically @@ -419,7 +155,7 @@ public static void createNotification(final Context context, final Bundle extras if (instances == null) { CleverTapAPI instance = createInstanceIfAvailable(context, _accountId); if (instance != null) { - instance._createNotification(context, extras, notificationId); + instance.coreState.getPushProviders()._createNotification(context, extras, notificationId); } return; } @@ -428,12 +164,14 @@ public static void createNotification(final Context context, final Bundle extras CleverTapAPI instance = CleverTapAPI.instances.get(accountId); boolean shouldProcess = false; if (instance != null) { - shouldProcess = (_accountId == null && instance.config.isDefaultInstance()) || instance.getAccountId() + shouldProcess = (_accountId == null && instance.coreState.getConfig().isDefaultInstance()) + || instance + .getAccountId() .equals(_accountId); } if (shouldProcess) { try { - instance._createNotification(context, extras, notificationId); + instance.coreState.getPushProviders()._createNotification(context, extras, notificationId); } catch (Throwable t) { // no-op } @@ -481,24 +219,26 @@ public static void createNotificationChannel(final Context context, final String } try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - instance.postAsyncSafely("createNotificationChannel", new Runnable() { + Task task = CTExecutorFactory.executors(instance.coreState.getConfig()).postAsyncSafelyTask(); + task.execute("createNotificationChannel", new Callable() { @RequiresApi(api = Build.VERSION_CODES.O) @Override - public void run() { + public Void call() { NotificationManager notificationManager = (NotificationManager) context .getSystemService(NOTIFICATION_SERVICE); if (notificationManager == null) { - return; + return null; } - NotificationChannel notificationChannel = new NotificationChannel(channelId, channelName, + NotificationChannel notificationChannel = new NotificationChannel(channelId, + channelName, importance); notificationChannel.setDescription(channelDescription); notificationChannel.setShowBadge(showBadge); notificationManager.createNotificationChannel(notificationChannel); instance.getConfigLogger().info(instance.getAccountId(), "Notification channel " + channelName.toString() + " has been created"); - + return null; } }); } @@ -526,6 +266,7 @@ public void run() { * @param showBadge An boolean value as to whether this channel shows a badge */ @SuppressWarnings("unused") + @RequiresApi(api = Build.VERSION_CODES.O) public static void createNotificationChannel(final Context context, final String channelId, final CharSequence channelName, final String channelDescription, final int importance, final String groupId, final boolean showBadge) { @@ -536,17 +277,17 @@ public static void createNotificationChannel(final Context context, final String } try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - instance.postAsyncSafely("creatingNotificationChannel", new Runnable() { - @RequiresApi(api = Build.VERSION_CODES.O) + Task task = CTExecutorFactory.executors(instance.coreState.getConfig()).postAsyncSafelyTask(); + task.execute("creatingNotificationChannel", new Callable() { @Override - public void run() { - + public Void call() { NotificationManager notificationManager = (NotificationManager) context .getSystemService(NOTIFICATION_SERVICE); if (notificationManager == null) { - return; + return null; } - NotificationChannel notificationChannel = new NotificationChannel(channelId, channelName, + NotificationChannel notificationChannel = new NotificationChannel(channelId, + channelName, importance); notificationChannel.setDescription(channelDescription); notificationChannel.setGroup(groupId); @@ -555,6 +296,7 @@ public void run() { instance.getConfigLogger().info(instance.getAccountId(), "Notification channel " + channelName.toString() + " has been created"); + return null; } }); } @@ -579,6 +321,7 @@ public void run() { * @param sound A String denoting the custom sound raw file for this channel */ @SuppressWarnings("unused") + @RequiresApi(api = Build.VERSION_CODES.O) public static void createNotificationChannel(final Context context, final String channelId, final CharSequence channelName, final String channelDescription, final int importance, final boolean showBadge, final String sound) { @@ -589,15 +332,14 @@ public static void createNotificationChannel(final Context context, final String } try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - instance.postAsyncSafely("createNotificationChannel", new Runnable() { - @RequiresApi(api = Build.VERSION_CODES.O) + Task task = CTExecutorFactory.executors(instance.coreState.getConfig()).postAsyncSafelyTask(); + task.execute("createNotificationChannel", new Callable() { @Override - public void run() { - + public Void call() { NotificationManager notificationManager = (NotificationManager) context .getSystemService(NOTIFICATION_SERVICE); if (notificationManager == null) { - return; + return null; } String soundfile = ""; @@ -617,7 +359,8 @@ public void run() { } - NotificationChannel notificationChannel = new NotificationChannel(channelId, channelName, + NotificationChannel notificationChannel = new NotificationChannel(channelId, + channelName, importance); notificationChannel.setDescription(channelDescription); notificationChannel.setShowBadge(showBadge); @@ -632,7 +375,7 @@ public void run() { notificationManager.createNotificationChannel(notificationChannel); instance.getConfigLogger().info(instance.getAccountId(), "Notification channel " + channelName.toString() + " has been created"); - + return null; } }); } @@ -660,6 +403,7 @@ public void run() { * @param sound A String denoting the custom sound raw file for this channel */ @SuppressWarnings({"unused"}) + @RequiresApi(api = Build.VERSION_CODES.O) public static void createNotificationChannel(final Context context, final String channelId, final CharSequence channelName, final String channelDescription, final int importance, final String groupId, final boolean showBadge, final String sound) { @@ -670,14 +414,14 @@ public static void createNotificationChannel(final Context context, final String } try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - instance.postAsyncSafely("creatingNotificationChannel", new Runnable() { - @RequiresApi(api = Build.VERSION_CODES.O) + Task task = CTExecutorFactory.executors(instance.coreState.getConfig()).postAsyncSafelyTask(); + task.execute("creatingNotificationChannel", new Callable() { @Override - public void run() { + public Void call() { NotificationManager notificationManager = (NotificationManager) context .getSystemService(NOTIFICATION_SERVICE); if (notificationManager == null) { - return; + return null; } String soundfile = ""; @@ -696,7 +440,8 @@ public void run() { } } - NotificationChannel notificationChannel = new NotificationChannel(channelId, channelName, + NotificationChannel notificationChannel = new NotificationChannel(channelId, + channelName, importance); notificationChannel.setDescription(channelDescription); notificationChannel.setGroup(groupId); @@ -712,7 +457,7 @@ public void run() { notificationManager.createNotificationChannel(notificationChannel); instance.getConfigLogger().info(instance.getAccountId(), "Notification channel " + channelName.toString() + " has been created"); - + return null; } }); } @@ -733,6 +478,7 @@ public void run() { * @param groupName A String for setting the name of the notification channel group */ @SuppressWarnings("unused") + @RequiresApi(api = Build.VERSION_CODES.O) public static void createNotificationChannelGroup(final Context context, final String groupId, final CharSequence groupName) { final CleverTapAPI instance = getDefaultInstanceOrFirstOther(context); @@ -742,21 +488,21 @@ public static void createNotificationChannelGroup(final Context context, final S } try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - instance.postAsyncSafely("creatingNotificationChannelGroup", new Runnable() { - @RequiresApi(api = Build.VERSION_CODES.O) + Task task = CTExecutorFactory.executors(instance.coreState.getConfig()).postAsyncSafelyTask(); + task.execute("creatingNotificationChannelGroup", new Callable() { @Override - public void run() { - + public Void call() { NotificationManager notificationManager = (NotificationManager) context .getSystemService(NOTIFICATION_SERVICE); if (notificationManager == null) { - return; + return null; } notificationManager - .createNotificationChannelGroup(new NotificationChannelGroup(groupId, groupName)); + .createNotificationChannelGroup( + new NotificationChannelGroup(groupId, groupName)); instance.getConfigLogger().info(instance.getAccountId(), "Notification channel group " + groupName.toString() + " has been created"); - + return null; } }); } @@ -776,6 +522,7 @@ public void run() { * @param channelId A String for setting the id of the notification channel */ @SuppressWarnings("unused") + @RequiresApi(api = Build.VERSION_CODES.O) public static void deleteNotificationChannel(final Context context, final String channelId) { final CleverTapAPI instance = getDefaultInstanceOrFirstOther(context); if (instance == null) { @@ -784,20 +531,19 @@ public static void deleteNotificationChannel(final Context context, final String } try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - instance.postAsyncSafely("deletingNotificationChannel", new Runnable() { - @RequiresApi(api = Build.VERSION_CODES.O) + Task task = CTExecutorFactory.executors(instance.coreState.getConfig()).postAsyncSafelyTask(); + task.execute("deletingNotificationChannel", new Callable() { @Override - public void run() { - + public Void call() { NotificationManager notificationManager = (NotificationManager) context .getSystemService(NOTIFICATION_SERVICE); if (notificationManager == null) { - return; + return null; } notificationManager.deleteNotificationChannel(channelId); instance.getConfigLogger().info(instance.getAccountId(), "Notification channel " + channelId + " has been deleted"); - + return null; } }); } @@ -816,6 +562,7 @@ public void run() { * @param groupId A String for setting the id of the notification channel group */ @SuppressWarnings("unused") + @RequiresApi(api = Build.VERSION_CODES.O) public static void deleteNotificationChannelGroup(final Context context, final String groupId) { final CleverTapAPI instance = getDefaultInstanceOrFirstOther(context); if (instance == null) { @@ -824,20 +571,21 @@ public static void deleteNotificationChannelGroup(final Context context, final S } try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - instance.postAsyncSafely("deletingNotificationChannelGroup", new Runnable() { - @RequiresApi(api = Build.VERSION_CODES.O) + Task task = CTExecutorFactory.executors(instance.coreState.getConfig()).postAsyncSafelyTask(); + task.execute("deletingNotificationChannelGroup", new Callable() { @Override - public void run() { + public Void call() { NotificationManager notificationManager = (NotificationManager) context .getSystemService(NOTIFICATION_SERVICE); if (notificationManager == null) { - return; + return null; } notificationManager.deleteNotificationChannelGroup(groupId); instance.getConfigLogger().info(instance.getAccountId(), "Notification channel group " + groupId + " has been deleted"); + return null; } }); } @@ -847,22 +595,21 @@ public void run() { } } - //Push @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public static void fcmTokenRefresh(Context context, String token) { for (CleverTapAPI instance : getAvailableInstances(context)) { - if (instance == null || instance.getConfig().isAnalyticsOnly()) { - Logger.d(instance.getAccountId(), - "Instance is Analytics Only not processing device token"); + if (instance == null || instance.getCoreState().getConfig().isAnalyticsOnly()) { + Logger.d("Instance is Analytics Only not processing device token"); continue; } //get token from Manifest - String tokenUsingManifestMetaEntry = Utils.getFcmTokenUsingManifestMetaEntry(context, instance.config); + String tokenUsingManifestMetaEntry = Utils + .getFcmTokenUsingManifestMetaEntry(context, instance.getCoreState().getConfig()); if (!TextUtils.isEmpty(tokenUsingManifestMetaEntry)) { token = tokenUsingManifestMetaEntry; } - instance.doTokenRefresh(token, PushType.FCM); + instance.getCoreState().getPushProviders().doTokenRefresh(token, PushType.FCM); } } @@ -951,6 +698,14 @@ CleverTapAPI getGlobalInstance(Context context, String _accountId) { return instance; } + public static HashMap getInstances() { + return instances; + } + + public static void setInstances(final HashMap instances) { + CleverTapAPI.instances = instances; + } + /** * Checks whether this notification is from CleverTap. * @@ -994,7 +749,9 @@ public static void handleNotificationClicked(Context context, Bundle notificatio CleverTapAPI instance = CleverTapAPI.instances.get(accountId); boolean shouldProcess = false; if (instance != null) { - shouldProcess = (_accountId == null && instance.config.isDefaultInstance()) || instance.getAccountId() + shouldProcess = (_accountId == null && instance.coreState.getConfig().isDefaultInstance()) + || instance + .getAccountId() .equals(_accountId); } if (shouldProcess) { @@ -1025,7 +782,7 @@ public static CleverTapAPI instanceWithConfig(Context context, CleverTapInstance @SuppressWarnings({"unused", "WeakerAccess"}) public static CleverTapAPI instanceWithConfig(Context context, @NonNull CleverTapInstanceConfig config, String cleverTapID) { - //noinspection ConstantConditions + //noinspection Constant Conditions if (config == null) { Logger.v("CleverTapInstanceConfig cannot be null"); return null; @@ -1039,22 +796,44 @@ public static CleverTapAPI instanceWithConfig(Context context, @NonNull CleverTa instance = new CleverTapAPI(context, config, cleverTapID); instances.put(config.getAccountId(), instance); final CleverTapAPI finalInstance = instance; - instance.postAsyncSafely("notifyProfileInitialized", new Runnable() { + Task task = CTExecutorFactory.executors(instance.coreState.getConfig()).postAsyncSafelyTask(); + task.execute("notifyProfileInitialized", new Callable() { @Override - public void run() { + public Void call() { if (finalInstance.getCleverTapID() != null) { - finalInstance.notifyUserProfileInitialized(); - finalInstance.recordDeviceIDErrors(); + finalInstance.coreState.getCallbackManager().notifyUserProfileInitialized(); + finalInstance.coreState.getLoginController().recordDeviceIDErrors(); } + return null; } }); } else if (instance.isErrorDeviceId() && instance.getConfig().getEnableCustomCleverTapId() && Utils .validateCTID(cleverTapID)) { - instance.asyncProfileSwitchUser(null, null, cleverTapID); + instance.coreState.getLoginController().asyncProfileSwitchUser(null, null, cleverTapID); } return instance; } + /** + * Returns whether or not the app is in the foreground. + * + * @return The foreground status + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public static boolean isAppForeground() { + return CoreMetaData.isAppForeground(); + } + + /** + * Use this method to notify CleverTap that the app is in foreground + * + * @param appForeground boolean true/false + */ + @SuppressWarnings({"unused", "WeakerAccess"}) + public static void setAppForeground(boolean appForeground) { + CoreMetaData.setAppForeground(appForeground); + } + @SuppressWarnings("WeakerAccess") public static void onActivityPaused() { if (instances == null) { @@ -1065,7 +844,7 @@ public static void onActivityPaused() { CleverTapAPI instance = CleverTapAPI.instances.get(accountId); try { if (instance != null) { - instance.activityPaused(); + instance.coreState.getActivityLifeCycleManager().activityPaused(); } } catch (Throwable t) { // Ignore @@ -1084,28 +863,29 @@ public static void onActivityResumed(Activity activity, String cleverTapID) { CleverTapAPI.createInstanceIfAvailable(activity.getApplicationContext(), null, cleverTapID); } - CleverTapAPI.setAppForeground(true); + CoreMetaData.setAppForeground(true); if (instances == null) { Logger.v("Instances is null in onActivityResumed!"); return; } - String currentActivityName = getCurrentActivityName(); - setCurrentActivity(activity); + String currentActivityName = CoreMetaData.getCurrentActivityName(); + CoreMetaData.setCurrentActivity(activity); if (currentActivityName == null || !currentActivityName.equals(activity.getLocalClassName())) { - activityCount++; + CoreMetaData.incrementActivityCount(); } - if (initialAppEnteredForegroundTime <= 0) { - initialAppEnteredForegroundTime = (int) System.currentTimeMillis() / 1000; + if (CoreMetaData.getInitialAppEnteredForegroundTime() <= 0) { + int initialAppEnteredForegroundTime = Utils.getNow(); + CoreMetaData.setInitialAppEnteredForegroundTime(initialAppEnteredForegroundTime); } for (String accountId : CleverTapAPI.instances.keySet()) { CleverTapAPI instance = CleverTapAPI.instances.get(accountId); try { if (instance != null) { - instance.activityResumed(activity); + instance.coreState.getActivityLifeCycleManager().activityResumed(activity); } } catch (Throwable t) { Logger.v("Throwable - " + t.getLocalizedMessage()); @@ -1125,7 +905,7 @@ public static void processPushNotification(Context context, Bundle extras) { if (instances == null) { CleverTapAPI instance = createInstanceIfAvailable(context, _accountId); if (instance != null) { - instance.processCustomPushNotification(extras); + instance.coreState.getPushProviders().processCustomPushNotification(extras); } return; } @@ -1133,7 +913,7 @@ public static void processPushNotification(Context context, Bundle extras) { for (String accountId : CleverTapAPI.instances.keySet()) { CleverTapAPI instance = CleverTapAPI.instances.get(accountId); if (instance != null) { - instance.processCustomPushNotification(extras); + instance.coreState.getPushProviders().processCustomPushNotification(extras); } } } @@ -1144,7 +924,7 @@ public static void runBackgroundIntentService(Context context) { CleverTapAPI instance = CleverTapAPI.getDefaultInstance(context); if (instance != null) { if (instance.getConfig().isBackgroundSync()) { - instance.runInstanceJobWork(context, null); + instance.coreState.getPushProviders().runInstanceJobWork(context, null); } else { Logger.d("Instance doesn't allow Background sync, not running the Job"); } @@ -1164,7 +944,7 @@ public static void runBackgroundIntentService(Context context) { Logger.d(accountId, "Instance doesn't allow Background sync, not running the Job"); continue; } - instance.runInstanceJobWork(context, null); + instance.coreState.getPushProviders().runInstanceJobWork(context, null); } } @@ -1174,7 +954,7 @@ public static void runJobWork(Context context, JobParameters parameters) { CleverTapAPI instance = CleverTapAPI.getDefaultInstance(context); if (instance != null) { if (instance.getConfig().isBackgroundSync()) { - instance.runInstanceJobWork(context, parameters); + instance.coreState.getPushProviders().runInstanceJobWork(context, parameters); } else { Logger.d("Instance doesn't allow Background sync, not running the Job"); } @@ -1191,126 +971,71 @@ public static void runJobWork(Context context, JobParameters parameters) { Logger.d(accountId, "Instance doesn't allow Background sync, not running the Job"); continue; } - instance.runInstanceJobWork(context, parameters); + instance.coreState.getPushProviders().runInstanceJobWork(context, parameters); } } - /** - * Deprecation Notice - This method has been deprecated by CleverTap, this code will be removed from future - * versions of the CleverTap Android SDK. - * - * Disables/Enables the ability to do UI Edits from the CleverTap Dashboard - * Disabled by default - */ - @Deprecated - public static void setUIEditorConnectionEnabled(boolean enabled) { - isUIEditorEnabled = enabled; - } - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public static void tokenRefresh(Context context, String token, PushType pushType) { for (CleverTapAPI instance : getAvailableInstances(context)) { - if (instance == null || instance.getConfig().isAnalyticsOnly()) { - Logger.d(instance.getAccountId(), - "Instance is Analytics Only not processing device token"); - continue; - } - instance.doTokenRefresh(token, pushType); + instance.coreState.getPushProviders().doTokenRefresh(token, pushType); } } // Initialize private CleverTapAPI(final Context context, final CleverTapInstanceConfig config, String cleverTapID) { - this.config = new CleverTapInstanceConfig(config); this.context = context; - this.handlerUsingMainLooper = new Handler(Looper.getMainLooper()); - this.es = Executors.newFixedThreadPool(1); - this.ns = Executors.newFixedThreadPool(1); - this.localDataStore = new LocalDataStore(context, config); - validationResultStack = new ValidationResultStack(); - this.deviceInfo = new DeviceInfo(context, config, cleverTapID); - if (this.deviceInfo.getDeviceID() != null) { - Logger.v("Initializing InAppFC with device Id = " + this.deviceInfo.getDeviceID()); - this.inAppFCManager = new InAppFCManager(context, config, this.deviceInfo.getDeviceID()); - } - initFeatureFlags(false); - this.validator = new Validator(); - this.pushProviders = PushProviders.load(this); + CoreState coreState = CleverTapFactory + .getCoreState(context, config, cleverTapID); + setCoreState(coreState); - postAsyncSafely("CleverTapAPI#initializeDeviceInfo", new Runnable() { + Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask(); + task.execute("CleverTapAPI#initializeDeviceInfo", new Callable() { @Override - public void run() { - + public Void call() { if (config.isDefaultInstance()) { manifestAsyncValidation(); } + return null; } }); - int now = (int) System.currentTimeMillis() / 1000; - if (now - initialAppEnteredForegroundTime > 5) { - this.config.setCreatedPostAppLaunch(); + int now = Utils.getNow(); + if (now - CoreMetaData.getInitialAppEnteredForegroundTime() > 5) { + this.coreState.getConfig().setCreatedPostAppLaunch(); } - setLastVisitTime(); - - // Default (flag is set in the config init) or first non-default instance gets the ABTestController - if (!config.isDefaultInstance()) { - if (instances == null || instances.size() <= 0) { - config.setEnableABTesting(true); - } - } - initABTesting(); + coreState.getSessionManager().setLastVisitTime(); - postAsyncSafely("setStatesAsync", new Runnable() { + task = CTExecutorFactory.executors(config).postAsyncSafelyTask(); + task.execute("setStatesAsync", new Callable() { @Override - public void run() { - setDeviceNetworkInfoReportingFromStorage(); - setCurrentUserOptOutStateFromStorage(); + public Void call() { + CleverTapAPI.this.coreState.getDeviceInfo().setDeviceNetworkInfoReportingFromStorage(); + CleverTapAPI.this.coreState.getDeviceInfo().setCurrentUserOptOutStateFromStorage(); + return null; } }); - postAsyncSafely("saveConfigtoSharedPrefs", new Runnable() { + task = CTExecutorFactory.executors(config).postAsyncSafelyTask(); + task.execute("saveConfigtoSharedPrefs", new Callable() { @Override - public void run() { + public Void call() { String configJson = config.toJSONString(); if (configJson == null) { Logger.v("Unable to save config to SharedPrefs, config Json is null"); - return; + return null; } StorageHelper.putString(context, StorageHelper.storageKeyWithSuffix(config, "instance"), configJson); + return null; } }); - if (this.config.isBackgroundSync() && !this.config.isAnalyticsOnly()) { - postAsyncSafely("createOrResetJobScheduler", new Runnable() { - @Override - public void run() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - createOrResetJobScheduler(context); - } else { - createAlarmScheduler(context); - } - } - }); - } Logger.i("CleverTap SDK initialized with accountId: " + config.getAccountId() + " accountToken: " + config .getAccountToken() + " accountRegion: " + config.getAccountRegion()); } - @Override - public void ABExperimentsUpdated() { - try { - final CTExperimentsListener sl = getCTExperimentsListener(); - if (sl != null) { - sl.CTExperimentsUpdated(); - } - } catch (Throwable t) { - // no-op - } - } - /** * Add a unique value to a multi-value user profile property * If the property does not exist it will be created @@ -1327,7 +1052,7 @@ public void ABExperimentsUpdated() { @SuppressWarnings({"unused", "WeakerAccess"}) public void addMultiValueForKey(String key, String value) { if (value == null || value.isEmpty()) { - _generateEmptyMultiValueError(key); + coreState.getAnalyticsManager()._generateEmptyMultiValueError(key); return; } @@ -1349,26 +1074,7 @@ public void addMultiValueForKey(String key, String value) { */ @SuppressWarnings({"unused", "WeakerAccess"}) public void addMultiValuesForKey(final String key, final ArrayList values) { - postAsyncSafely("addMultiValuesForKey", new Runnable() { - @Override - public void run() { - final String command = (getLocalDataStore().getProfileValueForKey(key) != null) - ? Constants.COMMAND_ADD : Constants.COMMAND_SET; - _handleMultiValues(values, key, command); - } - }); - } - - @Override - @RestrictTo(Scope.LIBRARY) - public CleverTapInstanceConfig config() { - return config; - } - - @Override - @RestrictTo(Scope.LIBRARY) - public Context context() { - return context; + coreState.getAnalyticsManager().addMultiValuesForKey(key, values); } /** @@ -1378,21 +1084,11 @@ public Context context() { */ @SuppressWarnings({"unused"}) public void deleteInboxMessage(final CTInboxMessage message) { - postAsyncSafely("deleteInboxMessage", new Runnable() { - @Override - public void run() { - synchronized (inboxControllerLock) { - if (ctInboxController != null) { - boolean update = ctInboxController.deleteMessageWithId(message.getMessageId()); - if (update) { - _notifyInboxMessagesDidUpdate(); - } - } else { - getConfigLogger().debug(getAccountId(), "Notification Inbox not initialized"); - } - } - } - }); + if (coreState.getControllerManager().getCTInboxController() != null) { + coreState.getControllerManager().getCTInboxController().deleteInboxMessage(message); + } else { + getConfigLogger().debug(getAccountId(), "Notification Inbox not initialized"); + } } /** @@ -1406,19 +1102,13 @@ public void deleteInboxMessage(String messageId) { deleteInboxMessage(message); } - @RestrictTo(Scope.LIBRARY) - @Override - public DeviceInfo deviceInfo() { - return deviceInfo; - } - /** * Disables the Profile/Events Read and Synchronization API * Personalization is enabled by default */ @SuppressWarnings({"unused"}) public void disablePersonalization() { - this.config.enablePersonalization(false); + this.coreState.getConfig().enablePersonalization(false); } /** @@ -1429,11 +1119,7 @@ public void disablePersonalization() { */ @SuppressWarnings({"unused"}) public void enableDeviceNetworkInfoReporting(boolean value) { - enableNetworkInfoReporting = value; - StorageHelper.putBoolean(context, StorageHelper.storageKeyWithSuffix(config, Constants.NETWORK_INFO), - enableNetworkInfoReporting); - getConfigLogger() - .verbose(getAccountId(), "Device Network Information reporting set to " + enableNetworkInfoReporting); + coreState.getDeviceInfo().enableDeviceNetworkInfoReporting(value); } /** @@ -1442,75 +1128,18 @@ public void enableDeviceNetworkInfoReporting(boolean value) { */ @SuppressWarnings({"unused"}) public void enablePersonalization() { - this.config.enablePersonalization(true); + this.coreState.getConfig().enablePersonalization(true); } - //Push - /** * @return object of {@link CTFeatureFlagsController} * Handler to get the feature flag values */ public CTFeatureFlagsController featureFlag() { - return ctFeatureFlagsController; - } - - /** - * This method is internal to the CleverTap SDK. - * Developers should not use this method - */ - @Override - public void featureFlagsDidUpdate() { - try { - if (featureFlagsListener != null && featureFlagsListener.get() != null) { - featureFlagsListener.get().featureFlagsUpdated(); - } - } catch (Throwable t) { - // no-op - } - } - - /** - * This method is internal to the CleverTap SDK. - * Developers should not use this method manually - */ - @Override - public void fetchFeatureFlags() { - if (config.isAnalyticsOnly()) { - return; - } - JSONObject event = new JSONObject(); - JSONObject notif = new JSONObject(); - try { - notif.put("t", Constants.FETCH_TYPE_FF); - event.put("evtName", Constants.WZRK_FETCH); - event.put("evtData", notif); - } catch (JSONException e) { - e.printStackTrace(); - } - - queueEvent(context, event, Constants.FETCH_EVENT); - } - - /** - * This method is internal to CleverTap SDK. - * Developers should not use this method manually. - */ - @Override - public void fetchProductConfig() { - JSONObject event = new JSONObject(); - JSONObject notif = new JSONObject(); - try { - notif.put("t", Constants.FETCH_TYPE_PC); - event.put("evtName", Constants.WZRK_FETCH); - event.put("evtData", notif); - } catch (JSONException e) { - e.printStackTrace(); + if (getConfig().isAnalyticsOnly()) { + getConfig().getLogger().debug(getAccountId(),"Feature flag is not supported with analytics only configuration"); } - - queueEvent(context, event, Constants.FETCH_EVENT); - isProductConfigRequested = true; - getConfigLogger().verbose(getAccountId(), Constants.LOG_TAG_PRODUCT_CONFIG + "Fetching product config"); + return coreState.getControllerManager().getCTFeatureFlagsController(); } /** @@ -1518,11 +1147,11 @@ public void fetchProductConfig() { */ @SuppressWarnings({"unused", "WeakerAccess"}) public void flush() { - flushQueueAsync(context, EventGroup.REGULAR); + coreState.getBaseEventQueueManager().flush(); } public String getAccountId() { - return config.getAccountId(); + return coreState.getConfig().getAccountId(); } /** @@ -1532,8 +1161,9 @@ public String getAccountId() { */ @Nullable public ArrayList getAllDisplayUnits() { - if (mCTDisplayUnitController != null) { - return mCTDisplayUnitController.getAllDisplayUnits(); + + if (coreState.getControllerManager().getCTDisplayUnitController() != null) { + return coreState.getControllerManager().getCTDisplayUnitController().getAllDisplayUnits(); } else { getConfigLogger() .verbose(getAccountId(), Constants.FEATURE_DISPLAY_UNIT + "Failed to get all Display Units"); @@ -1541,8 +1171,6 @@ public ArrayList getAllDisplayUnits() { } } - //Push - /** * Returns an ArrayList of all {@link CTInboxMessage} objects * @@ -1551,9 +1179,10 @@ public ArrayList getAllDisplayUnits() { @SuppressWarnings({"unused", "WeakerAccess"}) public ArrayList getAllInboxMessages() { ArrayList inboxMessageArrayList = new ArrayList<>(); - synchronized (inboxControllerLock) { - if (ctInboxController != null) { - ArrayList messageDAOArrayList = ctInboxController.getMessages(); + synchronized (coreState.getCTLockManager().getInboxControllerLock()) { + if (coreState.getControllerManager().getCTInboxController() != null) { + ArrayList messageDAOArrayList = + coreState.getControllerManager().getCTInboxController().getMessages(); for (CTMessageDAO messageDAO : messageDAOArrayList) { Logger.v("CTMessage Dao - " + messageDAO.toJSON().toString()); inboxMessageArrayList.add(new CTInboxMessage(messageDAO.toJSON())); @@ -1566,46 +1195,7 @@ public ArrayList getAllInboxMessages() { } } - /** - * Deprecation Notice - This method has been deprecated by CleverTap, this code will be removed from future - * versions of the CleverTap Android SDK. - * - * Returns the {@link Boolean} value of the named variable set via an AB Testing Experiment or the default value - * if unset - * - * @param name - the name of the variable - * @param defaultValue - the default value to return if the value has not been set via an AB Testing Experiment - * @return {@link Boolean} the value set by the Experiment or the default value if unset - */ - @RequiresApi(api = VERSION_CODES.KITKAT) - @Deprecated - public Boolean getBooleanVariable(String name, Boolean defaultValue) { - if (ctABTestController == null) { - getConfigLogger().verbose(getAccountId(), "ABTesting is not enabled for this instance"); - return defaultValue; - } - return ctABTestController.getBooleanVariable(name, defaultValue); - } - - /** - * Returns the CTExperimentsListener object - * - * @return The {@link CTExperimentsListener} object - */ - @SuppressWarnings("WeakerAccess") - public CTExperimentsListener getCTExperimentsListener() { - return experimentsListener; - } - - /** - * This method is used to set the CTExperimentsListener - * - * @param experimentsListener The {@link CTExperimentsListener} object - */ - @SuppressWarnings("unused") - public void setCTExperimentsListener(CTExperimentsListener experimentsListener) { - this.experimentsListener = experimentsListener; - } + //Debug /** * Returns the CTInboxListener object @@ -1614,11 +1204,9 @@ public void setCTExperimentsListener(CTExperimentsListener experimentsListener) */ @SuppressWarnings({"unused"}) public CTInboxListener getCTNotificationInboxListener() { - return inboxListener; + return coreState.getCallbackManager().getInboxListener(); } - //Debug - /** * This method sets the CTInboxListener * @@ -1626,7 +1214,7 @@ public CTInboxListener getCTNotificationInboxListener() { */ @SuppressWarnings({"unused"}) public void setCTNotificationInboxListener(CTInboxListener notificationInboxListener) { - inboxListener = notificationInboxListener; + coreState.getCallbackManager().setInboxListener(notificationInboxListener); } /** @@ -1636,7 +1224,7 @@ public void setCTNotificationInboxListener(CTInboxListener notificationInboxList */ @SuppressWarnings("WeakerAccess") public CTPushAmpListener getCTPushAmpListener() { - return pushAmpListener; + return coreState.getCallbackManager().getPushAmpListener(); } /** @@ -1646,7 +1234,7 @@ public CTPushAmpListener getCTPushAmpListener() { */ @SuppressWarnings("unused") public void setCTPushAmpListener(CTPushAmpListener pushAmpListener) { - this.pushAmpListener = pushAmpListener; + coreState.getCallbackManager().setPushAmpListener(pushAmpListener); } /** @@ -1656,7 +1244,7 @@ public void setCTPushAmpListener(CTPushAmpListener pushAmpListener) { */ @SuppressWarnings("WeakerAccess") public CTPushNotificationListener getCTPushNotificationListener() { - return pushNotificationListener; + return coreState.getCallbackManager().getPushNotificationListener(); } /** @@ -1666,9 +1254,11 @@ public CTPushNotificationListener getCTPushNotificationListener() { */ @SuppressWarnings("unused") public void setCTPushNotificationListener(CTPushNotificationListener pushNotificationListener) { - this.pushNotificationListener = pushNotificationListener; + coreState.getCallbackManager().setPushNotificationListener(pushNotificationListener); } + //Network Info handling + /** * Returns a unique CleverTap identifier suitable for use with install attribution providers. * @@ -1676,11 +1266,9 @@ public void setCTPushNotificationListener(CTPushNotificationListener pushNotific */ @SuppressWarnings("unused") public String getCleverTapAttributionIdentifier() { - return this.deviceInfo.getAttributionID(); + return coreState.getDeviceInfo().getAttributionID(); } - //Network Info handling - /** * Returns a unique identifier by which CleverTap identifies this user. * @@ -1688,7 +1276,16 @@ public String getCleverTapAttributionIdentifier() { */ @SuppressWarnings({"unused", "WeakerAccess"}) public String getCleverTapID() { - return this.deviceInfo.getDeviceID(); + return coreState.getDeviceInfo().getDeviceID(); + } + + @RestrictTo(Scope.LIBRARY) + public CoreState getCoreState() { + return coreState; + } + + void setCoreState(final CoreState cleverTapState) { + coreState = cleverTapState; } /** @@ -1699,7 +1296,7 @@ public String getCleverTapID() { */ @SuppressWarnings({"unused"}) public int getCount(String event) { - EventDetail eventDetail = getLocalDataStore().getEventDetail(event); + EventDetail eventDetail = coreState.getLocalDataStore().getEventDetail(event); if (eventDetail != null) { return eventDetail.getCount(); } @@ -1717,27 +1314,7 @@ public int getCount(String event) { */ @SuppressWarnings({"unused"}) public EventDetail getDetails(String event) { - return getLocalDataStore().getEventDetail(event); - } - - // OptOut handling - - public Map getDeviceInfo() { - final Map deviceInfo = new HashMap<>(); - deviceInfo.put("build", String.valueOf(this.deviceInfo.getBuild())); - deviceInfo.put("versionName", this.deviceInfo.getVersionName()); - deviceInfo.put("osName", this.deviceInfo.getOsName()); - deviceInfo.put("osVersion", this.deviceInfo.getOsVersion()); - deviceInfo.put("manufacturer", this.deviceInfo.getManufacturer()); - deviceInfo.put("model", this.deviceInfo.getModel()); - deviceInfo.put("sdkVersion", String.valueOf(this.deviceInfo.getSdkVersion())); - deviceInfo.put("dpi", String.valueOf(this.deviceInfo.getDPI())); - deviceInfo.put("device_width", String.valueOf(this.deviceInfo.getWidthPixels())); - deviceInfo.put("device_height", String.valueOf(this.deviceInfo.getHeightPixels())); - if (this.deviceInfo.getLibrary() != null) { - deviceInfo.put("library", this.deviceInfo.getLibrary()); - } - return deviceInfo; + return coreState.getLocalDataStore().getEventDetail(event); } /** @@ -1752,9 +1329,11 @@ public Map getDeviceInfo() { */ @SuppressWarnings("unused") public String getDevicePushToken(final PushType type) { - return pushProviders.getCachedToken(type); + return coreState.getPushProviders().getCachedToken(type); } + //Util + /** * Returns the DevicePushTokenRefreshListener * @@ -1762,7 +1341,7 @@ public String getDevicePushToken(final PushType type) { */ @SuppressWarnings("unused") public DevicePushTokenRefreshListener getDevicePushTokenRefreshListener() { - return tokenRefreshListener; + return coreState.getPushProviders().getDevicePushTokenRefreshListener(); } /** @@ -1772,7 +1351,8 @@ public DevicePushTokenRefreshListener getDevicePushTokenRefreshListener() { */ @SuppressWarnings("unused") public void setDevicePushTokenRefreshListener(DevicePushTokenRefreshListener tokenRefreshListener) { - this.tokenRefreshListener = tokenRefreshListener; + coreState.getPushProviders().setDevicePushTokenRefreshListener(tokenRefreshListener); + } /** @@ -1783,8 +1363,8 @@ public void setDevicePushTokenRefreshListener(DevicePushTokenRefreshListener tok */ @Nullable public CleverTapDisplayUnit getDisplayUnitForId(String unitID) { - if (mCTDisplayUnitController != null) { - return mCTDisplayUnitController.getDisplayUnitForID(unitID); + if (coreState.getControllerManager().getCTDisplayUnitController() != null) { + return coreState.getControllerManager().getCTDisplayUnitController().getDisplayUnitForID(unitID); } else { getConfigLogger().verbose(getAccountId(), Constants.FEATURE_DISPLAY_UNIT + "Failed to get Display Unit for id: " + unitID); @@ -1793,35 +1373,14 @@ public CleverTapDisplayUnit getDisplayUnitForId(String unitID) { } /** - * Deprecation Notice - This method has been deprecated by CleverTap, this code will be removed from future - * versions of the CleverTap Android SDK. - * - * Returns the {@link Double} value of the named variable set via an AB Testing Experiment or the default value if - * unset - * - * @param name - the name of the variable - * @param defaultValue - the default value to return if the value has not been set via an AB Testing Experiment - * @return {@link Double} the value set by the Experiment or the default value if unset - */ - @RequiresApi(api = VERSION_CODES.KITKAT) - @Deprecated - public Double getDoubleVariable(String name, Double defaultValue) { - if (ctABTestController == null) { - getConfigLogger().verbose(getAccountId(), "ABTesting is not enabled for this instance"); - return defaultValue; - } - return ctABTestController.getDoubleVariable(name, defaultValue); - } - - /** - * Returns the timestamp of the first time the given event was raised + * Returns the timestamp of the first time the given event was raised * * @param event The event name for which you want the first time timestamp * @return The timestamp in int */ @SuppressWarnings({"unused"}) public int getFirstTime(String event) { - EventDetail eventDetail = getLocalDataStore().getEventDetail(event); + EventDetail eventDetail = coreState.getLocalDataStore().getEventDetail(event); if (eventDetail != null) { return eventDetail.getFirstTime(); } @@ -1829,8 +1388,6 @@ public int getFirstTime(String event) { return -1; } - //Util - /** * Returns the GeofenceCallback object * @@ -1838,9 +1395,11 @@ public int getFirstTime(String event) { */ @SuppressWarnings("unused") public GeofenceCallback getGeofenceCallback() { - return this.geofenceCallback; + return coreState.getCallbackManager().getGeofenceCallback(); } + //DeepLink + /** * This method is used to set the geofence callback * Register to handle geofence responses from CleverTap @@ -1851,7 +1410,7 @@ public GeofenceCallback getGeofenceCallback() { @SuppressWarnings("unused") public void setGeofenceCallback(GeofenceCallback geofenceCallback) { - this.geofenceCallback = geofenceCallback; + coreState.getCallbackManager().setGeofenceCallback(geofenceCallback); } /** @@ -1861,11 +1420,9 @@ public void setGeofenceCallback(GeofenceCallback geofenceCallback) { */ @SuppressWarnings({"unused"}) public Map getHistory() { - return getLocalDataStore().getEventHistory(context); + return coreState.getLocalDataStore().getEventHistory(context); } - //Util - /** * Returns the InAppNotificationListener object * @@ -1873,11 +1430,9 @@ public Map getHistory() { */ @SuppressWarnings({"unused", "WeakerAccess"}) public InAppNotificationListener getInAppNotificationListener() { - return inAppNotificationListener; + return coreState.getCallbackManager().getInAppNotificationListener(); } - //Util - /** * This method sets the InAppNotificationListener * @@ -1885,11 +1440,9 @@ public InAppNotificationListener getInAppNotificationListener() { */ @SuppressWarnings({"unused"}) public void setInAppNotificationListener(InAppNotificationListener inAppNotificationListener) { - this.inAppNotificationListener = inAppNotificationListener; + coreState.getCallbackManager().setInAppNotificationListener(inAppNotificationListener); } - //DeepLink - /** * Returns the count of all inbox messages for the user * @@ -1897,9 +1450,9 @@ public void setInAppNotificationListener(InAppNotificationListener inAppNotifica */ @SuppressWarnings({"unused", "WeakerAccess"}) public int getInboxMessageCount() { - synchronized (inboxControllerLock) { - if (this.ctInboxController != null) { - return ctInboxController.count(); + synchronized (coreState.getCTLockManager().getInboxControllerLock()) { + if (coreState.getControllerManager().getCTInboxController() != null) { + return coreState.getControllerManager().getCTInboxController().count(); } else { getConfigLogger().debug(getAccountId(), "Notification Inbox not initialized"); return -1; @@ -1915,9 +1468,10 @@ public int getInboxMessageCount() { */ @SuppressWarnings({"unused", "WeakerAccess"}) public CTInboxMessage getInboxMessageForId(String messageId) { - synchronized (inboxControllerLock) { - if (this.ctInboxController != null) { - CTMessageDAO message = ctInboxController.getMessageForId(messageId); + synchronized (coreState.getCTLockManager().getInboxControllerLock()) { + if (coreState.getControllerManager().getCTInboxController() != null) { + CTMessageDAO message = coreState.getControllerManager().getCTInboxController() + .getMessageForId(messageId); return (message != null) ? new CTInboxMessage(message.toJSON()) : null; } else { getConfigLogger().debug(getAccountId(), "Notification Inbox not initialized"); @@ -1933,9 +1487,9 @@ public CTInboxMessage getInboxMessageForId(String messageId) { */ @SuppressWarnings({"unused"}) public int getInboxMessageUnreadCount() { - synchronized (inboxControllerLock) { - if (this.ctInboxController != null) { - return ctInboxController.unreadCount(); + synchronized (coreState.getCTLockManager().getInboxControllerLock()) { + if (coreState.getControllerManager().getCTInboxController() != null) { + return coreState.getControllerManager().getCTInboxController().unreadCount(); } else { getConfigLogger().debug(getAccountId(), "Notification Inbox not initialized"); return -1; @@ -1943,27 +1497,6 @@ public int getInboxMessageUnreadCount() { } } - /** - * Deprecation Notice - This method has been deprecated by CleverTap, this code will be removed from future - * versions of the CleverTap Android SDK. - * - * Returns the {@link Integer} value of the named variable set via an AB Testing Experiment or the default value - * if unset - * - * @param name - the name of the variable - * @param defaultValue - the default value to return if the value has not been set via an AB Testing Experiment - * @return {@link Integer} the value set by the Experiment or the default value if unset - */ - @RequiresApi(api = VERSION_CODES.KITKAT) - @SuppressWarnings({"unused"}) - public Integer getIntegerVariable(String name, Integer defaultValue) { - if (ctABTestController == null) { - getConfigLogger().verbose(getAccountId(), "ABTesting is not enabled for this instance"); - return defaultValue; - } - return ctABTestController.getIntegerVariable(name, defaultValue); - } - /** * Returns the timestamp of the last time the given event was raised * @@ -1972,7 +1505,7 @@ public Integer getIntegerVariable(String name, Integer defaultValue) { */ @SuppressWarnings({"unused"}) public int getLastTime(String event) { - EventDetail eventDetail = getLocalDataStore().getEventDetail(event); + EventDetail eventDetail = coreState.getLocalDataStore().getEventDetail(event); if (eventDetail != null) { return eventDetail.getLastTime(); } @@ -1980,92 +1513,6 @@ public int getLastTime(String event) { return -1; } - /** - * Deprecation Notice - This method has been deprecated by CleverTap, this code will be removed from future - * versions of the CleverTap Android SDK. - * - * Returns the {@link List} of {@link Boolean} value of the named variable set via an AB Testing Experiment or the - * default value if unset - * - * @param name - the name of the variable - * @param defaultValue - the default value to return if the value has not been set via an AB Testing Experiment - * @return {@link List} of {@link Boolean} the value set by the Experiment or the default value if unset - */ - @RequiresApi(api = VERSION_CODES.KITKAT) - @Deprecated - public List getListOfBooleanVariable(String name, List defaultValue) { - if (ctABTestController == null) { - getConfigLogger().verbose(getAccountId(), "ABTesting is not enabled for this instance"); - return defaultValue; - } - return ctABTestController.getListOfBooleanVariable(name, defaultValue); - - } - - /** - * Deprecation Notice - This method has been deprecated by CleverTap, this code will be removed from future - * versions of the CleverTap Android SDK. - * - * Returns the {@link List} of {@link Double} value of the named variable set via an AB Testing Experiment or the - * default value if unset - * - * @param name - the name of the variable - * @param defaultValue - the default value to return if the value has not been set via an AB Testing Experiment - * @return {@link List} of {@link Double} the value set by the Experiment or the default value if unset - */ - @RequiresApi(api = VERSION_CODES.KITKAT) - @Deprecated - public List getListOfDoubleVariable(String name, List defaultValue) { - if (ctABTestController == null) { - getConfigLogger().verbose(getAccountId(), "ABTesting is not enabled for this instance"); - return defaultValue; - } - return ctABTestController.getListOfDoubleVariable(name, defaultValue); - - } - - /** - * Deprecation Notice - This method has been deprecated by CleverTap, this code will be removed from future - * versions of the CleverTap Android SDK. - * - * Returns the {@link List} of {@link Integer} value of the named variable set via an AB Testing Experiment or the - * default value if unset - * - * @param name - the name of the variable - * @param defaultValue - the default value to return if the value has not been set via an AB Testing Experiment - * @return {@link List} of {@link Integer} the value set by the Experiment or the default value if unset - */ - @RequiresApi(api = VERSION_CODES.KITKAT) - @Deprecated - public List getListOfIntegerVariable(String name, List defaultValue) { - if (ctABTestController == null) { - getConfigLogger().verbose(getAccountId(), "ABTesting is not enabled for this instance"); - return defaultValue; - } - return ctABTestController.getListOfIntegerVariable(name, defaultValue); - } - - /** - * Deprecation Notice - This method has been deprecated by CleverTap, this code will be removed from future - * versions of the CleverTap Android SDK. - * - * Returns the {@link List} of {@link String} value of the named variable set via an AB Testing Experiment or the - * default value if unset - * - * @param name - the name of the variable - * @param defaultValue - the default value to return if the value has not been set via an AB Testing Experiment - * @return {@link List} of {@link String} the value set by the Experiment or the default value if unset - */ - @RequiresApi(api = VERSION_CODES.KITKAT) - @Deprecated - public List getListOfStringVariable(String name, List defaultValue) { - if (ctABTestController == null) { - getConfigLogger().verbose(getAccountId(), "ABTesting is not enabled for this instance"); - return defaultValue; - } - return ctABTestController.getListOfStringVariable(name, defaultValue); - } - /** * get the current device location * requires Location Permission in AndroidManifest e.g. "android.permission.ACCESS_COARSE_LOCATION" @@ -2076,7 +1523,7 @@ public List getListOfStringVariable(String name, List defaultVal */ @SuppressWarnings({"unused"}) public Location getLocation() { - return _getLocation(); + return coreState.getLocationManager()._getLocation(); } /** @@ -2087,91 +1534,7 @@ public Location getLocation() { */ @SuppressWarnings({"unused", "WeakerAccess"}) public void setLocation(Location location) { - _setLocation(location); - } - - /** - * Deprecation Notice - This method has been deprecated by CleverTap, this code will be removed from future - * versions of the CleverTap Android SDK. - * - * Returns the {@link Map} of {@link Boolean} value of the named variable set via an AB Testing Experiment or the - * default value if unset - * - * @param name - the name of the variable - * @param defaultValue - the default value to return if the value has not been set via an AB Testing Experiment - * @return {@link Map} of {@link Boolean} the value set by the Experiment or the default value if unset - */ - @RequiresApi(api = VERSION_CODES.KITKAT) - @Deprecated - public Map getMapOfBooleanVariable(String name, Map defaultValue) { - if (ctABTestController == null) { - getConfigLogger().verbose(getAccountId(), "ABTesting is not enabled for this instance"); - return defaultValue; - } - return ctABTestController.getMapOfBooleanVariable(name, defaultValue); - } - - /** - * Deprecation Notice - This method has been deprecated by CleverTap, this code will be removed from future - * versions of the CleverTap Android SDK. - * - * Returns the {@link Map} of {@link Double} value of the named variable set via an AB Testing Experiment or the - * default value if unset - * - * @param name - the name of the variable - * @param defaultValue - the default value to return if the value has not been set via an AB Testing Experiment - * @return {@link Map} of {@link Double} the value set by the Experiment or the default value if unset - */ - @RequiresApi(api = VERSION_CODES.KITKAT) - @Deprecated - public Map getMapOfDoubleVariable(String name, Map defaultValue) { - if (ctABTestController == null) { - getConfigLogger().verbose(getAccountId(), "ABTesting is not enabled for this instance"); - return defaultValue; - } - return ctABTestController.getMapOfDoubleVariable(name, defaultValue); - } - - /** - * Deprecation Notice - This method has been deprecated by CleverTap, this code will be removed from future - * versions of the CleverTap Android SDK. - * - * Returns the {@link Map} of {@link Integer} value of the named variable set via an AB Testing Experiment or the - * default value if unset - * - * @param name - the name of the variable - * @param defaultValue - the default value to return if the value has not been set via an AB Testing Experiment - * @return {@link Map} of {@link Integer} the value set by the Experiment or the default value if unset - */ - @RequiresApi(api = VERSION_CODES.KITKAT) - @Deprecated - public Map getMapOfIntegerVariable(String name, Map defaultValue) { - if (ctABTestController == null) { - getConfigLogger().verbose(getAccountId(), "ABTesting is not enabled for this instance"); - return defaultValue; - } - return ctABTestController.getMapOfIntegerVariable(name, defaultValue); - } - - /** - * Deprecation Notice - This method has been deprecated by CleverTap, this code will be removed from future - * versions of the CleverTap Android SDK. - * - * Returns the {@link Map} of {@link String} value of the named variable set via an AB Testing Experiment or the - * default value if unset - * - * @param name - the name of the variable - * @param defaultValue - the default value to return if the value has not been set via an AB Testing Experiment - * @return {@link Map} of {@link String} the value set by the Experiment or the default value if unset - */ - @RequiresApi(api = VERSION_CODES.KITKAT) - @Deprecated - public Map getMapOfStringVariable(String name, Map defaultValue) { - if (ctABTestController == null) { - getConfigLogger().verbose(getAccountId(), "ABTesting is not enabled for this instance"); - return defaultValue; - } - return ctABTestController.getMapOfStringVariable(name, defaultValue); + coreState.getLocationManager()._setLocation(location); } /** @@ -2181,7 +1544,7 @@ public Map getMapOfStringVariable(String name, Map getUnreadInboxMessages() { ArrayList inboxMessageArrayList = new ArrayList<>(); - synchronized (inboxControllerLock) { - if (ctInboxController != null) { - ArrayList messageDAOArrayList = ctInboxController.getUnreadMessages(); + synchronized (coreState.getCTLockManager().getInboxControllerLock()) { + if (coreState.getControllerManager().getCTInboxController() != null) { + ArrayList messageDAOArrayList = + coreState.getControllerManager().getCTInboxController().getUnreadMessages(); for (CTMessageDAO messageDAO : messageDAOArrayList) { inboxMessageArrayList.add(new CTInboxMessage(messageDAO.toJSON())); } @@ -2323,81 +1666,13 @@ public ArrayList getUnreadInboxMessages() { } } - @Override - public void inAppNotificationDidClick(CTInAppNotification inAppNotification, Bundle formData, - HashMap keyValueMap) { - pushInAppNotificationStateEvent(true, inAppNotification, formData); - if (keyValueMap != null && !keyValueMap.isEmpty()) { - if (inAppNotificationButtonListener != null && inAppNotificationButtonListener.get() != null) { - inAppNotificationButtonListener.get().onInAppButtonClick(keyValueMap); - } - } - } - - @Override - public void inAppNotificationDidDismiss(final Context context, final CTInAppNotification inAppNotification, - Bundle formData) { - inAppNotification.didDismiss(); - if (inAppFCManager != null) { - inAppFCManager.didDismiss(inAppNotification); - getConfigLogger().verbose(getAccountId(), "InApp Dismissed: " + inAppNotification.getCampaignId()); - } - try { - final InAppNotificationListener listener = getInAppNotificationListener(); - if (listener != null) { - final HashMap notifKVS; - - if (inAppNotification.getCustomExtras() != null) { - notifKVS = Utils.convertJSONObjectToHashMap(inAppNotification.getCustomExtras()); - } else { - notifKVS = new HashMap<>(); - } - - Logger.v("Calling the in-app listener on behalf of " + source); - - if (formData != null) { - listener.onDismissed(notifKVS, Utils.convertBundleObjectToHashMap(formData)); - } else { - listener.onDismissed(notifKVS, null); - } - } - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), "Failed to call the in-app notification listener", t); - } - - // Fire the next one, if any - runOnNotificationQueue(new Runnable() { - @Override - public void run() { - inAppDidDismiss(context, getConfig(), inAppNotification); - _showNotificationIfAvailable(context); - } - }); - } - - //InApp - @Override - public void inAppNotificationDidShow(CTInAppNotification inAppNotification, Bundle formData) { - pushInAppNotificationStateEvent(false, inAppNotification, formData); - } - /** * Initializes the inbox controller and sends a callback to the {@link CTInboxListener} * This method needs to be called separately for each instance of {@link CleverTapAPI} */ @SuppressWarnings({"unused"}) public void initializeInbox() { - if (getConfig().isAnalyticsOnly()) { - getConfigLogger() - .debug(getAccountId(), "Instance is analytics only, not initializing Notification Inbox"); - return; - } - postAsyncSafely("initializeInbox", new Runnable() { - @Override - public void run() { - _initializeInbox(); - } - }); + coreState.getControllerManager().initializeInbox(); } /** @@ -2408,23 +1683,15 @@ public void run() { //marks the message as read @SuppressWarnings({"unused", "WeakerAccess"}) public void markReadInboxMessage(final CTInboxMessage message) { - postAsyncSafely("markReadInboxMessage", new Runnable() { - @Override - public void run() { - synchronized (inboxControllerLock) { - if (ctInboxController != null) { - boolean read = ctInboxController.markReadForMessageWithId(message.getMessageId()); - if (read) { - _notifyInboxMessagesDidUpdate(); - } - } else { - getConfigLogger().debug(getAccountId(), "Notification Inbox not initialized"); - } - } - } - }); + if (coreState.getControllerManager().getCTInboxController() != null) { + coreState.getControllerManager().getCTInboxController().markReadInboxMessage(message); + } else { + getConfigLogger().debug(getAccountId(), "Notification Inbox not initialized"); + } } + //Session + /** * Marks the given messageId of {@link CTInboxMessage} object as read * @@ -2439,7 +1706,7 @@ public void markReadInboxMessage(String messageId) { @Override public void messageDidClick(CTInboxActivity ctInboxActivity, CTInboxMessage inboxMessage, Bundle data, HashMap keyValue) { - pushInboxMessageStateEvent(true, inboxMessage, data); + coreState.getAnalyticsManager().pushInboxMessageStateEvent(true, inboxMessage, data); if (keyValue != null && !keyValue.isEmpty()) { if (inboxMessageButtonListener != null && inboxMessageButtonListener.get() != null) { inboxMessageButtonListener.get().onInboxButtonClick(keyValue); @@ -2450,84 +1717,20 @@ public void messageDidClick(CTInboxActivity ctInboxActivity, CTInboxMessage inbo @Override public void messageDidShow(CTInboxActivity ctInboxActivity, final CTInboxMessage inboxMessage, final Bundle data) { - postAsyncSafely("handleMessageDidShow", new Runnable() { + Task task = CTExecutorFactory.executors(coreState.getConfig()).postAsyncSafelyTask(); + task.execute("handleMessageDidShow", new Callable() { @Override - public void run() { + public Void call() { CTInboxMessage message = getInboxMessageForId(inboxMessage.getMessageId()); if (!message.isRead()) { markReadInboxMessage(inboxMessage); - pushInboxMessageStateEvent(false, inboxMessage, data); + coreState.getAnalyticsManager().pushInboxMessageStateEvent(false, inboxMessage, data); } + return null; } }); } - //InApp - @Override - public void notificationReady(final CTInAppNotification inAppNotification) { - if (Looper.myLooper() != Looper.getMainLooper()) { - getHandlerUsingMainLooper().post(new Runnable() { - @Override - public void run() { - notificationReady(inAppNotification); - } - }); - return; - } - - if (inAppNotification.getError() != null) { - getConfigLogger() - .debug(getAccountId(), "Unable to process inapp notification " + inAppNotification.getError()); - return; - } - getConfigLogger().debug(getAccountId(), "Notification ready: " + inAppNotification.getJsonDescription()); - displayNotification(inAppNotification); - } - - /** - * This method is internal to CleverTap SDK. - * Developers should not use this method manually. - */ - @Override - public void onActivated() { - if (productConfigListener != null && productConfigListener.get() != null) { - productConfigListener.get().onActivated(); - } - } - - //Session - - /** - * This method is internal to CleverTap SDK. - * Developer should not use this method manually. - */ - @Override - public void onFetched() { - if (productConfigListener != null && productConfigListener.get() != null) { - productConfigListener.get().onFetched(); - } - } - - /** - * This method is internal to CleverTap SDK. - * Developers should not use this method manually. - */ - @Override - public void onInit() { - if (productConfigListener != null && productConfigListener.get() != null) { - getConfigLogger().verbose(config.getAccountId(), "Product Config initialized"); - productConfigListener.get().onInit(); - } - } - - @Override - public void onNewToken(String freshToken, PushType pushType) { - if (!TextUtils.isEmpty(freshToken)) { - doTokenRefresh(freshToken, pushType); - deviceTokenDidRefresh(freshToken, pushType); - } - } - /** * Creates a separate and distinct user profile identified by one or more of Identity, * Email, FBID or GPID values, @@ -2560,18 +1763,7 @@ public void onNewToken(String freshToken, PushType pushType) { */ @SuppressWarnings({"unused", "WeakerAccess"}) public void onUserLogin(final Map profile, final String cleverTapID) { - if (getConfig().getEnableCustomCleverTapId()) { - if (cleverTapID == null) { - Logger.i( - "CLEVERTAP_USE_CUSTOM_ID has been specified in the AndroidManifest.xml Please call onUserlogin() and pass a custom CleverTap ID"); - } - } else { - if (cleverTapID != null) { - Logger.i( - "CLEVERTAP_USE_CUSTOM_ID has not been specified in the AndroidManifest.xml Please call CleverTapAPI.defaultInstance() without a custom CleverTap ID"); - } - } - _onUserLogin(profile, cleverTapID); + coreState.getLoginController().onUserLogin(profile, cleverTapID); } /** @@ -2615,10 +1807,11 @@ public void onUserLogin(final Map profile) { */ @SuppressWarnings("WeakerAccess") public CTProductConfigController productConfig() { - if (ctProductConfigController == null) { - initProductConfig(false); + if (getConfig().isAnalyticsOnly()) { + getConfig().getLogger().debug(getAccountId(), + "Product config is not supported with analytics only configuration"); } - return ctProductConfigController; + return coreState.getCtProductConfigController(); } /** @@ -2632,7 +1825,7 @@ public CTProductConfigController productConfig() { */ @SuppressWarnings("unused") public void pushBaiduRegistrationId(String regId, boolean register) { - pushProviders.handleToken(regId, PushType.BPS, register); + coreState.getPushProviders().handleToken(regId, PushType.BPS, register); } /** @@ -2647,93 +1840,7 @@ public void pushBaiduRegistrationId(String regId, boolean register) { @SuppressWarnings({"unused"}) public void pushChargedEvent(HashMap chargeDetails, ArrayList> items) { - - if (chargeDetails == null || items == null) { - getConfigLogger().debug(getAccountId(), "Invalid Charged event: details and or items is null"); - return; - } - - if (items.size() > 50) { - ValidationResult error = ValidationResultFactory.create(522); - getConfigLogger().debug(getAccountId(), error.getErrorDesc()); - validationResultStack.pushValidationResult(error); - } - - JSONObject evtData = new JSONObject(); - JSONObject chargedEvent = new JSONObject(); - ValidationResult vr; - try { - for (String key : chargeDetails.keySet()) { - Object value = chargeDetails.get(key); - vr = validator.cleanObjectKey(key); - key = vr.getObject().toString(); - // Check for an error - if (vr.getErrorCode() != 0) { - chargedEvent.put(Constants.ERROR_KEY, getErrorObject(vr)); - } - - try { - vr = validator.cleanObjectValue(value, Validator.ValidationContext.Event); - } catch (IllegalArgumentException e) { - // The object was neither a String, Boolean, or any number primitives - ValidationResult error = ValidationResultFactory.create(511, - Constants.PROP_VALUE_NOT_PRIMITIVE, "Charged", key, - value != null ? value.toString() : ""); - validationResultStack.pushValidationResult(error); - getConfigLogger().debug(getAccountId(), error.getErrorDesc()); - // Skip this property - continue; - } - value = vr.getObject(); - // Check for an error - if (vr.getErrorCode() != 0) { - chargedEvent.put(Constants.ERROR_KEY, getErrorObject(vr)); - } - - evtData.put(key, value); - } - - JSONArray jsonItemsArray = new JSONArray(); - for (HashMap map : items) { - JSONObject itemDetails = new JSONObject(); - for (String key : map.keySet()) { - Object value = map.get(key); - vr = validator.cleanObjectKey(key); - key = vr.getObject().toString(); - // Check for an error - if (vr.getErrorCode() != 0) { - chargedEvent.put(Constants.ERROR_KEY, getErrorObject(vr)); - } - - try { - vr = validator.cleanObjectValue(value, Validator.ValidationContext.Event); - } catch (IllegalArgumentException e) { - // The object was neither a String, Boolean, or any number primitives - ValidationResult error = ValidationResultFactory - .create(511, Constants.OBJECT_VALUE_NOT_PRIMITIVE, key, - value != null ? value.toString() : ""); - getConfigLogger().debug(getAccountId(), error.getErrorDesc()); - validationResultStack.pushValidationResult(error); - // Skip this property - continue; - } - value = vr.getObject(); - // Check for an error - if (vr.getErrorCode() != 0) { - chargedEvent.put(Constants.ERROR_KEY, getErrorObject(vr)); - } - itemDetails.put(key, value); - } - jsonItemsArray.put(itemDetails); - } - evtData.put("Items", jsonItemsArray); - - chargedEvent.put("evtName", Constants.CHARGED_EVENT); - chargedEvent.put("evtData", evtData); - queueEvent(context, chargedEvent, Constants.RAISED_EVENT); - } catch (Throwable t) { - // We won't get here - } + coreState.getAnalyticsManager().pushChargedEvent(chargeDetails, items); } /** @@ -2743,35 +1850,7 @@ public void pushChargedEvent(HashMap chargeDetails, */ @SuppressWarnings({"unused", "WeakerAccess"}) public void pushDeepLink(Uri uri) { - pushDeepLink(uri, false); - } - - @RestrictTo(RestrictTo.Scope.LIBRARY) - @Override - public void pushDeviceTokenEvent(String token, boolean register, PushType pushType) { - if (pushType == null) { - return; - } - token = !TextUtils.isEmpty(token) ? token : pushProviders.getCachedToken(pushType); - if (TextUtils.isEmpty(token)) { - return; - } - synchronized (tokenLock) { - JSONObject event = new JSONObject(); - JSONObject data = new JSONObject(); - String action = register ? "register" : "unregister"; - try { - data.put("action", action); - data.put("id", token); - data.put("type", pushType.getType()); - event.put("data", data); - getConfigLogger().verbose(getAccountId(), pushType + action + " device token " + token); - queueEvent(context, event, Constants.DATA_EVENT); - } catch (Throwable t) { - // we won't get here - getConfigLogger().verbose(getAccountId(), pushType + action + " device token failed", t); - } - } + coreState.getAnalyticsManager().pushDeepLink(uri, false); } /** @@ -2781,33 +1860,7 @@ public void pushDeviceTokenEvent(String token, boolean register, PushType pushTy */ @SuppressWarnings("unused") public void pushDisplayUnitClickedEventForID(String unitID) { - JSONObject event = new JSONObject(); - - try { - event.put("evtName", Constants.NOTIFICATION_CLICKED_EVENT_NAME); - - //wzrk fields - if (mCTDisplayUnitController != null) { - CleverTapDisplayUnit displayUnit = mCTDisplayUnitController.getDisplayUnitForID(unitID); - if (displayUnit != null) { - JSONObject eventExtraData = displayUnit.getWZRKFields(); - if (eventExtraData != null) { - event.put("evtData", eventExtraData); - try { - setWzrkParams(eventExtraData); - } catch (Throwable t) { - // no-op - } - } - } - } - - queueEvent(context, event, Constants.RAISED_EVENT); - } catch (Throwable t) { - // We won't get here - getConfigLogger().verbose(getAccountId(), - Constants.FEATURE_DISPLAY_UNIT + "Failed to push Display Unit clicked event" + t); - } + coreState.getAnalyticsManager().pushDisplayUnitClickedEventForID(unitID); } /** @@ -2817,28 +1870,7 @@ public void pushDisplayUnitClickedEventForID(String unitID) { */ @SuppressWarnings("unused") public void pushDisplayUnitViewedEventForID(String unitID) { - JSONObject event = new JSONObject(); - - try { - event.put("evtName", Constants.NOTIFICATION_VIEWED_EVENT_NAME); - - //wzrk fields - if (mCTDisplayUnitController != null) { - CleverTapDisplayUnit displayUnit = mCTDisplayUnitController.getDisplayUnitForID(unitID); - if (displayUnit != null) { - JSONObject eventExtras = displayUnit.getWZRKFields(); - if (eventExtras != null) { - event.put("evtData", eventExtras); - } - } - } - - queueEvent(context, event, Constants.RAISED_EVENT); - } catch (Throwable t) { - // We won't get here - getConfigLogger().verbose(getAccountId(), - Constants.FEATURE_DISPLAY_UNIT + "Failed to push Display Unit viewed event" + t); - } + coreState.getAnalyticsManager().pushDisplayUnitViewedEventForID(unitID); } /** @@ -2849,23 +1881,7 @@ public void pushDisplayUnitViewedEventForID(String unitID) { */ @SuppressWarnings({"unused"}) public void pushError(final String errorMessage, final int errorCode) { - final HashMap props = new HashMap<>(); - props.put("Error Message", errorMessage); - props.put("Error Code", errorCode); - - try { - final String activityName = getCurrentActivityName(); - if (activityName != null) { - props.put("Location", activityName); - } else { - props.put("Location", "Unknown"); - } - } catch (Throwable t) { - // Ignore - props.put("Location", "Unknown"); - } - - pushEvent("Error Occurred", props); + coreState.getAnalyticsManager().pushError(errorMessage, errorCode); } /** @@ -2878,7 +1894,6 @@ public void pushEvent(String eventName) { if (eventName == null || eventName.trim().equals("")) { return; } - pushEvent(eventName, null); } @@ -2892,107 +1907,22 @@ public void pushEvent(String eventName) { */ @SuppressWarnings({"unused", "WeakerAccess"}) public void pushEvent(String eventName, Map eventActions) { + coreState.getAnalyticsManager().pushEvent(eventName, eventActions); + } - if (eventName == null || eventName.equals("")) { - return; - } - - ValidationResult validationResult = validator.isRestrictedEventName(eventName); - // Check for a restricted event name - if (validationResult.getErrorCode() > 0) { - validationResultStack.pushValidationResult(validationResult); - return; - } - - ValidationResult discardedResult = validator.isEventDiscarded(eventName); - // Check for a discarded event name - if (discardedResult.getErrorCode() > 0) { - validationResultStack.pushValidationResult(discardedResult); - return; - } - - if (eventActions == null) { - eventActions = new HashMap<>(); - } - - JSONObject event = new JSONObject(); - try { - // Validate - ValidationResult vr = validator.cleanEventName(eventName); - - // Check for an error - if (vr.getErrorCode() != 0) { - event.put(Constants.ERROR_KEY, getErrorObject(vr)); - } - - eventName = vr.getObject().toString(); - JSONObject actions = new JSONObject(); - for (String key : eventActions.keySet()) { - Object value = eventActions.get(key); - vr = validator.cleanObjectKey(key); - key = vr.getObject().toString(); - // Check for an error - if (vr.getErrorCode() != 0) { - event.put(Constants.ERROR_KEY, getErrorObject(vr)); - } - try { - vr = validator.cleanObjectValue(value, Validator.ValidationContext.Event); - } catch (IllegalArgumentException e) { - // The object was neither a String, Boolean, or any number primitives - ValidationResult error = ValidationResultFactory - .create(512, Constants.PROP_VALUE_NOT_PRIMITIVE, eventName, key, - value != null ? value.toString() : ""); - getConfigLogger().debug(getAccountId(), error.getErrorDesc()); - validationResultStack.pushValidationResult(error); - // Skip this record - continue; - } - value = vr.getObject(); - // Check for an error - if (vr.getErrorCode() != 0) { - event.put(Constants.ERROR_KEY, getErrorObject(vr)); - } - actions.put(key, value); - } - event.put("evtName", eventName); - event.put("evtData", actions); - queueEvent(context, event, Constants.RAISED_EVENT); - } catch (Throwable t) { - // We won't get here - } - } - - /** - * Deprecation Notice - This method has been deprecated by CleverTap, this code will be removed from future - * versions of the CleverTap Android SDK. - * Pushes everything available in the JSON object returned by the Facebook GraphRequest - * - * @param graphUser The object returned from Facebook - */ - @Deprecated - @SuppressWarnings({"unused"}) - public void pushFacebookUser(final JSONObject graphUser) { - postAsyncSafely("pushFacebookUser", new Runnable() { - @Override - public void run() { - _pushFacebookUser(graphUser); - } - }); - } - - /** - * Sends the FCM registration ID to CleverTap. - * - * @param fcmId The FCM registration ID - * @param register Boolean indicating whether to register - * or not for receiving push messages from CleverTap. - * Set this to true to receive push messages from CleverTap, - * and false to not receive any messages from CleverTap. - */ - @SuppressWarnings("unused") - public void pushFcmRegistrationId(String fcmId, boolean register) { - pushProviders.handleToken(fcmId, PushType.FCM, register); - } + /** + * Sends the FCM registration ID to CleverTap. + * + * @param fcmId The FCM registration ID + * @param register Boolean indicating whether to register + * or not for receiving push messages from CleverTap. + * Set this to true to receive push messages from CleverTap, + * and false to not receive any messages from CleverTap. + */ + @SuppressWarnings("unused") + public void pushFcmRegistrationId(String fcmId, boolean register) { + coreState.getPushProviders().handleToken(fcmId, PushType.FCM, register); + } /** * Used to record errors of the Geofence module @@ -3003,7 +1933,7 @@ public void pushFcmRegistrationId(String fcmId, boolean register) { @SuppressWarnings("unused") public void pushGeoFenceError(int errorCode, String errorMessage) { ValidationResult validationResult = new ValidationResult(errorCode, errorMessage); - validationResultStack.pushValidationResult(validationResult); + coreState.getValidationResultStack().pushValidationResult(validationResult); } /** @@ -3014,7 +1944,8 @@ public void pushGeoFenceError(int errorCode, String errorMessage) { */ @SuppressWarnings("unused") public Future pushGeoFenceExitedEvent(JSONObject geoFenceProperties) { - return raiseEventForGeofences(Constants.GEOFENCE_EXITED_EVENT_NAME, geoFenceProperties); + return coreState.getAnalyticsManager() + .raiseEventForGeofences(Constants.GEOFENCE_EXITED_EVENT_NAME, geoFenceProperties); } /** @@ -3025,7 +1956,8 @@ public Future pushGeoFenceExitedEvent(JSONObject geoFenceProperties) { */ @SuppressWarnings("unused") public Future pushGeofenceEnteredEvent(JSONObject geofenceProperties) { - return raiseEventForGeofences(Constants.GEOFENCE_ENTERED_EVENT_NAME, geofenceProperties); + return coreState.getAnalyticsManager() + .raiseEventForGeofences(Constants.GEOFENCE_ENTERED_EVENT_NAME, geofenceProperties); } /** @@ -3039,7 +1971,7 @@ public Future pushGeofenceEnteredEvent(JSONObject geofenceProperties) { */ @SuppressWarnings("unused") public void pushHuaweiRegistrationId(String regId, boolean register) { - pushProviders.handleToken(regId, PushType.HPS, register); + coreState.getPushProviders().handleToken(regId, PushType.HPS, register); } /** @@ -3050,7 +1982,7 @@ public void pushHuaweiRegistrationId(String regId, boolean register) { @SuppressWarnings("unused") public void pushInboxNotificationClickedEvent(String messageId) { CTInboxMessage message = getInboxMessageForId(messageId); - pushInboxMessageStateEvent(true, message, null); + coreState.getAnalyticsManager().pushInboxMessageStateEvent(true, message, null); } /** @@ -3061,53 +1993,7 @@ public void pushInboxNotificationClickedEvent(String messageId) { @SuppressWarnings("unused") public void pushInboxNotificationViewedEvent(String messageId) { CTInboxMessage message = getInboxMessageForId(messageId); - pushInboxMessageStateEvent(false, message, null); - } - - /** - * This method is used to push install referrer via Intent - * Deprecation warning because Google Play install referrer via intent will be deprecated in March 2020 - * - * @param intent An Intent with the install referrer parameters - */ - @SuppressWarnings({"unused", "WeakerAccess"}) - @Deprecated - public void pushInstallReferrer(Intent intent) { - try { - final Bundle extras = intent.getExtras(); - // Preliminary checks - if (extras == null || !extras.containsKey("referrer")) { - return; - } - final String url; - try { - url = URLDecoder.decode(extras.getString("referrer"), "UTF-8"); - - getConfigLogger().verbose(getAccountId(), "Referrer received: " + url); - } catch (Throwable e) { - // Could not decode - return; - } - if (url == null) { - return; - } - int now = (int) (System.currentTimeMillis() / 1000); - - //noinspection ConstantConditions - if (installReferrerMap.containsKey(url) && now - installReferrerMap.get(url) < 10) { - getConfigLogger() - .verbose(getAccountId(), "Skipping install referrer due to duplicate within 10 seconds"); - return; - } - - installReferrerMap.put(url, now); - - Uri uri = Uri.parse("wzrk://track?install=true&" + url); - - pushDeepLink(uri, true); - } catch (Throwable t) { - // no-op - } + coreState.getAnalyticsManager().pushInboxMessageStateEvent(false, message, null); } /** @@ -3117,29 +2003,7 @@ public void pushInstallReferrer(Intent intent) { */ @SuppressWarnings({"unused"}) public void pushInstallReferrer(String url) { - try { - getConfigLogger().verbose(getAccountId(), "Referrer received: " + url); - - if (url == null) { - return; - } - int now = (int) (System.currentTimeMillis() / 1000); - - //noinspection ConstantConditions - if (installReferrerMap.containsKey(url) && now - installReferrerMap.get(url) < 10) { - getConfigLogger() - .verbose(getAccountId(), "Skipping install referrer due to duplicate within 10 seconds"); - return; - } - - installReferrerMap.put(url, now); - - Uri uri = Uri.parse("wzrk://track?install=true&" + url); - - pushDeepLink(uri, true); - } catch (Throwable t) { - // no-op - } + coreState.getAnalyticsManager().pushInstallReferrer(url); } /** @@ -3151,44 +2015,7 @@ public void pushInstallReferrer(String url) { */ @SuppressWarnings({"unused"}) public synchronized void pushInstallReferrer(String source, String medium, String campaign) { - if (source == null && medium == null && campaign == null) { - return; - } - try { - // If already pushed, don't send it again - int status = StorageHelper.getInt(context, "app_install_status", 0); - if (status != 0) { - Logger.d("Install referrer has already been set. Will not override it"); - return; - } - StorageHelper.putInt(context, "app_install_status", 1); - - if (source != null) { - source = Uri.encode(source); - } - if (medium != null) { - medium = Uri.encode(medium); - } - if (campaign != null) { - campaign = Uri.encode(campaign); - } - - String uriStr = "wzrk://track?install=true"; - if (source != null) { - uriStr += "&utm_source=" + source; - } - if (medium != null) { - uriStr += "&utm_medium=" + medium; - } - if (campaign != null) { - uriStr += "&utm_campaign=" + campaign; - } - - Uri uri = Uri.parse(uriStr); - pushDeepLink(uri, true); - } catch (Throwable t) { - Logger.v("Failed to push install referrer", t); - } + coreState.getAnalyticsManager().pushInstallReferrer(source, medium, campaign); } /** @@ -3199,132 +2026,11 @@ public synchronized void pushInstallReferrer(String source, String medium, Strin */ @SuppressWarnings({"unused", "WeakerAccess"}) public void pushNotificationClickedEvent(final Bundle extras) { - - if (this.config.isAnalyticsOnly()) { - getConfigLogger() - .debug(getAccountId(), "is Analytics Only - will not process Notification Clicked event."); - return; - } - - if (extras == null || extras.isEmpty() || extras.get(Constants.NOTIFICATION_TAG) == null) { - getConfigLogger().debug(getAccountId(), - "Push notification: " + (extras == null ? "NULL" : extras.toString()) - + " not from CleverTap - will not process Notification Clicked event."); - return; - } - - String accountId = null; - try { - accountId = extras.getString(Constants.WZRK_ACCT_ID_KEY); - } catch (Throwable t) { - // no-op - } - - boolean shouldProcess = (accountId == null && config.isDefaultInstance()) || getAccountId().equals(accountId); - - if (!shouldProcess) { - getConfigLogger().debug(getAccountId(), - "Push notification not targeted at this instance, not processing Notification Clicked Event"); - return; - } - - if (extras.containsKey(Constants.INAPP_PREVIEW_PUSH_PAYLOAD_KEY)) { - pendingInappRunnable = new Runnable() { - @Override - public void run() { - try { - Logger.v("Received in-app via push payload: " + extras - .getString(Constants.INAPP_PREVIEW_PUSH_PAYLOAD_KEY)); - JSONObject r = new JSONObject(); - JSONArray inappNotifs = new JSONArray(); - r.put(Constants.INAPP_JSON_RESPONSE_KEY, inappNotifs); - inappNotifs.put(new JSONObject(extras.getString(Constants.INAPP_PREVIEW_PUSH_PAYLOAD_KEY))); - processInAppResponse(r, context); - } catch (Throwable t) { - Logger.v("Failed to display inapp notification from push notification payload", t); - } - } - }; - return; - } - - if (extras.containsKey(Constants.INBOX_PREVIEW_PUSH_PAYLOAD_KEY)) { - pendingInappRunnable = new Runnable() { - @Override - public void run() { - try { - Logger.v("Received inbox via push payload: " + extras - .getString(Constants.INBOX_PREVIEW_PUSH_PAYLOAD_KEY)); - JSONObject r = new JSONObject(); - JSONArray inappNotifs = new JSONArray(); - r.put(Constants.INBOX_JSON_RESPONSE_KEY, inappNotifs); - JSONObject testPushObject = new JSONObject( - extras.getString(Constants.INBOX_PREVIEW_PUSH_PAYLOAD_KEY)); - testPushObject.put("_id", String.valueOf(System.currentTimeMillis() / 1000)); - inappNotifs.put(testPushObject); - processInboxResponse(r); - } catch (Throwable t) { - Logger.v("Failed to process inbox message from push notification payload", t); - } - } - }; - return; - } - - if (extras.containsKey(Constants.DISPLAY_UNIT_PREVIEW_PUSH_PAYLOAD_KEY)) { - handleSendTestForDisplayUnits(extras); - return; - } - - if (!extras.containsKey(Constants.NOTIFICATION_ID_TAG) || (extras.getString(Constants.NOTIFICATION_ID_TAG) - == null)) { - getConfigLogger().debug(getAccountId(), - "Push notification ID Tag is null, not processing Notification Clicked event for: " + extras - .toString()); - return; - } - - // Check for dupe notification views; if same notficationdId within specified time interval (5 secs) don't process - boolean isDuplicate = checkDuplicateNotificationIds(extras, notificationIdTagMap, - Constants.NOTIFICATION_ID_TAG_INTERVAL); - if (isDuplicate) { - getConfigLogger().debug(getAccountId(), - "Already processed Notification Clicked event for " + extras.toString() - + ", dropping duplicate."); - return; - } - - JSONObject event = new JSONObject(); - JSONObject notif = new JSONObject(); - try { - for (String x : extras.keySet()) { - if (!x.startsWith(Constants.WZRK_PREFIX)) { - continue; - } - Object value = extras.get(x); - notif.put(x, value); - } - - event.put("evtName", Constants.NOTIFICATION_CLICKED_EVENT_NAME); - event.put("evtData", notif); - queueEvent(context, event, Constants.RAISED_EVENT); - - try { - setWzrkParams(getWzrkFields(extras)); - } catch (Throwable t) { - // no-op - } - } catch (Throwable t) { - // We won't get here - } - if (getCTPushNotificationListener() != null) { - getCTPushNotificationListener() - .onNotificationClickedPayloadReceived(Utils.convertBundleObjectToHashMap(extras)); - } else { - Logger.d("CTPushNotificationListener is not set"); - } + coreState.getAnalyticsManager().pushNotificationClickedEvent(extras); } + //Session + /** * Pushes the Notification Viewed event to CleverTap. * @@ -3333,42 +2039,7 @@ public void run() { */ @SuppressWarnings({"unused", "WeakerAccess"}) public void pushNotificationViewedEvent(Bundle extras) { - - if (extras == null || extras.isEmpty() || extras.get(Constants.NOTIFICATION_TAG) == null) { - getConfigLogger().debug(getAccountId(), - "Push notification: " + (extras == null ? "NULL" : extras.toString()) - + " not from CleverTap - will not process Notification Viewed event."); - return; - } - - if (!extras.containsKey(Constants.NOTIFICATION_ID_TAG) || (extras.getString(Constants.NOTIFICATION_ID_TAG) - == null)) { - getConfigLogger().debug(getAccountId(), - "Push notification ID Tag is null, not processing Notification Viewed event for: " + extras - .toString()); - return; - } - - // Check for dupe notification views; if same notficationdId within specified time interval (2 secs) don't process - boolean isDuplicate = checkDuplicateNotificationIds(extras, notificationViewedIdTagMap, - Constants.NOTIFICATION_VIEWED_ID_TAG_INTERVAL); - if (isDuplicate) { - getConfigLogger().debug(getAccountId(), - "Already processed Notification Viewed event for " + extras.toString() + ", dropping duplicate."); - return; - } - - getConfigLogger().debug("Recording Notification Viewed event for notification: " + extras.toString()); - - JSONObject event = new JSONObject(); - try { - JSONObject notif = getWzrkFields(extras); - event.put("evtName", Constants.NOTIFICATION_VIEWED_EVENT_NAME); - event.put("evtData", notif); - } catch (Throwable ignored) { - //no-op - } - queueEvent(context, event, Constants.NV_EVENT); + coreState.getAnalyticsManager().pushNotificationViewedEvent(extras); } /** @@ -3380,16 +2051,7 @@ public void pushNotificationViewedEvent(Bundle extras) { */ @SuppressWarnings({"unused", "WeakerAccess"}) public void pushProfile(final Map profile) { - if (profile == null || profile.isEmpty()) { - return; - } - - postAsyncSafely("profilePush", new Runnable() { - @Override - public void run() { - _push(profile); - } - }); + coreState.getAnalyticsManager().pushProfile(profile); } /** @@ -3403,7 +2065,7 @@ public void run() { */ @SuppressWarnings("unused") public void pushXiaomiRegistrationId(String regId, boolean register) { - pushProviders.handleToken(regId, PushType.XPS, register); + coreState.getPushProviders().handleToken(regId, PushType.XPS, register); } /** @@ -3413,371 +2075,126 @@ public void pushXiaomiRegistrationId(String regId, boolean register) { */ @SuppressWarnings({"unused"}) public void recordScreen(String screenName) { - if (screenName == null || (!currentScreenName.isEmpty() && currentScreenName.equals(screenName))) { + if (screenName == null || (!coreState.getCoreMetaData().getScreenName().isEmpty() && coreState + .getCoreMetaData().getScreenName().equals(screenName))) { return; } getConfigLogger().debug(getAccountId(), "Screen changed to " + screenName); - currentScreenName = screenName; - recordPageEventWithExtras(null); + coreState.getCoreMetaData().setCurrentScreenName(screenName); + coreState.getAnalyticsManager().recordPageEventWithExtras(null); } /** - * Deprecation Notice - This method has been deprecated by CleverTap, this code will be removed from future - * versions of the CleverTap Android SDK. - * - * Registers an ABTesting variable of type {@link Boolean} for ease of editing on the CleverTap Dashboard + * Remove a unique value from a multi-value user profile property + *

    + * If the key currently contains a scalar value, prior to performing the remove operation + * the key will be promoted to a multi-value property with the current value cast to a string. + * If the multi-value property is empty after the remove operation, the key will be removed. * - * @param name {@link String} the name of the variable + * @param key String + * @param value String */ - @RequiresApi(api = VERSION_CODES.KITKAT) - @Deprecated - public void registerBooleanVariable(String name) { - if (ctABTestController == null) { - getConfigLogger().verbose(getAccountId(), "ABTesting is not enabled for this instance"); + @SuppressWarnings({"unused", "WeakerAccess"}) + public void removeMultiValueForKey(String key, String value) { + if (value == null || value.isEmpty()) { + coreState.getAnalyticsManager()._generateEmptyMultiValueError(key); return; } - ctABTestController.registerBooleanVariable(name); + + removeMultiValuesForKey(key, new ArrayList<>(Collections.singletonList(value))); } /** - * Deprecation Notice - This method has been deprecated by CleverTap, this code will be removed from future - * versions of the CleverTap Android SDK. - * - * Registers an ABTesting variable of type {@link Double} for ease of editing on the CleverTap Dashboard + * Remove a collection of unique values from a multi-value user profile property + *

    + * If the key currently contains a scalar value, prior to performing the remove operation + * the key will be promoted to a multi-value property with the current value cast to a string. + *

    + * If the multi-value property is empty after the remove operation, the key will be removed. * - * @param name {@link String} the name of the variable + * @param key String + * @param values {@link ArrayList} with String values */ - @RequiresApi(api = VERSION_CODES.KITKAT) - @Deprecated - public void registerDoubleVariable(String name) { - if (ctABTestController == null) { - getConfigLogger().verbose(getAccountId(), "ABTesting is not enabled for this instance"); - return; - } - ctABTestController.registerDoubleVariable(name); + @SuppressWarnings({"unused", "WeakerAccess"}) + public void removeMultiValuesForKey(final String key, final ArrayList values) { + coreState.getAnalyticsManager().removeMultiValuesForKey(key, values); } /** - * Deprecation Notice - This method has been deprecated by CleverTap, this code will be removed from future - * versions of the CleverTap Android SDK. - * - * Registers an ABTesting variable of type {@link Integer} for ease of editing on the CleverTap Dashboard + * Remove the user profile property value specified by key from the user profile * - * @param name {@link String} the name of the variable + * @param key String */ - @RequiresApi(api = VERSION_CODES.KITKAT) - @Deprecated - public void registerIntegerVariable(String name) { - if (ctABTestController == null) { - getConfigLogger().verbose(getAccountId(), "ABTesting is not enabled for this instance"); - return; - } - ctABTestController.registerIntegerVariable(name); + @SuppressWarnings({"unused", "WeakerAccess"}) + public void removeValueForKey(final String key) { + coreState.getAnalyticsManager().removeValueForKey(key); } /** - * Deprecation Notice - This method has been deprecated by CleverTap, this code will be removed from future - * versions of the CleverTap Android SDK. - * - * Registers an ABTesting variable of type {@link List} of {@link Boolean} for ease of editing on the CleverTap - * Dashboard + * This method is used to set the CTFeatureFlagsListener + * Register to receive feature flag callbacks * - * @param name {@link String} the name of the variable + * @param featureFlagsListener The {@link CTFeatureFlagsListener} object */ - @RequiresApi(api = VERSION_CODES.KITKAT) - @Deprecated - public void registerListOfBooleanVariable(String name) { - if (ctABTestController == null) { - getConfigLogger().verbose(getAccountId(), "ABTesting is not enabled for this instance"); - return; - } - ctABTestController.registerListOfBooleanVariable(name); + @SuppressWarnings("unused") + public void setCTFeatureFlagsListener(CTFeatureFlagsListener featureFlagsListener) { + coreState.getCallbackManager().setFeatureFlagListener(featureFlagsListener); } /** - * Deprecation Notice - This method has been deprecated by CleverTap, this code will be removed from future - * versions of the CleverTap Android SDK. - * - * Registers an ABTesting variable of type {@link List} of {@link Double} for ease of editing on the CleverTap - * Dashboard + * This method is used to set the product config listener + * Register to receive callbacks * - * @param name {@link String} the name of the variable + * @param listener The {@link CTProductConfigListener} instance */ - @RequiresApi(api = VERSION_CODES.KITKAT) - @Deprecated - public void registerListOfDoubleVariable(String name) { - if (ctABTestController == null) { - getConfigLogger().verbose(getAccountId(), "ABTesting is not enabled for this instance"); - return; - } - ctABTestController.registerListOfDoubleVariable(name); + @SuppressWarnings("unused") + public void setCTProductConfigListener(CTProductConfigListener listener) { + coreState.getCallbackManager().setProductConfigListener(listener); } + //Listener + /** - * Deprecation Notice - This method has been deprecated by CleverTap, this code will be removed from future - * versions of the CleverTap Android SDK. - * - * Registers an ABTesting variable of type {@link List} of {@link Integer} for ease of editing on the CleverTap - * Dashboard + * Sets the listener to get the list of currently running Display Campaigns via callback * - * @param name {@link String} the name of the variable + * @param listener- {@link DisplayUnitListener} */ - @RequiresApi(api = VERSION_CODES.KITKAT) - @Deprecated - public void registerListOfIntegerVariable(String name) { - if (ctABTestController == null) { - getConfigLogger().verbose(getAccountId(), "ABTesting is not enabled for this instance"); - return; - } - ctABTestController.registerListOfIntegerVariable(name); + public void setDisplayUnitListener(DisplayUnitListener listener) { + coreState.getCallbackManager().setDisplayUnitListener(listener); } - /** - * Deprecation Notice - This method has been deprecated by CleverTap, this code will be removed from future - * versions of the CleverTap Android SDK. - * - * Registers an ABTesting variable of type {@link List} of {@link String} for ease of editing on the CleverTap - * Dashboard - * - * @param name {@link String} the name of the variable - */ - @RequiresApi(api = VERSION_CODES.KITKAT) - @Deprecated - public void registerListOfStringVariable(String name) { - if (ctABTestController == null) { - getConfigLogger().verbose(getAccountId(), "ABTesting is not enabled for this instance"); - return; - } - ctABTestController.registerListOfStringVariable(name); + @SuppressWarnings("unused") + public void setInAppNotificationButtonListener(InAppNotificationButtonListener listener) { + coreState.getCallbackManager().setInAppNotificationButtonListener(listener); } - /** - * Deprecation Notice - This method has been deprecated by CleverTap, this code will be removed from future - * versions of the CleverTap Android SDK. - * - * Registers an ABTesting variable of type {@link Map} of {@link Boolean} for ease of editing on the CleverTap - * Dashboard - * - * @param name {@link String} the name of the variable - */ - @RequiresApi(api = VERSION_CODES.KITKAT) - @Deprecated - public void registerMapOfBooleanVariable(String name) { - if (ctABTestController == null) { - getConfigLogger().verbose(getAccountId(), "ABTesting is not enabled for this instance"); - return; - } - ctABTestController.registerMapOfBooleanVariable(name); + @SuppressWarnings("unused") + public void setInboxMessageButtonListener(InboxMessageButtonListener listener) { + this.inboxMessageButtonListener = new WeakReference<>(listener); } /** - * Deprecation Notice - This method has been deprecated by CleverTap, this code will be removed from future - * versions of the CleverTap Android SDK. - * - * Registers an ABTesting variable of type {@link Map} of {@link Double} for ease of editing on the CleverTap - * Dashboard + * Not to be used by developers. This is used internally to help CleverTap know which library is wrapping the + * native SDK * - * @param name {@link String} the name of the variable + * @param library {@link String} library name */ - @RequiresApi(api = VERSION_CODES.KITKAT) - @Deprecated - public void registerMapOfDoubleVariable(String name) { - if (ctABTestController == null) { - getConfigLogger().verbose(getAccountId(), "ABTesting is not enabled for this instance"); - return; + public void setLibrary(String library) { + if (coreState.getDeviceInfo() != null) { + coreState.getDeviceInfo().setLibrary(library); } - ctABTestController.registerMapOfDoubleVariable(name); } /** - * Deprecation Notice - This method has been deprecated by CleverTap, this code will be removed from future - * versions of the CleverTap Android SDK. - * - * Registers an ABTesting variable of type {@link Map} of {@link Integer} for ease of editing on the CleverTap - * Dashboard + * Sets the location in CleverTap to get updated GeoFences * - * @param name {@link String} the name of the variable + * @param location android.location.Location */ - @RequiresApi(api = VERSION_CODES.KITKAT) - @Deprecated - public void registerMapOfIntegerVariable(String name) { - if (ctABTestController == null) { - getConfigLogger().verbose(getAccountId(), "ABTesting is not enabled for this instance"); - return; - } - ctABTestController.registerMapOfIntegerVariable(name); - } - - /** - * Deprecation Notice - This method has been deprecated by CleverTap, this code will be removed from future - * versions of the CleverTap Android SDK. - * - * Registers an ABTesting variable of type {@link Map} of {@link String} for ease of editing on the CleverTap - * Dashboard - * - * @param name {@link String} the name of the variable - */ - @RequiresApi(api = VERSION_CODES.KITKAT) - @Deprecated - public void registerMapOfStringVariable(String name) { - if (ctABTestController == null) { - getConfigLogger().verbose(getAccountId(), "ABTesting is not enabled for this instance"); - return; - } - ctABTestController.registerMapOfStringVariable(name); - } - - /** - * Deprecation Notice - This method has been deprecated by CleverTap, this code will be removed from future - * versions of the CleverTap Android SDK. - * - * Registers an ABTesting variable of type {@link String} for ease of editing on the CleverTap Dashboard - * - * @param name {@link String} the name of the variable - */ - @RequiresApi(api = VERSION_CODES.KITKAT) - @Deprecated - public void registerStringVariable(String name) { - if (ctABTestController == null) { - getConfigLogger().verbose(getAccountId(), "ABTesting is not enabled for this instance"); - return; - } - ctABTestController.registerStringVariable(name); - } - - @Override - public ValidationResultStack remoteErrorLogger() { - return validationResultStack; - } - - //Session - - /** - * Remove a unique value from a multi-value user profile property - *

    - * If the key currently contains a scalar value, prior to performing the remove operation - * the key will be promoted to a multi-value property with the current value cast to a string. - * If the multi-value property is empty after the remove operation, the key will be removed. - * - * @param key String - * @param value String - */ - @SuppressWarnings({"unused", "WeakerAccess"}) - public void removeMultiValueForKey(String key, String value) { - if (value == null || value.isEmpty()) { - _generateEmptyMultiValueError(key); - return; - } - - removeMultiValuesForKey(key, new ArrayList<>(Collections.singletonList(value))); - } - - /** - * Remove a collection of unique values from a multi-value user profile property - *

    - * If the key currently contains a scalar value, prior to performing the remove operation - * the key will be promoted to a multi-value property with the current value cast to a string. - *

    - * If the multi-value property is empty after the remove operation, the key will be removed. - * - * @param key String - * @param values {@link ArrayList} with String values - */ - @SuppressWarnings({"unused", "WeakerAccess"}) - public void removeMultiValuesForKey(final String key, final ArrayList values) { - postAsyncSafely("removeMultiValuesForKey", new Runnable() { - @Override - public void run() { - _handleMultiValues(values, key, Constants.COMMAND_REMOVE); - } - }); - } - - /** - * Remove the user profile property value specified by key from the user profile - * - * @param key String - */ - @SuppressWarnings({"unused", "WeakerAccess"}) - public void removeValueForKey(final String key) { - postAsyncSafely("removeValueForKey", new Runnable() { - @Override - public void run() { - _removeValueForKey(key); - } - }); - } - - /** - * This method is used to set the CTFeatureFlagsListener - * Register to receive feature flag callbacks - * - * @param featureFlagsListener The {@link CTFeatureFlagsListener} object - */ - @SuppressWarnings("unused") - public void setCTFeatureFlagsListener(CTFeatureFlagsListener featureFlagsListener) { - this.featureFlagsListener = new WeakReference<>(featureFlagsListener); - } - - /** - * This method is used to set the product config listener - * Register to receive callbacks - * - * @param listener The {@link CTProductConfigListener} instance - */ - @SuppressWarnings("unused") - public void setCTProductConfigListener(CTProductConfigListener listener) { - if (listener != null) { - this.productConfigListener = new WeakReference<>(listener); - } - } - - /** - * Sets the listener to get the list of currently running Display Campaigns via callback - * - * @param listener- {@link DisplayUnitListener} - */ - public void setDisplayUnitListener(DisplayUnitListener listener) { - if (listener != null) { - displayUnitListenerWeakReference = new WeakReference<>(listener); - } else { - getConfigLogger().verbose(getAccountId(), - Constants.FEATURE_DISPLAY_UNIT + "Failed to set - DisplayUnitListener can't be null"); - } - } - - public void setInAppNotificationButtonListener(InAppNotificationButtonListener listener) { - this.inAppNotificationButtonListener = new WeakReference<>(listener); - } - - public void setInboxMessageButtonListener(InboxMessageButtonListener listener) { - this.inboxMessageButtonListener = new WeakReference<>(listener); - } - - /** - * Not to be used by developers. This is used internally to help CleverTap know which library is wrapping the - * native SDK - * - * @param library {@link String} library name - */ - public void setLibrary(String library) { - if (this.deviceInfo != null) { - deviceInfo.setLibrary(library); - } - } - - //Listener - - /** - * Sets the location in CleverTap to get updated GeoFences - * - * @param location android.location.Location - */ - @SuppressWarnings("unused") - public Future setLocationForGeofences(Location location, int sdkVersion) { - setLocationForGeofence(true); - setGeofenceSDKVersion(sdkVersion); - return _setLocation(location); + @SuppressWarnings("unused") + public Future setLocationForGeofences(Location location, int sdkVersion) { + coreState.getCoreMetaData().setLocationForGeofence(true); + coreState.getCoreMetaData().setGeofenceSDKVersion(sdkVersion); + return coreState.getLocationManager()._setLocation(location); } /** @@ -3791,4621 +2208,179 @@ public Future setLocationForGeofences(Location location, int sdkVersion) { */ @SuppressWarnings({"unused", "WeakerAccess"}) public void setMultiValuesForKey(final String key, final ArrayList values) { - postAsyncSafely("setMultiValuesForKey", new Runnable() { - @Override - public void run() { - _handleMultiValues(values, key, Constants.COMMAND_SET); - } - }); - } - - /** - * Use this method to opt the current user out of all event/profile tracking. - * You must call this method separately for each active user profile (e.g. when switching user profiles using - * onUserLogin). - * Once enabled, no events will be saved remotely or locally for the current user. To re-enable tracking call this - * method with enabled set to false. - * - * @param userOptOut boolean Whether tracking opt out should be enabled/disabled. - */ - @SuppressWarnings({"unused"}) - public void setOptOut(boolean userOptOut) { - final boolean enable = userOptOut; - postAsyncSafely("setOptOut", new Runnable() { - @Override - public void run() { - // generate the data for a profile push to alert the server to the optOut state change - HashMap optOutMap = new HashMap<>(); - optOutMap.put(Constants.CLEVERTAP_OPTOUT, enable); - - // determine order of operations depending on enabled/disabled - if (enable) { // if opting out first push profile event then set the flag - pushProfile(optOutMap); - setCurrentUserOptedOut(true); - } else { // if opting back in first reset the flag to false then push the profile event - setCurrentUserOptedOut(false); - pushProfile(optOutMap); - } - // persist the new optOut state - String key = optOutKey(); - if (key == null) { - getConfigLogger() - .verbose(getAccountId(), "Unable to persist user OptOut state, storage key is null"); - return; - } - StorageHelper.putBoolean(context, StorageHelper.storageKeyWithSuffix(config, key), enable); - getConfigLogger().verbose(getAccountId(), "Set current user OptOut state to: " + enable); - } - }); + coreState.getAnalyticsManager().setMultiValuesForKey(key, values); } /** - * Opens {@link CTInboxActivity} to display Inbox Messages + * If you want to stop recorded events from being sent to the server, use this method to set the SDK instance to + * offline. + * Once offline, events will be recorded and queued locally but will not be sent to the server until offline is + * disabled. + * Calling this method again with offline set to false will allow events to be sent to server and the SDK instance + * will immediately attempt to send events that have been queued while offline. * - * @param styleConfig {@link CTInboxStyleConfig} configuration of various style parameters for the {@link - * CTInboxActivity} - */ - @SuppressWarnings({"unused", "WeakerAccess"}) - public void showAppInbox(CTInboxStyleConfig styleConfig) { - synchronized (inboxControllerLock) { - if (ctInboxController == null) { - getConfigLogger().debug(getAccountId(), "Notification Inbox not initialized"); - return; - } - } - - // make styleConfig immutable - final CTInboxStyleConfig _styleConfig = new CTInboxStyleConfig(styleConfig); - - Intent intent = new Intent(context, CTInboxActivity.class); - intent.putExtra("styleConfig", _styleConfig); - Bundle configBundle = new Bundle(); - configBundle.putParcelable("config", config); - intent.putExtra("configBundle", configBundle); - try { - Activity currentActivity = getCurrentActivity(); - if (currentActivity == null) { - throw new IllegalStateException("Current activity reference not found"); - } - currentActivity.startActivity(intent); - Logger.d("Displaying Notification Inbox"); - - } catch (Throwable t) { - Logger.v("Please verify the integration of your app." + - " It is not setup to support Notification Inbox yet.", t); - } - - } - - /** - * Opens {@link CTInboxActivity} to display Inbox Messages with default {@link CTInboxStyleConfig} object + * @param value boolean, true sets the sdk offline, false sets the sdk back online */ @SuppressWarnings({"unused"}) - public void showAppInbox() { - CTInboxStyleConfig styleConfig = new CTInboxStyleConfig(); - showAppInbox(styleConfig); - } - - //To be called from DeviceInfo AdID GUID generation - void deviceIDCreated(String deviceId) { - Logger.v("Initializing InAppFC after Device ID Created = " + deviceId); - this.inAppFCManager = new InAppFCManager(context, config, deviceId); - Logger.v("Initializing ABTesting after Device ID Created = " + deviceId); - initABTesting(); - initFeatureFlags(true); - initProductConfig(true); - getConfigLogger() - .verbose("Got device id from DeviceInfo, notifying user profile initialized to SyncListener"); - notifyUserProfileInitialized(deviceId); - } - - /** - * Raises the Notification Clicked event, if {@param clicked} is true, - * otherwise the Notification Viewed event, if {@param clicked} is false. - * - * @param clicked Whether or not this notification was clicked - * @param data The data to be attached as the event data - * @param customData Additional data such as form input to to be added to the event data - */ - @SuppressWarnings({"unused", "WeakerAccess"}) - void pushInAppNotificationStateEvent(boolean clicked, CTInAppNotification data, Bundle customData) { - JSONObject event = new JSONObject(); - try { - JSONObject notif = getWzrkFields(data); - - if (customData != null) { - for (String x : customData.keySet()) { - - Object value = customData.get(x); - if (value != null) { - notif.put(x, value); - } - } - } - - if (clicked) { - try { - setWzrkParams(notif); - } catch (Throwable t) { - // no-op - } - event.put("evtName", Constants.NOTIFICATION_CLICKED_EVENT_NAME); - } else { - event.put("evtName", Constants.NOTIFICATION_VIEWED_EVENT_NAME); - } - - event.put("evtData", notif); - queueEvent(context, event, Constants.RAISED_EVENT); - } catch (Throwable ignored) { - // We won't get here - } - } - - /** - * Raises the Notification Clicked event, if {@param clicked} is true, - * otherwise the Notification Viewed event, if {@param clicked} is false. - * - * @param clicked Whether or not this notification was clicked - * @param data The data to be attached as the event data - * @param customData Additional data such as form input to to be added to the event data - */ - @SuppressWarnings({"unused", "WeakerAccess"}) - void pushInboxMessageStateEvent(boolean clicked, CTInboxMessage data, Bundle customData) { - JSONObject event = new JSONObject(); - try { - JSONObject notif = getWzrkFields(data); - - if (customData != null) { - for (String x : customData.keySet()) { - - Object value = customData.get(x); - if (value != null) { - notif.put(x, value); - } - } - } - - if (clicked) { - try { - setWzrkParams(notif); - } catch (Throwable t) { - // no-op - } - event.put("evtName", Constants.NOTIFICATION_CLICKED_EVENT_NAME); - } else { - event.put("evtName", Constants.NOTIFICATION_VIEWED_EVENT_NAME); - } - - event.put("evtData", notif); - queueEvent(context, event, Constants.RAISED_EVENT); - } catch (Throwable ignored) { - // We won't get here - } - } - - private JSONArray _cleanMultiValues(ArrayList values, String key) { - - try { - if (values == null || key == null) { - return null; - } - - JSONArray cleanedValues = new JSONArray(); - ValidationResult vr; - - // loop through and clean the new values - for (String value : values) { - value = (value == null) ? "" : value; // so we will generate a validation error later on - - // validate value - vr = validator.cleanMultiValuePropertyValue(value); - - // Check for an error - if (vr.getErrorCode() != 0) { - validationResultStack.pushValidationResult(vr); - } - - // reset the value - Object _value = vr.getObject(); - value = (_value != null) ? vr.getObject().toString() : null; - - // if value is empty generate an error and return - if (value == null || value.isEmpty()) { - _generateEmptyMultiValueError(key); - // Abort - return null; - } - // add to the newValues to be merged - cleanedValues.put(value); - } - - return cleanedValues; - - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), "Error cleaning multi values for key " + key, t); - _generateEmptyMultiValueError(key); - return null; - } - } - - private JSONArray _constructExistingMultiValue(String key, String command) { - - boolean remove = command.equals(Constants.COMMAND_REMOVE); - boolean add = command.equals(Constants.COMMAND_ADD); - - // only relevant for add's and remove's; a set overrides the existing value, so return a new array - if (!remove && !add) { - return new JSONArray(); - } - - Object existing = _getProfilePropertyIgnorePersonalizationFlag(key); - - // if there is no existing value - if (existing == null) { - // if its a remove then return null to abort operation - // no point in running remove against a nonexistent value - if (remove) { - return null; - } - - // otherwise return an empty array - return new JSONArray(); - } - - // value exists - - // the value should only ever be a JSONArray or scalar (String really) - - // if its already a JSONArray return that - if (existing instanceof JSONArray) { - return (JSONArray) existing; - } - - // handle a scalar value as the existing value - /* - if its an add, our rule is to promote the scalar value to multi value and include the cleaned stringified - scalar value as the first element of the resulting array - - NOTE: the existing scalar value is currently limited to 120 bytes; when adding it to a multi value - it is subject to the current 40 byte limit - - if its a remove, our rule is to delete the key from the local copy - if the cleaned stringified existing value is equal to any of the cleaned values passed to the remove method - - if its an add, return an empty array as the default, - in the event the existing scalar value fails stringifying/cleaning - - returning null will signal that a remove operation should be aborted, - as there is no valid promoted multi value to remove against - */ - - JSONArray _default = (add) ? new JSONArray() : null; - - String stringified = _stringifyAndCleanScalarProfilePropValue(existing); - - return (stringified != null) ? new JSONArray().put(stringified) : _default; - } - - /** - * Launches an asynchronous task to download the notification icon from CleverTap, - * and create the Android notification. - *

    - * If your app is using CleverTap SDK's built in FCM message handling, - * this method does not need to be called explicitly. - *

    - * Use this method when implementing your own FCM handling mechanism. Refer to the - * SDK documentation for usage scenarios and examples. - * - * @param context A reference to an Android context - * @param extras The {@link Bundle} object received by the broadcast receiver - * @param notificationId A custom id to build a notification - */ - private void _createNotification(final Context context, final Bundle extras, final int notificationId) { - if (extras == null || extras.get(Constants.NOTIFICATION_TAG) == null) { - return; - } - - if (config.isAnalyticsOnly()) { - getConfigLogger().debug(getAccountId(), "Instance is set for Analytics only, cannot create notification"); - return; - } - - try { - postAsyncSafely("CleverTapAPI#_createNotification", new Runnable() { - @Override - public void run() { - try { - getConfigLogger().debug(getAccountId(), "Handling notification: " + extras.toString()); - dbAdapter = loadDBAdapter(context); - if (extras.getString(Constants.WZRK_PUSH_ID) != null) { - if (dbAdapter.doesPushNotificationIdExist(extras.getString(Constants.WZRK_PUSH_ID))) { - getConfigLogger().debug(getAccountId(), - "Push Notification already rendered, not showing again"); - return; - } - } - String notifMessage = extras.getString(Constants.NOTIF_MSG); - notifMessage = (notifMessage != null) ? notifMessage : ""; - if (notifMessage.isEmpty()) { - //silent notification - getConfigLogger() - .verbose(getAccountId(), "Push notification message is empty, not rendering"); - loadDBAdapter(context).storeUninstallTimestamp(); - String pingFreq = extras.getString("pf", ""); - if (!TextUtils.isEmpty(pingFreq)) { - updatePingFrequencyIfNeeded(context, Integer.parseInt(pingFreq)); - } - return; - } - String notifTitle = extras.getString(Constants.NOTIF_TITLE, ""); - notifTitle = notifTitle.isEmpty() ? context.getApplicationInfo().name : notifTitle; - triggerNotification(context, extras, notifMessage, notifTitle, notificationId); - } catch (Throwable t) { - // Occurs if the notification image was null - // Let's return, as we couldn't get a handle on the app's icon - // Some devices throw a PackageManager* exception too - getConfigLogger().debug(getAccountId(), "Couldn't render notification: ", t); - } - } - }); - } catch (Throwable t) { - getConfigLogger().debug(getAccountId(), "Failed to process push notification", t); - } - } - - private void _generateEmptyMultiValueError(String key) { - ValidationResult error = ValidationResultFactory.create(512, Constants.INVALID_MULTI_VALUE, key); - validationResultStack.pushValidationResult(error); - getConfigLogger().debug(getAccountId(), error.getErrorDesc()); - } - - private void _generateInvalidMultiValueKeyError(String key) { - ValidationResult error = ValidationResultFactory.create(523, Constants.INVALID_MULTI_VALUE_KEY, key); - validationResultStack.pushValidationResult(error); - getConfigLogger().debug(getAccountId(), - "Invalid multi-value property key " + key + " profile multi value operation aborted"); - } - - @SuppressLint("MissingPermission") - private Location _getLocation() { - try { - LocationManager lm = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); - if (lm == null) { - Logger.d("Location Manager is null."); - return null; - } - List providers = lm.getProviders(true); - Location bestLocation = null; - Location l = null; - for (String provider : providers) { - try { - l = lm.getLastKnownLocation(provider); - } catch (SecurityException e) { - //no-op - Logger.v("Location security exception", e); - } - - if (l == null) { - continue; - } - if (bestLocation == null || l.getAccuracy() < bestLocation.getAccuracy()) { - bestLocation = l; - } - } - - return bestLocation; - } catch (Throwable t) { - Logger.v("Couldn't get user's location", t); - return null; - } - } - - // use for internal profile getter doesn't do the personalization check - private Object _getProfilePropertyIgnorePersonalizationFlag(String key) { - return getLocalDataStore().getProfileValueForKey(key); - } - - private void _handleMultiValues(ArrayList values, String key, String command) { - if (key == null) { - return; - } - - if (values == null || values.isEmpty()) { - _generateEmptyMultiValueError(key); - return; - } - - ValidationResult vr; - - // validate the key - vr = validator.cleanMultiValuePropertyKey(key); - - // Check for an error - if (vr.getErrorCode() != 0) { - validationResultStack.pushValidationResult(vr); - } - - // reset the key - Object _key = vr.getObject(); - String cleanKey = (_key != null) ? vr.getObject().toString() : null; - - // if key is empty generate an error and return - if (cleanKey == null || cleanKey.isEmpty()) { - _generateInvalidMultiValueKeyError(key); - return; - } - - key = cleanKey; - - try { - JSONArray currentValues = _constructExistingMultiValue(key, command); - JSONArray newValues = _cleanMultiValues(values, key); - _validateAndPushMultiValue(currentValues, newValues, values, key, command); - - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), "Error handling multi value operation for key " + key, t); - } - } - - // always call async - private void _initializeInbox() { - synchronized (inboxControllerLock) { - if (this.ctInboxController != null) { - _notifyInboxInitialized(); - return; - } - if (getCleverTapID() != null) { - this.ctInboxController = new CTInboxController(getCleverTapID(), loadDBAdapter(context), - haveVideoPlayerSupport); - _notifyInboxInitialized(); - } else { - getConfigLogger().info("CRITICAL : No device ID found!"); - } - } - } - - private void _notifyInboxInitialized() { - if (this.inboxListener != null) { - this.inboxListener.inboxDidInitialize(); - } - } - - private void _notifyInboxMessagesDidUpdate() { - if (this.inboxListener != null) { - Utils.runOnUiThread(new Runnable() { - @Override - public void run() { - if (CleverTapAPI.this.inboxListener != null) { - CleverTapAPI.this.inboxListener.inboxMessagesDidUpdate(); - } - } - }); - } - } - - private void _onUserLogin(final Map profile, final String cleverTapID) { - if (profile == null) { - return; - } - - try { - final String currentGUID = getCleverTapID(); - if (currentGUID == null) { - return; - } - - boolean haveIdentifier = false; - LoginInfoProvider loginInfoProvider = new LoginInfoProvider(this); - // check for valid identifier keys - // use the first one we find - IdentityRepo iProfileHandler = IdentityRepoFactory.getRepo(this); - for (String key : profile.keySet()) { - Object value = profile.get(key); - boolean isProfileKey = iProfileHandler.hasIdentity(key); - if (isProfileKey) { - try { - String identifier = null; - if (value != null) { - identifier = value.toString(); - } - if (identifier != null && identifier.length() > 0) { - haveIdentifier = true; - cachedGUID = loginInfoProvider.getGUIDForIdentifier(key, identifier); - if (cachedGUID != null) { - break; - } - } - } catch (Throwable t) { - // no-op - } - } - } - - // if no valid identifier provided or there are no identified users on the device; just push on the current profile - if (!isErrorDeviceId()) { - if (!haveIdentifier || loginInfoProvider.isAnonymousDevice()) { - getConfigLogger().debug(getAccountId(), - "onUserLogin: no identifier provided or device is anonymous, pushing on current user profile"); - pushProfile(profile); - return; - } - } - - // if identifier maps to current guid, push on current profile - if (cachedGUID != null && cachedGUID.equals(currentGUID)) { - getConfigLogger().debug(getAccountId(), - "onUserLogin: " + profile.toString() + " maps to current device id " + currentGUID - + " pushing on current profile"); - pushProfile(profile); - return; - } - - // stringify profile to use as dupe blocker - String profileToString = profile.toString(); - - // as processing happens async block concurrent onUserLogin requests with the same profile, as our cache is set async - if (isProcessUserLoginWithIdentifier(profileToString)) { - getConfigLogger().debug(getAccountId(), "Already processing onUserLogin for " + profileToString); - return; - } - - // create new guid if necessary and reset - // block any concurrent onUserLogin call for the same profile - synchronized (processingUserLoginLock) { - processingUserLoginIdentifier = profileToString; - } - - getConfigLogger().verbose(getAccountId(), "onUserLogin: queuing reset profile for " + profileToString - + " with Cached GUID " + ((cachedGUID != null) ? cachedGUID : "NULL")); - - asyncProfileSwitchUser(profile, cachedGUID, cleverTapID); - - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), "onUserLogin failed", t); - } - } - - // always call async - private void _processInboxMessages(JSONArray messages) { - synchronized (inboxControllerLock) { - if (this.ctInboxController == null) { - _initializeInbox(); - } - if (this.ctInboxController != null) { - boolean update = this.ctInboxController.updateMessages(messages); - if (update) { - _notifyInboxMessagesDidUpdate(); - } - } - } - } - - private void _push(Map profile) { - if (profile == null || profile.isEmpty()) { - return; - } - - try { - ValidationResult vr; - JSONObject customProfile = new JSONObject(); - JSONObject fieldsToUpdateLocally = new JSONObject(); - for (String key : profile.keySet()) { - Object value = profile.get(key); - - vr = validator.cleanObjectKey(key); - key = vr.getObject().toString(); - // Check for an error - if (vr.getErrorCode() != 0) { - validationResultStack.pushValidationResult(vr); - } - - if (key.isEmpty()) { - ValidationResult keyError = ValidationResultFactory.create(512, Constants.PUSH_KEY_EMPTY); - validationResultStack.pushValidationResult(keyError); - getConfigLogger().debug(getAccountId(), keyError.getErrorDesc()); - // Skip this property - continue; - } - - try { - vr = validator.cleanObjectValue(value, Validator.ValidationContext.Profile); - } catch (Throwable e) { - // The object was neither a String, Boolean, or any number primitives - ValidationResult error = ValidationResultFactory.create(512, - Constants.OBJECT_VALUE_NOT_PRIMITIVE_PROFILE, - value != null ? value.toString() : "", key); - validationResultStack.pushValidationResult(error); - getConfigLogger().debug(getAccountId(), error.getErrorDesc()); - // Skip this property - continue; - } - value = vr.getObject(); - // Check for an error - if (vr.getErrorCode() != 0) { - validationResultStack.pushValidationResult(vr); - } - - // test Phone: if no device country code, test if phone starts with +, log but always send - if (key.equalsIgnoreCase("Phone")) { - try { - value = value.toString(); - String countryCode = this.deviceInfo.getCountryCode(); - if (countryCode == null || countryCode.isEmpty()) { - String _value = (String) value; - if (!_value.startsWith("+")) { - ValidationResult error = ValidationResultFactory - .create(512, Constants.INVALID_COUNTRY_CODE, _value); - validationResultStack.pushValidationResult(error); - getConfigLogger().debug(getAccountId(), error.getErrorDesc()); - } - } - getConfigLogger().verbose(getAccountId(), - "Profile phone is: " + value + " device country code is: " + ((countryCode != null) - ? countryCode : "null")); - } catch (Exception e) { - validationResultStack - .pushValidationResult(ValidationResultFactory.create(512, Constants.INVALID_PHONE)); - getConfigLogger().debug(getAccountId(), "Invalid phone number: " + e.getLocalizedMessage()); - continue; - } - } - - // add to the local profile update object - fieldsToUpdateLocally.put(key, value); - customProfile.put(key, value); - } - - getConfigLogger().verbose(getAccountId(), "Constructed custom profile: " + customProfile.toString()); - - // update local profile values - if (fieldsToUpdateLocally.length() > 0) { - getLocalDataStore().setProfileFields(fieldsToUpdateLocally); - } - - pushBasicProfile(customProfile); - - } catch (Throwable t) { - // Will not happen - getConfigLogger().verbose(getAccountId(), "Failed to push profile", t); - } - } - - @SuppressWarnings("ConstantConditions") - private void _pushFacebookUser(JSONObject graphUser) { - try { - if (graphUser == null) { - return; - } - // Note: No validations are required here, as everything is controlled - String name = getGraphUserPropertySafely(graphUser, "name", ""); - try { - // Certain users have nasty looking names - unicode chars, validate for any - // not allowed chars - ValidationResult vr = validator.cleanObjectValue(name, Validator.ValidationContext.Profile); - name = vr.getObject().toString(); - - if (vr.getErrorCode() != 0) { - validationResultStack.pushValidationResult(vr); - } - } catch (IllegalArgumentException e) { - // Weird name, wasn't a string, or any number - // This would never happen with FB - name = ""; - } - - String gender = getGraphUserPropertySafely(graphUser, "gender", null); - // Convert to WR format - if (gender != null) { - if (gender.toLowerCase().startsWith("m")) { - gender = "M"; - } else if (gender.toLowerCase().startsWith("f")) { - gender = "F"; - } else { - gender = ""; - } - } else { - gender = null; - } - String email = getGraphUserPropertySafely(graphUser, "email", ""); - - String birthday = getGraphUserPropertySafely(graphUser, "birthday", null); - if (birthday != null) { - // Some users don't have the year of birth mentioned - // FB returns only MM/dd in those cases - if (birthday.matches("^../..")) { - // This means that the year is not available(~30% of the times) - // Ignore - birthday = ""; - } else { - try { - Date date = Constants.FB_DOB_DATE_FORMAT.parse(birthday); - birthday = "$D_" + (int) (date.getTime() / 1000); - } catch (ParseException e) { - // Differs from the specs - birthday = ""; - } - } - } - - String work; - try { - JSONArray workArray = graphUser.getJSONArray("work"); - work = (workArray.length() > 0) ? "Y" : "N"; - } catch (Throwable t) { - work = null; - } - - String education; - try { - JSONArray eduArray = graphUser.getJSONArray("education"); - // FB returns the education levels in a descending order - highest = last entry - String fbEdu = eduArray.getJSONObject(eduArray.length() - 1).getString("type"); - if (fbEdu.toLowerCase().contains("high school")) { - education = "School"; - } else if (fbEdu.toLowerCase().contains("college")) { - education = "College"; - } else if (fbEdu.toLowerCase().contains("graduate school")) { - education = "Graduate"; - } else { - education = ""; - } - } catch (Throwable t) { - // No education info available - education = null; - } - - String id = getGraphUserPropertySafely(graphUser, "id", ""); - - String married = getGraphUserPropertySafely(graphUser, "relationship_status", null); - if (married != null) { - if (married.equalsIgnoreCase("married")) { - married = "Y"; - } else { - married = "N"; - } - } - - final JSONObject profile = new JSONObject(); - if (id != null && id.length() > 3) { - profile.put("FBID", id); - } - if (name != null && name.length() > 3) { - profile.put("Name", name); - } - if (email != null && email.length() > 3) { - profile.put("Email", email); - } - if (gender != null && !gender.trim().equals("")) { - profile.put("Gender", gender); - } - if (education != null && !education.trim().equals("")) { - profile.put("Education", education); - } - if (work != null && !work.trim().equals("")) { - profile.put("Employed", work); - } - if (birthday != null && birthday.length() > 3) { - profile.put("DOB", birthday); - } - if (married != null && !married.trim().equals("")) { - profile.put("Married", married); - } - - pushBasicProfile(profile); - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), "Failed to parse graph user object successfully", t); - } - } - - //Session - - private void _removeValueForKey(String key) { - try { - key = (key == null) ? "" : key; // so we will generate a validation error later on - - // validate the key - ValidationResult vr; - - vr = validator.cleanObjectKey(key); - key = vr.getObject().toString(); - - if (key.isEmpty()) { - ValidationResult error = ValidationResultFactory.create(512, Constants.KEY_EMPTY); - validationResultStack.pushValidationResult(error); - getConfigLogger().debug(getAccountId(), error.getErrorDesc()); - // Abort - return; - } - // Check for an error - if (vr.getErrorCode() != 0) { - validationResultStack.pushValidationResult(vr); - } - - // remove from the local profile - getLocalDataStore().removeProfileField(key); - - // send the delete command - JSONObject command = new JSONObject().put(Constants.COMMAND_DELETE, true); - JSONObject update = new JSONObject().put(key, command); - pushBasicProfile(update); - - getConfigLogger().verbose(getAccountId(), "removing value for key " + key + " from user profile"); - - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), "Failed to remove profile value for key " + key, t); - } - } - - private Future _setLocation(Location location) { - if (location == null) { - return null; - } - - locationFromUser = location; - getConfigLogger().verbose(getAccountId(), - "Location updated (" + location.getLatitude() + ", " + location.getLongitude() + ")"); - - // only queue the location ping if we are in the foreground - if (!isLocationForGeofence() && !isAppForeground()) { - return null; - } - - // Queue the ping event to transmit location update to server - // min 10 second interval between location pings - final int now = (int) (System.currentTimeMillis() / 1000); - Future future = null; - - if (isLocationForGeofence() && now > (lastLocationPingTimeForGeofence - + Constants.LOCATION_PING_INTERVAL_IN_SECONDS)) { - future = queueEvent(context, new JSONObject(), Constants.PING_EVENT); - lastLocationPingTimeForGeofence = now; - getConfigLogger().verbose(getAccountId(), - "Queuing location ping event for geofence location (" + location.getLatitude() + ", " + location - .getLongitude() + ")"); - } else if (!isLocationForGeofence() && now > (lastLocationPingTime - + Constants.LOCATION_PING_INTERVAL_IN_SECONDS)) { - future = queueEvent(context, new JSONObject(), Constants.PING_EVENT); - lastLocationPingTime = now; - getConfigLogger().verbose(getAccountId(), - "Queuing location ping event for location (" + location.getLatitude() + ", " + location - .getLongitude() + ")"); - } - - return future; - } - - //InApp - private void _showNotificationIfAvailable(Context context) { - SharedPreferences prefs = StorageHelper.getPreferences(context); - try { - if (!canShowInAppOnActivity()) { - Logger.v("Not showing notification on blacklisted activity"); - return; - } - - checkPendingNotifications(context, config); // see if we have any pending notifications - - JSONArray inapps = new JSONArray( - StorageHelper.getStringFromPrefs(context, config, Constants.INAPP_KEY, "[]")); - if (inapps.length() < 1) { - return; - } - - JSONObject inapp = inapps.getJSONObject(0); - prepareNotificationForDisplay(inapp); - - // JSON array doesn't have the feature to remove a single element, - // so we have to copy over the entire array, but the first element - JSONArray inappsUpdated = new JSONArray(); - for (int i = 0; i < inapps.length(); i++) { - if (i == 0) { - continue; - } - inappsUpdated.put(inapps.get(i)); - } - SharedPreferences.Editor editor = prefs.edit() - .putString(StorageHelper.storageKeyWithSuffix(config, Constants.INAPP_KEY), - inappsUpdated.toString()); - StorageHelper.persist(editor); - } catch (Throwable t) { - // We won't get here - getConfigLogger().verbose(getAccountId(), "InApp: Couldn't parse JSON array string from prefs", t); - } - } - - private String _stringifyAndCleanScalarProfilePropValue(Object value) { - String val = CTJsonConverter.toJsonString(value); - - if (val != null) { - ValidationResult vr = validator.cleanMultiValuePropertyValue(val); - - // Check for an error - if (vr.getErrorCode() != 0) { - validationResultStack.pushValidationResult(vr); - } - - Object _value = vr.getObject(); - val = (_value != null) ? vr.getObject().toString() : null; - } - - return val; - } - - private void _validateAndPushMultiValue(JSONArray currentValues, JSONArray newValues, - ArrayList originalValues, String key, String command) { - - try { - - // if any of these are null, indicates some problem along the way so abort operation - if (currentValues == null || newValues == null || originalValues == null || key == null - || command == null) { - return; - } - - String mergeOperation = command.equals(Constants.COMMAND_REMOVE) ? Validator.REMOVE_VALUES_OPERATION - : Validator.ADD_VALUES_OPERATION; - - // merge currentValues and newValues - ValidationResult vr = validator - .mergeMultiValuePropertyForKey(currentValues, newValues, mergeOperation, key); - - // Check for an error - if (vr.getErrorCode() != 0) { - validationResultStack.pushValidationResult(vr); - } - - // set the merged local values array - JSONArray localValues = (JSONArray) vr.getObject(); - - // update local profile - // remove an empty array - if (localValues == null || localValues.length() <= 0) { - getLocalDataStore().removeProfileField(key); - } else { - // not empty so save to local profile - getLocalDataStore().setProfileField(key, localValues); - } - - // push to server - JSONObject commandObj = new JSONObject(); - commandObj.put(command, new JSONArray(originalValues)); - - JSONObject fields = new JSONObject(); - fields.put(key, commandObj); - - pushBasicProfile(fields); - - getConfigLogger().verbose(getAccountId(), "Constructed multi-value profile push: " + fields.toString()); - - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), "Error pushing multiValue for key " + key, t); - } - } - - //Profile - - //Lifecycle - private void activityPaused() { - setAppForeground(false); - appLastSeen = System.currentTimeMillis(); - getConfigLogger().verbose(getAccountId(), "App in background"); - final int now = (int) (System.currentTimeMillis() / 1000); - if (inCurrentSession()) { - try { - StorageHelper - .putInt(context, StorageHelper.storageKeyWithSuffix(config, Constants.LAST_SESSION_EPOCH), - now); - getConfigLogger().verbose(getAccountId(), "Updated session time: " + now); - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), "Failed to update session time time: " + t.getMessage()); - } - } - } - - //Lifecycle - private void activityResumed(Activity activity) { - getConfigLogger().verbose(getAccountId(), "App in foreground"); - checkTimeoutSession(); - //Anything in this If block will run once per App Launch. - //Will not run for Apps which disable App Launched event - if (!isAppLaunchPushed()) { - pushAppLaunchedEvent(); - fetchFeatureFlags(); - onTokenRefresh(); - postAsyncSafely("HandlingInstallReferrer", new Runnable() { - @Override - public void run() { - if (!installReferrerDataSent && isFirstSession()) { - handleInstallReferrerOnFirstInstall(); - } - } - }); - - try { - if (geofenceCallback != null) { - geofenceCallback.triggerLocation(); - } - } catch (IllegalStateException e) { - getConfigLogger().verbose(getAccountId(), e.getLocalizedMessage()); - } catch (Exception e) { - getConfigLogger().verbose(getAccountId(), "Failed to trigger location"); - } - } - if (!inCurrentSession()) { - pushInitialEventsAsync(); - } - checkExistingInAppNotifications(activity); - checkPendingInAppNotifications(activity); - } - - /** - * Adds a new event to the queue, to be sent later. - * - * @param context The Android context - * @param event The event to be queued - * @param eventType The type of event to be queued - */ - - // only call async - private void addToQueue(final Context context, final JSONObject event, final int eventType) { - if (eventType == Constants.NV_EVENT) { - getConfigLogger().verbose(getAccountId(), "Pushing Notification Viewed event onto separate queue"); - processPushNotificationViewedEvent(context, event); - } else { - processEvent(context, event, eventType); - } - } - - private void asyncProfileSwitchUser(final Map profile, final String cacheGuid, - final String cleverTapID) { - postAsyncSafely("resetProfile", new Runnable() { - @Override - public void run() { - try { - getConfigLogger().verbose(getAccountId(), "asyncProfileSwitchUser:[profile " + profile - + " with Cached GUID " + ((cacheGuid != null) ? cachedGUID - : "NULL" + " and cleverTapID " + cleverTapID)); - //set optOut to false on the current user to unregister the device token - setCurrentUserOptedOut(false); - // unregister the device token on the current user - forcePushDeviceToken(false); - - // try and flush and then reset the queues - flushQueueSync(context, EventGroup.REGULAR); - flushQueueSync(context, EventGroup.PUSH_NOTIFICATION_VIEWED); - clearQueues(context); - - // clear out the old data - getLocalDataStore().changeUser(); - activityCount = 1; - destroySession(); - - // either force restore the cached GUID or generate a new one - if (cacheGuid != null) { - deviceInfo.forceUpdateDeviceId(cacheGuid); - notifyUserProfileInitialized(cacheGuid); - } else if (getConfig().getEnableCustomCleverTapId()) { - deviceInfo.forceUpdateCustomCleverTapID(cleverTapID); - } else { - deviceInfo.forceNewDeviceID(); - } - notifyUserProfileInitialized(getCleverTapID()); - setCurrentUserOptOutStateFromStorage(); // be sure to call this after the guid is updated - forcePushAppLaunchedEvent(); - if (profile != null) { - pushProfile(profile); - } - forcePushDeviceToken(true); - synchronized (processingUserLoginLock) { - processingUserLoginIdentifier = null; - } - resetInbox(); - resetABTesting(); - resetFeatureFlags(); - resetProductConfigs(); - recordDeviceIDErrors(); - resetDisplayUnits(); - inAppFCManager.changeUser(getCleverTapID()); - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), "Reset Profile error", t); - } - } - }); - } - - /** - * Attaches meta info about the current state of the device to an event. - * Typically, this meta is added only to the ping event. - */ - private void attachMeta(final JSONObject o, final Context context) { - // Memory consumption - try { - o.put("mc", Utils.getMemoryConsumption()); - } catch (Throwable t) { - // Ignore - } - - // Attach the network type - try { - o.put("nt", Utils.getCurrentNetworkType(context)); - } catch (Throwable t) { - // Ignore - } - } - - //Session - private void attachPackageNameIfRequired(final Context context, final JSONObject event) { - try { - final String type = event.getString("type"); - // Send it only for app launched events - if ("event".equals(type) && Constants.APP_LAUNCHED_EVENT.equals(event.getString("evtName"))) { - event.put("pai", context.getPackageName()); - } - } catch (Throwable t) { - // Ignore - } - } - - private HttpsURLConnection buildHttpsURLConnection(final String endpoint) - throws IOException { - - URL url = new URL(endpoint); - HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); - conn.setConnectTimeout(10000); - conn.setReadTimeout(10000); - conn.setRequestProperty("Content-Type", "application/json; charset=utf-8"); - conn.setRequestProperty("X-CleverTap-Account-ID", getAccountId()); - conn.setRequestProperty("X-CleverTap-Token", this.config.getAccountToken()); - conn.setInstanceFollowRedirects(false); - if (this.config.isSslPinningEnabled()) { - SSLContext _sslContext = getSSLContext(); - if (_sslContext != null) { - conn.setSSLSocketFactory(getPinnedCertsSslSocketfactory(_sslContext)); - } - } - return conn; - } - - private boolean canShowInAppOnActivity() { - updateBlacklistedActivitySet(); - - for (String blacklistedActivity : inappActivityExclude) { - String currentActivityName = getCurrentActivityName(); - if (currentActivityName != null && currentActivityName.contains(blacklistedActivity)) { - return false; - } - } - - return true; - } - - private boolean checkDuplicateNotificationIds(Bundle extras, HashMap notificationTagMap, - int interval) { - synchronized (notificationMapLock) { - // default to false; only return true if we are sure we've seen this one before - boolean isDupe = false; - try { - String notificationIdTag = extras.getString(Constants.NOTIFICATION_ID_TAG); - long now = System.currentTimeMillis(); - if (notificationTagMap.containsKey(notificationIdTag)) { - long timestamp; - // noinspection ConstantConditions - timestamp = (Long) notificationTagMap.get(notificationIdTag); - // same notificationId within time internal treat as dupe - if (now - timestamp < interval) { - isDupe = true; - } - } - notificationTagMap.put(notificationIdTag, now); - } catch (Throwable ignored) { - // no-op - } - return isDupe; - } - } - - private void checkExistingInAppNotifications(Activity activity) { - final boolean canShow = canShowInAppOnActivity(); - if (canShow) { - if (currentlyDisplayingInApp != null && ((System.currentTimeMillis() / 1000) < currentlyDisplayingInApp - .getTimeToLive())) { - Fragment inAppFragment = ((FragmentActivity) activity).getSupportFragmentManager() - .getFragment(new Bundle(), currentlyDisplayingInApp.getType()); - if (getCurrentActivity() != null && inAppFragment != null) { - FragmentTransaction fragmentTransaction = ((FragmentActivity) activity) - .getSupportFragmentManager() - .beginTransaction(); - Bundle bundle = new Bundle(); - bundle.putParcelable("inApp", currentlyDisplayingInApp); - bundle.putParcelable("config", config); - inAppFragment.setArguments(bundle); - fragmentTransaction.setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out); - fragmentTransaction.add(android.R.id.content, inAppFragment, currentlyDisplayingInApp.getType()); - Logger.v(config.getAccountId(), - "calling InAppFragment " + currentlyDisplayingInApp.getCampaignId()); - fragmentTransaction.commit(); - } - } - } - } - - private void checkPendingInAppNotifications(Activity activity) { - final boolean canShow = canShowInAppOnActivity(); - if (canShow) { - if (pendingInappRunnable != null) { - getConfigLogger().verbose(getAccountId(), "Found a pending inapp runnable. Scheduling it"); - getHandlerUsingMainLooper().postDelayed(pendingInappRunnable, 200); - pendingInappRunnable = null; - } else { - showNotificationIfAvailable(context); - } - } else { - Logger.d("In-app notifications will not be shown for this activity (" - + (activity != null ? activity.getLocalClassName() : "") + ")"); - } - } - - // private multi-value handlers and helpers - - // SessionManager/session management - private void checkTimeoutSession() { - if (appLastSeen <= 0) { - return; - } - long now = System.currentTimeMillis(); - if ((now - appLastSeen) > Constants.SESSION_LENGTH_MINS * 60 * 1000) { - getConfigLogger().verbose(getAccountId(), "Session Timed Out"); - destroySession(); - setCurrentActivity(null); - } - } - - private synchronized void clearCampaign() { - campaign = null; - } - - //Session - private void clearFirstRequestTimestampIfNeeded(Context context) { - StorageHelper.putInt(context, StorageHelper.storageKeyWithSuffix(config, Constants.KEY_FIRST_TS), 0); - } - - //Session - private void clearIJ(Context context) { - final SharedPreferences prefs = StorageHelper.getPreferences(context, Constants.NAMESPACE_IJ); - final SharedPreferences.Editor editor = prefs.edit(); - editor.clear(); - StorageHelper.persist(editor); - } - - //Session - private void clearLastRequestTimestamp(Context context) { - StorageHelper.putInt(context, StorageHelper.storageKeyWithSuffix(config, Constants.KEY_LAST_TS), 0); - } - - private synchronized void clearMedium() { - medium = null; - } - - /** - * Only call async - */ - private void clearQueues(final Context context) { - synchronized (eventLock) { - - DBAdapter adapter = loadDBAdapter(context); - DBAdapter.Table tableName = DBAdapter.Table.EVENTS; - - adapter.removeEvents(tableName); - tableName = DBAdapter.Table.PROFILE_EVENTS; - adapter.removeEvents(tableName); - - clearUserContext(context); - } - } - - private synchronized void clearSource() { - source = null; - } - - //Session - private void clearUserContext(final Context context) { - clearIJ(context); - clearFirstRequestTimestampIfNeeded(context); - clearLastRequestTimestamp(context); - } - - private synchronized void clearWzrkParams() { - wzrkParams = null; - } - - private void createAlarmScheduler(Context context) { - int pingFrequency = getPingFrequency(context); - if (pingFrequency > 0) { - AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - Intent intent = new Intent(CTBackgroundIntentService.MAIN_ACTION); - intent.setPackage(context.getPackageName()); - PendingIntent alarmPendingIntent = PendingIntent - .getService(context, getAccountId().hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT); - if (alarmManager != null) { - alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), - Constants.ONE_MIN_IN_MILLIS * pingFrequency, alarmPendingIntent); - } - } - } - - @SuppressLint("MissingPermission") - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - private void createOrResetJobScheduler(Context context) { - - int existingJobId = StorageHelper.getInt(context, Constants.PF_JOB_ID, -1); - JobScheduler jobScheduler = (JobScheduler) context.getSystemService(JOB_SCHEDULER_SERVICE); - - //Disable push amp for devices below Api 26 - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - if (existingJobId >= 0) {//cancel already running job - jobScheduler.cancel(existingJobId); - StorageHelper.putInt(context, Constants.PF_JOB_ID, -1); - } - - getConfigLogger().debug(getAccountId(), "Push Amplification feature is not supported below Oreo"); - return; - } - - if (jobScheduler == null) { - return; - } - int pingFrequency = getPingFrequency(context); - - if (existingJobId < 0 && pingFrequency < 0) { - return; //no running job and nothing to create - } - - if (pingFrequency < 0) { //running job but hard cancel - jobScheduler.cancel(existingJobId); - StorageHelper.putInt(context, Constants.PF_JOB_ID, -1); - return; - } - - ComponentName componentName = new ComponentName(context, CTBackgroundJobService.class); - boolean needsCreate = (existingJobId < 0 && pingFrequency > 0); - - //running job, no hard cancel so check for diff in ping frequency and recreate if needed - JobInfo existingJobInfo = getJobInfo(existingJobId, jobScheduler); - if (existingJobInfo != null - && existingJobInfo.getIntervalMillis() != pingFrequency * Constants.ONE_MIN_IN_MILLIS) { - jobScheduler.cancel(existingJobId); - StorageHelper.putInt(context, Constants.PF_JOB_ID, -1); - needsCreate = true; - } - - if (needsCreate) { - int jobid = getAccountId().hashCode(); - JobInfo.Builder builder = new JobInfo.Builder(jobid, componentName); - builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); - builder.setRequiresCharging(false); - - builder.setPeriodic(pingFrequency * Constants.ONE_MIN_IN_MILLIS, 5 * Constants.ONE_MIN_IN_MILLIS); - builder.setRequiresBatteryNotLow(true); - - if (Utils.hasPermission(context, "android.permission.RECEIVE_BOOT_COMPLETED")) { - builder.setPersisted(true); - } - - JobInfo jobInfo = builder.build(); - int resultCode = jobScheduler.schedule(jobInfo); - if (resultCode == JobScheduler.RESULT_SUCCESS) { - Logger.d(getAccountId(), "Job scheduled - " + jobid); - StorageHelper.putInt(context, Constants.PF_JOB_ID, jobid); - } else { - Logger.d(getAccountId(), "Job not scheduled - " + jobid); - } - } - } - - private void createSession(final Context context) { - currentSessionId = (int) (System.currentTimeMillis() / 1000); - - getConfigLogger().verbose(getAccountId(), "Session created with ID: " + currentSessionId); - - SharedPreferences prefs = StorageHelper.getPreferences(context); - - final int lastSessionID = StorageHelper.getIntFromPrefs(context, config, Constants.SESSION_ID_LAST, 0); - final int lastSessionTime = StorageHelper.getIntFromPrefs(context, config, Constants.LAST_SESSION_EPOCH, 0); - if (lastSessionTime > 0) { - lastSessionLength = lastSessionTime - lastSessionID; - } - - getConfigLogger().verbose(getAccountId(), "Last session length: " + lastSessionLength + " seconds"); - - if (lastSessionID == 0) { - firstSession = true; - } - - final SharedPreferences.Editor editor = prefs.edit() - .putInt(StorageHelper.storageKeyWithSuffix(config, Constants.SESSION_ID_LAST), currentSessionId); - StorageHelper.persist(editor); - } - - /** - * Destroys the current session and resets firstSession flag, if first session lasts more than 20 minutes - *

    For an app like Music Player

  • user installs an app and plays music and then moves to background. - *
  • User then re-launches an App after listening music in background for more than 20 minutes, in this case - * since an app is not yet killed due to background music app installed event must not be raised by SDK - */ - private void destroySession() { - currentSessionId = 0; - setAppLaunchPushed(false); - if (isFirstSession()) { - firstSession = false; - } - getConfigLogger().verbose(getAccountId(), "Session destroyed; Session ID is now 0"); - clearSource(); - clearMedium(); - clearCampaign(); - clearWzrkParams(); - } - - //Push - @SuppressWarnings("SameParameterValue") - private void deviceTokenDidRefresh(String token, PushType type) { - if (tokenRefreshListener != null) { - getConfigLogger().debug(getAccountId(), "Notifying devicePushTokenDidRefresh: " + token); - tokenRefreshListener.devicePushTokenDidRefresh(token, type); - } - } - - //util - - //Session - - //InApp - private void displayNotification(final CTInAppNotification inAppNotification) { - - if (Looper.myLooper() != Looper.getMainLooper()) { - getHandlerUsingMainLooper().post(new Runnable() { - @Override - public void run() { - displayNotification(inAppNotification); - } - }); - return; - } - - if (inAppFCManager != null) { - if (!inAppFCManager.canShow(inAppNotification)) { - getConfigLogger().verbose(getAccountId(), - "InApp has been rejected by FC, not showing " + inAppNotification.getCampaignId()); - showInAppNotificationIfAny(); - return; - } - - inAppFCManager.didShow(context, inAppNotification); - } else { - getConfigLogger().verbose(getAccountId(), - "InAppFCManager is NULL, not showing " + inAppNotification.getCampaignId()); - return; - } - - final InAppNotificationListener listener = getInAppNotificationListener(); - - final boolean goFromListener; - - if (listener != null) { - final HashMap kvs; - - if (inAppNotification.getCustomExtras() != null) { - kvs = Utils.convertJSONObjectToHashMap(inAppNotification.getCustomExtras()); - } else { - kvs = new HashMap<>(); - } - - goFromListener = listener.beforeShow(kvs); - } else { - goFromListener = true; - } - - if (!goFromListener) { - getConfigLogger().verbose(getAccountId(), - "Application has decided to not show this in-app notification: " + inAppNotification - .getCampaignId()); - showInAppNotificationIfAny(); - return; - } - showInApp(context, inAppNotification, config); - - } - - private void doTokenRefresh(String token, PushType pushType) { - if (TextUtils.isEmpty(token) || pushType == null) { - return; - } - switch (pushType) { - case FCM: - pushFcmRegistrationId(token, true); - break; - case XPS: - pushXiaomiRegistrationId(token, true); - break; - case HPS: - pushHuaweiRegistrationId(token, true); - break; - case BPS: - pushBaiduRegistrationId(token, true); - break; - case ADM: - pushAmazonRegistrationId(token, true); - break; - } - } - - private void flushDBQueue(final Context context, final EventGroup eventGroup) { - getConfigLogger().verbose(getAccountId(), "Somebody has invoked me to send the queue to CleverTap servers"); - - QueueCursor cursor; - QueueCursor previousCursor = null; - boolean loadMore = true; - - while (loadMore) { - - cursor = getQueuedEvents(context, 50, previousCursor, eventGroup); - - if (cursor == null || cursor.isEmpty()) { - getConfigLogger().verbose(getAccountId(), "No events in the queue, failing"); - break; - } - - previousCursor = cursor; - JSONArray queue = cursor.getData(); - - if (queue == null || queue.length() <= 0) { - getConfigLogger().verbose(getAccountId(), "No events in the queue, failing"); - break; - } - - loadMore = sendQueue(context, eventGroup, queue); - } - } - - private void flushQueueAsync(final Context context, final EventGroup eventGroup) { - postAsyncSafely("CommsManager#flushQueueAsync", new Runnable() { - @Override - public void run() { - if (eventGroup == EventGroup.PUSH_NOTIFICATION_VIEWED) { - getConfigLogger() - .verbose(getAccountId(), "Pushing Notification Viewed event onto queue flush sync"); - } else { - getConfigLogger().verbose(getAccountId(), "Pushing event onto queue flush sync"); - } - flushQueueSync(context, eventGroup); - } - }); - } - - private void flushQueueSync(final Context context, final EventGroup eventGroup) { - if (!isNetworkOnline(context)) { - getConfigLogger().verbose(getAccountId(), "Network connectivity unavailable. Will retry later"); - return; - } - - if (isOffline()) { - getConfigLogger() - .debug(getAccountId(), "CleverTap Instance has been set to offline, won't send events queue"); - return; - } - - if (needsHandshakeForDomain(eventGroup)) { - mResponseFailureCount = 0; - setDomain(context, null); - performHandshakeForDomain(context, eventGroup, new Runnable() { - @Override - public void run() { - flushDBQueue(context, eventGroup); - } - }); - } else { - getConfigLogger().verbose(getAccountId(), "Pushing Notification Viewed event onto queue DB flush"); - flushDBQueue(context, eventGroup); - } - } - - //Event - private void forcePushAppLaunchedEvent() { - setAppLaunchPushed(false); - pushAppLaunchedEvent(); - } - - /** - * push the device token outside of the normal course - */ - private void forcePushDeviceToken(final boolean register) { - - for (PushType pushType : pushProviders.getAvailablePushTypes()) { - pushDeviceTokenEvent(null, register, pushType); - } - } - - //Event - - /** - * The ARP is additional request parameters, which must be sent once - * received after any HTTP call. This is sort of a proxy for cookies. - * - * @return A JSON object containing the ARP key/values. Can be null. - */ - private JSONObject getARP() { - try { - final String nameSpaceKey = getNewNamespaceARPKey(); - if (nameSpaceKey == null) { - return null; - } - - SharedPreferences prefs = null; - - //checking whether new namespace is empty or not - //if not empty, using prefs of new namespace to send ARP - //if empty, checking for old prefs - if (!StorageHelper.getPreferences(context, nameSpaceKey).getAll().isEmpty()) { - //prefs point to new namespace - prefs = StorageHelper.getPreferences(context, nameSpaceKey); - } else { - //prefs point to new namespace migrated from old namespace - prefs = migrateARPToNewNameSpace(nameSpaceKey, getNamespaceARPKey()); - } - - final Map all = prefs.getAll(); - final Iterator> iter = all.entrySet().iterator(); - - while (iter.hasNext()) { - final Map.Entry kv = iter.next(); - final Object o = kv.getValue(); - if (o instanceof Number && ((Number) o).intValue() == -1) { - iter.remove(); - } - } - final JSONObject ret = new JSONObject(all); - getConfigLogger().verbose(getAccountId(), - "Fetched ARP for namespace key: " + nameSpaceKey + " values: " + all.toString()); - return ret; - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), "Failed to construct ARP object", t); - return null; - } - } - - //Profile - - private long getAppInstallTime() { - return appInstallTime; - } - - //Event - private JSONObject getAppLaunchedFields() { - - try { - boolean deviceIsMultiUser = false; - if (deviceInfo.getGoogleAdID() != null) { - deviceIsMultiUser = new LoginInfoProvider(this).deviceIsMultiUser(); - } - return CTJsonConverter.from(deviceInfo, locationFromUser, enableNetworkInfoReporting, - deviceIsMultiUser); - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), "Failed to construct App Launched event", t); - return new JSONObject(); - } - } - - private synchronized String getCampaign() { - return campaign; - } - - private synchronized void setCampaign(String campaign) { - if (this.campaign == null) { - this.campaign = campaign; - } - } - - private CleverTapInstanceConfig getConfig() { - return config; - } - - //Push - - private Logger getConfigLogger() { - return getConfig().getLogger(); - } - - private int getCurrentSession() { - return currentSessionId; - } - - //gives delay frequency based on region - //randomly adds delay to 1s delay in case of non-EU regions - private int getDelayFrequency() { - getConfigLogger().debug(getAccountId(), "Network retry #" + networkRetryCount); - - //Retry with delay as 1s for first 10 retries - if (networkRetryCount < 10) { - getConfigLogger().debug(getAccountId(), - "Failure count is " + networkRetryCount + ". Setting delay frequency to 1s"); - minDelayFrequency = Constants.PUSH_DELAY_MS; //reset minimum delay to 1s - return minDelayFrequency; - } - - if (config.getAccountRegion() == null) { - //Retry with delay as 1s if region is null in case of eu1 - getConfigLogger().debug(getAccountId(), "Setting delay frequency to 1s"); - return Constants.PUSH_DELAY_MS; - } else { - //Retry with delay as minimum delay frequency and add random number of seconds to scatter traffic - Random randomGen = new Random(); - int randomDelay = (randomGen.nextInt(10) + 1) * 1000; - minDelayFrequency += randomDelay; - if (minDelayFrequency < maxDelayFrequency) { - getConfigLogger().debug(getAccountId(), "Setting delay frequency to " + minDelayFrequency); - return minDelayFrequency; - } else { - minDelayFrequency = Constants.PUSH_DELAY_MS; - } - getConfigLogger().debug(getAccountId(), "Setting delay frequency to " + minDelayFrequency); - return minDelayFrequency; - } - } - - private String getDomain(boolean defaultToHandshakeURL, final EventGroup eventGroup) { - String domain = getDomainFromPrefsOrMetadata(eventGroup); - - final boolean emptyDomain = domain == null || domain.trim().length() == 0; - if (emptyDomain && !defaultToHandshakeURL) { - return null; - } - - if (emptyDomain) { - domain = Constants.PRIMARY_DOMAIN + "/hello"; - } else { - domain += "/a1"; - } - - return domain; - } - - private String getDomainFromPrefsOrMetadata(final EventGroup eventGroup) { - try { - final String region = this.config.getAccountRegion(); - if (region != null && region.trim().length() > 0) { - // Always set this to 0 so that the handshake is not performed during a HTTP failure - mResponseFailureCount = 0; - if (eventGroup.equals(EventGroup.PUSH_NOTIFICATION_VIEWED)) { - return region.trim().toLowerCase() + eventGroup.httpResource + "." + Constants.PRIMARY_DOMAIN; - } else { - return region.trim().toLowerCase() + "." + Constants.PRIMARY_DOMAIN; - } - } - } catch (Throwable t) { - // Ignore - } - if (eventGroup.equals(EventGroup.PUSH_NOTIFICATION_VIEWED)) { - return StorageHelper.getStringFromPrefs(context, config, Constants.SPIKY_KEY_DOMAIN_NAME, null); - } else { - return StorageHelper.getStringFromPrefs(context, config, Constants.KEY_DOMAIN_NAME, null); - } - } - - private String getEndpoint(final boolean defaultToHandshakeURL, final EventGroup eventGroup) { - String domain = getDomain(defaultToHandshakeURL, eventGroup); - if (domain == null) { - getConfigLogger().verbose(getAccountId(), "Unable to configure endpoint, domain is null"); - return null; - } - - final String accountId = getAccountId(); - if (accountId == null) { - getConfigLogger().verbose(getAccountId(), "Unable to configure endpoint, accountID is null"); - return null; - } - - String endpoint = "https://" + domain + "?os=Android&t=" + this.deviceInfo.getSdkVersion(); - endpoint += "&z=" + accountId; - - final boolean needsHandshake = needsHandshakeForDomain(eventGroup); - // Don't attach ts if its handshake - if (needsHandshake) { - return endpoint; - } - - currentRequestTimestamp = (int) (System.currentTimeMillis() / 1000); - endpoint += "&ts=" + currentRequestTimestamp; - - return endpoint; - } - - private int getFirstRequestTimestamp() { - return StorageHelper.getIntFromPrefs(context, config, Constants.KEY_FIRST_TS, 0); - } - - private int getGeofenceSDKVersion() { - return geofenceSDKVersion; - } - - private void setGeofenceSDKVersion(int geofenceSDKVersion) { - this.geofenceSDKVersion = geofenceSDKVersion; - } - - private String getGraphUserPropertySafely(JSONObject graphUser, String key, String def) { - try { - String prop = (String) graphUser.get(key); - if (prop != null) { - return prop; - } else { - return def; - } - } catch (Throwable t) { - return def; - } - } - - /** - * Returns the generic handler object which is used to post - * runnables. The returned value will never be null. - * - * @return The generic handler - * @see Handler - */ - private Handler getHandlerUsingMainLooper() { - return handlerUsingMainLooper; - } - - private long getI() { - return StorageHelper.getLongFromPrefs(context, config, Constants.KEY_I, 0, Constants.NAMESPACE_IJ); - } - - private long getJ() { - return StorageHelper.getLongFromPrefs(context, config, Constants.KEY_J, 0, Constants.NAMESPACE_IJ); - } - - private int getLastRequestTimestamp() { - return StorageHelper.getIntFromPrefs(context, config, Constants.KEY_LAST_TS, 0); - } - - //Session - private int getLastSessionLength() { - return lastSessionLength; - } - - private LocalDataStore getLocalDataStore() { - return localDataStore; - } - - private synchronized String getMedium() { - return medium; - } - - // only set if not already set during the session - private synchronized void setMedium(String medium) { - if (this.medium == null) { - this.medium = medium; - } - } - - //Session - //Old namespace for ARP Shared Prefs - private String getNamespaceARPKey() { - - final String accountId = getAccountId(); - if (accountId == null) { - return null; - } - - getConfigLogger().verbose(getAccountId(), "Old ARP Key = ARP:" + accountId); - return "ARP:" + accountId; - } - - //New namespace for ARP Shared Prefs - private String getNewNamespaceARPKey() { - - final String accountId = getAccountId(); - if (accountId == null) { - return null; - } - - getConfigLogger().verbose(getAccountId(), "New ARP Key = ARP:" + accountId + ":" + getCleverTapID()); - return "ARP:" + accountId + ":" + getCleverTapID(); - } - - private int getPingFrequency(Context context) { - return StorageHelper.getInt(context, Constants.PING_FREQUENCY, - Constants.PING_FREQUENCY_VALUE); //intentional global key because only one Job is running - } - - private QueueCursor getPushNotificationViewedQueuedEvents(final Context context, final int batchSize, - final QueueCursor previousCursor) { - return getQueueCursor(context, DBAdapter.Table.PUSH_NOTIFICATION_VIEWED, batchSize, previousCursor); - } - - private QueueCursor getQueueCursor(final Context context, DBAdapter.Table table, final int batchSize, - final QueueCursor previousCursor) { - synchronized (eventLock) { - DBAdapter adapter = loadDBAdapter(context); - DBAdapter.Table tableName = (previousCursor != null) ? previousCursor.getTableName() : table; - - // if previousCursor that means the batch represented by the previous cursor was processed so remove those from the db - if (previousCursor != null) { - adapter.cleanupEventsFromLastId(previousCursor.getLastId(), previousCursor.getTableName()); - } - - // grab the new batch - QueueCursor newCursor = new QueueCursor(); - newCursor.setTableName(tableName); - JSONObject queuedDBEvents = adapter.fetchEvents(tableName, batchSize); - newCursor = updateCursorForDBObject(queuedDBEvents, newCursor); - - return newCursor; - } - } - - private QueueCursor getQueuedDBEvents(final Context context, final int batchSize, - final QueueCursor previousCursor) { - - synchronized (eventLock) { - QueueCursor newCursor = getQueueCursor(context, DBAdapter.Table.EVENTS, batchSize, previousCursor); - - if (newCursor.isEmpty() && newCursor.getTableName().equals(DBAdapter.Table.EVENTS)) { - newCursor = getQueueCursor(context, DBAdapter.Table.PROFILE_EVENTS, batchSize, null); - } - - return newCursor.isEmpty() ? null : newCursor; - } - } - - @SuppressWarnings("SameParameterValue") - private QueueCursor getQueuedEvents(final Context context, final int batchSize, final QueueCursor previousCursor, - final EventGroup eventGroup) { - if (eventGroup == EventGroup.PUSH_NOTIFICATION_VIEWED) { - getConfigLogger().verbose(getAccountId(), "Returning Queued Notification Viewed events"); - return getPushNotificationViewedQueuedEvents(context, batchSize, previousCursor); - } else { - getConfigLogger().verbose(getAccountId(), "Returning Queued events"); - return getQueuedDBEvents(context, batchSize, previousCursor); - } - } - - private long getReferrerClickTime() { - return referrerClickTime; - } - - private String getScreenName() { - return currentScreenName.equals("") ? null : currentScreenName; - } - - private synchronized String getSource() { - return source; - } - - //UTM - // only set if not already set during the session - private synchronized void setSource(String source) { - if (this.source == null) { - this.source = source; - } - } - - private synchronized JSONObject getWzrkParams() { - return wzrkParams; - } - - private synchronized void setWzrkParams(JSONObject wzrkParams) { - if (this.wzrkParams == null) { - this.wzrkParams = wzrkParams; - } - } - - //Saves ARP directly to new namespace - private void handleARPUpdate(final Context context, final JSONObject arp) { - if (arp == null || arp.length() == 0) { - return; - } - - final String nameSpaceKey = getNewNamespaceARPKey(); - if (nameSpaceKey == null) { - return; - } - - final SharedPreferences prefs = StorageHelper.getPreferences(context, nameSpaceKey); - final SharedPreferences.Editor editor = prefs.edit(); - - final Iterator keys = arp.keys(); - while (keys.hasNext()) { - final String key = keys.next(); - try { - final Object o = arp.get(key); - if (o instanceof Number) { - final int update = ((Number) o).intValue(); - editor.putInt(key, update); - } else if (o instanceof String) { - if (((String) o).length() < 100) { - editor.putString(key, (String) o); - } else { - getConfigLogger().verbose(getAccountId(), - "ARP update for key " + key + " rejected (string value too long)"); - } - } else if (o instanceof Boolean) { - editor.putBoolean(key, (Boolean) o); - } else { - getConfigLogger() - .verbose(getAccountId(), "ARP update for key " + key + " rejected (invalid data type)"); - } - } catch (JSONException e) { - // Ignore - } - } - getConfigLogger().verbose(getAccountId(), - "Stored ARP for namespace key: " + nameSpaceKey + " values: " + arp.toString()); - StorageHelper.persist(editor); - } - - private void handleInstallReferrerOnFirstInstall() { - getConfigLogger().verbose(getAccountId(), "Starting to handle install referrer"); - try { - final InstallReferrerClient referrerClient = InstallReferrerClient.newBuilder(context).build(); - referrerClient.startConnection(new InstallReferrerStateListener() { - @Override - public void onInstallReferrerServiceDisconnected() { - if (!installReferrerDataSent) { - handleInstallReferrerOnFirstInstall(); - } - } - - @Override - public void onInstallReferrerSetupFinished(int responseCode) { - switch (responseCode) { - case InstallReferrerClient.InstallReferrerResponse.OK: - // Connection established. - ReferrerDetails response = null; - try { - response = referrerClient.getInstallReferrer(); - String referrerUrl = response.getInstallReferrer(); - referrerClickTime = response.getReferrerClickTimestampSeconds(); - appInstallTime = response.getInstallBeginTimestampSeconds(); - pushInstallReferrer(referrerUrl); - installReferrerDataSent = true; - getConfigLogger().debug(getAccountId(), - "Install Referrer data set [Referrer URL-" + referrerUrl + "]"); - } catch (RemoteException e) { - getConfigLogger().debug(getAccountId(), - "Remote exception caused by Google Play Install Referrer library - " + e - .getMessage()); - referrerClient.endConnection(); - installReferrerDataSent = false; - } - referrerClient.endConnection(); - break; - case InstallReferrerClient.InstallReferrerResponse.FEATURE_NOT_SUPPORTED: - // API not available on the current Play Store app. - getConfigLogger().debug(getAccountId(), - "Install Referrer data not set, API not supported by Play Store on device"); - break; - case InstallReferrerClient.InstallReferrerResponse.SERVICE_UNAVAILABLE: - // Connection couldn't be established. - getConfigLogger().debug(getAccountId(), - "Install Referrer data not set, connection to Play Store unavailable"); - break; - } - } - }); - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), - "Google Play Install Referrer's InstallReferrerClient Class not found - " + t - .getLocalizedMessage() - + " \n Please add implementation 'com.android.installreferrer:installreferrer:2.1' to your build.gradle"); - } - } - - //PN - private void handlePushNotificationsInResponse(JSONArray pushNotifications) { - try { - for (int i = 0; i < pushNotifications.length(); i++) { - Bundle pushBundle = new Bundle(); - JSONObject pushObject = pushNotifications.getJSONObject(i); - if (pushObject.has("wzrk_acct_id")) { - pushBundle.putString("wzrk_acct_id", pushObject.getString("wzrk_acct_id")); - } - if (pushObject.has("wzrk_acts")) { - pushBundle.putString("wzrk_acts", pushObject.getString("wzrk_acts")); - } - if (pushObject.has("nm")) { - pushBundle.putString("nm", pushObject.getString("nm")); - } - if (pushObject.has("nt")) { - pushBundle.putString("nt", pushObject.getString("nt")); - } - if (pushObject.has("wzrk_bp")) { - pushBundle.putString("wzrk_bp", pushObject.getString("wzrk_bp")); - } - if (pushObject.has("pr")) { - pushBundle.putString("pr", pushObject.getString("pr")); - } - if (pushObject.has("wzrk_pivot")) { - pushBundle.putString("wzrk_pivot", pushObject.getString("wzrk_pivot")); - } - if (pushObject.has("wzrk_sound")) { - pushBundle.putString("wzrk_sound", pushObject.getString("wzrk_sound")); - } - if (pushObject.has("wzrk_cid")) { - pushBundle.putString("wzrk_cid", pushObject.getString("wzrk_cid")); - } - if (pushObject.has("wzrk_bc")) { - pushBundle.putString("wzrk_bc", pushObject.getString("wzrk_bc")); - } - if (pushObject.has("wzrk_bi")) { - pushBundle.putString("wzrk_bi", pushObject.getString("wzrk_bi")); - } - if (pushObject.has("wzrk_id")) { - pushBundle.putString("wzrk_id", pushObject.getString("wzrk_id")); - } - if (pushObject.has("wzrk_pn")) { - pushBundle.putString("wzrk_pn", pushObject.getString("wzrk_pn")); - } - if (pushObject.has("ico")) { - pushBundle.putString("ico", pushObject.getString("ico")); - } - if (pushObject.has("wzrk_ck")) { - pushBundle.putString("wzrk_ck", pushObject.getString("wzrk_ck")); - } - if (pushObject.has("wzrk_dl")) { - pushBundle.putString("wzrk_dl", pushObject.getString("wzrk_dl")); - } - if (pushObject.has("wzrk_pid")) { - pushBundle.putString("wzrk_pid", pushObject.getString("wzrk_pid")); - } - if (pushObject.has("wzrk_ttl")) { - pushBundle.putLong("wzrk_ttl", pushObject.getLong("wzrk_ttl")); - } - if (pushObject.has("wzrk_rnv")) { - pushBundle.putString("wzrk_rnv", pushObject.getString("wzrk_rnv")); - } - Iterator iterator = pushObject.keys(); - while (iterator.hasNext()) { - String key = iterator.next().toString(); - pushBundle.putString(key, pushObject.getString(key)); - } - if (!pushBundle.isEmpty() && !dbAdapter - .doesPushNotificationIdExist(pushObject.getString("wzrk_pid"))) { - getConfigLogger().verbose("Creating Push Notification locally"); - if (pushAmpListener != null) { - pushAmpListener.onPushAmpPayloadReceived(pushBundle); - } else { - createNotification(context, pushBundle); - } - } else { - getConfigLogger().verbose(getAccountId(), - "Push Notification already shown, ignoring local notification :" + pushObject - .getString("wzrk_pid")); - } - } - } catch (JSONException e) { - getConfigLogger().verbose(getAccountId(), "Error parsing push notification JSON"); - } - } - - /** - * This method handles send Test flow for Display Units - * - * @param extras - bundled data of notification payload - */ - private void handleSendTestForDisplayUnits(Bundle extras) { - try { - String pushJsonPayload = extras.getString(Constants.DISPLAY_UNIT_PREVIEW_PUSH_PAYLOAD_KEY); - Logger.v("Received Display Unit via push payload: " + pushJsonPayload); - JSONObject r = new JSONObject(); - JSONArray displayUnits = new JSONArray(); - r.put(Constants.DISPLAY_UNIT_JSON_RESPONSE_KEY, displayUnits); - JSONObject testPushObject = new JSONObject(pushJsonPayload); - displayUnits.put(testPushObject); - processDisplayUnitsResponse(r); - } catch (Throwable t) { - Logger.v("Failed to process Display Unit from push notification payload", t); - } - } - - private boolean hasDomainChanged(final String newDomain) { - final String oldDomain = StorageHelper.getStringFromPrefs(context, config, Constants.KEY_DOMAIN_NAME, null); - return !newDomain.equals(oldDomain); - } - - private boolean inCurrentSession() { - return currentSessionId > 0; - } - - private void initABTesting() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - return; - } - if (!config.isAnalyticsOnly()) { - if (!config.isABTestingEnabled()) { - getConfigLogger().debug(config.getAccountId(), "AB Testing is not enabled for this instance"); - return; - } - - if (getCleverTapID() == null) { - getConfigLogger() - .verbose(config.getAccountId(), "GUID not set yet, deferring ABTesting initialization"); - return; - } - - config.setEnableUIEditor(isUIEditorEnabled); - if (ctABTestController == null) { - ctABTestController = new CTABTestController(context, config, getCleverTapID(), this); - getConfigLogger().verbose(config.getAccountId(), "AB Testing initialized"); - } - } - } - - private void initFeatureFlags(boolean fromPlayServices) { - Logger.v("Initializing Feature Flags with device Id = " + getCleverTapID()); - - if (config.isAnalyticsOnly()) { - getConfigLogger().debug(config.getAccountId(), "Feature Flag is not enabled for this instance"); - return; - } - - if (ctFeatureFlagsController == null) { - ctFeatureFlagsController = new CTFeatureFlagsController(context, getCleverTapID(), config, this); - getConfigLogger().verbose(config.getAccountId(), "Feature Flags initialized"); - } - - if (fromPlayServices && !ctFeatureFlagsController.isInitialized()) { - ctFeatureFlagsController.setGuidAndInit(getCleverTapID()); - } - } - - private void initProductConfig(boolean fromPlayServices) { - Logger.v("Initializing Product Config with device Id = " + getCleverTapID()); - - if (config.isAnalyticsOnly()) { - getConfigLogger().debug(config.getAccountId(), "Product Config is not enabled for this instance"); - return; - } - - if (ctProductConfigController == null) { - ctProductConfigController = new CTProductConfigController(context, getCleverTapID(), config, this); - } - if (fromPlayServices && !ctProductConfigController.isInitialized()) { - ctProductConfigController.setGuidAndInit(getCleverTapID()); - } - } - - //Networking - private String insertHeader(Context context, JSONArray arr) { - try { - final JSONObject header = new JSONObject(); - - String deviceId = getCleverTapID(); - if (deviceId != null && !deviceId.equals("")) { - header.put("g", deviceId); - } else { - getConfigLogger().verbose(getAccountId(), - "CRITICAL: Couldn't finalise on a device ID! Using error device ID instead!"); - } - - header.put("type", "meta"); - - JSONObject appFields = getAppLaunchedFields(); - header.put("af", appFields); - - long i = getI(); - if (i > 0) { - header.put("_i", i); - } - - long j = getJ(); - if (j > 0) { - header.put("_j", j); - } - - String accountId = getAccountId(); - String token = this.config.getAccountToken(); - - if (accountId == null || token == null) { - getConfigLogger() - .debug(getAccountId(), "Account ID/token not found, unable to configure queue request"); - return null; - } - - header.put("id", accountId); - header.put("tk", token); - header.put("l_ts", getLastRequestTimestamp()); - header.put("f_ts", getFirstRequestTimestamp()); - header.put("ct_pi", IdentityRepoFactory.getRepo(this).getIdentitySet().toString()); - header.put("ddnd", - !(this.deviceInfo.getNotificationsEnabledForUser() && (pushProviders.isNotificationSupported()))); - if (isBgPing) { - header.put("bk", 1); - isBgPing = false; - } - header.put("rtl", getRenderedTargetList(this.dbAdapter)); - if (!installReferrerDataSent) { - header.put("rct", getReferrerClickTime()); - header.put("ait", getAppInstallTime()); - } - header.put("frs", isFirstRequestInSession()); - setFirstRequestInSession(false); - - // Attach ARP - try { - final JSONObject arp = getARP(); - if (arp != null && arp.length() > 0) { - header.put("arp", arp); - } - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), "Failed to attach ARP", t); - } - - JSONObject ref = new JSONObject(); - try { - - String utmSource = getSource(); - if (utmSource != null) { - ref.put("us", utmSource); - } - - String utmMedium = getMedium(); - if (utmMedium != null) { - ref.put("um", utmMedium); - } - - String utmCampaign = getCampaign(); - if (utmCampaign != null) { - ref.put("uc", utmCampaign); - } - - if (ref.length() > 0) { - header.put("ref", ref); - } - - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), "Failed to attach ref", t); - } - - JSONObject wzrkParams = getWzrkParams(); - if (wzrkParams != null && wzrkParams.length() > 0) { - header.put("wzrk_ref", wzrkParams); - } - - if (inAppFCManager != null) { - Logger.v("Attaching InAppFC to Header"); - inAppFCManager.attachToHeader(context, header); - } - - // Resort to string concat for backward compatibility - return "[" + header.toString() + ", " + arr.toString().substring(1); - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), "CommsManager: Failed to attach header", t); - return arr.toString(); - } - } - - - private boolean isAppLaunchPushed() { - synchronized (appLaunchPushedLock) { - return appLaunchPushed; - } - } - - private void setAppLaunchPushed(boolean pushed) { - synchronized (appLaunchPushedLock) { - appLaunchPushed = pushed; - } - } - - private boolean isAppLaunchReportingDisabled() { - return this.config.isDisableAppLaunchedEvent(); - } - - private boolean isCurrentUserOptedOut() { - synchronized (optOutFlagLock) { - return currentUserOptedOut; - } - } - - private void setCurrentUserOptedOut(boolean enable) { - synchronized (optOutFlagLock) { - currentUserOptedOut = enable; - } - } - - private boolean isErrorDeviceId() { - return this.deviceInfo.isErrorDeviceId(); - } - - private boolean isFirstRequestInSession() { - return firstRequestInSession; - } - - private void setFirstRequestInSession(boolean firstRequestInSession) { - this.firstRequestInSession = firstRequestInSession; - } - - //Session - private boolean isFirstSession() { - return firstSession; - } - - private boolean isLocationForGeofence() { - return isLocationForGeofence; - } - - private void setLocationForGeofence(boolean locationForGeofence) { - isLocationForGeofence = locationForGeofence; - } - - /** - * @return true if the mute command was sent anytime between now and now - 24 hours. - */ - private boolean isMuted() { - final int now = (int) (System.currentTimeMillis() / 1000); - final int muteTS = StorageHelper.getIntFromPrefs(context, config, Constants.KEY_MUTED, 0); - - return now - muteTS < 24 * 60 * 60; - } - - //Notification Inbox public APIs - - //Util - private boolean isNetworkOnline(Context context) { - try { - ConnectivityManager cm = - (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - if (cm == null) { - // lets be optimistic, if we are truly offline we handle the exception - return true; - } - @SuppressLint("MissingPermission") NetworkInfo netInfo = cm.getActiveNetworkInfo(); - return netInfo != null && netInfo.isConnected(); - } catch (Throwable ignore) { - // lets be optimistic, if we are truly offline we handle the exception - return true; - } - } - - private boolean isOffline() { - return offline; - } - - /** - * If you want to stop recorded events from being sent to the server, use this method to set the SDK instance to - * offline. - * Once offline, events will be recorded and queued locally but will not be sent to the server until offline is - * disabled. - * Calling this method again with offline set to false will allow events to be sent to server and the SDK instance - * will immediately attempt to send events that have been queued while offline. - * - * @param value boolean, true sets the sdk offline, false sets the sdk back online - */ - @SuppressWarnings({"unused"}) - public void setOffline(boolean value) { - offline = value; - if (offline) { - getConfigLogger() - .debug(getAccountId(), "CleverTap Instance has been set to offline, won't send events queue"); - } else { - getConfigLogger() - .debug(getAccountId(), "CleverTap Instance has been set to online, sending events queue"); - flush(); - } - } - - private boolean isProcessUserLoginWithIdentifier(String identifier) { - synchronized (processingUserLoginLock) { - return processingUserLoginIdentifier != null && processingUserLoginIdentifier.equals(identifier); - } - } - - private boolean isTimeBetweenDNDTime(Date startTime, Date stopTime, Date currentTime) { - //Start Time - Calendar startTimeCalendar = Calendar.getInstance(); - startTimeCalendar.setTime(startTime); - //Current Time - Calendar currentTimeCalendar = Calendar.getInstance(); - currentTimeCalendar.setTime(currentTime); - //Stop Time - Calendar stopTimeCalendar = Calendar.getInstance(); - stopTimeCalendar.setTime(stopTime); - - if (stopTime.compareTo(startTime) < 0) { - if (currentTimeCalendar.compareTo(stopTimeCalendar) < 0) { - currentTimeCalendar.add(Calendar.DATE, 1); - } - stopTimeCalendar.add(Calendar.DATE, 1); - } - return currentTimeCalendar.compareTo(startTimeCalendar) >= 0 - && currentTimeCalendar.compareTo(stopTimeCalendar) < 0; - } - - //Session - private void lazyCreateSession(Context context) { - if (!inCurrentSession()) { - setFirstRequestInSession(true); - if (validator != null) { - validator.setDiscardedEvents(null); - } - createSession(context); - pushInitialEventsAsync(); - } - } - - //Util - private DBAdapter loadDBAdapter(Context context) { - if (dbAdapter == null) { - dbAdapter = new DBAdapter(context, this.config); - dbAdapter.cleanupStaleEvents(DBAdapter.Table.EVENTS); - dbAdapter.cleanupStaleEvents(DBAdapter.Table.PROFILE_EVENTS); - dbAdapter.cleanupStaleEvents(DBAdapter.Table.PUSH_NOTIFICATION_VIEWED); - dbAdapter.cleanUpPushNotifications(); - } - return dbAdapter; - } - - //Run manifest validation in async - private void manifestAsyncValidation() { - postAsyncSafely("Manifest Validation", new Runnable() { - @Override - public void run() { - ManifestValidator.validate(context, deviceInfo, pushProviders); - } - }); - } - - private SharedPreferences migrateARPToNewNameSpace(String newKey, String oldKey) { - SharedPreferences oldPrefs = StorageHelper.getPreferences(context, oldKey); - SharedPreferences newPrefs = StorageHelper.getPreferences(context, newKey); - SharedPreferences.Editor editor = newPrefs.edit(); - Map all = oldPrefs.getAll(); - - for (Map.Entry kv : all.entrySet()) { - final Object o = kv.getValue(); - if (o instanceof Number) { - final int update = ((Number) o).intValue(); - editor.putInt(kv.getKey(), update); - } else if (o instanceof String) { - if (((String) o).length() < 100) { - editor.putString(kv.getKey(), (String) o); - } else { - getConfigLogger().verbose(getAccountId(), - "ARP update for key " + kv.getKey() + " rejected (string value too long)"); - } - } else if (o instanceof Boolean) { - editor.putBoolean(kv.getKey(), (Boolean) o); - } else { - getConfigLogger().verbose(getAccountId(), - "ARP update for key " + kv.getKey() + " rejected (invalid data type)"); - } - } - getConfigLogger().verbose(getAccountId(), "Completed ARP update for namespace key: " + newKey + ""); - StorageHelper.persist(editor); - oldPrefs.edit().clear().apply(); - return newPrefs; - } - - //Networking - private boolean needsHandshakeForDomain(final EventGroup eventGroup) { - final String domain = getDomainFromPrefsOrMetadata(eventGroup); - return domain == null || mResponseFailureCount > 5; - } - - /** - * Notify the registered Display Unit listener about the running Display Unit campaigns - * - * @param displayUnits - Array of Display Units {@link CleverTapDisplayUnit} - */ - private void notifyDisplayUnitsLoaded(final ArrayList displayUnits) { - if (displayUnits != null && !displayUnits.isEmpty()) { - if (displayUnitListenerWeakReference != null && displayUnitListenerWeakReference.get() != null) { - runOnUiThread(new Runnable() { - @Override - public void run() { - //double check to ensure null safety - if (displayUnitListenerWeakReference != null - && displayUnitListenerWeakReference.get() != null) { - displayUnitListenerWeakReference.get().onDisplayUnitsLoaded(displayUnits); - } - } - }); - } else { - getConfigLogger().verbose(getAccountId(), - Constants.FEATURE_DISPLAY_UNIT + "No registered listener, failed to notify"); - } - } else { - getConfigLogger().verbose(getAccountId(), Constants.FEATURE_DISPLAY_UNIT + "No Display Units found"); - } - } - - //Profile - private void notifyUserProfileInitialized(String deviceID) { - deviceID = (deviceID != null) ? deviceID : getCleverTapID(); - - if (deviceID == null) { - return; - } - - final SyncListener sl; - try { - sl = getSyncListener(); - if (sl != null) { - sl.profileDidInitialize(deviceID); - } - } catch (Throwable t) { - // Ignore - } - } - - private void notifyUserProfileInitialized() { - notifyUserProfileInitialized(this.deviceInfo.getDeviceID()); - } - - private void onProductConfigFailed() { - if (isProductConfigRequested) { - if (ctProductConfigController != null) { - ctProductConfigController.onFetchFailed(); - } - isProductConfigRequested = false; - } - } - - //Push - private void onTokenRefresh() { - if (getConfig().isAnalyticsOnly()) { - Logger.d(getAccountId(), "Instance is Analytics Only not processing device token"); - return; - } - pushProviders.refreshAllTokens(); - } - - private String optOutKey() { - String guid = getCleverTapID(); - if (guid == null) { - return null; - } - return "OptOut:" + guid; - } - - /** - * Parses the Display Units using the JSON response - * - * @param messages - Json array of Display Unit items - */ - private void parseDisplayUnits(JSONArray messages) { - if (messages == null || messages.length() == 0) { - getConfigLogger().verbose(getAccountId(), - Constants.FEATURE_DISPLAY_UNIT + "Can't parse Display Units, jsonArray is either empty or null"); - return; - } - - synchronized (displayUnitControllerLock) {// lock to avoid multiple instance creation for controller - if (mCTDisplayUnitController == null) { - mCTDisplayUnitController = new CTDisplayUnitController(); - } - } - ArrayList displayUnits = mCTDisplayUnitController.updateDisplayUnits(messages); - - notifyDisplayUnitsLoaded(displayUnits); - } - - private void parseFeatureFlags(JSONObject responseKV) throws JSONException { - JSONArray kvArray = responseKV.getJSONArray(Constants.KEY_KV); - - if (kvArray != null && ctFeatureFlagsController != null) { - ctFeatureFlagsController.updateFeatureFlags(responseKV); - } - } - - private void parseProductConfigs(JSONObject responseKV) throws JSONException { - JSONArray kvArray = responseKV.getJSONArray(Constants.KEY_KV); - - if (kvArray != null && ctProductConfigController != null) { - productConfig().onFetchSuccess(responseKV); - } else { - onProductConfigFailed(); - } - } - - private Date parseTimeToDate(String time) { - - final String inputFormat = "HH:mm"; - SimpleDateFormat inputParser = new SimpleDateFormat(inputFormat, Locale.US); - try { - return inputParser.parse(time); - } catch (java.text.ParseException e) { - return new Date(0); - } - } - - private void performHandshakeForDomain(final Context context, final EventGroup eventGroup, - final Runnable handshakeSuccessCallback) { - - final String endpoint = getEndpoint(true, eventGroup); - if (endpoint == null) { - getConfigLogger().verbose(getAccountId(), "Unable to perform handshake, endpoint is null"); - } - getConfigLogger().verbose(getAccountId(), "Performing handshake with " + endpoint); - - HttpsURLConnection conn = null; - try { - conn = buildHttpsURLConnection(endpoint); - final int responseCode = conn.getResponseCode(); - if (responseCode != 200) { - getConfigLogger() - .verbose(getAccountId(), "Invalid HTTP status code received for handshake - " + responseCode); - return; - } - - getConfigLogger().verbose(getAccountId(), "Received success from handshake :)"); - - if (processIncomingHeaders(context, conn)) { - getConfigLogger().verbose(getAccountId(), "We are not muted"); - // We have a new domain, run the callback - handshakeSuccessCallback.run(); - } - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), "Failed to perform handshake!", t); - } finally { - if (conn != null) { - try { - conn.getInputStream().close(); - conn.disconnect(); - } catch (Throwable t) { - // Ignore - } - } - } - } - - /** - * Use this to safely post a runnable to the async handler. - * It adds try/catch blocks around the runnable and the handler itself. - */ - @SuppressWarnings("UnusedParameters") - private @Nullable - Future postAsyncSafely(final String name, final Runnable runnable) { - Future future = null; - try { - final boolean executeSync = Thread.currentThread().getId() == EXECUTOR_THREAD_ID; - - if (executeSync) { - runnable.run(); - } else { - future = es.submit(new Runnable() { - @Override - public void run() { - EXECUTOR_THREAD_ID = Thread.currentThread().getId(); - try { - runnable.run(); - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), - "Executor service: Failed to complete the scheduled task", t); - } - } - }); - } - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), "Failed to submit task to the executor service", t); - } - - return future; - } - - //InApp - private void prepareNotificationForDisplay(final JSONObject jsonObject) { - getConfigLogger().debug(getAccountId(), "Preparing In-App for display: " + jsonObject.toString()); - runOnNotificationQueue(new NotificationPrepareRunnable(this, jsonObject)); - } - - /** - * Stores silent push notification in DB for smooth working of Push Amplification - * Background Job Service and also stores wzrk_pid to the DB to avoid duplication of Push - * Notifications from Push Amplification. - * - * @param extras - Bundle - */ - private void processCustomPushNotification(final Bundle extras) { - postAsyncSafely("customHandlePushAmplification", new Runnable() { - @Override - public void run() { - String notifMessage = extras.getString(Constants.NOTIF_MSG); - notifMessage = (notifMessage != null) ? notifMessage : ""; - if (notifMessage.isEmpty()) { - //silent notification - getConfigLogger().verbose(getAccountId(), "Push notification message is empty, not rendering"); - loadDBAdapter(context).storeUninstallTimestamp(); - String pingFreq = extras.getString("pf", ""); - if (!TextUtils.isEmpty(pingFreq)) { - updatePingFrequencyIfNeeded(context, Integer.parseInt(pingFreq)); - } - } else { - String wzrk_pid = extras.getString(Constants.WZRK_PUSH_ID); - String ttl = extras.getString(Constants.WZRK_TIME_TO_LIVE, - (System.currentTimeMillis() + Constants.DEFAULT_PUSH_TTL) / 1000 + ""); - long wzrk_ttl = Long.parseLong(ttl); - DBAdapter dbAdapter = loadDBAdapter(context); - getConfigLogger().verbose("Storing Push Notification..." + wzrk_pid + " - with ttl - " + ttl); - dbAdapter.storePushNotificationId(wzrk_pid, wzrk_ttl); - } - } - }); - } - - private void processDiscardedEventsList(JSONObject response) { - if (!response.has(Constants.DISCARDED_EVENT_JSON_KEY)) { - getConfigLogger().verbose(getAccountId(), "ARP doesn't contain the Discarded Events key"); - return; - } - - try { - ArrayList discardedEventsList = new ArrayList<>(); - JSONArray discardedEventsArray = response.getJSONArray(Constants.DISCARDED_EVENT_JSON_KEY); - - if (discardedEventsArray != null) { - for (int i = 0; i < discardedEventsArray.length(); i++) { - discardedEventsList.add(discardedEventsArray.getString(i)); - } - } - if (validator != null) { - validator.setDiscardedEvents(discardedEventsList); - } else { - getConfigLogger().verbose(getAccountId(), "Validator object is NULL"); - } - } catch (JSONException e) { - getConfigLogger() - .verbose(getAccountId(), "Error parsing discarded events list" + e.getLocalizedMessage()); - } - } - - /** - * Logic for the processing of Display Unit response - * - * @param response - Display Unit json response object - */ - private void processDisplayUnitsResponse(JSONObject response) { - if (response == null) { - getConfigLogger().verbose(getAccountId(), Constants.FEATURE_DISPLAY_UNIT - + "Can't parse Display Unit Response, JSON response object is null"); - return; - } - - if (!response.has(Constants.DISPLAY_UNIT_JSON_RESPONSE_KEY)) { - getConfigLogger().verbose(getAccountId(), - Constants.FEATURE_DISPLAY_UNIT + "JSON object doesn't contain the Display Units key"); - return; - } - try { - getConfigLogger() - .verbose(getAccountId(), Constants.FEATURE_DISPLAY_UNIT + "Processing Display Unit response"); - parseDisplayUnits(response.getJSONArray(Constants.DISPLAY_UNIT_JSON_RESPONSE_KEY)); - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), Constants.FEATURE_DISPLAY_UNIT + "Failed to parse response", t); - } - } - - //Event - private void processEvent(final Context context, final JSONObject event, final int eventType) { - synchronized (eventLock) { - try { - activityCount = activityCount == 0 ? 1 : activityCount; - String type; - if (eventType == Constants.PAGE_EVENT) { - type = "page"; - } else if (eventType == Constants.PING_EVENT) { - type = "ping"; - attachMeta(event, context); - if (event.has("bk")) { - isBgPing = true; - event.remove("bk"); - } - - //Add a flag to denote, PING event is for geofences - if (isLocationForGeofence()) { - event.put("gf", true); - setLocationForGeofence(false); - event.put("gfSDKVersion", getGeofenceSDKVersion()); - setGeofenceSDKVersion(0); - } - } else if (eventType == Constants.PROFILE_EVENT) { - type = "profile"; - } else if (eventType == Constants.DATA_EVENT) { - type = "data"; - } else { - type = "event"; - } - - // Complete the received event with the other params - - String currentActivityName = getScreenName(); - if (currentActivityName != null) { - event.put("n", currentActivityName); - } - - int session = getCurrentSession(); - event.put("s", session); - event.put("pg", activityCount); - event.put("type", type); - event.put("ep", System.currentTimeMillis() / 1000); - event.put("f", isFirstSession()); - event.put("lsl", getLastSessionLength()); - attachPackageNameIfRequired(context, event); - - // Report any pending validation error - ValidationResult vr = validationResultStack.popValidationResult(); - if (vr != null) { - event.put(Constants.ERROR_KEY, getErrorObject(vr)); - } - getLocalDataStore().setDataSyncFlag(event); - queueEventToDB(context, event, eventType); - updateLocalStore(context, event, eventType); - scheduleQueueFlush(context); - - } catch (Throwable e) { - getConfigLogger().verbose(getAccountId(), "Failed to queue event: " + event.toString(), e); - } - } - } - - private void processFeatureFlagsResponse(JSONObject response) { - if (response == null) { - getConfigLogger().verbose(getAccountId(), - Constants.FEATURE_FLAG_UNIT + "Can't parse Feature Flags Response, JSON response object is null"); - return; - } - - if (!response.has(Constants.FEATURE_FLAG_JSON_RESPONSE_KEY)) { - getConfigLogger().verbose(getAccountId(), - Constants.FEATURE_FLAG_UNIT + "JSON object doesn't contain the Feature Flags key"); - return; - } - try { - getConfigLogger() - .verbose(getAccountId(), Constants.FEATURE_FLAG_UNIT + "Processing Feature Flags response"); - parseFeatureFlags(response.getJSONObject(Constants.FEATURE_FLAG_JSON_RESPONSE_KEY)); - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), Constants.FEATURE_FLAG_UNIT + "Failed to parse response", t); - } - } - - //ABTesting - - private void processGeofenceResponse(JSONObject response) { - if (response == null) { - getConfigLogger().verbose(getAccountId(), - Constants.LOG_TAG_GEOFENCES + "Can't parse Geofences Response, JSON response object is null"); - return; - } - - if (!response.has(Constants.GEOFENCES_JSON_RESPONSE_KEY)) { - getConfigLogger().verbose(getAccountId(), - Constants.LOG_TAG_GEOFENCES + "JSON object doesn't contain the Geofences key"); - return; - } - try { - if (this.geofenceCallback != null) { - JSONObject jsonObject = new JSONObject(); - jsonObject.put("geofences", response.getJSONArray(Constants.GEOFENCES_JSON_RESPONSE_KEY)); - - getConfigLogger() - .verbose(getAccountId(), Constants.LOG_TAG_GEOFENCES + "Processing Geofences response"); - this.geofenceCallback.handleGeoFences(jsonObject); - } else { - getConfigLogger().debug(getAccountId(), - Constants.LOG_TAG_GEOFENCES + "Geofence SDK has not been initialized to handle the response"); - } - } catch (Throwable t) { - getConfigLogger() - .verbose(getAccountId(), Constants.LOG_TAG_GEOFENCES + "Failed to handle Geofences response", t); - } - - } - - //InApp - private void processInAppResponse(final JSONObject response, final Context context) { - try { - getConfigLogger().verbose(getAccountId(), "InApp: Processing response"); - - if (!response.has("inapp_notifs")) { - getConfigLogger().verbose(getAccountId(), - "InApp: Response JSON object doesn't contain the inapp key, failing"); - return; - } - - int perSession = 10; - int perDay = 10; - if (response.has(Constants.INAPP_MAX_PER_SESSION) && response - .get(Constants.INAPP_MAX_PER_SESSION) instanceof Integer) { - perSession = response.getInt(Constants.INAPP_MAX_PER_SESSION); - } - - if (response.has("imp") && response.get("imp") instanceof Integer) { - perDay = response.getInt("imp"); - } - - if (inAppFCManager != null) { - Logger.v("Updating InAppFC Limits"); - inAppFCManager.updateLimits(context, perDay, perSession); - } - - JSONArray inappNotifs; - try { - inappNotifs = response.getJSONArray(Constants.INAPP_JSON_RESPONSE_KEY); - } catch (JSONException e) { - getConfigLogger().debug(getAccountId(), "InApp: In-app key didn't contain a valid JSON array"); - return; - } - - // Add all the new notifications to the queue - SharedPreferences prefs = StorageHelper.getPreferences(context); - SharedPreferences.Editor editor = prefs.edit(); - try { - JSONArray inappsFromPrefs = new JSONArray( - StorageHelper.getStringFromPrefs(context, config, Constants.INAPP_KEY, "[]")); - - // Now add the rest of them :) - if (inappNotifs != null && inappNotifs.length() > 0) { - for (int i = 0; i < inappNotifs.length(); i++) { - try { - JSONObject inappNotif = inappNotifs.getJSONObject(i); - inappsFromPrefs.put(inappNotif); - } catch (JSONException e) { - Logger.v("InAppManager: Malformed inapp notification"); - } - } - } - - // Commit all the changes - editor.putString(StorageHelper.storageKeyWithSuffix(config, Constants.INAPP_KEY), - inappsFromPrefs.toString()); - StorageHelper.persist(editor); - } catch (Throwable e) { - getConfigLogger().verbose(getAccountId(), "InApp: Failed to parse the in-app notifications properly"); - getConfigLogger().verbose(getAccountId(), "InAppManager: Reason: " + e.getMessage(), e); - } - // Fire the first notification, if any - runOnNotificationQueue(new Runnable() { - @Override - public void run() { - _showNotificationIfAvailable(context); - } - }); - } catch (Throwable t) { - Logger.v("InAppManager: Failed to parse response", t); - } - } - - //NotificationInbox - private void processInboxResponse(final JSONObject response) { - if (getConfig().isAnalyticsOnly()) { - getConfigLogger().verbose(getAccountId(), - "CleverTap instance is configured to analytics only, not processing inbox messages"); - return; - } - - getConfigLogger().verbose(getAccountId(), "Inbox: Processing response"); - - if (!response.has("inbox_notifs")) { - getConfigLogger().verbose(getAccountId(), "Inbox: Response JSON object doesn't contain the inbox key"); - return; - } - try { - _processInboxMessages(response.getJSONArray("inbox_notifs")); - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), "InboxResponse: Failed to parse response", t); - } - } - - private void processIncomingExperiments(JSONObject response) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - return; - } - try { - JSONArray experimentsArray = response.getJSONArray("ab_exps"); - if (this.ctABTestController != null) { - ctABTestController.updateExperiments(experimentsArray); - } - } catch (JSONException e) { - getConfigLogger() - .debug(config.getAccountId(), "Error parsing AB Testing response " + e.getLocalizedMessage()); - } - } - - /** - * Processes the incoming response headers for a change in domain and/or mute. - * - * @return True to continue sending requests, false otherwise. - */ - private boolean processIncomingHeaders(final Context context, final HttpsURLConnection conn) { - - final String muteCommand = conn.getHeaderField(Constants.HEADER_MUTE); - if (muteCommand != null && muteCommand.trim().length() > 0) { - if (muteCommand.equals("true")) { - setMuted(context, true); - return false; - } else { - setMuted(context, false); - } - } - - final String domainName = conn.getHeaderField(Constants.HEADER_DOMAIN_NAME); - Logger.v("Getting domain from header - " + domainName); - if (domainName == null || domainName.trim().length() == 0) { - return true; - } - - final String spikyDomainName = conn.getHeaderField(Constants.SPIKY_HEADER_DOMAIN_NAME); - Logger.v("Getting spiky domain from header - " + spikyDomainName); - - setMuted(context, false); - setDomain(context, domainName); - Logger.v("Setting spiky domain from header as -" + spikyDomainName); - if (spikyDomainName == null) { - setSpikyDomain(context, domainName); - } else { - setSpikyDomain(context, spikyDomainName); - } - return true; - } - - private void processProductConfigResponse(JSONObject response) { - if (response == null) { - getConfigLogger().verbose(getAccountId(), Constants.LOG_TAG_PRODUCT_CONFIG - + "Can't parse Product Config Response, JSON response object is null"); - onProductConfigFailed(); - return; - } - - if (!response.has(Constants.REMOTE_CONFIG_FLAG_JSON_RESPONSE_KEY)) { - getConfigLogger().verbose(getAccountId(), - Constants.LOG_TAG_PRODUCT_CONFIG + "JSON object doesn't contain the Product Config key"); - onProductConfigFailed(); - return; - } - try { - getConfigLogger() - .verbose(getAccountId(), Constants.LOG_TAG_PRODUCT_CONFIG + "Processing Product Config response"); - parseProductConfigs(response.getJSONObject(Constants.REMOTE_CONFIG_FLAG_JSON_RESPONSE_KEY)); - } catch (Throwable t) { - onProductConfigFailed(); - getConfigLogger().verbose(getAccountId(), - Constants.LOG_TAG_PRODUCT_CONFIG + "Failed to parse Product Config response", t); - } - } - - private void processPushNotificationViewedEvent(final Context context, final JSONObject event) { - synchronized (eventLock) { - try { - int session = getCurrentSession(); - event.put("s", session); - event.put("type", "event"); - event.put("ep", System.currentTimeMillis() / 1000); - // Report any pending validation error - ValidationResult vr = validationResultStack.popValidationResult(); - if (vr != null) { - event.put(Constants.ERROR_KEY, getErrorObject(vr)); - } - getConfigLogger().verbose(getAccountId(), "Pushing Notification Viewed event onto DB"); - queuePushNotificationViewedEventToDB(context, event); - getConfigLogger().verbose(getAccountId(), "Pushing Notification Viewed event onto queue flush"); - schedulePushNotificationViewedQueueFlush(context); - } catch (Throwable t) { - getConfigLogger() - .verbose(getAccountId(), "Failed to queue notification viewed event: " + event.toString(), t); - } - } - } - - //Event - private void processResponse(final Context context, final String responseStr) { - if (responseStr == null) { - getConfigLogger().verbose(getAccountId(), "Problem processing queue response, response is null"); - return; - } - try { - getConfigLogger().verbose(getAccountId(), "Trying to process response: " + responseStr); - - JSONObject response = new JSONObject(responseStr); - try { - if (!this.config.isAnalyticsOnly()) { - processInAppResponse(response, context); - } - } catch (Throwable t) { - getConfigLogger() - .verbose(getAccountId(), "Failed to process in-app notifications from the response!", t); - } - - // Always look for a GUID in the response, and if present, then perform a force update - try { - if (response.has("g")) { - final String deviceID = response.getString("g"); - this.deviceInfo.forceUpdateDeviceId(deviceID); - getConfigLogger().verbose(getAccountId(), "Got a new device ID: " + deviceID); - } - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), "Failed to update device ID!", t); - } - - try { - getLocalDataStore().syncWithUpstream(context, response); - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), "Failed to sync local cache with upstream", t); - } - - // Handle "arp" (additional request parameters) - try { - if (response.has("arp")) { - final JSONObject arp = (JSONObject) response.get("arp"); - if (arp.length() > 0) { - if (ctProductConfigController != null) { - ctProductConfigController.setArpValue(arp); - } - //Handle Discarded events in ARP - try { - processDiscardedEventsList(arp); - } catch (Throwable t) { - getConfigLogger() - .verbose("Error handling discarded events response: " + t.getLocalizedMessage()); - } - handleARPUpdate(context, arp); - } - } - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), "Failed to process ARP", t); - } - - // Handle i - try { - if (response.has("_i")) { - final long i = response.getLong("_i"); - setI(context, i); - } - } catch (Throwable t) { - // Ignore - } - - // Handle j - try { - if (response.has("_j")) { - final long j = response.getLong("_j"); - setJ(context, j); - } - } catch (Throwable t) { - // Ignore - } - - // Handle "console" - print them as info to the console - try { - if (response.has("console")) { - final JSONArray console = (JSONArray) response.get("console"); - if (console.length() > 0) { - for (int i = 0; i < console.length(); i++) { - getConfigLogger().debug(getAccountId(), console.get(i).toString()); - } - } - } - } catch (Throwable t) { - // Ignore - } - - // Handle server set debug level - try { - if (response.has("dbg_lvl")) { - final int debugLevel = response.getInt("dbg_lvl"); - if (debugLevel >= 0) { - CleverTapAPI.setDebugLevel(debugLevel); - getConfigLogger().verbose(getAccountId(), - "Set debug level to " + debugLevel + " for this session (set by upstream)"); - } - } - } catch (Throwable t) { - // Ignore - } - - // Handle stale_inapp - try { - if (inAppFCManager != null) { - inAppFCManager.processResponse(context, response); - } - } catch (Throwable t) { - // Ignore - } - - //Handle notification inbox response - if (!getConfig().isAnalyticsOnly()) { - try { - getConfigLogger().verbose(getAccountId(), "Processing inbox messages..."); - processInboxResponse(response); - } catch (Throwable t) { - getConfigLogger().verbose("Notification inbox exception: " + t.getLocalizedMessage()); - } - } - - //Handle Push Amplification response - if (!getConfig().isAnalyticsOnly()) { - try { - if (response.has("pushamp_notifs")) { - getConfigLogger().verbose(getAccountId(), "Processing pushamp messages..."); - JSONObject pushAmpObject = response.getJSONObject("pushamp_notifs"); - final JSONArray pushNotifications = pushAmpObject.getJSONArray("list"); - if (pushNotifications.length() > 0) { - getConfigLogger().verbose(getAccountId(), "Handling Push payload locally"); - handlePushNotificationsInResponse(pushNotifications); - } - if (pushAmpObject.has("pf")) { - try { - int frequency = pushAmpObject.getInt("pf"); - updatePingFrequencyIfNeeded(context, frequency); - } catch (Throwable t) { - getConfigLogger() - .verbose("Error handling ping frequency in response : " + t.getMessage()); - } - - } - if (pushAmpObject.has("ack")) { - boolean ack = pushAmpObject.getBoolean("ack"); - getConfigLogger().verbose("Received ACK -" + ack); - if (ack) { - JSONArray rtlArray = getRenderedTargetList(this.dbAdapter); - String[] rtlStringArray = new String[0]; - if (rtlArray != null) { - rtlStringArray = new String[rtlArray.length()]; - } - for (int i = 0; i < rtlStringArray.length; i++) { - rtlStringArray[i] = rtlArray.getString(i); - } - getConfigLogger().verbose("Updating RTL values..."); - this.dbAdapter.updatePushNotificationIds(rtlStringArray); - } - } - } - } catch (Throwable t) { - //Ignore - } - } - - //Handle ABTesting response - if (!getConfig().isAnalyticsOnly()) { - try { - if (response.has("ab_exps")) { - getConfigLogger().verbose(getAccountId(), "Processing ABTest experiments..."); - processIncomingExperiments(response); - } - } catch (Throwable t) { - getConfigLogger().verbose("Error handling AB Testing response : " + t.getMessage()); - } - } - - //Handle Display Unit response - if (!getConfig().isAnalyticsOnly()) { - try { - getConfigLogger().verbose(getAccountId(), "Processing Display Unit items..."); - processDisplayUnitsResponse(response); - } catch (Throwable t) { - getConfigLogger().verbose("Error handling Display Unit response: " + t.getLocalizedMessage()); - } - } - - //Handle Feature Flag response - if (!getConfig().isAnalyticsOnly()) { - try { - getConfigLogger().verbose(getAccountId(), "Processing Feature Flags response..."); - processFeatureFlagsResponse(response); - } catch (Throwable t) { - getConfigLogger().verbose("Error handling Feature Flags response: " + t.getLocalizedMessage()); - } - } - - //Handle Product Config response - if (!getConfig().isAnalyticsOnly()) { - try { - getConfigLogger().verbose(getAccountId(), "Processing Product Config response..."); - processProductConfigResponse(response); - } catch (Throwable t) { - getConfigLogger().verbose("Error handling Product Config response: " + t.getLocalizedMessage()); - } - } - - //Handle GeoFences Response - if (!getConfig().isAnalyticsOnly()) { - try { - getConfigLogger().verbose(getAccountId(), "Processing GeoFences response..."); - processGeofenceResponse(response); - } catch (Throwable t) { - getConfigLogger().verbose("Error handling GeoFences response: " + t.getLocalizedMessage()); - } - } - } catch (Throwable t) { - mResponseFailureCount++; - getConfigLogger().verbose(getAccountId(), "Problem process send queue response", t); - } - } - - private void pushAmazonRegistrationId(String token, boolean register) { - pushProviders.handleToken(token, PushType.ADM, register); - } - - //Event - private void pushAppLaunchedEvent() { - if (isAppLaunchReportingDisabled()) { - setAppLaunchPushed(true); - getConfigLogger().debug(getAccountId(), "App Launched Events disabled in the Android Manifest file"); - return; - } - if (isAppLaunchPushed()) { - getConfigLogger() - .verbose(getAccountId(), "App Launched has already been triggered. Will not trigger it "); - return; - } else { - getConfigLogger().verbose(getAccountId(), "Firing App Launched event"); - } - setAppLaunchPushed(true); - JSONObject event = new JSONObject(); - try { - event.put("evtName", Constants.APP_LAUNCHED_EVENT); - event.put("evtData", getAppLaunchedFields()); - } catch (Throwable t) { - // We won't get here - } - queueEvent(context, event, Constants.RAISED_EVENT); - } - - //Profile - private void pushBasicProfile(JSONObject baseProfile) { - try { - String guid = getCleverTapID(); - - JSONObject profileEvent = new JSONObject(); - - if (baseProfile != null && baseProfile.length() > 0) { - Iterator i = baseProfile.keys(); - IdentityRepo iProfileHandler = IdentityRepoFactory.getRepo(this); - LoginInfoProvider loginInfoProvider = new LoginInfoProvider(this); - while (i.hasNext()) { - String next = i.next().toString(); - - // need to handle command-based JSONObject props here now - Object value = null; - try { - value = baseProfile.getJSONObject(next); - } catch (Throwable t) { - try { - value = baseProfile.get(next); - } catch (JSONException e) { - //no-op - } - } - - if (value != null) { - profileEvent.put(next, value); - - // cache the valid identifier: guid pairs - boolean isProfileKey = iProfileHandler.hasIdentity(next); - if (isProfileKey) { - try { - loginInfoProvider.cacheGUIDForIdentifier(guid, next, value.toString()); - } catch (Throwable t) { - // no-op - } - } - } - } - } - - try { - String carrier = this.deviceInfo.getCarrier(); - if (carrier != null && !carrier.equals("")) { - profileEvent.put("Carrier", carrier); - } - - String cc = this.deviceInfo.getCountryCode(); - if (cc != null && !cc.equals("")) { - profileEvent.put("cc", cc); - } - - profileEvent.put("tz", TimeZone.getDefault().getID()); - - JSONObject event = new JSONObject(); - event.put("profile", profileEvent); - queueEvent(context, event, Constants.PROFILE_EVENT); - } catch (JSONException e) { - getConfigLogger().verbose(getAccountId(), "FATAL: Creating basic profile update event failed!"); - } - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), "Basic profile sync", t); - } - } - - private synchronized void pushDeepLink(Uri uri, boolean install) { - if (uri == null) { - return; - } - - try { - JSONObject referrer = UriHelper.getUrchinFromUri(uri); - if (referrer.has("us")) { - setSource(referrer.get("us").toString()); - } - if (referrer.has("um")) { - setMedium(referrer.get("um").toString()); - } - if (referrer.has("uc")) { - setCampaign(referrer.get("uc").toString()); - } - - referrer.put("referrer", uri.toString()); - if (install) { - referrer.put("install", true); - } - recordPageEventWithExtras(referrer); - - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), "Failed to push deep link", t); - } - } - - //Event - private void pushInitialEventsAsync() { - postAsyncSafely("CleverTapAPI#pushInitialEventsAsync", new Runnable() { - @Override - public void run() { - try { - getConfigLogger().verbose(getAccountId(), "Queuing daily events"); - pushBasicProfile(null); - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), "Daily profile sync failed", t); - } - } - }); - } - - //Event - private Future queueEvent(final Context context, final JSONObject event, final int eventType) { - return postAsyncSafely("queueEvent", new Runnable() { - @Override - public void run() { - if (shouldDropEvent(event, eventType)) { - return; - } - if (shouldDeferProcessingEvent(event, eventType)) { - getConfigLogger().debug(getAccountId(), - "App Launched not yet processed, re-queuing event " + event + "after 2s"); - getHandlerUsingMainLooper().postDelayed(new Runnable() { - @Override - public void run() { - postAsyncSafely("queueEventWithDelay", new Runnable() { - @Override - public void run() { - lazyCreateSession(context); - addToQueue(context, event, eventType); - } - }); - } - }, 2000); - } else { - if (eventType == Constants.FETCH_EVENT) { - addToQueue(context, event, eventType); - } else { - lazyCreateSession(context); - addToQueue(context, event, eventType); - } - } - } - }); - } - - private void queueEventInternal(final Context context, final JSONObject event, DBAdapter.Table table) { - synchronized (eventLock) { - DBAdapter adapter = loadDBAdapter(context); - int returnCode = adapter.storeObject(event, table); - - if (returnCode > 0) { - getConfigLogger().debug(getAccountId(), "Queued event: " + event.toString()); - getConfigLogger() - .verbose(getAccountId(), "Queued event to DB table " + table + ": " + event.toString()); - } - } - } - - //Event - private void queueEventToDB(final Context context, final JSONObject event, final int type) { - DBAdapter.Table table = (type == Constants.PROFILE_EVENT) ? DBAdapter.Table.PROFILE_EVENTS - : DBAdapter.Table.EVENTS; - queueEventInternal(context, event, table); - } - - private void queuePushNotificationViewedEventToDB(final Context context, final JSONObject event) { - queueEventInternal(context, event, DBAdapter.Table.PUSH_NOTIFICATION_VIEWED); - } - - private Future raiseEventForGeofences(String eventName, JSONObject geofenceProperties) { - - Future future = null; - - JSONObject event = new JSONObject(); - try { - event.put("evtName", eventName); - event.put("evtData", geofenceProperties); - - Location location = new Location(""); - location.setLatitude(geofenceProperties.getDouble("triggered_lat")); - location.setLongitude(geofenceProperties.getDouble("triggered_lng")); - - geofenceProperties.remove("triggered_lat"); - geofenceProperties.remove("triggered_lng"); - - locationFromUser = location; - - future = queueEvent(context, event, Constants.RAISED_EVENT); - } catch (JSONException e) { - getConfigLogger().debug(getAccountId(), Constants.LOG_TAG_GEOFENCES + - "JSON Exception when raising GeoFence event " - + eventName + " - " + e.getLocalizedMessage()); - } - - return future; - } - - private void recordDeviceIDErrors() { - for (ValidationResult validationResult : this.deviceInfo.getValidationResults()) { - validationResultStack.pushValidationResult(validationResult); - } - } - - //Event - private void recordPageEventWithExtras(JSONObject extras) { - try { - JSONObject jsonObject = new JSONObject(); - // Add the extras - if (extras != null && extras.length() > 0) { - Iterator keys = extras.keys(); - while (keys.hasNext()) { - try { - String key = (String) keys.next(); - jsonObject.put(key, extras.getString(key)); - } catch (ClassCastException ignore) { - // Really won't get here - } - } - } - queueEvent(context, jsonObject, Constants.PAGE_EVENT); - } catch (Throwable t) { - // We won't get here - } - } - - private void resetABTesting() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - return; - } - if (!this.config.isAnalyticsOnly()) { - if (!config.isABTestingEnabled()) { - getConfigLogger().debug(config.getAccountId(), "AB Testing is not enabled for this instance"); - return; - } - } - if (ctABTestController != null) { - ctABTestController.resetWithGuid(getCleverTapID()); - } - } - - private void resetAlarmScheduler(Context context) { - if (getPingFrequency(context) <= 0) { - stopAlarmScheduler(context); - } else { - stopAlarmScheduler(context); - createAlarmScheduler(context); - } - } - - /** - * Resets the Display Units in the cache - */ - private void resetDisplayUnits() { - if (mCTDisplayUnitController != null) { - mCTDisplayUnitController.reset(); - } else { - getConfigLogger().verbose(getAccountId(), - Constants.FEATURE_DISPLAY_UNIT + "Can't reset Display Units, DisplayUnitcontroller is null"); - } - } - - private void resetFeatureFlags() { - if (ctFeatureFlagsController != null && ctFeatureFlagsController.isInitialized()) { - ctFeatureFlagsController.resetWithGuid(getCleverTapID()); - ctFeatureFlagsController.fetchFeatureFlags(); - } - } - - // always call async - private void resetInbox() { - synchronized (inboxControllerLock) { - this.ctInboxController = null; - } - _initializeInbox(); - } - - private void resetProductConfigs() { - if (this.config.isAnalyticsOnly()) { - getConfigLogger().debug(config.getAccountId(), "Product Config is not enabled for this instance"); - return; - } - if (ctProductConfigController != null) { - ctProductConfigController.resetSettings(); - } - ctProductConfigController = new CTProductConfigController(context, getCleverTapID(), config, this); - getConfigLogger().verbose(config.getAccountId(), "Product Config reset"); - } - - private void runInstanceJobWork(final Context context, final JobParameters parameters) { - postAsyncSafely("runningJobService", new Runnable() { - @Override - public void run() { - if (pushProviders.isNotificationSupported()) { - Logger.v(getAccountId(), "Token is not present, not running the Job"); - return; - } - - Calendar now = Calendar.getInstance(); - - int hour = now.get(Calendar.HOUR_OF_DAY); // Get hour in 24 hour format - int minute = now.get(Calendar.MINUTE); - - Date currentTime = parseTimeToDate(hour + ":" + minute); - Date startTime = parseTimeToDate(Constants.DND_START); - Date endTime = parseTimeToDate(Constants.DND_STOP); - - if (isTimeBetweenDNDTime(startTime, endTime, currentTime)) { - Logger.v(getAccountId(), "Job Service won't run in default DND hours"); - return; - } - - long lastTS = loadDBAdapter(context).getLastUninstallTimestamp(); - - if (lastTS == 0 || lastTS > System.currentTimeMillis() - 24 * 60 * 60 * 1000) { - try { - JSONObject eventObject = new JSONObject(); - eventObject.put("bk", 1); - queueEvent(context, eventObject, Constants.PING_EVENT); - - if (parameters == null) { - int pingFrequency = getPingFrequency(context); - AlarmManager alarmManager = (AlarmManager) context - .getSystemService(Context.ALARM_SERVICE); - Intent cancelIntent = new Intent(CTBackgroundIntentService.MAIN_ACTION); - cancelIntent.setPackage(context.getPackageName()); - PendingIntent alarmPendingIntent = PendingIntent - .getService(context, getAccountId().hashCode(), cancelIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - if (alarmManager != null) { - alarmManager.cancel(alarmPendingIntent); - } - Intent alarmIntent = new Intent(CTBackgroundIntentService.MAIN_ACTION); - alarmIntent.setPackage(context.getPackageName()); - PendingIntent alarmServicePendingIntent = PendingIntent - .getService(context, getAccountId().hashCode(), alarmIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - if (alarmManager != null) { - if (pingFrequency != -1) { - alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, - SystemClock.elapsedRealtime() + (pingFrequency - * Constants.ONE_MIN_IN_MILLIS), - Constants.ONE_MIN_IN_MILLIS * pingFrequency, alarmServicePendingIntent); - } - } - } - } catch (JSONException e) { - Logger.v("Unable to raise background Ping event"); - } - - } - } - }); - } - - //InApp - private void runOnNotificationQueue(final Runnable runnable) { - try { - final boolean executeSync = Thread.currentThread().getId() == NOTIFICATION_THREAD_ID; - - if (executeSync) { - runnable.run(); - } else { - ns.submit(new Runnable() { - @Override - public void run() { - NOTIFICATION_THREAD_ID = Thread.currentThread().getId(); - try { - runnable.run(); - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), - "Notification executor service: Failed to complete the scheduled task", t); - } - } - }); - } - } catch (Throwable t) { + public void setOffline(boolean value) { + coreState.getCoreMetaData().setOffline(value); + if (value) { getConfigLogger() - .verbose(getAccountId(), "Failed to submit task to the notification executor service", t); - } - } - - private void schedulePushNotificationViewedQueueFlush(final Context context) { - if (pushNotificationViewedRunnable == null) { - pushNotificationViewedRunnable = new Runnable() { - @Override - public void run() { - getConfigLogger() - .verbose(getAccountId(), "Pushing Notification Viewed event onto queue flush async"); - flushQueueAsync(context, EventGroup.PUSH_NOTIFICATION_VIEWED); - } - }; - } - getHandlerUsingMainLooper().removeCallbacks(pushNotificationViewedRunnable); - getHandlerUsingMainLooper().post(pushNotificationViewedRunnable); - } - - //Event - private void scheduleQueueFlush(final Context context) { - if (commsRunnable == null) { - commsRunnable = new Runnable() { - @Override - public void run() { - flushQueueAsync(context, EventGroup.REGULAR); - flushQueueAsync(context, EventGroup.PUSH_NOTIFICATION_VIEWED); - } - }; - } - // Cancel any outstanding send runnables, and issue a new delayed one - getHandlerUsingMainLooper().removeCallbacks(commsRunnable); - getHandlerUsingMainLooper().postDelayed(commsRunnable, getDelayFrequency()); - - getConfigLogger().verbose(getAccountId(), "Scheduling delayed queue flush on main event loop"); - } - - /** - * @return true if the network request succeeded. Anything non 200 results in a false. - */ - private boolean sendQueue(final Context context, final EventGroup eventGroup, final JSONArray queue) { - - if (queue == null || queue.length() <= 0) { - return false; - } - - if (getCleverTapID() == null) { - getConfigLogger().debug(getAccountId(), "CleverTap Id not finalized, unable to send queue"); - return false; - } - - HttpsURLConnection conn = null; - try { - final String endpoint = getEndpoint(false, eventGroup); - - // This is just a safety check, which would only arise - // if upstream didn't adhere to the protocol (sent nothing during the initial handshake) - if (endpoint == null) { - getConfigLogger().debug(getAccountId(), "Problem configuring queue endpoint, unable to send queue"); - return false; - } - - conn = buildHttpsURLConnection(endpoint); - - final String body; - final String req = insertHeader(context, queue); - if (req == null) { - getConfigLogger().debug(getAccountId(), "Problem configuring queue request, unable to send queue"); - return false; - } - - getConfigLogger().debug(getAccountId(), "Send queue contains " + queue.length() + " items: " + req); - getConfigLogger().debug(getAccountId(), "Sending queue to: " + endpoint); - conn.setDoOutput(true); - // noinspection all - conn.getOutputStream().write(req.getBytes("UTF-8")); - - final int responseCode = conn.getResponseCode(); - - // Always check for a 200 OK - if (responseCode != 200) { - throw new IOException("Response code is not 200. It is " + responseCode); - } - - // Check for a change in domain - final String newDomain = conn.getHeaderField(Constants.HEADER_DOMAIN_NAME); - if (newDomain != null && newDomain.trim().length() > 0) { - if (hasDomainChanged(newDomain)) { - // The domain has changed. Return a status of -1 so that the caller retries - setDomain(context, newDomain); - getConfigLogger().debug(getAccountId(), - "The domain has changed to " + newDomain + ". The request will be retried shortly."); - return false; - } - } - - if (processIncomingHeaders(context, conn)) { - // noinspection all - BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8")); - - StringBuilder sb = new StringBuilder(); - String line; - while ((line = br.readLine()) != null) { - sb.append(line); - } - body = sb.toString(); - processResponse(context, body); - } - - setLastRequestTimestamp(context, currentRequestTimestamp); - setFirstRequestTimestampIfNeeded(context, currentRequestTimestamp); - - getConfigLogger().debug(getAccountId(), "Queue sent successfully"); - - mResponseFailureCount = 0; - networkRetryCount = 0; //reset retry count when queue is sent successfully - return true; - } catch (Throwable e) { - getConfigLogger().debug(getAccountId(), - "An exception occurred while sending the queue, will retry: " + e.getLocalizedMessage()); - mResponseFailureCount++; - networkRetryCount++; - scheduleQueueFlush(context); - return false; - } finally { - if (conn != null) { - try { - conn.getInputStream().close(); - conn.disconnect(); - } catch (Throwable t) { - // Ignore - } - } - } - } - - // -----------------------------------------------------------------------// - // ******** Display Unit LOGIC *****// - // -----------------------------------------------------------------------// - - private void setCurrentUserOptOutStateFromStorage() { - String key = optOutKey(); - if (key == null) { - getConfigLogger().verbose(getAccountId(), - "Unable to set current user OptOut state from storage: storage key is null"); - return; - } - boolean storedOptOut = StorageHelper.getBooleanFromPrefs(context, config, key); - setCurrentUserOptedOut(storedOptOut); - getConfigLogger().verbose(getAccountId(), - "Set current user OptOut state from storage to: " + storedOptOut + " for key: " + key); - } - - private void setDeviceNetworkInfoReportingFromStorage() { - boolean enabled = StorageHelper.getBooleanFromPrefs(context, config, Constants.NETWORK_INFO); - getConfigLogger() - .verbose(getAccountId(), "Setting device network info reporting state from storage to " + enabled); - enableNetworkInfoReporting = enabled; - } - - private void setDomain(final Context context, String domainName) { - getConfigLogger().verbose(getAccountId(), "Setting domain to " + domainName); - StorageHelper.putString(context, StorageHelper.storageKeyWithSuffix(config, Constants.KEY_DOMAIN_NAME), - domainName); - } - - private void setFirstRequestTimestampIfNeeded(Context context, int ts) { - if (getFirstRequestTimestamp() > 0) { - return; - } - StorageHelper.putInt(context, StorageHelper.storageKeyWithSuffix(config, Constants.KEY_FIRST_TS), ts); - } - - @SuppressLint("CommitPrefEdits") - private void setI(Context context, long i) { - final SharedPreferences prefs = StorageHelper.getPreferences(context, Constants.NAMESPACE_IJ); - final SharedPreferences.Editor editor = prefs.edit(); - editor.putLong(StorageHelper.storageKeyWithSuffix(config, Constants.KEY_I), i); - StorageHelper.persist(editor); - } - - @SuppressLint("CommitPrefEdits") - private void setJ(Context context, long j) { - final SharedPreferences prefs = StorageHelper.getPreferences(context, Constants.NAMESPACE_IJ); - final SharedPreferences.Editor editor = prefs.edit(); - editor.putLong(StorageHelper.storageKeyWithSuffix(config, Constants.KEY_J), j); - StorageHelper.persist(editor); - } - - //Session - private void setLastRequestTimestamp(Context context, int ts) { - StorageHelper.putInt(context, StorageHelper.storageKeyWithSuffix(config, Constants.KEY_LAST_TS), ts); - } - - //Session - private void setLastVisitTime() { - EventDetail ed = getLocalDataStore().getEventDetail(Constants.APP_LAUNCHED_EVENT); - if (ed == null) { - lastVisitTime = -1; - } else { - lastVisitTime = ed.getLastTime(); - } - } - - //Util - private void setMuted(final Context context, boolean mute) { - if (mute) { - final int now = (int) (System.currentTimeMillis() / 1000); - StorageHelper.putInt(context, StorageHelper.storageKeyWithSuffix(config, Constants.KEY_MUTED), now); - setDomain(context, null); - - // Clear all the queues - postAsyncSafely("CommsManager#setMuted", new Runnable() { - @Override - public void run() { - clearQueues(context); - } - }); - } else { - StorageHelper.putInt(context, StorageHelper.storageKeyWithSuffix(config, Constants.KEY_MUTED), 0); - } - } - - private void setPingFrequency(Context context, int pingFrequency) { - StorageHelper.putInt(context, Constants.PING_FREQUENCY, pingFrequency); - } - - // -----------------------------------------------------------------------// - // ******** Feature Flags Logic *****// - // -----------------------------------------------------------------------// - - // ******** Feature Flags Public API *****// - - private void setSpikyDomain(final Context context, String spikyDomainName) { - getConfigLogger().verbose(getAccountId(), "Setting spiky domain to " + spikyDomainName); - StorageHelper.putString(context, StorageHelper.storageKeyWithSuffix(config, Constants.SPIKY_KEY_DOMAIN_NAME), - spikyDomainName); - } - - private boolean shouldDeferProcessingEvent(JSONObject event, int eventType) { - //noinspection SimplifiableIfStatement - if (getConfig().isCreatedPostAppLaunch()) { - return false; - } - if (event.has("evtName")) { - try { - if (Arrays.asList(Constants.SYSTEM_EVENTS).contains(event.getString("evtName"))) { - return false; - } - } catch (JSONException e) { - //no-op - } - } - return (eventType == Constants.RAISED_EVENT && !isAppLaunchPushed()); - } - - // ******** Feature Flags Internal methods *****// - - private boolean shouldDropEvent(JSONObject event, int eventType) { - if (eventType == Constants.FETCH_EVENT) { - return false; - } - - if (isCurrentUserOptedOut()) { - String eventString = event == null ? "null" : event.toString(); - getConfigLogger().debug(getAccountId(), "Current user is opted out dropping event: " + eventString); - return true; - } - - if (isMuted()) { - getConfigLogger().verbose(getAccountId(), "CleverTap is muted, dropping event - " + event.toString()); - return true; - } - - return false; - } - - private void showInAppNotificationIfAny() { - if (!this.config.isAnalyticsOnly()) { - runOnNotificationQueue(new Runnable() { - @Override - public void run() { - _showNotificationIfAvailable(context); - } - }); - } - } - - //InApp - @SuppressWarnings({"unused"}) - private void showNotificationIfAvailable(final Context context) { - if (!this.config.isAnalyticsOnly()) { - runOnNotificationQueue(new Runnable() { - @Override - public void run() { - _showNotificationIfAvailable(context); - } - }); - } - } - - private void stopAlarmScheduler(Context context) { - AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - Intent cancelIntent = new Intent(CTBackgroundIntentService.MAIN_ACTION); - cancelIntent.setPackage(context.getPackageName()); - PendingIntent alarmPendingIntent = PendingIntent - .getService(context, getAccountId().hashCode(), cancelIntent, PendingIntent.FLAG_UPDATE_CURRENT); - if (alarmManager != null && alarmPendingIntent != null) { - alarmManager.cancel(alarmPendingIntent); - } - } - - private void triggerNotification(Context context, Bundle extras, String notifMessage, String notifTitle, - int notificationId) { - NotificationManager notificationManager = - (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE); - - if (notificationManager == null) { - String notificationManagerError = "Unable to render notification, Notification Manager is null."; - getConfigLogger().debug(getAccountId(), notificationManagerError); - return; - } - - String channelId = extras.getString(Constants.WZRK_CHANNEL_ID, ""); - boolean requiresChannelId = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - int messageCode = -1; - String value = ""; - - if (channelId.isEmpty()) { - messageCode = Constants.CHANNEL_ID_MISSING_IN_PAYLOAD; - value = extras.toString(); - } else if (notificationManager.getNotificationChannel(channelId) == null) { - messageCode = Constants.CHANNEL_ID_NOT_REGISTERED; - value = channelId; - } - if (messageCode != -1) { - ValidationResult channelIdError = ValidationResultFactory.create(512, messageCode, value); - getConfigLogger().debug(getAccountId(), channelIdError.getErrorDesc()); - validationResultStack.pushValidationResult(channelIdError); - return; - } - } - - String icoPath = extras.getString(Constants.NOTIF_ICON); - Intent launchIntent = new Intent(context, CTPushNotificationReceiver.class); - - PendingIntent pIntent; - - // Take all the properties from the notif and add it to the intent - launchIntent.putExtras(extras); - launchIntent.removeExtra(Constants.WZRK_ACTIONS); - pIntent = PendingIntent.getBroadcast(context, (int) System.currentTimeMillis(), - launchIntent, PendingIntent.FLAG_UPDATE_CURRENT); - - NotificationCompat.Style style; - String bigPictureUrl = extras.getString(Constants.WZRK_BIG_PICTURE); - if (bigPictureUrl != null && bigPictureUrl.startsWith("http")) { - try { - Bitmap bpMap = Utils.getNotificationBitmap(bigPictureUrl, false, context); - - if (bpMap == null) { - throw new Exception("Failed to fetch big picture!"); - } - - if (extras.containsKey(Constants.WZRK_MSG_SUMMARY)) { - String summaryText = extras.getString(Constants.WZRK_MSG_SUMMARY); - style = new NotificationCompat.BigPictureStyle() - .setSummaryText(summaryText) - .bigPicture(bpMap); - } else { - style = new NotificationCompat.BigPictureStyle() - .setSummaryText(notifMessage) - .bigPicture(bpMap); - } - } catch (Throwable t) { - style = new NotificationCompat.BigTextStyle() - .bigText(notifMessage); - getConfigLogger() - .verbose(getAccountId(), "Falling back to big text notification, couldn't fetch big picture", - t); - } - } else { - style = new NotificationCompat.BigTextStyle() - .bigText(notifMessage); - } - - int smallIcon; - try { - String x = ManifestInfo.getInstance(context).getNotificationIcon(); - if (x == null) { - throw new IllegalArgumentException(); - } - smallIcon = context.getResources().getIdentifier(x, "drawable", context.getPackageName()); - if (smallIcon == 0) { - throw new IllegalArgumentException(); - } - } catch (Throwable t) { - smallIcon = DeviceInfo.getAppIconAsIntId(context); - } - - int priorityInt = NotificationCompat.PRIORITY_DEFAULT; - String priority = extras.getString(Constants.NOTIF_PRIORITY); - if (priority != null) { - if (priority.equals(Constants.PRIORITY_HIGH)) { - priorityInt = NotificationCompat.PRIORITY_HIGH; - } - if (priority.equals(Constants.PRIORITY_MAX)) { - priorityInt = NotificationCompat.PRIORITY_MAX; - } - } - - // if we have no user set notificationID then try collapse key - if (notificationId == Constants.EMPTY_NOTIFICATION_ID) { - try { - Object collapse_key = extras.get(Constants.WZRK_COLLAPSE); - if (collapse_key != null) { - if (collapse_key instanceof Number) { - notificationId = ((Number) collapse_key).intValue(); - } else if (collapse_key instanceof String) { - try { - notificationId = Integer.parseInt(collapse_key.toString()); - getConfigLogger().debug(getAccountId(), - "Converting collapse_key: " + collapse_key + " to notificationId int: " - + notificationId); - } catch (NumberFormatException e) { - notificationId = (collapse_key.toString().hashCode()); - getConfigLogger().debug(getAccountId(), - "Converting collapse_key: " + collapse_key + " to notificationId int: " - + notificationId); - } - } - } - } catch (NumberFormatException e) { - // no-op - } - } else { - getConfigLogger().debug(getAccountId(), "Have user provided notificationId: " + notificationId - + " won't use collapse_key (if any) as basis for notificationId"); - } - - // if after trying collapse_key notification is still empty set to random int - if (notificationId == Constants.EMPTY_NOTIFICATION_ID) { - notificationId = (int) (Math.random() * 100); - getConfigLogger().debug(getAccountId(), "Setting random notificationId: " + notificationId); - } - - NotificationCompat.Builder nb; - if (requiresChannelId) { - nb = new NotificationCompat.Builder(context, channelId); - - // choices here are Notification.BADGE_ICON_NONE = 0, Notification.BADGE_ICON_SMALL = 1, Notification.BADGE_ICON_LARGE = 2. Default is Notification.BADGE_ICON_LARGE - String badgeIconParam = extras.getString(Constants.WZRK_BADGE_ICON, null); - if (badgeIconParam != null) { - try { - int badgeIconType = Integer.parseInt(badgeIconParam); - if (badgeIconType >= 0) { - nb.setBadgeIconType(badgeIconType); - } - } catch (Throwable t) { - // no-op - } - } - - String badgeCountParam = extras.getString(Constants.WZRK_BADGE_COUNT, null); - if (badgeCountParam != null) { - try { - int badgeCount = Integer.parseInt(badgeCountParam); - if (badgeCount >= 0) { - nb.setNumber(badgeCount); - } - } catch (Throwable t) { - // no-op - } - } - if (extras.containsKey(Constants.WZRK_SUBTITLE)) { - nb.setSubText(extras.getString(Constants.WZRK_SUBTITLE)); - } - } else { - // noinspection all - nb = new NotificationCompat.Builder(context); - } - - if (extras.containsKey(Constants.WZRK_COLOR)) { - int color = Color.parseColor(extras.getString(Constants.WZRK_COLOR)); - nb.setColor(color); - nb.setColorized(true); - } - - nb.setContentTitle(notifTitle) - .setContentText(notifMessage) - .setContentIntent(pIntent) - .setAutoCancel(true) - .setStyle(style) - .setPriority(priorityInt) - .setSmallIcon(smallIcon); - - nb.setLargeIcon(Utils.getNotificationBitmap(icoPath, true, context)); - - try { - if (extras.containsKey(Constants.WZRK_SOUND)) { - Uri soundUri = null; - - Object o = extras.get(Constants.WZRK_SOUND); - - if ((o instanceof Boolean && (Boolean) o)) { - soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); - } else if (o instanceof String) { - String s = (String) o; - if (s.equals("true")) { - soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); - } else if (!s.isEmpty()) { - if (s.contains(".mp3") || s.contains(".ogg") || s.contains(".wav")) { - s = s.substring(0, (s.length() - 4)); - } - soundUri = Uri - .parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.getPackageName() - + "/raw/" + s); - - } - } - - if (soundUri != null) { - nb.setSound(soundUri); - } - } - } catch (Throwable t) { - getConfigLogger().debug(getAccountId(), "Could not process sound parameter", t); - } - - // add actions if any - JSONArray actions = null; - String actionsString = extras.getString(Constants.WZRK_ACTIONS); - if (actionsString != null) { - try { - actions = new JSONArray(actionsString); - } catch (Throwable t) { - getConfigLogger() - .debug(getAccountId(), "error parsing notification actions: " + t.getLocalizedMessage()); - } - } - - String intentServiceName = ManifestInfo.getInstance(context).getIntentServiceName(); - Class clazz = null; - if (intentServiceName != null) { - try { - clazz = Class.forName(intentServiceName); - } catch (ClassNotFoundException e) { - try { - clazz = Class.forName("com.clevertap.android.sdk.pushnotification.CTNotificationIntentService"); - } catch (ClassNotFoundException ex) { - Logger.d("No Intent Service found"); - } - } + .debug(getAccountId(), "CleverTap Instance has been set to offline, won't send events queue"); } else { - try { - clazz = Class.forName("com.clevertap.android.sdk.pushnotification.CTNotificationIntentService"); - } catch (ClassNotFoundException ex) { - Logger.d("No Intent Service found"); - } + getConfigLogger() + .debug(getAccountId(), "CleverTap Instance has been set to online, sending events queue"); + flush(); } + } - boolean isCTIntentServiceAvailable = isServiceAvailable(context, clazz); - - if (actions != null && actions.length() > 0) { - for (int i = 0; i < actions.length(); i++) { - try { - JSONObject action = actions.getJSONObject(i); - String label = action.optString("l"); - String dl = action.optString("dl"); - String ico = action.optString(Constants.NOTIF_ICON); - String id = action.optString("id"); - boolean autoCancel = action.optBoolean("ac", true); - if (label.isEmpty() || id.isEmpty()) { - getConfigLogger().debug(getAccountId(), - "not adding push notification action: action label or id missing"); - continue; - } - int icon = 0; - if (!ico.isEmpty()) { - try { - icon = context.getResources().getIdentifier(ico, "drawable", context.getPackageName()); - } catch (Throwable t) { - getConfigLogger().debug(getAccountId(), - "unable to add notification action icon: " + t.getLocalizedMessage()); - } - } - - boolean sendToCTIntentService = (autoCancel && isCTIntentServiceAvailable); - - Intent actionLaunchIntent; - if (sendToCTIntentService) { - actionLaunchIntent = new Intent(CTNotificationIntentService.MAIN_ACTION); - actionLaunchIntent.setPackage(context.getPackageName()); - actionLaunchIntent.putExtra("ct_type", CTNotificationIntentService.TYPE_BUTTON_CLICK); - if (!dl.isEmpty()) { - actionLaunchIntent.putExtra("dl", dl); - } - } else { - if (!dl.isEmpty()) { - actionLaunchIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(dl)); - } else { - actionLaunchIntent = context.getPackageManager() - .getLaunchIntentForPackage(context.getPackageName()); - } - } - - if (actionLaunchIntent != null) { - actionLaunchIntent.putExtras(extras); - actionLaunchIntent.removeExtra(Constants.WZRK_ACTIONS); - actionLaunchIntent.putExtra("actionId", id); - actionLaunchIntent.putExtra("autoCancel", autoCancel); - actionLaunchIntent.putExtra("wzrk_c2a", id); - actionLaunchIntent.putExtra("notificationId", notificationId); - - actionLaunchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); - } - - PendingIntent actionIntent; - int requestCode = ((int) System.currentTimeMillis()) + i; - if (sendToCTIntentService) { - actionIntent = PendingIntent.getService(context, requestCode, - actionLaunchIntent, PendingIntent.FLAG_UPDATE_CURRENT); - } else { - actionIntent = PendingIntent.getActivity(context, requestCode, - actionLaunchIntent, PendingIntent.FLAG_UPDATE_CURRENT); - } - nb.addAction(icon, label, actionIntent); + /** + * Use this method to opt the current user out of all event/profile tracking. + * You must call this method separately for each active user profile (e.g. when switching user profiles using + * onUserLogin). + * Once enabled, no events will be saved remotely or locally for the current user. To re-enable tracking call this + * method with enabled set to false. + * + * @param userOptOut boolean Whether tracking opt out should be enabled/disabled. + */ + @SuppressWarnings({"unused"}) + public void setOptOut(boolean userOptOut) { + final boolean enable = userOptOut; + Task task = CTExecutorFactory.executors(coreState.getConfig()).postAsyncSafelyTask(); + task.execute("setOptOut", new Callable() { + @Override + public Void call() { + // generate the data for a profile push to alert the server to the optOut state change + HashMap optOutMap = new HashMap<>(); + optOutMap.put(Constants.CLEVERTAP_OPTOUT, enable); - } catch (Throwable t) { + // determine order of operations depending on enabled/disabled + if (enable) { // if opting out first push profile event then set the flag + pushProfile(optOutMap); + coreState.getCoreMetaData().setCurrentUserOptedOut(true); + } else { // if opting back in first reset the flag to false then push the profile event + coreState.getCoreMetaData().setCurrentUserOptedOut(false); + pushProfile(optOutMap); + } + // persist the new optOut state + String key = coreState.getDeviceInfo().optOutKey(); + if (key == null) { getConfigLogger() - .debug(getAccountId(), "error adding notification action : " + t.getLocalizedMessage()); + .verbose(getAccountId(), "Unable to persist user OptOut state, storage key is null"); + return null; } + StorageHelper.putBoolean(context, StorageHelper.storageKeyWithSuffix(getConfig(), key), enable); + getConfigLogger().verbose(getAccountId(), "Set current user OptOut state to: " + enable); + return null; } - } - - Notification n = nb.build(); - notificationManager.notify(notificationId, n); - getConfigLogger().debug(getAccountId(), "Rendered notification: " + n.toString()); - - String ttl = extras.getString(Constants.WZRK_TIME_TO_LIVE, - (System.currentTimeMillis() + Constants.DEFAULT_PUSH_TTL) / 1000 + ""); - long wzrk_ttl = Long.parseLong(ttl); - String wzrk_pid = extras.getString(Constants.WZRK_PUSH_ID); - DBAdapter dbAdapter = loadDBAdapter(context); - getConfigLogger().verbose("Storing Push Notification..." + wzrk_pid + " - with ttl - " + ttl); - dbAdapter.storePushNotificationId(wzrk_pid, wzrk_ttl); - - boolean notificationViewedEnabled = "true".equals(extras.getString(Constants.WZRK_RNV, "")); - if (!notificationViewedEnabled) { - ValidationResult notificationViewedError = ValidationResultFactory - .create(512, Constants.NOTIFICATION_VIEWED_DISABLED, extras.toString()); - getConfigLogger().debug(notificationViewedError.getErrorDesc()); - validationResultStack.pushValidationResult(notificationViewedError); - return; - } - pushNotificationViewedEvent(extras); + }); } - private void updateBlacklistedActivitySet() { - if (inappActivityExclude == null) { - inappActivityExclude = new HashSet<>(); - try { - String activities = ManifestInfo.getInstance(context).getExcludedActivities(); - if (activities != null) { - String[] split = activities.split(","); - for (String a : split) { - inappActivityExclude.add(a.trim()); - } - } - } catch (Throwable t) { - // Ignore + /** + * Opens {@link CTInboxActivity} to display Inbox Messages + * + * @param styleConfig {@link CTInboxStyleConfig} configuration of various style parameters for the {@link + * CTInboxActivity} + */ + @SuppressWarnings({"unused", "WeakerAccess"}) + public void showAppInbox(CTInboxStyleConfig styleConfig) { + synchronized (coreState.getCTLockManager().getInboxControllerLock()) { + if (coreState.getControllerManager().getCTInboxController() == null) { + getConfigLogger().debug(getAccountId(), "Notification Inbox not initialized"); + return; } - getConfigLogger().debug(getAccountId(), - "In-app notifications will not be shown on " + Arrays.toString(inappActivityExclude.toArray())); } - } - // -----------------------------------------------------------------------// - // ******** PRODUCT CONFIG Logic *****// - // -----------------------------------------------------------------------// - - // ******** PRODUCT CONFIG Public API *****// - - // helper extracts the cursor data from the db object - private QueueCursor updateCursorForDBObject(JSONObject dbObject, QueueCursor cursor) { - if (dbObject == null) { - return cursor; - } + // make styleConfig immutable + final CTInboxStyleConfig _styleConfig = new CTInboxStyleConfig(styleConfig); - Iterator keys = dbObject.keys(); - if (keys.hasNext()) { - String key = keys.next(); - cursor.setLastId(key); - try { - cursor.setData(dbObject.getJSONArray(key)); - } catch (JSONException e) { - cursor.setLastId(null); - cursor.setData(null); + Intent intent = new Intent(context, CTInboxActivity.class); + intent.putExtra("styleConfig", _styleConfig); + Bundle configBundle = new Bundle(); + configBundle.putParcelable("config", getConfig()); + intent.putExtra("configBundle", configBundle); + try { + Activity currentActivity = CoreMetaData.getCurrentActivity(); + if (currentActivity == null) { + throw new IllegalStateException("Current activity reference not found"); } + currentActivity.startActivity(intent); + Logger.d("Displaying Notification Inbox"); + + } catch (Throwable t) { + Logger.v("Please verify the integration of your app." + + " It is not setup to support Notification Inbox yet.", t); } - return cursor; } - //Util - // only call async - private void updateLocalStore(final Context context, final JSONObject event, final int type) { - if (type == Constants.RAISED_EVENT) { - getLocalDataStore().persistEvent(context, event, type); - } + /** + * Opens {@link CTInboxActivity} to display Inbox Messages with default {@link CTInboxStyleConfig} object + */ + @SuppressWarnings({"unused"}) + public void showAppInbox() { + CTInboxStyleConfig styleConfig = new CTInboxStyleConfig(); + showAppInbox(styleConfig); } - // ******** PRODUCT CONFIG Internal API *****// + //To be called from DeviceInfo AdID GUID generation + void deviceIDCreated(String deviceId) { + Logger.v("Initializing InAppFC after Device ID Created = " + deviceId); + coreState.getControllerManager() + .setInAppFCManager(new InAppFCManager(context, coreState.getConfig(), deviceId)); + Logger.v("Initializing ABTesting after Device ID Created = " + deviceId); - /** - * updates the ping frequency if there is a change & reschedules existing ping tasks. - */ - private void updatePingFrequencyIfNeeded(final Context context, int frequency) { - getConfigLogger().verbose("Ping frequency received - " + frequency); - getConfigLogger().verbose("Stored Ping Frequency - " + getPingFrequency(context)); - if (frequency != getPingFrequency(context)) { - setPingFrequency(context, frequency); - if (this.config.isBackgroundSync() && !this.config.isAnalyticsOnly()) { - postAsyncSafely("createOrResetJobScheduler", new Runnable() { - @Override - public void run() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - getConfigLogger().verbose("Creating job"); - createOrResetJobScheduler(context); - } else { - getConfigLogger().verbose("Resetting alarm"); - resetAlarmScheduler(context); - } - } - }); - } + /* + Reinitialising product config & Feature Flag controllers with google ad id. + */ + if (coreState.getControllerManager().getCTFeatureFlagsController() != null) { + coreState.getControllerManager().getCTFeatureFlagsController().setGuidAndInit(deviceId); + } + if (coreState.getControllerManager().getCTProductConfigController() != null) { + coreState.getControllerManager().getCTProductConfigController().setGuidAndInit(deviceId); } + getConfigLogger() + .verbose("Got device id from DeviceInfo, notifying user profile initialized to SyncListener"); + coreState.getCallbackManager().notifyUserProfileInitialized(deviceId); } - //Deprecation warning because Google Play install referrer via intent will be deprecated in March 2020 - @Deprecated - static void handleInstallReferrer(Context context, Intent intent) { - if (instances == null) { - Logger.v("No CleverTap Instance found"); - CleverTapAPI instance = CleverTapAPI.getDefaultInstance(context); - if (instance != null) { - instance.pushInstallReferrer(intent); - } - return; - } + private CleverTapInstanceConfig getConfig() { + return coreState.getConfig(); + } - for (String accountId : CleverTapAPI.instances.keySet()) { - CleverTapAPI instance = CleverTapAPI.instances.get(accountId); - if (instance != null) { - instance.pushInstallReferrer(intent); - } - } + private Logger getConfigLogger() { + return getConfig().getLogger(); + } + private boolean isErrorDeviceId() { + return coreState.getDeviceInfo().isErrorDeviceId(); } - /** - * Returns whether or not the app is in the foreground. - * - * @return The foreground status - */ - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - static boolean isAppForeground() { - return appForeground; + //Run manifest validation in async + private void manifestAsyncValidation() { + Task task = CTExecutorFactory.executors(coreState.getConfig()).postAsyncSafelyTask(); + task.execute("Manifest Validation", new Callable() { + @Override + public Void call() { + ManifestValidator + .validate(context, coreState.getDeviceInfo(), coreState.getPushProviders()); + return null; + } + }); } /** - * Use this method to notify CleverTap that the app is in foreground + * Sends the ADM registration ID to CleverTap. * - * @param appForeground boolean true/false + * @param token The ADM registration ID + * @param register Boolean indicating whether to register + * or not for receiving push messages from CleverTap. + * Set this to true to receive push messages from CleverTap, + * and false to not receive any messages from CleverTap. */ - @SuppressWarnings({"unused", "WeakerAccess"}) - public static void setAppForeground(boolean appForeground) { - CleverTapAPI.appForeground = appForeground; + @SuppressWarnings("unused") + private void pushAmazonRegistrationId(String token, boolean register) { + coreState.getPushProviders().handleToken(token, PushType.ADM, register); } static void onActivityCreated(Activity activity) { @@ -8467,80 +2442,9 @@ static void onActivityCreated(Activity activity, String cleverTapID) { return; } - try { - for (String accountId : CleverTapAPI.instances.keySet()) { - CleverTapAPI instance = CleverTapAPI.instances.get(accountId); - - boolean shouldProcess = false; - if (instance != null) { - shouldProcess = (_accountId == null && instance.config.isDefaultInstance()) || instance - .getAccountId().equals(_accountId); - } - - if (shouldProcess) { - if (notification != null && !notification.isEmpty() && notification - .containsKey(Constants.NOTIFICATION_TAG)) { - instance.pushNotificationClickedEvent(notification); - } - - if (deepLink != null) { - try { - instance.pushDeepLink(deepLink); - } catch (Throwable t) { - // no-op - } - } - break; - } - } - } catch (Throwable t) { - Logger.v("Throwable - " + t.getLocalizedMessage()); - } - } - - /** - * Method to check whether app has ExoPlayer dependencies - * - * @return boolean - true/false depending on app's availability of ExoPlayer dependencies - */ - private static boolean checkForExoPlayer() { - boolean exoPlayerPresent = false; - Class className = null; - try { - className = Class.forName("com.google.android.exoplayer2.SimpleExoPlayer"); - className = Class.forName("com.google.android.exoplayer2.source.hls.HlsMediaSource"); - className = Class.forName("com.google.android.exoplayer2.ui.PlayerView"); - Logger.d("ExoPlayer is present"); - exoPlayerPresent = true; - } catch (Throwable t) { - Logger.d("ExoPlayer library files are missing!!!"); - Logger.d( - "Please add ExoPlayer dependencies to render InApp or Inbox messages playing video. For more information checkout CleverTap documentation."); - if (className != null) { - Logger.d("ExoPlayer classes not found " + className.getName()); - } else { - Logger.d("ExoPlayer classes not found"); - } - } - return exoPlayerPresent; - } - - private static void checkPendingNotifications(final Context context, final CleverTapInstanceConfig config) { - Logger.v(config.getAccountId(), "checking Pending Notifications"); - if (pendingNotifications != null && !pendingNotifications.isEmpty()) { - try { - final CTInAppNotification notification = pendingNotifications.get(0); - pendingNotifications.remove(0); - Handler mainHandler = new Handler(context.getMainLooper()); - mainHandler.post(new Runnable() { - @Override - public void run() { - showInApp(context, notification, config); - } - }); - } catch (Throwable t) { - // no-op - } + CleverTapAPI instance = CleverTapAPI.instances.get(_accountId); + if (instance != null) { + instance.coreState.getActivityLifeCycleManager().onActivityCreated(notification, deepLink); } } @@ -8567,7 +2471,8 @@ CleverTapAPI createInstanceIfAvailable(Context context, String _accountId, Strin } else { try { CleverTapAPI instance = CleverTapAPI.getDefaultInstance(context); - return (instance != null && instance.config.getAccountId().equals(_accountId)) ? instance : null; + return (instance != null && instance.coreState.getConfig().getAccountId().equals(_accountId)) + ? instance : null; } catch (Throwable t) { Logger.v("Error creating shared Instance: ", t.getCause()); return null; @@ -8578,8 +2483,6 @@ CleverTapAPI createInstanceIfAvailable(Context context, String _accountId, Strin } } - //GEOFENCE APIs - private static ArrayList getAvailableInstances(Context context) { ArrayList apiArrayList = new ArrayList<>(); if (instances == null || instances.isEmpty()) { @@ -8593,25 +2496,6 @@ private static ArrayList getAvailableInstances(Context context) { return apiArrayList; } - private static Activity getCurrentActivity() { - return (currentActivity == null) ? null : currentActivity.get(); - } - - private static void setCurrentActivity(@Nullable Activity activity) { - if (activity == null) { - currentActivity = null; - return; - } - if (!activity.getLocalClassName().contains("InAppNotificationActivity")) { - currentActivity = new WeakReference<>(activity); - } - } - - private static String getCurrentActivityName() { - Activity current = getCurrentActivity(); - return (current != null) ? current.getLocalClassName() : null; - } - /** * Creates default {@link CleverTapInstanceConfig} object and returns it * @@ -8649,180 +2533,4 @@ CleverTapAPI getDefaultInstanceOrFirstOther(Context context) { } return instance; } - - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - private static JobInfo getJobInfo(int jobId, JobScheduler jobScheduler) { - for (JobInfo jobInfo : jobScheduler.getAllPendingJobs()) { - if (jobInfo.getId() == jobId) { - return jobInfo; - } - } - return null; - } - - private static SSLSocketFactory getPinnedCertsSslSocketfactory(SSLContext sslContext) { - if (sslContext == null) { - return null; - } - - if (sslSocketFactory == null) { - try { - sslSocketFactory = sslContext.getSocketFactory(); - Logger.d("Pinning SSL session to DigiCertGlobalRoot CA certificate"); - } catch (Throwable e) { - Logger.d("Issue in pinning SSL,", e); - } - } - return sslSocketFactory; - } - - private static synchronized SSLContext getSSLContext() { - if (sslContext == null) { - sslContext = new SSLContextBuilder().build(); - } - return sslContext; - } - - //InApp - private static void inAppDidDismiss(Context context, CleverTapInstanceConfig config, - CTInAppNotification inAppNotification) { - Logger.v(config.getAccountId(), "Running inAppDidDismiss"); - if (currentlyDisplayingInApp != null && (currentlyDisplayingInApp.getCampaignId() - .equals(inAppNotification.getCampaignId()))) { - currentlyDisplayingInApp = null; - checkPendingNotifications(context, config); - } - } - - @SuppressWarnings("SameParameterValue") - private static boolean isServiceAvailable(Context context, Class clazz) { - if (clazz == null) { - return false; - } - - PackageManager pm = context.getPackageManager(); - String packageName = context.getPackageName(); - - PackageInfo packageInfo; - try { - packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SERVICES); - ServiceInfo[] services = packageInfo.services; - for (ServiceInfo serviceInfo : services) { - if (serviceInfo.name.equals(clazz.getName())) { - Logger.v("Service " + serviceInfo.name + " found"); - return true; - } - } - } catch (PackageManager.NameNotFoundException e) { - Logger.d("Intent Service name not found exception - " + e.getLocalizedMessage()); - } - return false; - } - - //InApp - private static void showInApp(Context context, final CTInAppNotification inAppNotification, - CleverTapInstanceConfig config) { - - Logger.v(config.getAccountId(), "Attempting to show next In-App"); - - if (!appForeground) { - pendingNotifications.add(inAppNotification); - Logger.v(config.getAccountId(), "Not in foreground, queueing this In App"); - return; - } - - if (currentlyDisplayingInApp != null) { - pendingNotifications.add(inAppNotification); - Logger.v(config.getAccountId(), "In App already displaying, queueing this In App"); - return; - } - - if ((System.currentTimeMillis() / 1000) > inAppNotification.getTimeToLive()) { - Logger.d("InApp has elapsed its time to live, not showing the InApp"); - return; - } - - currentlyDisplayingInApp = inAppNotification; - - CTInAppBaseFragment inAppFragment = null; - CTInAppType type = inAppNotification.getInAppType(); - switch (type) { - case CTInAppTypeCoverHTML: - case CTInAppTypeInterstitialHTML: - case CTInAppTypeHalfInterstitialHTML: - case CTInAppTypeCover: - case CTInAppTypeHalfInterstitial: - case CTInAppTypeInterstitial: - case CTInAppTypeAlert: - case CTInAppTypeInterstitialImageOnly: - case CTInAppTypeHalfInterstitialImageOnly: - case CTInAppTypeCoverImageOnly: - - Intent intent = new Intent(context, InAppNotificationActivity.class); - intent.putExtra("inApp", inAppNotification); - Bundle configBundle = new Bundle(); - configBundle.putParcelable("config", config); - intent.putExtra("configBundle", configBundle); - try { - Activity currentActivity = getCurrentActivity(); - if (currentActivity == null) { - throw new IllegalStateException("Current activity reference not found"); - } - config.getLogger().verbose(config.getAccountId(), - "calling InAppActivity for notification: " + inAppNotification.getJsonDescription()); - currentActivity.startActivity(intent); - Logger.d("Displaying In-App: " + inAppNotification.getJsonDescription()); - - } catch (Throwable t) { - Logger.v("Please verify the integration of your app." + - " It is not setup to support in-app notifications yet.", t); - } - break; - case CTInAppTypeFooterHTML: - inAppFragment = new CTInAppHtmlFooterFragment(); - break; - case CTInAppTypeHeaderHTML: - inAppFragment = new CTInAppHtmlHeaderFragment(); - break; - case CTInAppTypeFooter: - inAppFragment = new CTInAppNativeFooterFragment(); - break; - case CTInAppTypeHeader: - inAppFragment = new CTInAppNativeHeaderFragment(); - break; - default: - Logger.d(config.getAccountId(), "Unknown InApp Type found: " + type); - currentlyDisplayingInApp = null; - return; - } - - if (inAppFragment != null) { - Logger.d("Displaying In-App: " + inAppNotification.getJsonDescription()); - try { - //noinspection ConstantConditions - FragmentTransaction fragmentTransaction = ((FragmentActivity) getCurrentActivity()) - .getSupportFragmentManager() - .beginTransaction(); - Bundle bundle = new Bundle(); - bundle.putParcelable("inApp", inAppNotification); - bundle.putParcelable("config", config); - inAppFragment.setArguments(bundle); - fragmentTransaction.setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out); - fragmentTransaction.add(android.R.id.content, inAppFragment, inAppNotification.getType()); - Logger.v(config.getAccountId(), "calling InAppFragment " + inAppNotification.getCampaignId()); - fragmentTransaction.commit(); - - } catch (ClassCastException e) { - Logger.v(config.getAccountId(), - "Fragment not able to render, please ensure your Activity is an instance of AppCompatActivity" - + e.getMessage()); - } catch (Throwable t) { - Logger.v(config.getAccountId(), "Fragment not able to render", t); - } - } - } - - static { - haveVideoPlayerSupport = checkForExoPlayer(); - } } \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapAPIListener.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapAPIListener.java deleted file mode 100644 index e7cd8d15e..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapAPIListener.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.clevertap.android.sdk; - -import com.clevertap.android.sdk.ab_testing.CTABTestListener; -import com.clevertap.android.sdk.featureFlags.FeatureFlagListener; -import com.clevertap.android.sdk.product_config.CTProductConfigControllerListener; -import com.clevertap.android.sdk.product_config.CTProductConfigListener; -import com.clevertap.android.sdk.pushnotification.CTApiPushListener; - -public interface CleverTapAPIListener extends - CTInAppNotification.CTInAppNotificationListener, - InAppNotificationActivity.InAppActivityListener, - CTInAppBaseFragment.InAppListener, - CTInboxActivity.InboxActivityListener, - CTABTestListener, - FeatureFlagListener, - CTProductConfigControllerListener, - CTProductConfigListener, - CTApiPushListener { - -} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.java new file mode 100644 index 000000000..7960a497e --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.java @@ -0,0 +1,125 @@ +package com.clevertap.android.sdk; + +import android.content.Context; +import com.clevertap.android.sdk.db.DBManager; +import com.clevertap.android.sdk.events.EventMediator; +import com.clevertap.android.sdk.events.EventQueueManager; +import com.clevertap.android.sdk.featureFlags.CTFeatureFlagsFactory; +import com.clevertap.android.sdk.inapp.InAppController; +import com.clevertap.android.sdk.login.LoginController; +import com.clevertap.android.sdk.network.NetworkManager; +import com.clevertap.android.sdk.pushnotification.PushProviders; +import com.clevertap.android.sdk.task.MainLooperHandler; +import com.clevertap.android.sdk.validation.ValidationResultStack; +import com.clevertap.android.sdk.validation.Validator; + +class CleverTapFactory { + + static CoreState getCoreState(Context context, CleverTapInstanceConfig cleverTapInstanceConfig, + String cleverTapID) { + CoreState coreState = new CoreState(context); + + CoreMetaData coreMetaData = new CoreMetaData(); + coreState.setCoreMetaData(coreMetaData); + + Validator validator = new Validator(); + + ValidationResultStack validationResultStack = new ValidationResultStack(); + coreState.setValidationResultStack(validationResultStack); + + CTLockManager ctLockManager = new CTLockManager(); + coreState.setCTLockManager(ctLockManager); + + MainLooperHandler mainLooperHandler = new MainLooperHandler(); + coreState.setMainLooperHandler(mainLooperHandler); + + CleverTapInstanceConfig config = new CleverTapInstanceConfig(cleverTapInstanceConfig); + coreState.setConfig(config); + + EventMediator eventMediator = new EventMediator(context, config, coreMetaData); + coreState.setEventMediator(eventMediator); + + LocalDataStore localDataStore = new LocalDataStore(context, config); + coreState.setLocalDataStore(localDataStore); + + DeviceInfo deviceInfo = new DeviceInfo(context, config, cleverTapID, coreMetaData); + coreState.setDeviceInfo(deviceInfo); + + BaseCallbackManager callbackManager = new CallbackManager(config, deviceInfo); + coreState.setCallbackManager(callbackManager); + + SessionManager sessionManager = new SessionManager(config, coreMetaData, validator, localDataStore); + coreState.setSessionManager(sessionManager); + + DBManager baseDatabaseManager = new DBManager(config, ctLockManager); + coreState.setDatabaseManager(baseDatabaseManager); + + ControllerManager controllerManager = new ControllerManager(context, config, + ctLockManager, callbackManager, deviceInfo, baseDatabaseManager); + coreState.setControllerManager(controllerManager); + + if (coreState.getDeviceInfo() != null && coreState.getDeviceInfo().getDeviceID() != null) { + coreState.getConfig().getLogger() + .verbose("Initializing InAppFC with device Id = " + coreState.getDeviceInfo().getDeviceID()); + controllerManager + .setInAppFCManager(new InAppFCManager(context, config, coreState.getDeviceInfo().getDeviceID())); + } + + NetworkManager networkManager = new NetworkManager(context, config, deviceInfo, coreMetaData, + validationResultStack, controllerManager,baseDatabaseManager, + callbackManager, ctLockManager, validator, localDataStore); + coreState.setNetworkManager(networkManager); + + EventQueueManager baseEventQueueManager = new EventQueueManager(baseDatabaseManager, context, config, + eventMediator, + sessionManager, callbackManager, + mainLooperHandler, deviceInfo, validationResultStack, + networkManager, coreMetaData, ctLockManager, localDataStore); + coreState.setBaseEventQueueManager(baseEventQueueManager); + + AnalyticsManager analyticsManager = new AnalyticsManager(context, config, baseEventQueueManager, validator, + validationResultStack, coreMetaData, localDataStore, deviceInfo, + mainLooperHandler, callbackManager, controllerManager, ctLockManager); + coreState.setAnalyticsManager(analyticsManager); + + InAppController inAppController = new InAppController(context, config, mainLooperHandler, + controllerManager, callbackManager, analyticsManager, coreMetaData); + coreState.setInAppController(inAppController); + coreState.getControllerManager().setInAppController(inAppController); + + initFeatureFlags(context, controllerManager, config, deviceInfo, callbackManager, analyticsManager); + + LocationManager locationManager = new LocationManager(context, config, coreMetaData, baseEventQueueManager); + coreState.setLocationManager(locationManager); + + PushProviders pushProviders = PushProviders + .load(context, config, baseDatabaseManager, validationResultStack, + analyticsManager, controllerManager); + coreState.setPushProviders(pushProviders); + + ActivityLifeCycleManager activityLifeCycleManager = new ActivityLifeCycleManager(context, config, + analyticsManager, coreMetaData, sessionManager, pushProviders, callbackManager, inAppController, + baseEventQueueManager); + coreState.setActivityLifeCycleManager(activityLifeCycleManager); + + LoginController loginController = new LoginController(context, config, deviceInfo, + validationResultStack, baseEventQueueManager, analyticsManager, + coreMetaData, controllerManager, sessionManager, + localDataStore, callbackManager, baseDatabaseManager, ctLockManager); + coreState.setLoginController(loginController); + return coreState; + } + + static void initFeatureFlags(Context context, ControllerManager controllerManager, CleverTapInstanceConfig config, + DeviceInfo deviceInfo, BaseCallbackManager callbackManager, AnalyticsManager analyticsManager) { + Logger.v("Initializing Feature Flags with device Id = " + deviceInfo.getDeviceID()); + if (config.isAnalyticsOnly()) { + config.getLogger().debug(config.getAccountId(), "Feature Flag is not enabled for this instance"); + } else { + controllerManager.setCTFeatureFlagsController(CTFeatureFlagsFactory.getInstance(context, + deviceInfo.getDeviceID(), + config, callbackManager, analyticsManager)); + config.getLogger().verbose(config.getAccountId(), "Feature Flags initialized"); + } + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapInstanceConfig.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapInstanceConfig.java index 5bcffe200..6be7b2dde 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapInstanceConfig.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapInstanceConfig.java @@ -1,9 +1,9 @@ package com.clevertap.android.sdk; -import static com.clevertap.android.sdk.JsonUtil.toArray; -import static com.clevertap.android.sdk.JsonUtil.toJsonArray; -import static com.clevertap.android.sdk.JsonUtil.toList; import static com.clevertap.android.sdk.pushnotification.PushNotificationUtil.getAll; +import static com.clevertap.android.sdk.utils.CTJsonConverter.toArray; +import static com.clevertap.android.sdk.utils.CTJsonConverter.toJsonArray; +import static com.clevertap.android.sdk.utils.CTJsonConverter.toList; import android.content.Context; import android.os.Parcel; @@ -11,7 +11,9 @@ import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; import com.clevertap.android.sdk.Constants.IdentityType; +import com.clevertap.android.sdk.login.LoginConstants; import java.util.ArrayList; import java.util.Arrays; import org.json.JSONObject; @@ -53,12 +55,8 @@ public CleverTapInstanceConfig[] newArray(int size) { private boolean disableAppLaunchedEvent; - private boolean enableABTesting; - private boolean enableCustomCleverTapId; - private boolean enableUIEditor; - private String fcmSenderId; private boolean isDefaultInstance; @@ -69,7 +67,7 @@ public CleverTapInstanceConfig[] newArray(int size) { private boolean personalization; - private String[] identityKeys = NullConstants.NULL_STRING_ARRAY; + private String[] identityKeys = Constants.NULL_STRING_ARRAY; private boolean sslPinning; @@ -114,8 +112,6 @@ public static CleverTapInstanceConfig createInstance(Context context, @NonNull S this.backgroundSync = config.backgroundSync; this.enableCustomCleverTapId = config.enableCustomCleverTapId; this.fcmSenderId = config.fcmSenderId; - this.enableABTesting = config.enableABTesting; - this.enableUIEditor = config.enableUIEditor; this.packageName = config.packageName; this.beta = config.beta; this.allowedPushTypes = config.allowedPushTypes; @@ -133,8 +129,6 @@ private CleverTapInstanceConfig(Context context, String accountId, String accoun this.debugLevel = CleverTapAPI.LogLevel.INFO.intValue(); this.logger = new Logger(this.debugLevel); this.createdPostAppLaunch = false; - this.enableABTesting = this.isDefaultInstance; - this.enableUIEditor = this.enableABTesting; ManifestInfo manifest = ManifestInfo.getInstance(context); this.useGoogleAdId = manifest.useGoogleAdId(); @@ -150,7 +144,7 @@ private CleverTapInstanceConfig(Context context, String accountId, String accoun */ if (isDefaultInstance) { identityKeys = manifest.getProfileKeys(); - log(LogConstants.LOG_TAG_ON_USER_LOGIN, "Setting Profile Keys from Manifest: " + Arrays + log(LoginConstants.LOG_TAG_ON_USER_LOGIN, "Setting Profile Keys from Manifest: " + Arrays .toString(identityKeys)); } } @@ -186,12 +180,7 @@ private CleverTapInstanceConfig(String jsonString) throws Throwable { this.debugLevel = configJsonObject.getInt(Constants.KEY_DEBUG_LEVEL); } this.logger = new Logger(this.debugLevel); - if (configJsonObject.has(Constants.KEY_ENABLE_ABTEST)) { - this.enableABTesting = configJsonObject.getBoolean(Constants.KEY_ENABLE_ABTEST); - } - if (configJsonObject.has(Constants.KEY_ENABLE_UIEDITOR)) { - this.enableUIEditor = configJsonObject.getBoolean(Constants.KEY_ENABLE_UIEDITOR); - } + if (configJsonObject.has(Constants.KEY_PACKAGE_NAME)) { this.packageName = configJsonObject.getString(Constants.KEY_PACKAGE_NAME); } @@ -241,8 +230,6 @@ private CleverTapInstanceConfig(Parcel in) { backgroundSync = in.readByte() != 0x00; enableCustomCleverTapId = in.readByte() != 0x00; fcmSenderId = in.readString(); - enableABTesting = in.readByte() != 0x00; - enableUIEditor = in.readByte() != 0x00; packageName = in.readString(); logger = new Logger(debugLevel); beta = in.readByte() != 0x00; @@ -315,11 +302,6 @@ public String[] getIdentityKeys() { return identityKeys; } - @SuppressWarnings({"BooleanMethodIsAlwaysInverted", "WeakerAccess"}) - public boolean isABTestingEnabled() { - return enableABTesting; - } - @SuppressWarnings({"unused", "WeakerAccess"}) public boolean isAnalyticsOnly() { return analyticsOnly; @@ -338,11 +320,6 @@ public boolean isDefaultInstance() { return isDefaultInstance; } - @SuppressWarnings({"unused"}) - public boolean isUIEditorEnabled() { - return enableUIEditor; - } - @RestrictTo(RestrictTo.Scope.LIBRARY) public void log(@NonNull String tag, @NonNull String message) { logger.verbose(getDefaultSuffix(tag), message); @@ -353,22 +330,10 @@ public void log(@NonNull String tag, @NonNull String message, Throwable throwabl logger.verbose(getDefaultSuffix(tag), message, throwable); } - @SuppressWarnings("SameParameterValue") - @RestrictTo(RestrictTo.Scope.LIBRARY) - public void setEnableABTesting(boolean enableABTesting) { - this.enableABTesting = enableABTesting; - } - - @SuppressWarnings({"unused"}) - @RestrictTo(RestrictTo.Scope.LIBRARY) - public void setEnableUIEditor(boolean enableUIEditor) { - this.enableUIEditor = enableUIEditor; - } - public void setIdentityKeys(@IdentityType String... identityKeys) { if (!isDefaultInstance) { this.identityKeys = identityKeys; - log(LogConstants.LOG_TAG_ON_USER_LOGIN, "Setting Profile Keys via setter: " + Arrays + log(LoginConstants.LOG_TAG_ON_USER_LOGIN, "Setting Profile Keys via setter: " + Arrays .toString(this.identityKeys)); } } @@ -394,15 +359,13 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeByte((byte) (backgroundSync ? 0x01 : 0x00)); dest.writeByte((byte) (enableCustomCleverTapId ? 0x01 : 0x00)); dest.writeString(fcmSenderId); - dest.writeByte((byte) (enableABTesting ? 0x01 : 0x00)); - dest.writeByte((byte) (enableUIEditor ? 0x01 : 0x00)); dest.writeString(packageName); dest.writeByte((byte) (beta ? 0x01 : 0x00)); dest.writeList(allowedPushTypes); dest.writeStringArray(identityKeys); } - boolean getEnableCustomCleverTapId() { + public boolean getEnableCustomCleverTapId() { return enableCustomCleverTapId; } @@ -411,7 +374,8 @@ public void setEnableCustomCleverTapId(boolean enableCustomCleverTapId) { this.enableCustomCleverTapId = enableCustomCleverTapId; } - boolean isBackgroundSync() { + @RestrictTo(Scope.LIBRARY) + public boolean isBackgroundSync() { return backgroundSync; } @@ -420,7 +384,7 @@ public void setBackgroundSync(boolean backgroundSync) { this.backgroundSync = backgroundSync; } - boolean isCreatedPostAppLaunch() { + public boolean isCreatedPostAppLaunch() { return createdPostAppLaunch; } @@ -437,7 +401,7 @@ boolean isPersonalizationEnabled() { return personalization; } - boolean isSslPinningEnabled() { + public boolean isSslPinningEnabled() { return sslPinning; } @@ -468,8 +432,6 @@ String toJSONString() { configJsonObject.put(Constants.KEY_ENABLE_CUSTOM_CT_ID, getEnableCustomCleverTapId()); configJsonObject.put(Constants.KEY_PACKAGE_NAME, getPackageName()); configJsonObject.put(Constants.KEY_BETA, isBeta()); - configJsonObject.put(Constants.KEY_ENABLE_UIEDITOR, isUIEditorEnabled()); - configJsonObject.put(Constants.KEY_ENABLE_ABTEST, isABTestingEnabled()); configJsonObject.put(Constants.KEY_ALLOWED_PUSH_TYPES, toJsonArray(allowedPushTypes)); return configJsonObject.toString(); } catch (Throwable e) { @@ -479,7 +441,7 @@ String toJSONString() { } private String getDefaultSuffix(@NonNull String tag) { - return "[" + ((!TextUtils.isEmpty(tag) ? ": " + tag : "") + ":" + accountId + "]"); + return "[" + ((!TextUtils.isEmpty(tag) ? ":" + tag : "") + ":" + accountId + "]"); } // convenience to construct the internal only default config diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapMetaData.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapMetaData.java new file mode 100644 index 000000000..317f3deec --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapMetaData.java @@ -0,0 +1,8 @@ +package com.clevertap.android.sdk; + +/* +Marker interface for future abstraction purposes. +Example - If any other module uses MetaData, then they can extend this class and common +properties can be abstracted. + */ +abstract class CleverTapMetaData {} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapState.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapState.java new file mode 100644 index 000000000..007b8d937 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapState.java @@ -0,0 +1,31 @@ +package com.clevertap.android.sdk; + +import android.content.Context; +import com.clevertap.android.sdk.db.BaseDatabaseManager; +import com.clevertap.android.sdk.network.BaseNetworkManager; + +abstract class CleverTapState { + + protected final Context context; + + CleverTapState(final Context context) { + this.context = context; + } + + public Context getContext() { + return context; + } + + abstract BaseDatabaseManager getDatabaseManager(); + + abstract void setDatabaseManager(final BaseDatabaseManager databaseManager); + + abstract BaseNetworkManager getNetworkManager(); + + abstract void setNetworkManager(final BaseNetworkManager networkManager); + + abstract BaseLocationManager getLocationManager(); + + abstract void setLocationManager(BaseLocationManager baseLocationManager); + +} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/Constants.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/Constants.java index 8b0f3ef35..3460d2f2c 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/Constants.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/Constants.java @@ -18,7 +18,7 @@ public interface Constants { @interface IdentityType { } - + String TAG_FEATURE_IN_APPS = "TAG_FEATURE_IN_APPS"; @NonNull String TYPE_IDENTITY = "Identity"; @NonNull @@ -136,7 +136,7 @@ public interface Constants { String[] SYSTEM_EVENTS = {NOTIFICATION_CLICKED_EVENT_NAME, NOTIFICATION_VIEWED_EVENT_NAME, GEOFENCE_ENTERED_EVENT_NAME, GEOFENCE_EXITED_EVENT_NAME}; - long DEFAULT_PUSH_TTL = 1000 * 60 * 60 * 24 * 4; + long DEFAULT_PUSH_TTL = 1000L * 60 * 60 * 24 * 4;// 4 days String PF_JOB_ID = "pfjobid"; int PING_FREQUENCY_VALUE = 240; String PING_FREQUENCY = "pf"; @@ -187,8 +187,6 @@ public interface Constants { String KEY_ENABLE_CUSTOM_CT_ID = "getEnableCustomCleverTapId"; String KEY_BETA = "beta"; String KEY_PACKAGE_NAME = "packageName"; - String KEY_ENABLE_UIEDITOR = "enableUIEditor"; - String KEY_ENABLE_ABTEST = "enableABTesting"; String KEY_ALLOWED_PUSH_TYPES = "allowedPushTypes"; String KEY_IDENTITY_TYPES = "identityTypes"; String WZRK_PUSH_ID = "wzrk_pid"; @@ -282,4 +280,8 @@ public interface Constants { // valid profile identifier keys HashSet LEGACY_IDENTITY_KEYS = new HashSet<>(Arrays.asList(TYPE_IDENTITY, TYPE_EMAIL)); HashSet ALL_IDENTITY_KEYS = new HashSet<>(Arrays.asList(TYPE_IDENTITY, TYPE_EMAIL, TYPE_PHONE)); + + int MAX_DELAY_FREQUENCY = 1000 * 60 * 10; + + String[] NULL_STRING_ARRAY = new String[0]; } \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/ControllerManager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/ControllerManager.java new file mode 100644 index 000000000..48e1d1bc5 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/ControllerManager.java @@ -0,0 +1,156 @@ +package com.clevertap.android.sdk; + +import android.content.Context; +import com.clevertap.android.sdk.db.BaseDatabaseManager; +import com.clevertap.android.sdk.displayunits.CTDisplayUnitController; +import com.clevertap.android.sdk.featureFlags.CTFeatureFlagsController; +import com.clevertap.android.sdk.inapp.InAppController; +import com.clevertap.android.sdk.inbox.CTInboxController; +import com.clevertap.android.sdk.product_config.CTProductConfigController; +import com.clevertap.android.sdk.pushnotification.PushProviders; +import com.clevertap.android.sdk.task.CTExecutorFactory; +import com.clevertap.android.sdk.task.Task; +import java.util.concurrent.Callable; + +public class ControllerManager { + + private InAppFCManager inAppFCManager; + + private final BaseDatabaseManager baseDatabaseManager; + + private CTDisplayUnitController ctDisplayUnitController; + + private CTFeatureFlagsController ctFeatureFlagsController; + + private CTInboxController ctInboxController; + + private final CTLockManager ctLockManager; + + private CTProductConfigController ctProductConfigController; + + private final BaseCallbackManager callbackManager; + + private final CleverTapInstanceConfig config; + + private final Context context; + + private final DeviceInfo deviceInfo; + + private InAppController inAppController; + + private PushProviders pushProviders; + + public ControllerManager(Context context, + CleverTapInstanceConfig config, + CTLockManager ctLockManager, + BaseCallbackManager callbackManager, + DeviceInfo deviceInfo, + BaseDatabaseManager databaseManager) { + this.config = config; + this.ctLockManager = ctLockManager; + this.callbackManager = callbackManager; + this.deviceInfo = deviceInfo; + this.context = context; + baseDatabaseManager = databaseManager; + } + + public CTDisplayUnitController getCTDisplayUnitController() { + return ctDisplayUnitController; + } + + public void setCTDisplayUnitController( + final CTDisplayUnitController CTDisplayUnitController) { + ctDisplayUnitController = CTDisplayUnitController; + } + + public CTFeatureFlagsController getCTFeatureFlagsController() { + + return ctFeatureFlagsController; + } + + public void setCTFeatureFlagsController( + final CTFeatureFlagsController CTFeatureFlagsController) { + ctFeatureFlagsController = CTFeatureFlagsController; + } + + public CTInboxController getCTInboxController() { + return ctInboxController; + } + + public void setCTInboxController(final CTInboxController CTInboxController) { + ctInboxController = CTInboxController; + } + + public CTProductConfigController getCTProductConfigController() { + return ctProductConfigController; + } + + public void setCTProductConfigController( + final CTProductConfigController CTProductConfigController) { + ctProductConfigController = CTProductConfigController; + } + + public CleverTapInstanceConfig getConfig() { + return config; + } + + public InAppController getInAppController() { + return inAppController; + } + + public void setInAppController(final InAppController inAppController) { + this.inAppController = inAppController; + } + + public InAppFCManager getInAppFCManager() { + return inAppFCManager; + } + + public void setInAppFCManager(final InAppFCManager inAppFCManager) { + this.inAppFCManager = inAppFCManager; + } + + public PushProviders getPushProviders() { + return pushProviders; + } + + public void setPushProviders(final PushProviders pushProviders) { + this.pushProviders = pushProviders; + } + + public void initializeInbox() { + if (config.isAnalyticsOnly()) { + config.getLogger() + .debug(config.getAccountId(), "Instance is analytics only, not initializing Notification Inbox"); + return; + } + Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask(); + task.execute("initializeInbox", new Callable() { + @Override + public Void call() { + _initializeInbox(); + return null; + } + }); + } + + // always call async + private void _initializeInbox() { + synchronized (ctLockManager.getInboxControllerLock()) { + if (getCTInboxController() != null) { + callbackManager._notifyInboxInitialized(); + return; + } + if (deviceInfo.getDeviceID() != null) { + setCTInboxController(new CTInboxController(config, deviceInfo.getDeviceID(), + baseDatabaseManager.loadDBAdapter(context), + ctLockManager, + callbackManager, + Utils.haveVideoPlayerSupport)); + callbackManager._notifyInboxInitialized(); + } else { + config.getLogger().info("CRITICAL : No device ID found!"); + } + } + } +} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CoreMetaData.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/CoreMetaData.java new file mode 100644 index 000000000..431e80e28 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/CoreMetaData.java @@ -0,0 +1,312 @@ +package com.clevertap.android.sdk; + +import android.app.Activity; +import android.location.Location; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; +import java.lang.ref.WeakReference; +import org.json.JSONObject; + +/** + * This class stores run time state of CleverTap's instance + */ +@RestrictTo(Scope.LIBRARY) +public class CoreMetaData extends CleverTapMetaData { + + private static boolean appForeground = false; + + private static WeakReference currentActivity; + + private static int activityCount = 0; + + private long appInstallTime = 0; + + private boolean appLaunchPushed = false; + + private final Object appLaunchPushedLock = new Object(); + + private String currentScreenName = ""; + + private int currentSessionId = 0; + + private boolean currentUserOptedOut = false; + + private boolean firstRequestInSession = false; + + private boolean firstSession = false; + + private int geofenceSDKVersion = 0; + + private boolean installReferrerDataSent = false; + + private boolean isBgPing = false; + + private boolean isLocationForGeofence = false; + + private boolean isProductConfigRequested; + + private int lastSessionLength = 0; + + private Location locationFromUser = null; + + private boolean offline; + + private final Object optOutFlagLock = new Object(); + + private long referrerClickTime = 0; + + private String source = null, medium = null, campaign = null; + + private JSONObject wzrkParams = null; + + private static int initialAppEnteredForegroundTime = 0; + + public static Activity getCurrentActivity() { + return (currentActivity == null) ? null : currentActivity.get(); + } + + static int getInitialAppEnteredForegroundTime() { + return initialAppEnteredForegroundTime; + } + + public static void setCurrentActivity(@Nullable Activity activity) { + if (activity == null) { + currentActivity = null; + return; + } + if (!activity.getLocalClassName().contains("InAppNotificationActivity")) { + currentActivity = new WeakReference<>(activity); + } + } + + public static String getCurrentActivityName() { + Activity current = getCurrentActivity(); + return (current != null) ? current.getLocalClassName() : null; + } + + public static boolean isAppForeground() { + return appForeground; + } + + public static void setAppForeground(boolean isForeground) { + appForeground = isForeground; + } + + static void setInitialAppEnteredForegroundTime(final int initialAppEnteredForegroundTime) { + CoreMetaData.initialAppEnteredForegroundTime = initialAppEnteredForegroundTime; + } + + public long getAppInstallTime() { + return appInstallTime; + } + + public void setAppInstallTime(final long appInstallTime) { + this.appInstallTime = appInstallTime; + } + + public Location getLocationFromUser() { + return locationFromUser; + } + + public void setLocationFromUser(final Location locationFromUser) { + this.locationFromUser = locationFromUser; + } + + public boolean isProductConfigRequested() { + return isProductConfigRequested; + } + + public void setProductConfigRequested(final boolean productConfigRequested) { + isProductConfigRequested = productConfigRequested; + } + + public void setCurrentScreenName(final String currentScreenName) { + this.currentScreenName = currentScreenName; + } + + synchronized void clearCampaign() { + campaign = null; + } + + synchronized void clearMedium() { + medium = null; + } + + synchronized void clearSource() { + source = null; + } + + synchronized void clearWzrkParams() { + wzrkParams = null; + } + + public static int getActivityCount() { + return activityCount; + } + + synchronized void setCampaign(String campaign) { + if (this.campaign == null) { + this.campaign = campaign; + } + } + + public synchronized String getCampaign() { + return campaign; + } + + public int getCurrentSessionId() { + return currentSessionId; + } + + public int getGeofenceSDKVersion() { + return geofenceSDKVersion; + } + + public void setGeofenceSDKVersion(int geofenceSDKVersion) { + this.geofenceSDKVersion = geofenceSDKVersion; + } + + void setLastSessionLength(final int lastSessionLength) { + this.lastSessionLength = lastSessionLength; + } + + //Session + public int getLastSessionLength() { + return lastSessionLength; + } + + // only set if not already set during the session + synchronized void setMedium(String medium) { + if (this.medium == null) { + this.medium = medium; + } + } + + public synchronized String getMedium() { + return medium; + } + + void setReferrerClickTime(final long referrerClickTime) { + this.referrerClickTime = referrerClickTime; + } + + public long getReferrerClickTime() { + return referrerClickTime; + } + + public synchronized String getSource() { + return source; + } + + //UTM + // only set if not already set during the session + synchronized void setSource(String source) { + if (this.source == null) { + this.source = source; + } + } + + public String getScreenName() { + return currentScreenName.equals("") ? null : currentScreenName; + } + + public synchronized void setWzrkParams(JSONObject wzrkParams) { + if (this.wzrkParams == null) { + this.wzrkParams = wzrkParams; + } + } + + public synchronized JSONObject getWzrkParams() { + return wzrkParams; + } + + public boolean inCurrentSession() { + return currentSessionId > 0; + } + + void setAppLaunchPushed(boolean pushed) { + synchronized (appLaunchPushedLock) { + appLaunchPushed = pushed; + } + } + + public boolean isAppLaunchPushed() { + synchronized (appLaunchPushedLock) { + return appLaunchPushed; + } + } + + public boolean isBgPing() { + return isBgPing; + } + + public void setBgPing(final boolean bgPing) { + isBgPing = bgPing; + } + + public void setCurrentUserOptedOut(boolean enable) { + synchronized (optOutFlagLock) { + currentUserOptedOut = enable; + } + } + + public boolean isCurrentUserOptedOut() { + synchronized (optOutFlagLock) { + return currentUserOptedOut; + } + } + + public boolean isFirstRequestInSession() { + return firstRequestInSession; + } + + public void setFirstRequestInSession(boolean firstRequestInSession) { + this.firstRequestInSession = firstRequestInSession; + } + + void setFirstSession(final boolean firstSession) { + this.firstSession = firstSession; + } + + //Session + public boolean isFirstSession() { + return firstSession; + } + + void setInstallReferrerDataSent(final boolean installReferrerDataSent) { + this.installReferrerDataSent = installReferrerDataSent; + } + + public boolean isInstallReferrerDataSent() { + return installReferrerDataSent; + } + + public boolean isLocationForGeofence() { + return isLocationForGeofence; + } + + public void setLocationForGeofence(boolean locationForGeofence) { + isLocationForGeofence = locationForGeofence; + } + + void setOffline(boolean value) { + offline = value; + } + + void setCurrentSessionId(int sessionId) { + this.currentSessionId = sessionId; + } + + public boolean isOffline() { + return offline; + } + + public static void setActivityCount(final int count) { + activityCount = count; + } + + static void incrementActivityCount() { + activityCount++; + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CoreState.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/CoreState.java new file mode 100644 index 000000000..2fa72dea1 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/CoreState.java @@ -0,0 +1,247 @@ +package com.clevertap.android.sdk; + +import android.content.Context; +import com.clevertap.android.sdk.db.BaseDatabaseManager; +import com.clevertap.android.sdk.events.BaseEventQueueManager; +import com.clevertap.android.sdk.events.EventMediator; +import com.clevertap.android.sdk.inapp.InAppController; +import com.clevertap.android.sdk.login.LoginController; +import com.clevertap.android.sdk.network.BaseNetworkManager; +import com.clevertap.android.sdk.product_config.CTProductConfigController; +import com.clevertap.android.sdk.product_config.CTProductConfigFactory; +import com.clevertap.android.sdk.pushnotification.PushProviders; +import com.clevertap.android.sdk.task.MainLooperHandler; +import com.clevertap.android.sdk.validation.ValidationResultStack; + +public class CoreState extends CleverTapState { + + private BaseLocationManager baseLocationManager; + + private CleverTapInstanceConfig config; + + private CoreMetaData coreMetaData; + + private BaseDatabaseManager databaseManager; + + private DeviceInfo deviceInfo; + + private EventMediator eventMediator; + + private LocalDataStore localDataStore; + + private ActivityLifeCycleManager activityLifeCycleManager; + + private AnalyticsManager analyticsManager; + + private BaseEventQueueManager baseEventQueueManager; + + private CTLockManager ctLockManager; + + private BaseCallbackManager callbackManager; + + private ControllerManager controllerManager; + + private InAppController inAppController; + + private LoginController loginController; + + private SessionManager sessionManager; + + private ValidationResultStack validationResultStack; + + private MainLooperHandler mainLooperHandler; + + private BaseNetworkManager networkManager; + + private PushProviders pushProviders; + + CoreState(final Context context) { + super(context); + } + + public ActivityLifeCycleManager getActivityLifeCycleManager() { + return activityLifeCycleManager; + } + + public void setActivityLifeCycleManager(final ActivityLifeCycleManager activityLifeCycleManager) { + this.activityLifeCycleManager = activityLifeCycleManager; + } + + public AnalyticsManager getAnalyticsManager() { + return analyticsManager; + } + + public void setAnalyticsManager(final AnalyticsManager analyticsManager) { + this.analyticsManager = analyticsManager; + } + + public BaseEventQueueManager getBaseEventQueueManager() { + return baseEventQueueManager; + } + + void setBaseEventQueueManager(final BaseEventQueueManager baseEventQueueManager) { + this.baseEventQueueManager = baseEventQueueManager; + } + + public CTLockManager getCTLockManager() { + return ctLockManager; + } + + public void setCTLockManager(final CTLockManager CTLockManager) { + ctLockManager = CTLockManager; + } + + public BaseCallbackManager getCallbackManager() { + return callbackManager; + } + + void setCallbackManager(final BaseCallbackManager callbackManager) { + this.callbackManager = callbackManager; + } + + public CleverTapInstanceConfig getConfig() { + return config; + } + + public void setConfig(final CleverTapInstanceConfig config) { + this.config = config; + } + + public ControllerManager getControllerManager() { + return controllerManager; + } + + public void setControllerManager(final ControllerManager controllerManager) { + this.controllerManager = controllerManager; + } + + public CoreMetaData getCoreMetaData() { + return coreMetaData; + } + + void setCoreMetaData(final CoreMetaData coreMetaData) { + this.coreMetaData = coreMetaData; + } + + public CTProductConfigController getCtProductConfigController() { + initProductConfig(); + return getControllerManager().getCTProductConfigController(); + } + + @Override + public BaseDatabaseManager getDatabaseManager() { + return databaseManager; + } + + @Override + void setDatabaseManager(final BaseDatabaseManager databaseManager) { + this.databaseManager = databaseManager; + } + + public DeviceInfo getDeviceInfo() { + return deviceInfo; + } + + public void setDeviceInfo(final DeviceInfo deviceInfo) { + this.deviceInfo = deviceInfo; + } + + public InAppController getInAppController() { + return inAppController; + } + + public void setInAppController(final InAppController inAppController) { + this.inAppController = inAppController; + } + + public LocalDataStore getLocalDataStore() { + return localDataStore; + } + + public void setLocalDataStore(final LocalDataStore localDataStore) { + this.localDataStore = localDataStore; + } + + public LoginController getLoginController() { + return loginController; + } + + public void setLoginController(final LoginController loginController) { + this.loginController = loginController; + } + + @Override + public BaseNetworkManager getNetworkManager() { + return networkManager; + } + + @Override + void setNetworkManager(final BaseNetworkManager networkManager) { + this.networkManager = networkManager; + } + + public PushProviders getPushProviders() { + return pushProviders; + } + + public void setPushProviders(final PushProviders pushProviders) { + this.pushProviders = pushProviders; + } + + public SessionManager getSessionManager() { + return sessionManager; + } + + public void setSessionManager(final SessionManager sessionManager) { + this.sessionManager = sessionManager; + } + + public ValidationResultStack getValidationResultStack() { + return validationResultStack; + } + + public void setValidationResultStack(final ValidationResultStack validationResultStack) { + this.validationResultStack = validationResultStack; + } + + @Override + BaseLocationManager getLocationManager() { + return baseLocationManager; + } + + @Override + void setLocationManager(final BaseLocationManager baseLocationManager) { + this.baseLocationManager = baseLocationManager; + } + + public EventMediator getEventMediator() { + return eventMediator; + } + + public void setEventMediator(final EventMediator eventMediator) { + this.eventMediator = eventMediator; + } + + public MainLooperHandler getMainLooperHandler() { + return mainLooperHandler; + } + + public void setMainLooperHandler(final MainLooperHandler mainLooperHandler) { + this.mainLooperHandler = mainLooperHandler; + } + + private void initProductConfig() { + Logger.v("Initializing Product Config with device Id = " + getDeviceInfo().getDeviceID()); + if (getConfig().isAnalyticsOnly()) { + getConfig().getLogger() + .debug(getConfig().getAccountId(), "Product Config is not enabled for this instance"); + return; + } + if (getControllerManager().getCTProductConfigController() == null) { + CTProductConfigController ctProductConfigController = CTProductConfigFactory + .getInstance(context, getDeviceInfo(), + getConfig(), analyticsManager, coreMetaData, callbackManager); + getControllerManager().setCTProductConfigController(ctProductConfigController); + } + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/DeviceInfo.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/DeviceInfo.java index e123be719..30568da8e 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/DeviceInfo.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/DeviceInfo.java @@ -19,11 +19,16 @@ import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; import androidx.core.app.NotificationManagerCompat; +import com.clevertap.android.sdk.login.LoginInfoProvider; +import com.clevertap.android.sdk.utils.CTJsonConverter; +import com.clevertap.android.sdk.validation.ValidationResult; +import com.clevertap.android.sdk.validation.ValidationResultFactory; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.UUID; +import org.json.JSONObject; @RestrictTo(Scope.LIBRARY) public class DeviceInfo { @@ -304,11 +309,19 @@ private double toTwoPlaces(double n) { private final ArrayList validationResults = new ArrayList<>(); + private boolean enableNetworkInfoReporting = false; - DeviceInfo(Context context, CleverTapInstanceConfig config, String cleverTapID) { + private final CoreMetaData mCoreMetaData; + + DeviceInfo(Context context, CleverTapInstanceConfig config, String cleverTapID, CoreMetaData coreMetaData) { this.context = context; this.config = config; this.library = null; + mCoreMetaData = coreMetaData; + onInitDeviceInfo(cleverTapID); + } + + void onInitDeviceInfo(final String cleverTapID) { Thread deviceInfoCacheThread = new Thread(new Runnable() { @Override public void run() { @@ -323,12 +336,12 @@ public boolean isErrorDeviceId() { return getDeviceID() != null && getDeviceID().startsWith(Constants.ERROR_PROFILE_PREFIX); } - void forceNewDeviceID() { + public void forceNewDeviceID() { String deviceID = generateGUID(); forceUpdateDeviceId(deviceID); } - void forceUpdateCustomCleverTapID(String cleverTapID) { + public void forceUpdateCustomCleverTapID(String cleverTapID) { if (Utils.validateCTID(cleverTapID)) { getConfigLogger() .info(config.getAccountId(), "Setting CleverTap ID to custom CleverTap ID : " + cleverTapID); @@ -350,7 +363,7 @@ void forceUpdateCustomCleverTapID(String cleverTapID) { * @param id The new device ID */ @SuppressLint("CommitPrefEdits") - void forceUpdateDeviceId(String id) { + public void forceUpdateDeviceId(String id) { getConfigLogger().verbose(this.config.getAccountId(), "Force updating the device ID to " + id); synchronized (deviceIDLock) { StorageHelper.putString(context, getDeviceIdStorageKey(), id); @@ -361,49 +374,97 @@ String getAttributionID() { return getDeviceID(); } - String getBluetoothVersion() { + /** + * Determines if a device is tablet, smart phone or TV + * + * @param context context + * @return one of the possible value of {@link DeviceType} + */ + @DeviceType + public static int getDeviceType(final Context context) { + + if (sDeviceType == NULL) { + + try { + UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE); + if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) { + sDeviceType = TV; + return sDeviceType; + } + } catch (Exception e) { + //uiModeManager or context is null + Logger.d("Failed to decide whether device is a TV!"); + e.printStackTrace(); + } + + try { + sDeviceType = context.getResources().getBoolean(R.bool.ctIsTablet) ? TABLET : SMART_PHONE; + } catch (Exception e) { + // resource not found or context is null + Logger.d("Failed to decide whether device is a smart phone or tablet!"); + e.printStackTrace(); + sDeviceType = UNKNOWN; + } + + } + return sDeviceType; + } + + //Event + public JSONObject getAppLaunchedFields() { + + try { + boolean deviceIsMultiUser = false; + if (getGoogleAdID() != null) { + deviceIsMultiUser = new LoginInfoProvider(context, config, this).deviceIsMultiUser(); + } + return CTJsonConverter.from(this, mCoreMetaData.getLocationFromUser(), enableNetworkInfoReporting, + deviceIsMultiUser); + } catch (Throwable t) { + config.getLogger().verbose(config.getAccountId(), "Failed to construct App Launched event", t); + return new JSONObject(); + } + } + + public String getBluetoothVersion() { return getDeviceCachedInfo().bluetoothVersion; } - int getBuild() { + public int getBuild() { return getDeviceCachedInfo().build; } - String getCarrier() { + public String getCarrier() { return getDeviceCachedInfo().carrier; } - Context getContext() { + public Context getContext() { return context; } - String getCountryCode() { - return getDeviceCachedInfo().countryCode; + public String getDeviceID() { + return _getDeviceID() != null ? _getDeviceID() : getFallBackDeviceID(); } - int getDPI() { - return getDeviceCachedInfo().dpi; + public String getCountryCode() { + return getDeviceCachedInfo().countryCode; } - String getDeviceID() { - return _getDeviceID() != null ? _getDeviceID() : getFallBackDeviceID(); + public int getDPI() { + return getDeviceCachedInfo().dpi; } - String getGoogleAdID() { + public String getGoogleAdID() { synchronized (adIDLock) { return googleAdID; } } - double getHeight() { + public double getHeight() { return getDeviceCachedInfo().height; } - int getHeightPixels() { - return getDeviceCachedInfo().heightPixels; - } - - String getLibrary() { + public String getLibrary() { return library; } @@ -411,56 +472,52 @@ void setLibrary(String library) { this.library = library; } - String getManufacturer() { + public String getManufacturer() { return getDeviceCachedInfo().manufacturer; } - String getModel() { + public String getModel() { return getDeviceCachedInfo().model; } - String getNetworkType() { + public String getNetworkType() { return getDeviceCachedInfo().networkType; } - boolean getNotificationsEnabledForUser() { + public boolean getNotificationsEnabledForUser() { return getDeviceCachedInfo().notificationsEnabled; } - String getOsName() { + public String getOsName() { return getDeviceCachedInfo().osName; } - String getOsVersion() { + public String getOsVersion() { return getDeviceCachedInfo().osVersion; } - int getSdkVersion() { - return getDeviceCachedInfo().sdkVersion; - } - - ArrayList getValidationResults() { + public ArrayList getValidationResults() { // noinspection unchecked ArrayList tempValidationResults = (ArrayList) validationResults.clone(); validationResults.clear(); return tempValidationResults; } - String getVersionName() { - return getDeviceCachedInfo().versionName; + public int getSdkVersion() { + return getDeviceCachedInfo().sdkVersion; } - double getWidth() { - return getDeviceCachedInfo().width; + public String getVersionName() { + return getDeviceCachedInfo().versionName; } - int getWidthPixels() { - return getDeviceCachedInfo().widthPixels; + public double getWidth() { + return getDeviceCachedInfo().width; } @SuppressLint("MissingPermission") @SuppressWarnings("MissingPermission") - Boolean isBluetoothEnabled() { + public Boolean isBluetoothEnabled() { Boolean isBluetoothEnabled = null; try { PackageManager pm = context.getPackageManager(); @@ -477,13 +534,13 @@ Boolean isBluetoothEnabled() { return isBluetoothEnabled; } - boolean isLimitAdTrackingEnabled() { + public boolean isLimitAdTrackingEnabled() { synchronized (adIDLock) { return limitAdTracking; } } - Boolean isWifiConnected() { + public Boolean isWifiConnected() { Boolean ret = null; if (PackageManager.PERMISSION_GRANTED == context @@ -671,44 +728,54 @@ private void updateFallbackID(String fallbackId) { * @param context The Android context * @return The integer identifier for the image resource */ - static int getAppIconAsIntId(final Context context) { + public static int getAppIconAsIntId(final Context context) { ApplicationInfo ai = context.getApplicationInfo(); return ai.icon; } - /** - * Determines if a device is tablet, smart phone or TV - * - * @param context context - * @return one of the possible value of {@link DeviceType} - */ - @DeviceType - static int getDeviceType(final Context context) { + int getHeightPixels() { + return getDeviceCachedInfo().heightPixels; + } - if (sDeviceType == NULL) { + void enableDeviceNetworkInfoReporting(boolean value) { + enableNetworkInfoReporting = value; + StorageHelper.putBoolean(context, StorageHelper.storageKeyWithSuffix(config, Constants.NETWORK_INFO), + enableNetworkInfoReporting); + config.getLogger() + .verbose(config.getAccountId(), + "Device Network Information reporting set to " + enableNetworkInfoReporting); + } - try { - UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE); - if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) { - sDeviceType = TV; - return sDeviceType; - } - } catch (Exception e) { - //uiModeManager or context is null - Logger.d("Failed to decide whether device is a TV!"); - e.printStackTrace(); - } + void setDeviceNetworkInfoReportingFromStorage() { + boolean enabled = StorageHelper.getBooleanFromPrefs(context, config, Constants.NETWORK_INFO); + config.getLogger() + .verbose(config.getAccountId(), + "Setting device network info reporting state from storage to " + enabled); + enableNetworkInfoReporting = enabled; + } - try { - sDeviceType = context.getResources().getBoolean(R.bool.ctIsTablet) ? TABLET : SMART_PHONE; - } catch (Exception e) { - // resource not found or context is null - Logger.d("Failed to decide whether device is a smart phone or tablet!"); - e.printStackTrace(); - sDeviceType = UNKNOWN; - } + int getWidthPixels() { + return getDeviceCachedInfo().widthPixels; + } + public void setCurrentUserOptOutStateFromStorage() { + String key = optOutKey(); + if (key == null) { + config.getLogger().verbose(config.getAccountId(), + "Unable to set current user OptOut state from storage: storage key is null"); + return; } - return sDeviceType; + boolean storedOptOut = StorageHelper.getBooleanFromPrefs(context, config, key); + mCoreMetaData.setCurrentUserOptedOut(storedOptOut); + config.getLogger().verbose(config.getAccountId(), + "Set current user OptOut state from storage to: " + storedOptOut + " for key: " + key); + } + + String optOutKey() { + String guid = getDeviceID(); + if (guid == null) { + return null; + } + return "OptOut:" + guid; } } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/FailureFlushListener.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/FailureFlushListener.java new file mode 100644 index 000000000..5fd436324 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/FailureFlushListener.java @@ -0,0 +1,8 @@ +package com.clevertap.android.sdk; + +import android.content.Context; + +public interface FailureFlushListener { + + void failureFlush(Context context); +} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/FileUtils.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/FileUtils.java deleted file mode 100644 index b68c90abd..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/FileUtils.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.clevertap.android.sdk; - -import android.content.Context; -import android.text.TextUtils; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileWriter; -import java.io.InputStream; -import java.io.InputStreamReader; -import org.json.JSONObject; - -public class FileUtils { - - public static void deleteDirectory(Context context, CleverTapInstanceConfig config, String dirName) { - if (TextUtils.isEmpty(dirName) || context == null) { - return; - } - try { - File file = new File(context.getFilesDir(), dirName); - if (file.exists() && file.isDirectory()) { - String[] children = file.list(); - for (String child : children) { - new File(file, child).delete(); - } - } - } catch (Exception e) { - e.printStackTrace(); - if (config != null) { - config.getLogger().verbose(config.getAccountId(), - "writeFileOnInternalStorage: failed" + dirName + " Error:" + e.getLocalizedMessage()); - } - } - } - - public static void deleteFile(Context context, CleverTapInstanceConfig config, String fileName) throws Exception { - if (TextUtils.isEmpty(fileName) || context == null) { - return; - } - try { - File file = new File(context.getFilesDir(), fileName); - if (file.exists()) { - if (file.delete()) { - if (config != null) { - config.getLogger().verbose(config.getAccountId(), "File Deleted:" + fileName); - } - } else { - if (config != null) { - config.getLogger().verbose(config.getAccountId(), "Failed to delete file" + fileName); - } - } - } - } catch (Exception e) { - e.printStackTrace(); - if (config != null) { - config.getLogger().verbose(config.getAccountId(), - "writeFileOnInternalStorage: failed" + fileName + " Error:" + e.getLocalizedMessage()); - } - } - } - - public static String readFromFile(Context context, CleverTapInstanceConfig config, String fileNameWithPath) - throws Exception { - - String content = ""; - //Make sure to use a try-catch statement to catch any errors - try { - //Make your FilePath and File - String yourFilePath = context.getFilesDir() + "/" + fileNameWithPath; - File yourFile = new File(yourFilePath); - //Make an InputStream with your File in the constructor - InputStream inputStream = new FileInputStream(yourFile); - StringBuilder stringBuilder = new StringBuilder(); - //Check to see if your inputStream is null - //If it isn't use the inputStream to make a InputStreamReader - //Use that to make a BufferedReader - //Also create an empty String - InputStreamReader inputStreamReader = new InputStreamReader(inputStream); - BufferedReader bufferedReader = new BufferedReader(inputStreamReader); - String receiveString = ""; - //Use a while loop to append the lines from the Buffered reader - while ((receiveString = bufferedReader.readLine()) != null) { - stringBuilder.append(receiveString); - } - //Close your InputStream and save stringBuilder as a String - inputStream.close(); - content = stringBuilder.toString(); - } catch (Exception e) { - if (config != null) { - config.getLogger() - .verbose(config.getAccountId(), "[Exception While Reading: " + e.getLocalizedMessage()); - } - //Log your error with Log.e - } - return content; - } - - public static void writeJsonToFile(Context context, CleverTapInstanceConfig config, String dirName, - String fileName, JSONObject jsonObject) { - try { - if (jsonObject == null || TextUtils.isEmpty(dirName) || TextUtils.isEmpty(fileName)) { - return; - } - File file = new File(context.getFilesDir(), dirName); - if (!file.exists()) { - if (!file.mkdir()) { - return;// if directory is not created don't proceed and return - } - } - - File file1 = new File(file, fileName); - FileWriter writer = new FileWriter(file1, false); - writer.append(jsonObject.toString()); - writer.flush(); - writer.close(); - } catch (Exception e) { - e.printStackTrace(); - if (config != null) { - config.getLogger().verbose(config.getAccountId(), - "writeFileOnInternalStorage: failed" + e.getLocalizedMessage()); - } - } - } -} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/InAppFCManager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/InAppFCManager.java index 3370c1bfa..33b3e9fa7 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/InAppFCManager.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/InAppFCManager.java @@ -4,6 +4,9 @@ import android.content.Context; import android.content.SharedPreferences; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; +import com.clevertap.android.sdk.inapp.CTInAppNotification; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; @@ -13,13 +16,14 @@ import org.json.JSONArray; import org.json.JSONObject; -class InAppFCManager { +@RestrictTo(Scope.LIBRARY) +public class InAppFCManager { - private static final SimpleDateFormat ddMMyyyy = new SimpleDateFormat("ddMMyyyy", Locale.US); + private final SimpleDateFormat ddMMyyyy = new SimpleDateFormat("ddMMyyyy", Locale.US); - private CleverTapInstanceConfig config; + private final CleverTapInstanceConfig config; - private Context context; + private final Context context; private String deviceId; @@ -37,38 +41,7 @@ class InAppFCManager { init(deviceId); } - void attachToHeader(final Context context, JSONObject header) { - try { - // Trigger reset for dates - - header.put("imp", getIntFromPrefs(getKeyWithDeviceId(Constants.KEY_COUNTS_SHOWN_TODAY, deviceId), 0)); - - // tlc: [[targetID, todayCount, lifetime]] - JSONArray arr = new JSONArray(); - final SharedPreferences prefs = StorageHelper - .getPreferences(context, getKeyWithDeviceId(Constants.KEY_COUNTS_PER_INAPP, deviceId)); - final Map all = prefs.getAll(); - for (String inapp : all.keySet()) { - final Object o = all.get(inapp); - if (o instanceof String) { - final String[] parts = ((String) o).split(","); - if (parts.length == 2) { - JSONArray a = new JSONArray(); - a.put(0, inapp); - a.put(1, Integer.parseInt(parts[0])); - a.put(2, Integer.parseInt(parts[1])); - arr.put(a); - } - } - } - - header.put("tlc", arr); - } catch (Throwable t) { - Logger.v("Failed to attach FC to header", t); - } - } - - boolean canShow(CTInAppNotification inapp) { + public boolean canShow(CTInAppNotification inapp) { try { if (inapp == null) { return false; @@ -95,7 +68,7 @@ boolean canShow(CTInAppNotification inapp) { return false; } - void changeUser(String deviceId) { + public void changeUser(String deviceId) { // reset counters mShownThisSession.clear(); mShownThisSessionCount = 0; @@ -104,14 +77,14 @@ void changeUser(String deviceId) { init(deviceId); } - void didDismiss(CTInAppNotification inapp) { + public void didDismiss(CTInAppNotification inapp) { final Object id = inapp.getId(); if (id != null) { mDismissedThisSession.add(id.toString()); } } - void didShow(final Context context, CTInAppNotification inapp) { + public void didShow(final Context context, CTInAppNotification inapp) { final String id = getInAppID(inapp); if (id == null) { return; @@ -134,7 +107,38 @@ void didShow(final Context context, CTInAppNotification inapp) { ++shownToday); } - void processResponse(final Context context, final JSONObject response) { + public void attachToHeader(final Context context, JSONObject header) { + try { + // Trigger reset for dates + + header.put("imp", getIntFromPrefs(getKeyWithDeviceId(Constants.KEY_COUNTS_SHOWN_TODAY, deviceId), 0)); + + // tlc: [[targetID, todayCount, lifetime]] + JSONArray arr = new JSONArray(); + final SharedPreferences prefs = StorageHelper + .getPreferences(context, getKeyWithDeviceId(Constants.KEY_COUNTS_PER_INAPP, deviceId)); + final Map all = prefs.getAll(); + for (String inapp : all.keySet()) { + final Object o = all.get(inapp); + if (o instanceof String) { + final String[] parts = ((String) o).split(","); + if (parts.length == 2) { + JSONArray a = new JSONArray(); + a.put(0, inapp); + a.put(1, Integer.parseInt(parts[0])); + a.put(2, Integer.parseInt(parts[1])); + arr.put(a); + } + } + } + + header.put("tlc", arr); + } catch (Throwable t) { + Logger.v("Failed to attach FC to header", t); + } + } + + public void processResponse(final Context context, final JSONObject response) { try { if (!response.has("inapp_stale")) { return; @@ -163,7 +167,7 @@ void processResponse(final Context context, final JSONObject response) { } } - synchronized void updateLimits(final Context context, int perDay, int perSession) { + public synchronized void updateLimits(final Context context, int perDay, int perSession) { StorageHelper.putInt(context, storageKeyWithSuffix(getKeyWithDeviceId(Constants.KEY_MAX_PER_DAY, deviceId)), perDay); StorageHelper diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/InAppNotificationActivity.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/InAppNotificationActivity.java index 62b2a9a4a..e8faae6d7 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/InAppNotificationActivity.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/InAppNotificationActivity.java @@ -9,20 +9,23 @@ import android.os.Bundle; import android.view.WindowManager; import androidx.fragment.app.FragmentActivity; +import com.clevertap.android.sdk.inapp.CTInAppBaseFullFragment; +import com.clevertap.android.sdk.inapp.CTInAppHtmlCoverFragment; +import com.clevertap.android.sdk.inapp.CTInAppHtmlHalfInterstitialFragment; +import com.clevertap.android.sdk.inapp.CTInAppHtmlInterstitialFragment; +import com.clevertap.android.sdk.inapp.CTInAppNativeCoverFragment; +import com.clevertap.android.sdk.inapp.CTInAppNativeCoverImageFragment; +import com.clevertap.android.sdk.inapp.CTInAppNativeHalfInterstitialFragment; +import com.clevertap.android.sdk.inapp.CTInAppNativeHalfInterstitialImageFragment; +import com.clevertap.android.sdk.inapp.CTInAppNativeInterstitialFragment; +import com.clevertap.android.sdk.inapp.CTInAppNativeInterstitialImageFragment; +import com.clevertap.android.sdk.inapp.CTInAppNotification; +import com.clevertap.android.sdk.inapp.CTInAppType; +import com.clevertap.android.sdk.inapp.InAppListener; import java.lang.ref.WeakReference; import java.util.HashMap; -public final class InAppNotificationActivity extends FragmentActivity implements CTInAppBaseFragment.InAppListener { - - interface InAppActivityListener { - - void inAppNotificationDidClick(CTInAppNotification inAppNotification, Bundle formData, - HashMap keyValuePayload); - - void inAppNotificationDidDismiss(Context context, CTInAppNotification inAppNotification, Bundle formData); - - void inAppNotificationDidShow(CTInAppNotification inAppNotification, Bundle formData); - } +public final class InAppNotificationActivity extends FragmentActivity implements InAppListener { private static boolean isAlertVisible = false; @@ -30,7 +33,7 @@ void inAppNotificationDidClick(CTInAppNotification inAppNotification, Bundle for private CTInAppNotification inAppNotification; - private WeakReference listenerWeakReference; + private WeakReference listenerWeakReference; public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -48,7 +51,7 @@ public void onCreate(Bundle savedInstanceState) { if (configBundle != null) { config = configBundle.getParcelable("config"); } - setListener(CleverTapAPI.instanceWithConfig(getApplicationContext(), config)); + setListener(CleverTapAPI.instanceWithConfig(this, config).getCoreState().getInAppController()); } catch (Throwable t) { Logger.v("Cannot find a valid notification bundle to show!", t); finish(); @@ -137,7 +140,7 @@ public void setTheme(int resid) { } void didClick(Bundle data, HashMap keyValueMap) { - InAppActivityListener listener = getListener(); + InAppListener listener = getListener(); if (listener != null) { listener.inAppNotificationDidClick(inAppNotification, data, keyValueMap); } @@ -148,14 +151,14 @@ void didDismiss(Bundle data) { isAlertVisible = false; } finish(); - InAppActivityListener listener = getListener(); + InAppListener listener = getListener(); if (listener != null && getBaseContext() != null) { listener.inAppNotificationDidDismiss(getBaseContext(), inAppNotification, data); } } void didShow(Bundle data) { - InAppActivityListener listener = getListener(); + InAppListener listener = getListener(); if (listener != null) { listener.inAppNotificationDidShow(inAppNotification, data); } @@ -171,8 +174,8 @@ void fireUrlThroughIntent(String url, Bundle formData) { didDismiss(formData); } - InAppActivityListener getListener() { - InAppActivityListener listener = null; + InAppListener getListener() { + InAppListener listener = null; try { listener = listenerWeakReference.get(); } catch (Throwable t) { @@ -185,7 +188,7 @@ InAppActivityListener getListener() { return listener; } - void setListener(InAppActivityListener listener) { + void setListener(InAppListener listener) { listenerWeakReference = new WeakReference<>(listener); } @@ -347,10 +350,13 @@ public void onClick(DialogInterface dialogInterface, int i) { }); } } - //noinspection ConstantConditions - alertDialog.show(); - isAlertVisible = true; - didShow(null); + if(alertDialog != null){ + alertDialog.show(); + isAlertVisible = true; + didShow(null); + }else{ + config.getLogger().debug("InAppNotificationActivity: Alert Dialog is null, not showing Alert InApp"); + } break; } default: { diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/JsonUtil.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/JsonUtil.java deleted file mode 100644 index 3dc0e2906..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/JsonUtil.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.clevertap.android.sdk; - -import androidx.annotation.NonNull; -import java.util.ArrayList; -import java.util.List; -import org.json.JSONArray; -import org.json.JSONException; - -public class JsonUtil { - - public static Object[] toArray(@NonNull JSONArray jsonArray) { - Object[] array = new Object[jsonArray.length()]; - try { - for (int i = 0; i < jsonArray.length(); i++) { - array[i] = jsonArray.get(i); - } - } catch (JSONException e) { - e.printStackTrace(); - } - return array; - } - - public static JSONArray toJsonArray(@NonNull List list) { - JSONArray array = new JSONArray(); - for (Object item : list) { - if (item != null) { - array.put(item); - } - } - return array; - } - - public static ArrayList toList(@NonNull JSONArray array) { - ArrayList list = new ArrayList<>(); - for (int i = 0; i < array.length(); i++) { - try { - list.add(array.get(i)); - } catch (JSONException e) { - e.printStackTrace(); - } - } - return list; - } -} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/LocalDataStore.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/LocalDataStore.java index a9219c7c9..decd53cd3 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/LocalDataStore.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/LocalDataStore.java @@ -3,6 +3,10 @@ import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; +import com.clevertap.android.sdk.db.DBAdapter; +import com.clevertap.android.sdk.events.EventDetail; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; @@ -14,7 +18,8 @@ import org.json.JSONObject; @SuppressWarnings("unused") -class LocalDataStore { +@RestrictTo(Scope.LIBRARY) +public class LocalDataStore { private static long EXECUTOR_THREAD_ID = 0; @@ -28,13 +33,13 @@ class LocalDataStore { private final HashMap PROFILE_FIELDS_IN_THIS_SESSION = new HashMap<>(); - private CleverTapInstanceConfig config; + private final CleverTapInstanceConfig config; - private Context context; + private final Context context; private DBAdapter dbAdapter; - private ExecutorService es; + private final ExecutorService es; private final String eventNamespace = "local_events"; @@ -46,7 +51,7 @@ class LocalDataStore { inflateLocalProfileAsync(context); } - void changeUser() { + public void changeUser() { resetLocalProfileSync(); } @@ -98,7 +103,7 @@ Object getProfileValueForKey(String key) { return _getProfileProperty(key); } - void persistEvent(Context context, JSONObject event, int type) { + public void persistEvent(Context context, JSONObject event, int type) { if (event == null) { return; @@ -124,7 +129,7 @@ void removeProfileFields(ArrayList fields) { removeProfileFields(fields, false); } - void setDataSyncFlag(JSONObject event) { + public void setDataSyncFlag(JSONObject event) { try { // Check the personalisation flag boolean enablePersonalisation = this.config.isPersonalizationEnabled(); @@ -180,7 +185,7 @@ void setProfileFields(JSONObject fields) { } @SuppressWarnings("rawtypes") - void syncWithUpstream(Context context, JSONObject response) { + public void syncWithUpstream(Context context, JSONObject response) { try { JSONObject eventUpdates = null; JSONObject profileUpdates = null; diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/LocationManager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/LocationManager.java new file mode 100644 index 000000000..b86d273cf --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/LocationManager.java @@ -0,0 +1,138 @@ +package com.clevertap.android.sdk; + +import static com.clevertap.android.sdk.CleverTapAPI.isAppForeground; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.location.Location; +import com.clevertap.android.sdk.events.BaseEventQueueManager; +import java.util.List; +import java.util.concurrent.Future; +import org.json.JSONObject; + +class LocationManager extends BaseLocationManager { + + private int lastLocationPingTime = 0; + + private int lastLocationPingTimeForGeofence = 0; + + private final BaseEventQueueManager mBaseEventQueueManager; + + private final CleverTapInstanceConfig mConfig; + + private final Context mContext; + + private final CoreMetaData mCoreMetaData; + + private final Logger mLogger; + + LocationManager(Context context, + CleverTapInstanceConfig config, + CoreMetaData coreMetaData, + BaseEventQueueManager baseEventQueueManager) { + mContext = context; + mConfig = config; + mLogger = mConfig.getLogger(); + mCoreMetaData = coreMetaData; + mBaseEventQueueManager = baseEventQueueManager; + } + + @SuppressLint("MissingPermission") + @Override + public Location _getLocation() { + try { + android.location.LocationManager lm = (android.location.LocationManager) mContext + .getSystemService(Context.LOCATION_SERVICE); + if (lm == null) { + Logger.d("Location Manager is null."); + return null; + } + List providers = lm.getProviders(true); + Location bestLocation = null; + Location l = null; + for (String provider : providers) { + try { + l = lm.getLastKnownLocation(provider); + } catch (SecurityException e) { + //no-op + Logger.v("Location security exception", e); + } + + if (l == null) { + continue; + } + if (bestLocation == null || l.getAccuracy() < bestLocation.getAccuracy()) { + bestLocation = l; + } + } + + return bestLocation; + } catch (Throwable t) { + Logger.v("Couldn't get user's location", t); + return null; + } + } + + @Override + Future _setLocation(Location location) { + if (location == null) { + return null; + } + + mCoreMetaData.setLocationFromUser(location); + mLogger.verbose(mConfig.getAccountId(), + "Location updated (" + location.getLatitude() + ", " + location.getLongitude() + ")"); + + // only queue the location ping if we are in the foreground + if (!mCoreMetaData.isLocationForGeofence() && !isAppForeground()) { + return null; + } + + // Queue the ping event to transmit location update to server + // min 10 second interval between location pings + final int now = getNow(); + Future future = null; + + if (mCoreMetaData.isLocationForGeofence() && now > (lastLocationPingTimeForGeofence + + Constants.LOCATION_PING_INTERVAL_IN_SECONDS)) { + + future = mBaseEventQueueManager.queueEvent(mContext, new JSONObject(), Constants.PING_EVENT); + setLastLocationPingTimeForGeofence(now); + mLogger.verbose(mConfig.getAccountId(), + "Queuing location ping event for geofence location (" + location.getLatitude() + ", " + location + .getLongitude() + ")"); + + } else if (!mCoreMetaData.isLocationForGeofence() && now > (lastLocationPingTime + + Constants.LOCATION_PING_INTERVAL_IN_SECONDS)) { + + future = mBaseEventQueueManager.queueEvent(mContext, new JSONObject(), Constants.PING_EVENT); + setLastLocationPingTime(now); + mLogger.verbose(mConfig.getAccountId(), + "Queuing location ping event for location (" + location.getLatitude() + ", " + location + .getLongitude() + ")"); + } + + return future; + } + + int getLastLocationPingTime() { + return lastLocationPingTime; + } + + void setLastLocationPingTime(final int lastLocationPingTime) { + this.lastLocationPingTime = lastLocationPingTime; + } + + int getLastLocationPingTimeForGeofence() { + return lastLocationPingTimeForGeofence; + } + + void setLastLocationPingTimeForGeofence(final int lastLocationPingTimeForGeofence) { + this.lastLocationPingTimeForGeofence = lastLocationPingTimeForGeofence; + } + + int getNow() { + return (int) (System.currentTimeMillis() / 1000); + } + +} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/LogConstants.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/LogConstants.java deleted file mode 100644 index 4c805bacb..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/LogConstants.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.clevertap.android.sdk; - -public interface LogConstants { - - String LOG_TAG_ON_USER_LOGIN = "ON_USER_LOGIN"; - -} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/ManifestInfo.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/ManifestInfo.java index cd1bd696d..a5bd20a38 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/ManifestInfo.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/ManifestInfo.java @@ -129,15 +129,15 @@ String getAcountToken() { return accountToken; } - String getExcludedActivities() { + public String getExcludedActivities() { return excludedActivities; } - String getIntentServiceName() { + public String getIntentServiceName() { return intentServiceName; } - String getNotificationIcon() { + public String getNotificationIcon() { return notificationIcon; } @@ -165,10 +165,11 @@ boolean useGoogleAdId() { return useADID; } + @SuppressWarnings("ConstantConditions") private String[] parseProfileKeys(final Bundle metaData) { String profileKeyString = _getManifestStringValueForKey(metaData, Constants.CLEVERTAP_IDENTIFIER); return !TextUtils.isEmpty(profileKeyString) ? profileKeyString.split(Constants.SEPARATOR_COMMA) - : NullConstants.NULL_STRING_ARRAY; + : Constants.NULL_STRING_ARRAY; } static void changeCredentials(String id, String token, String region) { @@ -177,6 +178,13 @@ static void changeCredentials(String id, String token, String region) { accountRegion = region; } + /** + * This returns string representation of int,boolean,string,float value of given key + * + * @param manifest bundle to retrieve values from + * @param name key of bundle + * @return string representation of int,boolean,string,float + */ private static String _getManifestStringValueForKey(Bundle manifest, String name) { try { Object o = manifest.get(name); diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/NullConstants.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/NullConstants.java deleted file mode 100644 index cb232d9e0..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/NullConstants.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.clevertap.android.sdk; - -import androidx.annotation.RestrictTo; - -@RestrictTo(RestrictTo.Scope.LIBRARY) -public interface NullConstants { - - String[] NULL_STRING_ARRAY = new String[0]; -} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/SessionManager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/SessionManager.java new file mode 100644 index 000000000..104645edd --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/SessionManager.java @@ -0,0 +1,118 @@ +package com.clevertap.android.sdk; + +import android.content.Context; +import android.content.SharedPreferences; +import com.clevertap.android.sdk.events.EventDetail; +import com.clevertap.android.sdk.validation.Validator; + +public class SessionManager extends BaseSessionManager { + + private long appLastSeen = 0; + + private int lastVisitTime; + + private final CoreMetaData cleverTapMetaData; + + private final CleverTapInstanceConfig config; + + private final LocalDataStore localDataStore; + + private final Validator validator; + + public SessionManager(CleverTapInstanceConfig config, CoreMetaData coreMetaData, Validator validator, + LocalDataStore localDataStore) { + this.config = config; + cleverTapMetaData = coreMetaData; + this.validator = validator; + this.localDataStore = localDataStore; + } + + // SessionManager/session management + public void checkTimeoutSession() { + if (appLastSeen <= 0) { + return; + } + long now = System.currentTimeMillis(); + if ((now - appLastSeen) > Constants.SESSION_LENGTH_MINS * 60 * 1000) { + config.getLogger().verbose(config.getAccountId(), "Session Timed Out"); + destroySession(); + CoreMetaData.setCurrentActivity(null); + } + } + + @Override + public void destroySession() { + cleverTapMetaData.setCurrentSessionId(0); + cleverTapMetaData.setAppLaunchPushed(false); + if (cleverTapMetaData.isFirstSession()) { + cleverTapMetaData.setFirstSession(false); + } + config.getLogger().verbose(config.getAccountId(), "Session destroyed; Session ID is now 0"); + cleverTapMetaData.clearSource(); + cleverTapMetaData.clearMedium(); + cleverTapMetaData.clearCampaign(); + cleverTapMetaData.clearWzrkParams(); + } + + public long getAppLastSeen() { + return appLastSeen; + } + + public void setAppLastSeen(final long appLastSeen) { + this.appLastSeen = appLastSeen; + } + + public int getLastVisitTime() { + return lastVisitTime; + } + + @Override + public void lazyCreateSession(Context context) { + if (!cleverTapMetaData.inCurrentSession()) { + cleverTapMetaData.setFirstRequestInSession(true); + if (validator != null) { + validator.setDiscardedEvents(null); + } + createSession(context); + } + } + + //Session + void setLastVisitTime() { + EventDetail ed = localDataStore.getEventDetail(Constants.APP_LAUNCHED_EVENT); + if (ed == null) { + lastVisitTime = -1; + } else { + lastVisitTime = ed.getLastTime(); + } + } + + private void createSession(final Context context) { + int sessionId = (int) (System.currentTimeMillis() / 1000); + cleverTapMetaData.setCurrentSessionId(sessionId); + + config.getLogger().verbose(config.getAccountId(), + "Session created with ID: " + cleverTapMetaData.getCurrentSessionId()); + + SharedPreferences prefs = StorageHelper.getPreferences(context); + + final int lastSessionID = StorageHelper.getIntFromPrefs(context, config, Constants.SESSION_ID_LAST, 0); + final int lastSessionTime = StorageHelper.getIntFromPrefs(context, config, Constants.LAST_SESSION_EPOCH, 0); + if (lastSessionTime > 0) { + cleverTapMetaData.setLastSessionLength(lastSessionTime - lastSessionID); + } + + config.getLogger().verbose(config.getAccountId(), + "Last session length: " + cleverTapMetaData.getLastSessionLength() + " seconds"); + + if (lastSessionID == 0) { + cleverTapMetaData.setFirstSession(true); + } + + final SharedPreferences.Editor editor = prefs.edit() + .putInt(StorageHelper.storageKeyWithSuffix(config, Constants.SESSION_ID_LAST), + cleverTapMetaData.getCurrentSessionId()); + StorageHelper.persist(editor); + } + +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/StorageHelper.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/StorageHelper.java index 2d96a46b1..820b074e4 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/StorageHelper.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/StorageHelper.java @@ -108,12 +108,13 @@ static boolean getBooleanFromPrefs(Context context, CleverTapInstanceConfig conf } } - static int getInt(Context context, String key, int defaultValue) { + public static int getInt(Context context, String key, int defaultValue) { return getPreferences(context).getInt(key, defaultValue); } @SuppressWarnings("SameParameterValue") - static int getIntFromPrefs(Context context, CleverTapInstanceConfig config, String rawKey, int defaultValue) { + public static int getIntFromPrefs(Context context, CleverTapInstanceConfig config, String rawKey, + int defaultValue) { if (config.isDefaultInstance()) { int dummy = -1000; int _new = getInt(context, storageKeyWithSuffix(config, rawKey), dummy); @@ -132,7 +133,8 @@ static long getLong(Context context, String nameSpace, String key, long defaultV } @SuppressWarnings("SameParameterValue") - static long getLongFromPrefs(Context context, CleverTapInstanceConfig config, String rawKey, int defaultValue, + public static long getLongFromPrefs(Context context, CleverTapInstanceConfig config, String rawKey, + int defaultValue, String nameSpace) { if (config.isDefaultInstance()) { long dummy = -1000; @@ -153,7 +155,7 @@ static void putBoolean(Context context, String key, boolean value) { persist(editor); } - static void putInt(Context context, String key, int value) { + public static void putInt(Context context, String key, int value) { SharedPreferences prefs = getPreferences(context); SharedPreferences.Editor editor = prefs.edit().putInt(key, value); persist(editor); diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/TaskManager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/TaskManager.java deleted file mode 100644 index b5c8098c3..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/TaskManager.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.clevertap.android.sdk; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -/** - * Singleton class to do heavy loaded task in the background and get the result on the main thread. - * Suitable for Android general purpose use cases. - */ -public class TaskManager { - - /** - * Interface for the callbacks - */ - public interface TaskListener { - - /** - * does task in the background thread - */ - Result doInBackground(Params params); - - /** - * Gives callback on the main thread - */ - void onPostExecute(Result result); - } - - private static TaskManager sInstance; - - private final ExecutorService service; - - public static synchronized TaskManager getInstance() { - if (sInstance == null) { - sInstance = new TaskManager(); - } - return sInstance; - } - - private TaskManager() { - this.service = Executors.newFixedThreadPool(10); - } - - /** - * Execute task in the background with a callback - * - * @param listener - to get the callback - * @param - no parameter - * @param - result returned by the background task - */ - public void execute(final TaskListener listener) { - execute(null, listener); - } - - /** - * Execute the task with parameters with a callback - * - * @param params params to be passed on the background execution - * @param listener - to get the callback - * @param - params to be passed on the background execution - * @param - result returned by the background task - */ - public void execute(final Params params, final TaskListener listener) { - - service.execute(new Runnable() { - @Override - public void run() { - if (listener != null) { - final Result result = listener.doInBackground(params); - - // post the result callback on the main thread - Utils.runOnUiThread(new Runnable() { - @Override - public void run() { - listener.onPostExecute(result); - } - }); - } - } - }); - } -} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/Utils.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/Utils.java index c00d43182..91e83e4f0 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/Utils.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/Utils.java @@ -40,9 +40,10 @@ import org.json.JSONException; import org.json.JSONObject; -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public final class Utils { + public static boolean haveVideoPlayerSupport; + public static boolean containsIgnoreCase(Collection collection, String key) { if (collection == null || key == null) { return false; @@ -55,6 +56,41 @@ public static boolean containsIgnoreCase(Collection collection, String k return false; } + public static HashMap convertBundleObjectToHashMap(Bundle b) { + final HashMap map = new HashMap<>(); + for (String s : b.keySet()) { + final Object o = b.get(s); + + if (o instanceof Bundle) { + map.putAll(convertBundleObjectToHashMap((Bundle) o)); + } else { + map.put(s, b.get(s)); + } + } + return map; + } + + public static HashMap convertJSONObjectToHashMap(JSONObject b) { + final HashMap map = new HashMap<>(); + final Iterator keys = b.keys(); + + while (keys.hasNext()) { + try { + final String s = keys.next(); + final Object o = b.get(s); + if (o instanceof JSONObject) { + map.putAll(convertJSONObjectToHashMap((JSONObject) o)); + } else { + map.put(s, b.get(s)); + } + } catch (Throwable ignored) { + // Ignore + } + } + + return map; + } + public static String convertToTitleCase(String text) { if (text == null || text.isEmpty()) { return text; @@ -78,6 +114,88 @@ public static String convertToTitleCase(String text) { return converted.toString(); } + public static Bitmap getBitmapFromURL(String srcUrl) { + // Safe bet, won't have more than three /s + srcUrl = srcUrl.replace("///", "/"); + srcUrl = srcUrl.replace("//", "/"); + srcUrl = srcUrl.replace("http:/", "http://"); + srcUrl = srcUrl.replace("https:/", "https://"); + HttpURLConnection connection = null; + try { + URL url = new URL(srcUrl); + connection = (HttpURLConnection) url.openConnection(); + connection.setDoInput(true); + connection.connect(); + InputStream input = connection.getInputStream(); + return BitmapFactory.decodeStream(input); + } catch (IOException e) { + + Logger.v("Couldn't download the notification icon. URL was: " + srcUrl); + return null; + } finally { + try { + if (connection != null) { + connection.disconnect(); + } + } catch (Throwable t) { + Logger.v("Couldn't close connection!", t); + } + } + } + + public static byte[] getByteArrayFromImageURL(String srcUrl) { + srcUrl = srcUrl.replace("///", "/"); + srcUrl = srcUrl.replace("//", "/"); + srcUrl = srcUrl.replace("http:/", "http://"); + srcUrl = srcUrl.replace("https:/", "https://"); + HttpsURLConnection connection = null; + try { + URL url = new URL(srcUrl); + connection = (HttpsURLConnection) url.openConnection(); + InputStream is = connection.getInputStream(); + byte[] buffer = new byte[8192]; + int bytesRead; + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + while ((bytesRead = is.read(buffer)) != -1) { + baos.write(buffer, 0, bytesRead); + } + return baos.toByteArray(); + } catch (IOException e) { + Logger.v("Error processing image bytes from url: " + srcUrl); + return null; + } finally { + try { + if (connection != null) { + connection.disconnect(); + } + } catch (Throwable t) { + Logger.v("Couldn't close connection!", t); + } + } + } + + public static Bitmap getNotificationBitmap(String icoPath, boolean fallbackToAppIcon, final Context context) + throws NullPointerException { + // If the icon path is not specified + if (icoPath == null || icoPath.equals("")) { + return fallbackToAppIcon ? getAppIcon(context) : null; + } + // Simply stream the bitmap + if (!icoPath.startsWith("http")) { + icoPath = Constants.ICON_BASE_URL + "/" + icoPath; + } + Bitmap ic = getBitmapFromURL(icoPath); + return (ic != null) ? ic : ((fallbackToAppIcon) ? getAppIcon(context) : null); + } + + public static int getThumbnailImage(Context context, String image) { + if (context != null) { + return context.getResources().getIdentifier(image, "drawable", context.getPackageName()); + } else { + return -1; + } + } + public static boolean isActivityDead(Activity activity) { if (activity == null) { return true; @@ -152,20 +270,7 @@ public static Bundle stringToBundle(String content) throws JSONException { return bundle; } - static HashMap convertBundleObjectToHashMap(Bundle b) { - final HashMap map = new HashMap<>(); - for (String s : b.keySet()) { - final Object o = b.get(s); - if (o instanceof Bundle) { - map.putAll(convertBundleObjectToHashMap((Bundle) o)); - } else { - map.put(s, b.get(s)); - } - } - return map; - } - - static ArrayList convertJSONArrayToArrayList(JSONArray array) { + public static ArrayList convertJSONArrayToArrayList(JSONArray array) { ArrayList listdata = new ArrayList<>(); if (array != null) { for (int i = 0; i < array.length(); i++) { @@ -179,27 +284,6 @@ static ArrayList convertJSONArrayToArrayList(JSONArray array) { return listdata; } - static HashMap convertJSONObjectToHashMap(JSONObject b) { - final HashMap map = new HashMap<>(); - final Iterator keys = b.keys(); - - while (keys.hasNext()) { - try { - final String s = keys.next(); - final Object o = b.get(s); - if (o instanceof JSONObject) { - map.putAll(convertJSONObjectToHashMap((JSONObject) o)); - } else { - map.put(s, b.get(s)); - } - } catch (Throwable ignored) { - // Ignore - } - } - - return map; - } - static Bitmap drawableToBitmap(Drawable drawable) throws NullPointerException { if (drawable instanceof BitmapDrawable) { @@ -215,68 +299,8 @@ static Bitmap drawableToBitmap(Drawable drawable) return bitmap; } - static Bitmap getBitmapFromURL(String srcUrl) { - // Safe bet, won't have more than three /s - srcUrl = srcUrl.replace("///", "/"); - srcUrl = srcUrl.replace("//", "/"); - srcUrl = srcUrl.replace("http:/", "http://"); - srcUrl = srcUrl.replace("https:/", "https://"); - HttpURLConnection connection = null; - try { - URL url = new URL(srcUrl); - connection = (HttpURLConnection) url.openConnection(); - connection.setDoInput(true); - connection.connect(); - InputStream input = connection.getInputStream(); - return BitmapFactory.decodeStream(input); - } catch (IOException e) { - - Logger.v("Couldn't download the notification icon. URL was: " + srcUrl); - return null; - } finally { - try { - if (connection != null) { - connection.disconnect(); - } - } catch (Throwable t) { - Logger.v("Couldn't close connection!", t); - } - } - } - - static byte[] getByteArrayFromImageURL(String srcUrl) { - srcUrl = srcUrl.replace("///", "/"); - srcUrl = srcUrl.replace("//", "/"); - srcUrl = srcUrl.replace("http:/", "http://"); - srcUrl = srcUrl.replace("https:/", "https://"); - HttpsURLConnection connection = null; - try { - URL url = new URL(srcUrl); - connection = (HttpsURLConnection) url.openConnection(); - InputStream is = connection.getInputStream(); - byte[] buffer = new byte[8192]; - int bytesRead; - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - while ((bytesRead = is.read(buffer)) != -1) { - baos.write(buffer, 0, bytesRead); - } - return baos.toByteArray(); - } catch (IOException e) { - Logger.v("Error processing image bytes from url: " + srcUrl); - return null; - } finally { - try { - if (connection != null) { - connection.disconnect(); - } - } catch (Throwable t) { - Logger.v("Couldn't close connection!", t); - } - } - } - @SuppressLint("MissingPermission") - static String getCurrentNetworkType(final Context context) { + public static String getCurrentNetworkType(final Context context) { try { // First attempt to check for WiFi connectivity ConnectivityManager connManager = (ConnectivityManager) context @@ -299,7 +323,7 @@ static String getCurrentNetworkType(final Context context) { } @SuppressLint("MissingPermission") - static String getDeviceNetworkType(final Context context) { + public static String getDeviceNetworkType(final Context context) { // Fall back to network type TelephonyManager teleMan = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); if (teleMan == null) { @@ -307,7 +331,7 @@ static String getDeviceNetworkType(final Context context) { } int networkType = TelephonyManager.NETWORK_TYPE_UNKNOWN; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (hasPermission(context, Manifest.permission.READ_PHONE_STATE)) { try { networkType = teleMan.getDataNetworkType(); @@ -318,11 +342,7 @@ static String getDeviceNetworkType(final Context context) { Logger.d("READ_PHONE_STATE permission not asked by the app or not granted by the user"); } } else { - try { - networkType = teleMan.getNetworkType(); - } catch (SecurityException se) { - Logger.d("Security Exception caught while fetch network type" + se.getMessage()); - } + networkType = teleMan.getNetworkType(); } switch (networkType) { @@ -351,41 +371,19 @@ static String getDeviceNetworkType(final Context context) { } } - static long getMemoryConsumption() { + public static long getMemoryConsumption() { long free = Runtime.getRuntime().freeMemory(); long total = Runtime.getRuntime().totalMemory(); return total - free; } - static Bitmap getNotificationBitmap(String icoPath, boolean fallbackToAppIcon, final Context context) - throws NullPointerException { - // If the icon path is not specified - if (icoPath == null || icoPath.equals("")) { - return fallbackToAppIcon ? getAppIcon(context) : null; - } - // Simply stream the bitmap - if (!icoPath.startsWith("http")) { - icoPath = Constants.ICON_BASE_URL + "/" + icoPath; - } - Bitmap ic = getBitmapFromURL(icoPath); - return (ic != null) ? ic : ((fallbackToAppIcon) ? getAppIcon(context) : null); - } - - static int getThumbnailImage(Context context, String image) { - if (context != null) { - return context.getResources().getIdentifier(image, "drawable", context.getPackageName()); - } else { - return -1; - } - } - /** * Checks whether a particular permission is available or not. * * @param context The Android {@link Context} * @param permission The fully qualified Android permission name */ - static boolean hasPermission(final Context context, String permission) { + public static boolean hasPermission(final Context context, String permission) { try { return PackageManager.PERMISSION_GRANTED == ContextCompat.checkSelfPermission(context, permission); } catch (Throwable t) { @@ -393,7 +391,7 @@ static boolean hasPermission(final Context context, String permission) { } } - static boolean validateCTID(String cleverTapID) { + public static boolean validateCTID(String cleverTapID) { if (cleverTapID == null) { Logger.i( "CLEVERTAP_USE_CUSTOM_ID has been set as 1 in AndroidManifest.xml but custom CleverTap ID passed is NULL."); @@ -415,6 +413,37 @@ static boolean validateCTID(String cleverTapID) { return true; } + public static int getNow() { + return (int) (System.currentTimeMillis() / 1000); + } + + /** + * Method to check whether app has ExoPlayer dependencies + * + * @return boolean - true/false depending on app's availability of ExoPlayer dependencies + */ + private static boolean checkForExoPlayer() { + boolean exoPlayerPresent = false; + Class className = null; + try { + className = Class.forName("com.google.android.exoplayer2.SimpleExoPlayer"); + className = Class.forName("com.google.android.exoplayer2.source.hls.HlsMediaSource"); + className = Class.forName("com.google.android.exoplayer2.ui.PlayerView"); + Logger.d("ExoPlayer is present"); + exoPlayerPresent = true; + } catch (Throwable t) { + Logger.d("ExoPlayer library files are missing!!!"); + Logger.d( + "Please add ExoPlayer dependencies to render InApp or Inbox messages playing video. For more information checkout CleverTap documentation."); + if (className != null) { + Logger.d("ExoPlayer classes not found " + className.getName()); + } else { + Logger.d("ExoPlayer classes not found"); + } + } + return exoPlayerPresent; + } + private static Bitmap getAppIcon(final Context context) throws NullPointerException { // Try to get the app logo first try { @@ -430,6 +459,10 @@ private static Bitmap getAppIcon(final Context context) throws NullPointerExcept } } + static { + haveVideoPlayerSupport = checkForExoPlayer(); + } + @RestrictTo(Scope.LIBRARY) public static String getFcmTokenUsingManifestMetaEntry(Context context, CleverTapInstanceConfig config) { String token = null; diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/CTABTestController.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/CTABTestController.java deleted file mode 100644 index e9db22a43..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/CTABTestController.java +++ /dev/null @@ -1,1253 +0,0 @@ -package com.clevertap.android.sdk.ab_testing; - -import android.app.Activity; -import android.app.Application; -import android.content.Context; -import android.content.SharedPreferences; -import android.hardware.Sensor; -import android.hardware.SensorManager; -import android.os.Build; -import android.os.Build.VERSION_CODES; -import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; -import android.os.Message; -import android.os.Process; -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; -import com.clevertap.android.sdk.CleverTapAPI; -import com.clevertap.android.sdk.CleverTapInstanceConfig; -import com.clevertap.android.sdk.Logger; -import com.clevertap.android.sdk.ab_testing.gesture.ConnectionGesture; -import com.clevertap.android.sdk.ab_testing.models.CTABVariant; -import com.clevertap.android.sdk.ab_testing.uieditor.UIEditor; -import com.clevertap.android.sdk.java_websocket.client.WebSocketClient; -import com.clevertap.android.sdk.java_websocket.drafts.Draft_6455; -import com.clevertap.android.sdk.java_websocket.enums.Opcode; -import com.clevertap.android.sdk.java_websocket.exceptions.NotSendableException; -import com.clevertap.android.sdk.java_websocket.exceptions.WebsocketNotConnectedException; -import com.clevertap.android.sdk.java_websocket.handshake.ServerHandshake; -import java.io.BufferedOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.lang.ref.WeakReference; -import java.net.URI; -import java.nio.ByteBuffer; -import java.security.GeneralSecurityException; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocketFactory; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -@RequiresApi(api = VERSION_CODES.KITKAT) -public class CTABTestController { - - @SuppressWarnings("unused") - public static class LayoutErrorMessage { - - private final String errorName; - - private final String errorType; - - public LayoutErrorMessage(String type, String name) { - errorType = type; - errorName = name; - } - - public String getName() { - return errorName; - } - - public String getType() { - return errorType; - } - } - - private class ExecutionThreadHandler extends Handler { - - private class DashboardClient extends WebSocketClient { - - private URI dashboardURI; - - private DashboardClient(URI uri, int connectTimeout) { - super(uri, new Draft_6455(), null, connectTimeout); - this.dashboardURI = uri; - setSocketFactory(SSLSocketFactory); - } - - @Override - public void onClose(int code, String reason, boolean remote) { - getConfigLogger().verbose(getAccountId(), - "WebSocket closed. Code: " + code + ", reason: " + reason + "\nURI: " + dashboardURI); - handleOnClose(); - } - - @Override - public void onError(Exception ex) { - if (ex != null && ex.getMessage() != null) { - getConfigLogger().verbose(getAccountId(), "Websocket Error: " + ex.getMessage()); - } else { - getConfigLogger().verbose(getAccountId(), "Unknown websocket error"); - } - } - - @Override - public void onMessage(String message) { - try { - final JSONObject messageJson = new JSONObject(message); - if (messageJson.has("data")) { - if (messageJson.getJSONObject("data").keys().hasNext()) { - getConfigLogger().verbose(getAccountId(), "Received message from dashboard:\n" + message); - } - } - if (!connectionIsValid()) { - getConfigLogger().verbose(getAccountId(), - "Dashboard connection is stale, dropping message: " + message); - return; - } - handleDashboardMessage(messageJson); - } catch (final JSONException e) { - getConfigLogger().verbose(getAccountId(), "Bad JSON message received:" + message, e); - } - } - - @Override - public void onOpen(ServerHandshake handshakedata) { - getConfigLogger().verbose(getAccountId(), "Websocket connected"); - handleOnOpen(); - } - } - - private class WebSocketOutputStream extends OutputStream { - - @Override - public void close() { - try { - wsClient.sendFragmentedFrame(Opcode.TEXT, EMPTY_BYTE_BUFFER, true); - } catch (final WebsocketNotConnectedException e) { - getConfigLogger().debug(getAccountId(), "Web socket not connected", e); - } catch (final NotSendableException e) { - getConfigLogger().debug(getAccountId(), "Unable to send data to web socket", e); - } - } - - @Override - public void write(@NonNull byte[] b) { - write(b, 0, b.length); - } - - @Override - public void write(@NonNull byte[] b, int off, int len) { - final ByteBuffer message = ByteBuffer.wrap(b, off, len); - try { - wsClient.sendFragmentedFrame(Opcode.TEXT, message, false); - } catch (final WebsocketNotConnectedException e) { - getConfigLogger().debug(getAccountId(), "Web socket not connected", e); - } catch (final NotSendableException e) { - getConfigLogger().debug(getAccountId(), "Unable to send data to web socket", e); - } - } - - @Override - public void write(int b) { - final byte[] oneByte = new byte[1]; - oneByte[0] = (byte) b; - write(oneByte, 0, 1); - } - } - - static final int MESSAGE_UNKNOWN = -1; - - static final int MESSAGE_INITIALIZE_EXPERIMENTS = 0; - - static final int MESSAGE_CONNECT_TO_EDITOR = 1; - - static final int MESSAGE_SEND_SNAPSHOT = 2; - - static final int MESSAGE_HANDLE_EDITOR_CHANGES_RECEIVED = 3; - - static final int MESSAGE_SEND_DEVICE_INFO = 4; - - static final int MESSAGE_HANDLE_DISCONNECT = 5; - - static final int MESSAGE_EXPERIMENTS_RECEIVED = 6; - - static final int MESSAGE_HANDLE_EDITOR_CHANGES_CLEARED = 7; - - static final int MESSAGE_HANDLE_EDITOR_VARS_RECEIVED = 8; - - static final int MESSAGE_SEND_LAYOUT_ERROR = 9; - - static final int MESSAGE_PERSIST_EXPERIMENTS = 10; - - static final int MESSAGE_SEND_VARS = 11; - - static final int MESSAGE_TEST_VARS = 12; - - static final int MESSAGE_MATCHED = 13; - - private static final String EXPERIMENTS_KEY = "experiments"; - - private static final int CONNECT_TIMEOUT = 5000; - - private CleverTapInstanceConfig config; - - private Context context; - - private CTABVariant editorSessionVariant; - - private HashSet editorSessionVariantSet; - - private final Lock lock = new ReentrantLock(); - - private Set variants; - - private DashboardClient wsClient; - - @SuppressWarnings("unused") - ExecutionThreadHandler(Context context, CleverTapInstanceConfig config, Looper looper) { - super(looper); - this.config = config; - this.context = context; - this.variants = new HashSet<>(); - lock.lock(); - } - - @Override - public void handleMessage(Message msg) { - lock.lock(); - try { - final int what = msg.what; - Object data = msg.obj; - switch (what) { - case MESSAGE_INITIALIZE_EXPERIMENTS: - loadStoredExperiments(); - break; - case MESSAGE_CONNECT_TO_EDITOR: - createConnection(); - break; - case MESSAGE_MATCHED: - handleMatched(); - break; - case MESSAGE_HANDLE_DISCONNECT: - handleDashboardDisconnect(); - break; - case MESSAGE_SEND_DEVICE_INFO: - sendDeviceInfo(); - break; - case MESSAGE_SEND_SNAPSHOT: - sendSnapshot((JSONObject) data); - break; - case MESSAGE_SEND_LAYOUT_ERROR: - sendLayoutError((LayoutErrorMessage) msg.obj); - break; - case MESSAGE_EXPERIMENTS_RECEIVED: - applyExperiments((JSONArray) data, true); - break; - case MESSAGE_HANDLE_EDITOR_CHANGES_RECEIVED: - handleEditorChangesReceived((JSONObject) data); - break; - case MESSAGE_HANDLE_EDITOR_CHANGES_CLEARED: - handleEditorChangesCleared((JSONObject) data); - break; - case MESSAGE_HANDLE_EDITOR_VARS_RECEIVED: - case MESSAGE_TEST_VARS: - handleEditorVarsReceived((JSONObject) data); - break; - case MESSAGE_PERSIST_EXPERIMENTS: - persistExperiments((JSONArray) data); - break; - case MESSAGE_SEND_VARS: - sendVars(); - break; - } - } finally { - lock.unlock(); - } - } - - public void start() { - lock.unlock(); - } - - void handleMatched() { - varCache.reset(); - stopVariants(); - } - - boolean isConnected() { - return wsClient != null && wsClient.isOpen(); - } - - private void applyExperiments(JSONArray experiments, boolean areNew) { - loadVariants(experiments); - applyVariants(); - if (areNew) { - persistExperiments(experiments); - } - notifyExperimentsUpdated(); - } - - private void applyVariants() { - for (CTABVariant variant : variants) { - applyVars(variant.getVars()); - } - uiEditor.applyVariants(variants, false); - } - - private void applyVars(JSONArray vars) { - try { - for (int i = 0; i < vars.length(); i++) { - JSONObject var = vars.getJSONObject(i); - _registerVar(var.getString("name"), CTVar.CTVarType.fromString(var.getString("type")), - var.get("value")); - } - } catch (Throwable t) { - getConfigLogger().debug(getAccountId(), "Unable to apply Vars - " + t); - } - } - - private void closeConnection() { - if (connectionIsValid()) { - try { - getConfigLogger().verbose(getAccountId(), "disconnecting from dashboard"); - wsClient.closeBlocking(); - } catch (final Exception e) { - getConfigLogger().verbose(getAccountId(), "Unable to close dashboard connection", e); - } - } - } - - private boolean connectionIsValid() { - return wsClient != null && !wsClient.isClosed() && !wsClient.isClosing() && !wsClient.isFlushAndClose(); - } - - private void createConnection() { - getConfigLogger().verbose(getAccountId(), "connecting to dashboard"); - if (isConnected() && connectionIsValid()) { - getConfigLogger().verbose(getAccountId(), "There is already a valid dashboard connection."); - return; - } - - if (SSLSocketFactory == null) { - getConfigLogger().verbose(getAccountId(), - "SSL is not available on this device, dashboard connection is not available."); - return; - } - - final String protocol = "wss"; - String region = config.getAccountRegion() != null ? config.getAccountRegion() : DEFAULT_REGION; - region = config.isBeta() ? region + "-dashboard-beta" : region; - final String domain = region + "." + DASHBOARD_URL; - final String url = protocol + "://" + domain + "/" + getAccountId() + "/" + "websocket/screenab/sdk?tk=" - + config.getAccountToken(); - getConfigLogger().verbose(getAccountId(), "Websocket URL - " + url); - try { - wsClient = new DashboardClient(new URI(url), CONNECT_TIMEOUT); - wsClient.connectBlocking(); - } catch (final Exception e) { - getConfigLogger().verbose(getAccountId(), "Unable to connect to dashboard", e); - } - } - - private String getAccountId() { - return config.getAccountId(); - } - - private BufferedOutputStream getBufferedOutputStream() { - return new BufferedOutputStream(new WebSocketOutputStream()); - } - - private Logger getConfigLogger() { - return config.getLogger(); - } - - private JSONObject getDeviceInfo() { - if (cachedDeviceInfo == null) { - JSONObject data = new JSONObject(); - try { - Map deviceInfo = CleverTapAPI.instanceWithConfig(context, config).getDeviceInfo(); - for (final Map.Entry entry : deviceInfo.entrySet()) { - data.put(entry.getKey(), entry.getValue()); - } - } catch (Throwable t) { - // no-op - } - cachedDeviceInfo = data; - } - return cachedDeviceInfo; - } - - private CTABVariant getEditorSessionVariant() { - if (editorSessionVariant == null) { - try { - JSONObject variant = new JSONObject(); - variant.put("id", "0"); - variant.put("experiment_id", "0"); - editorSessionVariant = CTABVariant.initWithJSON(variant); - editorSessionVariantSet = new HashSet<>(); - editorSessionVariantSet.add(editorSessionVariant); - } catch (Throwable t) { - getConfigLogger().verbose(getAccountId(), "Error creating editor session variant", t); - } - } - return editorSessionVariant; - } - - private SharedPreferences getSharedPreferences() { - return context.getSharedPreferences(getSharedPrefsName(), Context.MODE_PRIVATE); - } - - private String getSharedPrefsName() { - return "clevertap.abtesting." + getAccountId() + "." + guid; - } - - private void handleDashboardDisconnect() { - stopVariants(); - closeConnection(); - } - - private void handleEditorChangesCleared(JSONObject request) { - try { - JSONArray changes = request.optJSONArray("actions"); - if (changes == null || changes.length() <= 0) { - getEditorSessionVariant().clearActions(); - } else { - getEditorSessionVariant().removeActionsByName(changes); - } - uiEditor.applyVariants(editorSessionVariantSet, true); - } catch (Throwable t) { - getConfigLogger().debug(getAccountId(), "Unable to clear dashboard changes - " + t); - } - } - - private void handleEditorChangesReceived(JSONObject request) { - try { - JSONArray changes = request.optJSONArray("actions"); - if (changes == null || changes.length() <= 0) { - getConfigLogger().debug(getAccountId(), "No changes received from dashboard"); - return; - } else { - getEditorSessionVariant().addActions(changes); - } - uiEditor.applyVariants(editorSessionVariantSet, true); - } catch (Throwable t) { - getConfigLogger().debug(getAccountId(), "Unable to handle dashboard changes received - " + t); - } - } - - private void handleEditorVarsReceived(JSONObject request) { - try { - JSONArray vars = request.optJSONArray("vars"); - if (vars == null || vars.length() <= 0) { - getConfigLogger().debug(getAccountId(), "No Vars received from dashboard"); - return; - } - applyVars(vars); - notifyExperimentsUpdated(); - } catch (Throwable t) { - getConfigLogger().debug(getAccountId(), "Unable to handle dashboard Vars received - " + t); - } - } - - private void handleOnClose() { - getConfigLogger().verbose(getAccountId(), "handle websocket on close"); - stopVariants(); - getEditorSessionVariant().clearActions(); - varCache.reset(); - applyVariants(); - } - - private void handleOnOpen() { - sendHandshake(); - } - - private void loadStoredExperiments() { - final SharedPreferences preferences = getSharedPreferences(); - final String storedExperiments = preferences.getString(EXPERIMENTS_KEY, null); - if (storedExperiments != null) { - try { - getConfigLogger().debug(getAccountId(), - "Loading Stored Experiments: " + storedExperiments + " for key: " + getSharedPrefsName()); - final JSONArray _experiments = new JSONArray(storedExperiments); - applyExperiments(_experiments, false); - } catch (JSONException e) { - final SharedPreferences.Editor editor = preferences.edit(); - editor.remove(EXPERIMENTS_KEY); - editor.apply(); - } - } else { - getConfigLogger().debug(getAccountId(), "No Stored Experiments for key: " + getSharedPrefsName()); - } - } - - private void loadVariants(JSONArray experiments) { - if (experiments == null) { - return; - } - // note: experiments here will be all the currently running experiments for the user - try { - Set toRemove = new HashSet<>(this.variants); - Set allVariants = new HashSet<>(this.variants); - - final int experimentsLength = experiments.length(); - for (int i = 0; i < experimentsLength; i++) { - final JSONObject nextVariant = experiments.getJSONObject(i); - final CTABVariant variant = CTABVariant.initWithJSON(nextVariant); - if (variant != null) { - boolean added = allVariants.add(variant); - if (added) { - toRemove.remove(variant); - } - } - } - if (!allVariants.containsAll(toRemove)) { - if (toRemove.size() > 0) { - for (CTABVariant v : toRemove) { - v.cleanup(); - allVariants.remove(v); - } - } - } - - //This will revert changes at SDK level when all experiments are stopped/revert without needing - //another App Launched event - if (experiments.length() == 0) { - allVariants.clear(); - } - - this.variants = allVariants; - } catch (JSONException e) { - getConfigLogger().verbose(getAccountId(), "Error loading variants, clearing all running variants", e); - this.variants.clear(); - } - } - - private void notifyExperimentsUpdated() { - CTABTestListener listener = getListener(); - if (listener != null) { - listener.ABExperimentsUpdated(); - } - } - - private void persistExperiments(JSONArray experiments) { - final SharedPreferences preferences = getSharedPreferences(); - final SharedPreferences.Editor editor = preferences.edit(); - editor.putString(EXPERIMENTS_KEY, experiments.toString()); - editor.apply(); - } - - private void sendDeviceInfo() { - try { - JSONObject payload = new JSONObject(); - payload.put(TYPE_KEY, MESSAGE_TYPE_DEVICE_INFO_RESPONSE); - payload.put(DATA_KEY, getDeviceInfo()); - sendMessage(payload.toString()); - } catch (Throwable t) { - getConfigLogger().debug(getAccountId(), "Unable to create deviceInfo message", t); - } - } - - @SuppressWarnings("SameParameterValue") - private void sendError(String errorMessage) { - try { - JSONObject data = new JSONObject(); - data.put("error", errorMessage); - JSONObject payload = new JSONObject(); - payload.put(TYPE_KEY, MESSAGE_TYPE_GENERIC_ERROR); - payload.put(DATA_KEY, data); - sendMessage(payload.toString()); - } catch (Throwable t) { - getConfigLogger().debug(getAccountId(), "Unable to create error message", t); - } - } - - private void sendHandshake() { - try { - JSONObject deviceInfo = getDeviceInfo(); - JSONObject data = new JSONObject(); - data.put("id", guid); - data.put("os", deviceInfo.getString("osName")); - data.put("name", deviceInfo.getString("manufacturer") + " " + deviceInfo.getString("model")); - if (deviceInfo.has("library")) { - data.put("library", deviceInfo.getString("library")); - } - JSONObject payload = new JSONObject(); - payload.put(TYPE_KEY, MESSAGE_TYPE_HANDSHAKE); - payload.put(DATA_KEY, data); - sendMessage(payload.toString()); - } catch (Throwable t) { - getConfigLogger().debug(getAccountId(), "Unable to create handshake message", t); - } - } - - private void sendLayoutError(LayoutErrorMessage errorMessage) { - try { - JSONObject data = new JSONObject(); - data.put("type", errorMessage.getType()); - data.put("name", errorMessage.getName()); - JSONObject payload = new JSONObject(); - payload.put(TYPE_KEY, MESSAGE_TYPE_LAYOUT_ERROR); - payload.put(DATA_KEY, data); - sendMessage(payload.toString()); - } catch (Throwable t) { - getConfigLogger().debug(getAccountId(), "Unable to create error message", t); - } - } - - private void sendMessage(String message) { - if (!connectionIsValid()) { - getConfigLogger().debug(getAccountId(), - "Unable to send websocket message: " + message + " connection is invalid"); - return; - } - final OutputStreamWriter writer = new OutputStreamWriter(getBufferedOutputStream()); - getConfigLogger().verbose("Sending message to dashboard - " + message); - try { - writer.write(message); - } catch (final IOException e) { - getConfigLogger().verbose(getAccountId(), "Can't message to editor", e); - } finally { - try { - writer.close(); - } catch (final IOException e) { - getConfigLogger().verbose(getAccountId(), "Could not close output writer to editor", e); - } - } - } - - private void sendSnapshot(JSONObject data) { - final long startSnapshot = System.currentTimeMillis(); - boolean isValidSnapshot = uiEditor.loadSnapshotConfig(data); - if (!isValidSnapshot) { - String err = "Missing or invalid snapshot configuration."; - sendError(err); - getConfigLogger().debug(getAccountId(), err); - return; - } - - final OutputStream out = getBufferedOutputStream(); - final OutputStreamWriter writer = new OutputStreamWriter(out); - - try { - writer.write("{"); - writer.write("\"" + TYPE_KEY + "\": \"" + MESSAGE_TYPE_SNAPSHOT_RESPONSE + "\","); - writer.write("\"" + DATA_KEY + "\": {"); - { - writer.write("\"activities\":"); - writer.flush(); - uiEditor.writeSnapshot(out); - } - - final long snapshotTime = System.currentTimeMillis() - startSnapshot; - writer.write(",\"snapshot_time_millis\": "); - writer.write(Long.toString(snapshotTime)); - - writer.write("}"); // } payload - writer.write("}"); // } whole message - } catch (final IOException e) { - getConfigLogger().verbose(getAccountId(), "Failure sending snapshot", e); - } finally { - try { - writer.close(); - } catch (final IOException e) { - getConfigLogger().verbose(getAccountId(), "Failure closing json writer", e); - } - } - } - - private void sendVars() { - try { - JSONObject data = new JSONObject(); - data.put("vars", varCache.serializeVars()); - JSONObject payload = new JSONObject(); - payload.put(TYPE_KEY, MESSAGE_TYPE_VARS_RESPONSE); - payload.put(DATA_KEY, data); - sendMessage(payload.toString()); - } catch (Throwable t) { - getConfigLogger().debug(getAccountId(), "Unable to create vars message", t); - } - } - - private void stopVariants() { - uiEditor.stopVariants(); - } - } - - private class EmulatorConnectRunnable implements Runnable { - - private volatile boolean stopped; - - EmulatorConnectRunnable() { - stopped = true; - } - - @Override - public void run() { - if (!stopped) { - final Message message = executionThreadHandler - .obtainMessage(ExecutionThreadHandler.MESSAGE_CONNECT_TO_EDITOR); - executionThreadHandler.sendMessage(message); - } - executionThreadHandler.postDelayed(this, EMULATOR_CONNECT_ATTEMPT_INTERVAL_MILLIS); - } - - void start() { - stopped = false; - executionThreadHandler.post(this); - } - - void stop() { - stopped = true; - executionThreadHandler.removeCallbacks(this); - } - } - - private class LifecycleCallbacks - implements Application.ActivityLifecycleCallbacks, ConnectionGesture.OnGestureListener { - - private EmulatorConnectRunnable emulatorConnectRunnable; - - private ConnectionGesture gesture; - - private LifecycleCallbacks() { - gesture = new ConnectionGesture(this); - emulatorConnectRunnable = new EmulatorConnectRunnable(); - } - - @Override - public void onActivityCreated(Activity activity, Bundle bundle) { - - } - - @Override - public void onActivityDestroyed(Activity activity) { - } - - @Override - public void onActivityPaused(Activity activity) { - uiEditor.removeActivity(activity); - deregisterConnectionTrigger(activity); - } - - @Override - public void onActivityResumed(Activity activity) { - registerConnectionTrigger(activity); - uiEditor.addActivity(activity); - } - - @Override - public void onActivitySaveInstanceState(Activity activity, Bundle bundle) { - } - - @Override - public void onActivityStarted(Activity activity) { - } - - @Override - public void onActivityStopped(Activity activity) { - } - - @Override - public void onGesture() { - final Message message = executionThreadHandler - .obtainMessage(ExecutionThreadHandler.MESSAGE_CONNECT_TO_EDITOR); - executionThreadHandler.sendMessage(message); - } - - private void deregisterConnectionTrigger(final Activity activity) { - if (!enableEditor) { - config.getLogger().debug(config.getAccountId(), "UIEditor is disabled"); - return; - } - if (inEmulator()) { - emulatorConnectRunnable.stop(); - } else { - final SensorManager sensorManager = (SensorManager) activity.getSystemService(Context.SENSOR_SERVICE); - if (sensorManager != null) { - sensorManager.unregisterListener(gesture); - } - } - } - - private boolean inEmulator() { - if (!Build.HARDWARE.toLowerCase().equals("goldfish") && !Build.HARDWARE.toLowerCase().equals("ranchu")) { - return false; - } - - if (!Build.BRAND.toLowerCase().startsWith("generic") && !Build.BRAND.toLowerCase().equals("android") - && !Build.BRAND.toLowerCase().equals("google")) { - return false; - } - - if (!Build.DEVICE.toLowerCase().startsWith("generic")) { - return false; - } - - if (!Build.PRODUCT.toLowerCase().contains("sdk")) { - return false; - } - - return Build.MODEL.toLowerCase(Locale.US).contains("sdk"); - } - - private void registerConnectionTrigger(final Activity activity) { - if (!enableEditor) { - config.getLogger().debug(config.getAccountId(), "UIEditor is disabled"); - return; - } - if (inEmulator()) { - emulatorConnectRunnable.start(); - } else { - try { - final SensorManager sensorManager = (SensorManager) activity - .getSystemService(Context.SENSOR_SERVICE); - // noinspection ConstantConditions - final Sensor accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); - sensorManager.registerListener(gesture, accelerometer, SensorManager.SENSOR_DELAY_NORMAL); - } catch (Throwable t) { - // no-op - config.getLogger().debug(config.getAccountId(), "Unable to register UIEditor connection gesture"); - } - } - } - } - - private static final ByteBuffer EMPTY_BYTE_BUFFER = ByteBuffer.allocate(0); - - private static final int EMULATOR_CONNECT_ATTEMPT_INTERVAL_MILLIS = 1000 * 30; - - private static final String DASHBOARD_URL = "dashboard.clevertap.com"; - - private static final String DEFAULT_REGION = "eu1"; - - private static final String MESSAGE_TYPE_HANDSHAKE = "handshake"; - - private static final String MESSAGE_TYPE_CLEAR_REQUEST = "clear_request"; - - private static final String MESSAGE_TYPE_CHANGE_REQUEST = "change_request"; - - private static final String MESSAGE_TYPE_DEVICE_INFO_REQUEST = "device_info_request"; - - private static final String MESSAGE_TYPE_DEVICE_INFO_RESPONSE = "device_info_response"; - - private static final String MESSAGE_TYPE_SNAPSHOT_REQUEST = "snapshot_request"; - - private static final String MESSAGE_TYPE_SNAPSHOT_RESPONSE = "snapshot_response"; - - private static final String MESSAGE_TYPE_VARS_REQUEST = "vars_request"; - - private static final String MESSAGE_TYPE_VARS_RESPONSE = "vars_response"; - - private static final String MESSAGE_TYPE_LAYOUT_ERROR = "layout_error"; - - private static final String MESSAGE_TYPE_GENERIC_ERROR = "error"; - - private static final String MESSAGE_TYPE_VARS_TEST = "test_vars"; - - private static final String MESSAGE_TYPE_MATCHED = "matched"; - - private static final String MESSAGE_TYPE_DISCONNECT = "disconnect"; - - private static final String DATA_KEY = "data"; - - private static final String TYPE_KEY = "type"; - - private static javax.net.ssl.SSLSocketFactory SSLSocketFactory; - - private JSONObject cachedDeviceInfo; - - private CleverTapInstanceConfig config; - - private boolean enableEditor; - - private ExecutionThreadHandler executionThreadHandler; - - private String guid; - - private WeakReference listenerWeakReference; - - private UIEditor uiEditor; - - private CTVarCache varCache; - - public CTABTestController(Context context, CleverTapInstanceConfig config, String guid, - CTABTestListener listener) { - try { - this.varCache = new CTVarCache(); - this.enableEditor = config.isUIEditorEnabled(); - this.config = config; - this.guid = guid; - this.setListener(listener); - this.uiEditor = new UIEditor(context, config); - - final HandlerThread thread = new HandlerThread(CTABTestController.class.getCanonicalName()); - thread.setPriority(Process.THREAD_PRIORITY_BACKGROUND); - thread.start(); - executionThreadHandler = new ExecutionThreadHandler(context, config, thread.getLooper()); - executionThreadHandler.start(); - - if (enableEditor) { - final Application app = (Application) context.getApplicationContext(); - app.registerActivityLifecycleCallbacks(new LifecycleCallbacks()); - } else { - config.getLogger().debug(config.getAccountId(), "UIEditor connection is disabled"); - } - applyStoredExperiments(); - } catch (Throwable t) { - config.setEnableABTesting(false); - config.setEnableUIEditor(false); - config.getLogger().debug(config.getAccountId(), t); - } - } - - @SuppressWarnings({"unused"}) - public Boolean getBooleanVariable(String name, Boolean defaultValue) { - CTVar var = this.varCache.getVar(name); - try { - if (var != null && var.booleanValue() != null) { - return var.booleanValue(); - } - } catch (Throwable t) { - config.getLogger().debug(config.getAccountId(), "Error getting variable with name: " + name, t); - return defaultValue; - } - return defaultValue; - } - - @SuppressWarnings({"unused"}) - public Double getDoubleVariable(String name, Double defaultValue) { - CTVar var = this.varCache.getVar(name); - try { - if (var != null && var.doubleValue() != null) { - return var.doubleValue(); - } - } catch (Throwable t) { - config.getLogger().debug(config.getAccountId(), "Error getting variable with name: " + name, t); - return defaultValue; - } - return defaultValue; - } - - @SuppressWarnings({"unused"}) - public Integer getIntegerVariable(String name, Integer defaultValue) { - CTVar var = this.varCache.getVar(name); - try { - if (var != null && var.integerValue() != null) { - return var.integerValue(); - } - } catch (Throwable t) { - config.getLogger().debug(config.getAccountId(), "Error getting variable with name: " + name, t); - return defaultValue; - } - return defaultValue; - } - - @SuppressWarnings({"unused"}) - public List getListOfBooleanVariable(String name, List defaultValue) { - CTVar var = this.varCache.getVar(name); - try { - if (var != null && var.listValue() != null) { - // noinspection unchecked - return (List) var.listValue(); - } - } catch (Throwable t) { - config.getLogger().debug(config.getAccountId(), "Error getting variable with name: " + name, t); - return defaultValue; - } - return defaultValue; - } - - @SuppressWarnings({"unused"}) - public List getListOfDoubleVariable(String name, List defaultValue) { - CTVar var = this.varCache.getVar(name); - try { - if (var != null && var.listValue() != null) { - // noinspection unchecked - return (List) var.listValue(); - } - } catch (Throwable t) { - config.getLogger().debug(config.getAccountId(), "Error getting variable with name: " + name, t); - return defaultValue; - } - return defaultValue; - } - - @SuppressWarnings({"unused"}) - public List getListOfIntegerVariable(String name, List defaultValue) { - CTVar var = this.varCache.getVar(name); - try { - if (var != null && var.listValue() != null) { - // noinspection unchecked - return (List) var.listValue(); - } - } catch (Throwable t) { - config.getLogger().debug(config.getAccountId(), "Error getting variable with name: " + name, t); - return defaultValue; - } - return defaultValue; - } - - @SuppressWarnings({"unused"}) - public List getListOfStringVariable(String name, List defaultValue) { - CTVar var = this.varCache.getVar(name); - try { - if (var != null && var.listValue() != null) { - // noinspection unchecked - return (List) var.listValue(); - } - } catch (Throwable t) { - config.getLogger().debug(config.getAccountId(), "Error getting variable with name: " + name, t); - return defaultValue; - } - return defaultValue; - } - - @SuppressWarnings({"unused"}) - public Map getMapOfBooleanVariable(String name, Map defaultValue) { - CTVar var = this.varCache.getVar(name); - try { - if (var != null && var.mapValue() != null) { - // noinspection unchecked - return (Map) var.mapValue(); - } - } catch (Throwable t) { - config.getLogger().debug(config.getAccountId(), "Error getting variable with name: " + name, t); - return defaultValue; - } - return defaultValue; - } - - @SuppressWarnings({"unused"}) - public Map getMapOfDoubleVariable(String name, Map defaultValue) { - CTVar var = this.varCache.getVar(name); - try { - if (var != null && var.mapValue() != null) { - // noinspection unchecked - return (Map) var.mapValue(); - } - } catch (Throwable t) { - config.getLogger().debug(config.getAccountId(), "Error getting variable with name: " + name, t); - return defaultValue; - } - return defaultValue; - } - - @SuppressWarnings({"unused"}) - public Map getMapOfIntegerVariable(String name, Map defaultValue) { - CTVar var = this.varCache.getVar(name); - try { - if (var != null && var.mapValue() != null) { - // noinspection unchecked - return (Map) var.mapValue(); - } - } catch (Throwable t) { - config.getLogger().debug(config.getAccountId(), "Error getting variable with name: " + name, t); - return defaultValue; - } - return defaultValue; - } - - @SuppressWarnings({"unused"}) - public Map getMapOfStringVariable(String name, Map defaultValue) { - CTVar var = this.varCache.getVar(name); - try { - if (var != null && var.mapValue() != null) { - // noinspection unchecked - return (Map) var.mapValue(); - } - } catch (Throwable t) { - config.getLogger().debug(config.getAccountId(), "Error getting variable with name: " + name, t); - return defaultValue; - } - return defaultValue; - } - - @SuppressWarnings({"unused"}) - public String getStringVariable(String name, String defaultValue) { - CTVar var = this.varCache.getVar(name); - try { - if (var != null && var.stringValue() != null) { - return var.stringValue(); - } - } catch (Throwable t) { - config.getLogger().debug(config.getAccountId(), "Error getting variable with name: " + name, t); - return defaultValue; - } - return defaultValue; - } - - @SuppressWarnings({"unused"}) - public void registerBooleanVariable(String name) { - _registerVar(name, CTVar.CTVarType.CTVarTypeBool, null); - } - - @SuppressWarnings({"unused"}) - public void registerDoubleVariable(String name) { - _registerVar(name, CTVar.CTVarType.CTVarTypeDouble, null); - } - - @SuppressWarnings({"unused"}) - public void registerIntegerVariable(String name) { - _registerVar(name, CTVar.CTVarType.CTVarTypeInteger, null); - } - - @SuppressWarnings({"unused"}) - public void registerListOfBooleanVariable(String name) { - _registerVar(name, CTVar.CTVarType.CTVarTypeListOfBool, null); - } - - @SuppressWarnings({"unused"}) - public void registerListOfDoubleVariable(String name) { - _registerVar(name, CTVar.CTVarType.CTVarTypeListOfDouble, null); - } - - @SuppressWarnings({"unused"}) - public void registerListOfIntegerVariable(String name) { - _registerVar(name, CTVar.CTVarType.CTVarTypeListOfInteger, null); - } - - @SuppressWarnings({"unused"}) - public void registerListOfStringVariable(String name) { - _registerVar(name, CTVar.CTVarType.CTVarTypeListOfString, null); - } - - @SuppressWarnings({"unused"}) - public void registerMapOfBooleanVariable(String name) { - _registerVar(name, CTVar.CTVarType.CTVarTypeMapOfBool, null); - } - - @SuppressWarnings({"unused"}) - public void registerMapOfDoubleVariable(String name) { - _registerVar(name, CTVar.CTVarType.CTVarTypeMapOfDouble, null); - } - - @SuppressWarnings({"unused"}) - public void registerMapOfIntegerVariable(String name) { - _registerVar(name, CTVar.CTVarType.CTVarTypeMapOfInteger, null); - } - - @SuppressWarnings({"unused"}) - public void registerMapOfStringVariable(String name) { - _registerVar(name, CTVar.CTVarType.CTVarTypeMapOfString, null); - } - - @SuppressWarnings({"unused"}) - public void registerStringVariable(String name) { - _registerVar(name, CTVar.CTVarType.CTVarTypeString, null); - } - - public void resetWithGuid(String guid) { - this.guid = guid; - this.varCache.reset(); - uiEditor.stopVariants(); - applyStoredExperiments(); - } - - public void updateExperiments(JSONArray experiments) { - if (experiments != null) { - final Message message = executionThreadHandler - .obtainMessage(ExecutionThreadHandler.MESSAGE_EXPERIMENTS_RECEIVED); - message.obj = experiments; - executionThreadHandler.sendMessage(message); - } - } - - @SuppressWarnings("SameParameterValue") - private void _registerVar(String name, CTVar.CTVarType type, Object value) { - this.varCache.registerVar(name, type, value); - config.getLogger().verbose(config.getAccountId(), - "Registered Var with name: " + name + " type: " + type.toString() + " and value: " + ((value != null) - ? value.toString() : "null")); - } - - private void applyStoredExperiments() { - executionThreadHandler.sendMessage( - executionThreadHandler.obtainMessage(ExecutionThreadHandler.MESSAGE_INITIALIZE_EXPERIMENTS)); - } - - private CTABTestListener getListener() { - CTABTestListener listener = null; - try { - listener = listenerWeakReference.get(); - } catch (Throwable t) { - // no-op - } - if (listener == null) { - config.getLogger().verbose(config.getAccountId(), "CTABTestListener is null in CTABTestController"); - } - return listener; - } - - private void setListener(CTABTestListener listener) { - listenerWeakReference = new WeakReference<>(listener); - } - - private void handleDashboardMessage(JSONObject msg) { - - String type = msg.optString(TYPE_KEY, "unknown"); - int messageCode = ExecutionThreadHandler.MESSAGE_UNKNOWN; - switch (type) { - case MESSAGE_TYPE_CHANGE_REQUEST: - messageCode = ExecutionThreadHandler.MESSAGE_HANDLE_EDITOR_CHANGES_RECEIVED; - break; - case MESSAGE_TYPE_CLEAR_REQUEST: - messageCode = ExecutionThreadHandler.MESSAGE_HANDLE_EDITOR_CHANGES_CLEARED; - break; - case MESSAGE_TYPE_DEVICE_INFO_REQUEST: - messageCode = ExecutionThreadHandler.MESSAGE_SEND_DEVICE_INFO; - break; - case MESSAGE_TYPE_SNAPSHOT_REQUEST: - messageCode = ExecutionThreadHandler.MESSAGE_SEND_SNAPSHOT; - break; - case MESSAGE_TYPE_VARS_REQUEST: - messageCode = ExecutionThreadHandler.MESSAGE_SEND_VARS; - break; - case MESSAGE_TYPE_VARS_TEST: - messageCode = ExecutionThreadHandler.MESSAGE_TEST_VARS; - break; - case MESSAGE_TYPE_MATCHED: - messageCode = ExecutionThreadHandler.MESSAGE_MATCHED; - break; - case MESSAGE_TYPE_DISCONNECT: - messageCode = ExecutionThreadHandler.MESSAGE_HANDLE_DISCONNECT; - default: - break; - } - final Message m = executionThreadHandler.obtainMessage(messageCode); - - JSONObject messageObject; - try { - messageObject = msg.getJSONObject(DATA_KEY); - } catch (Throwable t) { - // no-op - messageObject = new JSONObject(); - } - m.obj = messageObject; - - executionThreadHandler.sendMessage(m); - } - - static { - SSLSocketFactory found; - try { - final SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, null, null); - found = sslContext.getSocketFactory(); - } catch (final GeneralSecurityException e) { - Logger.d("No SSL support. ABTest editor not available", e.getLocalizedMessage()); - found = null; - } - SSLSocketFactory = found; - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/CTABTestListener.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/CTABTestListener.java deleted file mode 100644 index 9a0016f8e..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/CTABTestListener.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.clevertap.android.sdk.ab_testing; - -public interface CTABTestListener { - - void ABExperimentsUpdated(); -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/CTVar.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/CTVar.java deleted file mode 100644 index 605069bb7..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/CTVar.java +++ /dev/null @@ -1,291 +0,0 @@ -package com.clevertap.android.sdk.ab_testing; - -import androidx.annotation.NonNull; -import com.clevertap.android.sdk.Logger; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.json.JSONObject; - -final class CTVar { - - enum CTVarType { - CTVarTypeBool("bool"), - CTVarTypeDouble("double"), - CTVarTypeInteger("integer"), - CTVarTypeString("string"), - CTVarTypeListOfBool("arrayofbool"), - CTVarTypeListOfDouble("arrayofdouble"), - CTVarTypeListOfInteger("arrayofinteger"), - CTVarTypeListOfString("arrayofstring"), - CTVarTypeMapOfBool("dictionaryofbool"), - CTVarTypeMapOfDouble("dictionaryofdouble"), - CTVarTypeMapOfInteger("dictionaryofinteger"), - CTVarTypeMapOfString("dictionaryofstring"), - CTVarTypeUnknown("unknown"); - - private final String varType; - - CTVarType(String type) { - this.varType = type; - } - - @NonNull - @Override - public String toString() { - return varType; - } - - @SuppressWarnings({"unused"}) - static CTVarType fromString(String type) { - switch (type) { - case "bool": { - return CTVarTypeBool; - } - case "double": { - return CTVarTypeDouble; - } - case "integer": { - return CTVarTypeInteger; - } - case "string": { - return CTVarTypeString; - } - case "arrayofbool": { - return CTVarTypeListOfBool; - } - case "arrayofdouble": { - return CTVarTypeListOfDouble; - } - case "arrayofinteger": { - return CTVarTypeListOfInteger; - } - case "arrayofstring": { - return CTVarTypeListOfString; - } - case "dictionaryofbool": { - return CTVarTypeMapOfBool; - } - case "dictionaryofdouble": { - return CTVarTypeMapOfDouble; - } - case "dictionaryofinteger": { - return CTVarTypeMapOfInteger; - } - case "dictionaryofstring": { - return CTVarTypeMapOfString; - } - default: - return CTVarTypeUnknown; - } - } - - } - - private List _listValue; - - private Map _mapValue; - - private Double _numberValue; - - private String _stringValue; - - private Object _value; - - private String name; - - private CTVarType type; - - CTVar(String name, CTVarType type, Object value) { - this.name = name; - this.type = type; - this._value = value; - _computeValue(); - } - - Boolean booleanValue() { - if (_stringValue == null) { - return null; - } - try { - return Boolean.valueOf(_stringValue); - } catch (Throwable t) { - return null; - } - } - - void clearValue() { - this._value = null; - _computeValue(); - } - - Double doubleValue() { - return _numberValue; - } - - String getName() { - return name; - } - - CTVarType getType() { - return type; - } - - Integer integerValue() { - if (_numberValue == null) { - return null; - } - try { - return _numberValue.intValue(); - } catch (Throwable t) { - return null; - } - } - - List listValue() { - return _listValue; - } - - Map mapValue() { - return _mapValue; - } - - String stringValue() { - return _stringValue; - } - - JSONObject toJSON() { - JSONObject json = new JSONObject(); - try { - json.put("name", name); - json.put("type", type.toString()); - } catch (Throwable t) { - // no-op - } - return json; - } - - @SuppressWarnings("unused") - void update(CTVarType type, Object value) { - this.type = type; - this._value = value; - _computeValue(); - } - - private void _computeValue() { - _stringValue = null; - _numberValue = null; - _listValue = null; - _mapValue = null; - if (_value == null) { - return; - } - - if (_value instanceof String) { - _stringValue = (String) _value; - try { - _numberValue = Double.valueOf(_stringValue); - } catch (Throwable t) { - // no-op - } - } else if (_value instanceof Number) { - _stringValue = "" + _value; - _numberValue = ((Number) _value).doubleValue(); - } else { - try { - _stringValue = _value.toString(); - } catch (Throwable t) { - Logger.d("Error parsing var", t); - return; - } - } - - switch (type) { - case CTVarTypeListOfBool: - case CTVarTypeListOfDouble: - case CTVarTypeListOfInteger: - case CTVarTypeListOfString: - _listValue = listFromString(_stringValue, type); - break; - case CTVarTypeMapOfBool: - case CTVarTypeMapOfDouble: - case CTVarTypeMapOfInteger: - case CTVarTypeMapOfString: - _mapValue = mapFromString(_stringValue, type); - break; - } - } - - private List listFromString(String stringValue, CTVarType type) { - try { - String[] stringArray = stringValue.replace("[", "") - .replace("]", "").replace("\"", "") - .split(","); // ["value", "value"...] - - if (type == CTVarType.CTVarTypeListOfString) { - return Arrays.asList(stringArray); - } - - ArrayList parsed = new ArrayList<>(); - - for (String s : stringArray) { - switch (type) { - case CTVarTypeListOfBool: - parsed.add(Boolean.valueOf(s)); - break; - case CTVarTypeListOfDouble: - parsed.add(Double.valueOf(s)); - break; - case CTVarTypeListOfInteger: - parsed.add(Integer.valueOf(s)); - break; - } - } - return parsed; - } catch (Throwable t) { - Logger.d("Unable to parse list of type: " + type.toString() + " from : " + stringValue); - return null; - } - } - - private Map mapFromString(String stringValue, CTVarType type) { - try { - String[] stringArray = stringValue.replace("\"", "") - .replace("{", "") - .replace("}", "") - .split(","); // ["key:value", "key:value"...] - - Map objectMap = new HashMap<>(); - - for (String s : stringArray) { - String[] stringValuesArray = s.split(":"); - String key = stringValuesArray[0]; - String _stringValue = stringValuesArray[1]; - Object value = null; - switch (type) { - case CTVarTypeMapOfBool: - value = Boolean.valueOf(_stringValue); - break; - case CTVarTypeMapOfDouble: - value = Double.valueOf(_stringValue); - break; - case CTVarTypeMapOfInteger: - value = Integer.valueOf(_stringValue); - break; - case CTVarTypeMapOfString: - value = _stringValue; - break; - } - if (value != null) { - objectMap.put(key, value); - } - } - return objectMap; - } catch (Throwable t) { - Logger.d("Unable to parse map of type: " + type.toString() + " from : " + stringValue); - return null; - } - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/CTVarCache.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/CTVarCache.java deleted file mode 100644 index 0a17749f7..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/CTVarCache.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.clevertap.android.sdk.ab_testing; - -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import org.json.JSONArray; - -final class CTVarCache { - - private final Map vars = new ConcurrentHashMap<>(); - - @SuppressWarnings({"WeakerAccess"}) - void clearVar(String name) { - CTVar var = getVar(name); - if (var != null) { - var.clearValue(); - } - } - - CTVar getVar(String name) { - return vars.get(name); - } - - void registerVar(String name, CTVar.CTVarType type, Object value) { - CTVar var = getVar(name); - if (var == null) { - vars.put(name, new CTVar(name, type, value)); - } else if (value - != null) { // only overwrite if we have a new value, to explicitly clear the value use clearVar - var.update(type, value); - } - } - - @SuppressWarnings("unused") - void reset() { - for (String name : new HashMap<>(vars).keySet()) { - clearVar(name); - } - } - - JSONArray serializeVars() { - JSONArray serialized = new JSONArray(); - for (String name : new HashMap<>(vars).keySet()) { - CTVar var = vars.get(name); - if (var != null) { - serialized.put(var.toJSON()); - } - } - return serialized; - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/gesture/ConnectionGesture.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/gesture/ConnectionGesture.java deleted file mode 100644 index c4ff4cd0a..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/gesture/ConnectionGesture.java +++ /dev/null @@ -1,115 +0,0 @@ -package com.clevertap.android.sdk.ab_testing.gesture; - -import android.hardware.Sensor; -import android.hardware.SensorEvent; -import android.hardware.SensorEventListener; -import com.clevertap.android.sdk.Logger; - -public class ConnectionGesture implements SensorEventListener { - - public interface OnGestureListener { - - void onGesture(); - } - - private static final float MINIMUM_GRAVITY = 9.8f - 2.0f; - - private static final float MAXIMUM_GRAVITY = 9.8f + 2.0f; - - private static final long MINIMUM_UP_DOWN_DURATION = 250000000; // 1/4 second - - private static final long MINIMUM_CANCEL_DURATION = 1000000000; // one second - - private static final int STATE_UP = -1; - - private static final int STATE_NONE = 0; - - private static final int STATE_DOWN = 1; - - private static final int TRIGGER_NONE = 0; - - private static final int TRIGGER_BEGIN = 1; - - private static final float SMOOTHING_FACTOR = 0.7f; - - private int gestureState = STATE_NONE; - - private long lastTime = -1; - - private final OnGestureListener listener; - - private final float[] smoothed = new float[3]; - - private int triggerState = -1; - - public ConnectionGesture(OnGestureListener listener) { - this.listener = listener; - } - - @Override - public void onAccuracyChanged(Sensor sensor, int accuracy) { - // no-op - } - - @Override - public void onSensorChanged(SensorEvent event) { - final float[] smoothedValues = smoothSamples(event.values); - final int oldState = gestureState; - gestureState = STATE_NONE; - - final float totalGravitySquared = smoothedValues[0] * smoothedValues[0] - + smoothedValues[1] * smoothedValues[1] + smoothedValues[2] * smoothedValues[2]; - - final float minimumGravitySquared = MINIMUM_GRAVITY * MINIMUM_GRAVITY; - final float maximumGravitySquared = MAXIMUM_GRAVITY * MAXIMUM_GRAVITY; - - if (smoothed[2] > MINIMUM_GRAVITY && smoothed[2] < MAXIMUM_GRAVITY) { - gestureState = STATE_UP; - } - - if (smoothed[2] < -MINIMUM_GRAVITY && smoothed[2] > -MAXIMUM_GRAVITY) { - gestureState = STATE_DOWN; - } - - if (totalGravitySquared < minimumGravitySquared || totalGravitySquared > maximumGravitySquared) { - gestureState = STATE_NONE; - } - - if (oldState != gestureState) { - lastTime = event.timestamp; - } - - final long durationNanos = event.timestamp - lastTime; - - switch (gestureState) { - case STATE_DOWN: - if (durationNanos > MINIMUM_UP_DOWN_DURATION && triggerState == TRIGGER_NONE) { - Logger.v("Connection gesture started"); - triggerState = TRIGGER_BEGIN; - } - break; - case STATE_UP: - if (durationNanos > MINIMUM_UP_DOWN_DURATION && triggerState == TRIGGER_BEGIN) { - Logger.v("Connection gesture completed"); - triggerState = TRIGGER_NONE; - listener.onGesture(); - } - break; - case STATE_NONE: - if (durationNanos > MINIMUM_CANCEL_DURATION && triggerState != TRIGGER_NONE) { - Logger.v("Connection gesture canceled"); - triggerState = TRIGGER_NONE; - } - break; - } - } - - private float[] smoothSamples(final float[] samples) { - for (int i = 0; i < 3; i++) { - final float old = smoothed[i]; - smoothed[i] = old + (SMOOTHING_FACTOR * (samples[i] - old)); - } - return smoothed; - } -} - diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/models/CTABVariant.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/models/CTABVariant.java deleted file mode 100644 index 503f39228..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/models/CTABVariant.java +++ /dev/null @@ -1,208 +0,0 @@ -package com.clevertap.android.sdk.ab_testing.models; - -import androidx.annotation.NonNull; -import com.clevertap.android.sdk.ImageCache; -import com.clevertap.android.sdk.Logger; -import com.clevertap.android.sdk.Utils; -import java.util.ArrayList; -import java.util.List; -import org.json.JSONArray; -import org.json.JSONObject; - -public class CTABVariant { - - final public class CTVariantAction { - - private String activityName; - - private JSONObject change; - - private String name; - - CTVariantAction(String name, String activityName, JSONObject change) { - this.name = name; - this.activityName = activityName; - this.change = change; - } - - public String getActivityName() { - return activityName; - } - - public JSONObject getChange() { - return change; - } - - public String getName() { - return name; - } - } - - private ArrayList actions = new ArrayList<>(); - - private final Object actionsLock = new Object(); - - private String experimentId; - - private String id; - - private ArrayList imageUrls; - - private String variantId; - - private JSONArray vars; - - private int version; - - public static CTABVariant initWithJSON(JSONObject json) { - try { - String experimentId = json.optString("exp_id", "0"); - String variantId = json.optString("var_id", "0"); - int version = json.optInt("version", 0); - final JSONArray actions = json.optJSONArray("actions"); - final JSONArray vars = json.optJSONArray("vars"); - CTABVariant variant = new CTABVariant(experimentId, variantId, version, actions, vars); - Logger.v("Created CTABVariant: " + variant.toString()); - return variant; - } catch (Throwable t) { - Logger.v("Error creating variant", t); - return null; - } - } - - private CTABVariant(String experimentId, String variantId, int version, JSONArray actions, JSONArray vars) { - this.experimentId = experimentId; - this.variantId = variantId; - this.id = variantId + ":" + experimentId; - this.version = version; - imageUrls = new ArrayList<>(); - addActions(actions); - this.vars = vars == null ? new JSONArray() : vars; - } - - public void addActions(JSONArray actions) { - synchronized (actionsLock) { - if (actions == null || actions.length() <= 0) { - return; - } - final int actionsLength = actions.length(); - try { - for (int j = 0; j < actionsLength; j++) { - final JSONObject change = actions.getJSONObject(j); - if (change == null) { - continue; - } - final String targetActivity = Utils.optionalStringKey(change, "target_activity"); - final String name = change.getString("name"); - boolean exists = false; - CTVariantAction existingAction = null; - for (CTVariantAction action : this.actions) { - if (action.getName().equals(name)) { - exists = true; - existingAction = action; - break; - } - } - if (exists) { - this.actions.remove(existingAction); - } - final CTVariantAction action = new CTVariantAction(name, targetActivity, change); - this.actions.add(action); - } - } catch (Throwable t) { - Logger.v("Error adding variant actions", t); - } - } - } - - public void addImageUrls(List urls) { - if (urls == null) { - return; - } - this.imageUrls.addAll(urls); - } - - public void cleanup() { - for (String url : imageUrls) { - ImageCache.removeBitmap(url, true); - } - } - - public void clearActions() { - synchronized (actionsLock) { - actions.clear(); - } - } - - @Override - public boolean equals(Object o) { - if (o instanceof CTABVariant) { - CTABVariant other = (CTABVariant) o; - return this.getId().equals(other.getId()) && this.getVersion() == other.getVersion(); - } - return false; - } - - public ArrayList getActions() { - synchronized (actionsLock) { - return this.actions; - } - } - - public String getId() { - return id; - } - - public JSONArray getVars() { - return vars; - } - - public int getVersion() { - return version; - } - - @Override - public int hashCode() { - return this.getId().hashCode(); - } - - public void removeActionsByName(JSONArray names) { - if (names == null || names.length() <= 0) { - return; - } - synchronized (actionsLock) { - ArrayList _names = new ArrayList<>(); - for (int i = 0; i < names.length(); i++) { - try { - _names.add(names.getString(i)); - } catch (Throwable t) { - // no-op - } - } - ArrayList newActions = new ArrayList<>(); - for (CTVariantAction action : actions) { - if (!_names.contains(action.getName())) { - newActions.add(action); - } - } - this.actions = newActions; - } - } - - @NonNull - @Override - public String toString() { - return "< id: " + getId() + ", version: " + getVersion() + ", actions count: " + actions.size() - + ", vars count: " + getVars().length() + " >"; - } - - @SuppressWarnings("unused") - String getExperimentId() { - return experimentId; - } - - @SuppressWarnings("unused") - String getVariantId() { - return variantId; - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/uieditor/ResourceIds.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/uieditor/ResourceIds.java deleted file mode 100644 index aa0715d97..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/uieditor/ResourceIds.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.clevertap.android.sdk.ab_testing.uieditor; - -import android.util.SparseArray; -import com.clevertap.android.sdk.Logger; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.util.HashMap; -import java.util.Map; - -final class ResourceIds { - - private final Map idNameToId; - - private final SparseArray idToIdName; - - private final String resourcePackageName; - - ResourceIds(String resourcePackageName) { - idNameToId = new HashMap<>(); - idToIdName = new SparseArray<>(); - this.resourcePackageName = resourcePackageName; - read(); - } - - int idFromName(String name) { - // noinspection ConstantConditions - return idNameToId.get(name); - } - - boolean knownIdName(String name) { - return idNameToId.containsKey(name); - } - - String nameForId(int id) { - return idToIdName.get(id); - } - - private String getLocalClassName() { - return resourcePackageName + ".R$id"; - } - - private Class getSystemClass() { - return android.R.id.class; - } - - private void read() { - idNameToId.clear(); - idToIdName.clear(); - - final Class sysIdClass = getSystemClass(); - readClassIds(sysIdClass, "android", idNameToId); - - final String localClassName = getLocalClassName(); - try { - final Class rIdClass = Class.forName(localClassName); - readClassIds(rIdClass, null, idNameToId); - } catch (ClassNotFoundException e) { - Logger.d("Can't load names for Android view ids from '" + localClassName - + "', ids by name will not be available in the events editor."); - Logger.d("You may be missing a Resources class for your package due to your proguard configuration, " + - "or you may be using an applicationId in your build that isn't the same as the package declared in your AndroidManifest.xml file.\n" - + - "If you're using proguard, you can fix this issue by adding the following to your proguard configuration:\n\n" - + - "-keep class **.R$* {\n" + - " ;\n" + - "}\n\n" + - "If you're not using proguard, or if your proguard configuration already contains the directive above, " - + - "you can add the following to your AndroidManifest.xml file to explicitly point CleverTap SDK to " - + - "the appropriate library for your resources class:\n\n" + - "\n\n" + - "where YOUR_PACKAGE_NAME is the same string you use for the \"package\" attribute in your tag." - ); - } - - for (Map.Entry idMapping : idNameToId.entrySet()) { - idToIdName.put(idMapping.getValue(), idMapping.getKey()); - } - } - - private static void readClassIds(Class platformIdClass, String namespace, Map namesToIds) { - try { - final Field[] fields = platformIdClass.getFields(); - for (final Field field : fields) { - final int modifiers = field.getModifiers(); - if (Modifier.isStatic(modifiers)) { - final Class fieldType = field.getType(); - if (fieldType == int.class) { - final String name = field.getName(); - final int value = field.getInt(null); - final String namespacedName; - if (null == namespace) { - namespacedName = name; - } else { - namespacedName = namespace + ":" + name; - } - - namesToIds.put(namespacedName, value); - } - } - } - } catch (IllegalAccessException e) { - Logger.v("Can't read built-in id names from " + platformIdClass.getName(), e); - } - } -} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/uieditor/SnapshotBuilder.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/uieditor/SnapshotBuilder.java deleted file mode 100644 index b86a33b39..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/uieditor/SnapshotBuilder.java +++ /dev/null @@ -1,473 +0,0 @@ -package com.clevertap.android.sdk.ab_testing.uieditor; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.res.ColorStateList; -import android.content.res.Configuration; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.os.Handler; -import android.os.Looper; -import android.util.Base64; -import android.util.Base64OutputStream; -import android.util.DisplayMetrics; -import android.util.JsonWriter; -import android.util.LruCache; -import android.view.View; -import android.view.ViewGroup; -import android.widget.RelativeLayout; -import com.clevertap.android.sdk.CleverTapInstanceConfig; -import com.clevertap.android.sdk.Logger; -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.FutureTask; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import org.json.JSONObject; - -final class SnapshotBuilder { - - final static class ViewSnapshotConfig { - - final List propertyDescriptionList; - - ResourceIds resourceIds; - - ViewSnapshotConfig(List propertyDescriptions, ResourceIds resourceIds) { - this.resourceIds = resourceIds; - this.propertyDescriptionList = propertyDescriptions; - } - } - - private static class ClassCache extends LruCache, String> { - - ClassCache(int maxSize) { - super(maxSize); - } - - @Override - protected String create(Class klass) { - return klass.getCanonicalName(); - } - } - - private static class RootView { - - private final static String UNSPECIFIED = "unspecified"; - - private final static String LANDSCAPE = "landscape"; - - private final static String PORTRAIT = "portrait"; - - final String activityName; - - String orientation = UNSPECIFIED; - - final View rootView; - - float scale; - - Screenshot screenshot; - - RootView(String activityName, View rootView, int activityOrientation) { - this.activityName = activityName; - this.rootView = rootView; - this.screenshot = null; - this.scale = 1.0f; - setOrientation(activityOrientation); - } - - private void setOrientation(final int orientation) { - this.orientation = (orientation == Configuration.ORIENTATION_LANDSCAPE) ? LANDSCAPE : PORTRAIT; - } - } - - private static class Screenshot { - - private Bitmap cachedScreenshot; - - private final Paint paint; - - Screenshot() { - cachedScreenshot = null; - paint = new Paint(Paint.FILTER_BITMAP_FLAG); - } - - @SuppressWarnings("SameParameterValue") - synchronized void regenerate(int width, int height, int destDensity, Bitmap source) { - if (null == cachedScreenshot || cachedScreenshot.getWidth() != width - || cachedScreenshot.getHeight() != height) { - try { - cachedScreenshot = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565); - } catch (final OutOfMemoryError e) { - cachedScreenshot = null; - } - - if (cachedScreenshot != null) { - cachedScreenshot.setDensity(destDensity); - } - } - - if (cachedScreenshot != null) { - final Canvas scaledCanvas = new Canvas(cachedScreenshot); - scaledCanvas.drawBitmap(source, 0, 0, paint); - } - } - - @SuppressWarnings({"SameParameterValue", "unused"}) - synchronized void writeJSON(Bitmap.CompressFormat format, int quality, OutputStream out) throws IOException { - if (cachedScreenshot == null || cachedScreenshot.getWidth() == 0 || cachedScreenshot.getHeight() == 0) { - out.write("null".getBytes()); - } else { - out.write('"'); - final Base64OutputStream imageOut = new Base64OutputStream(out, Base64.NO_WRAP); - cachedScreenshot.compress(Bitmap.CompressFormat.PNG, 100, imageOut); - imageOut.flush(); - out.write('"'); - } - } - } - - private static class RootViewsGenerator implements Callable> { - - private UIEditor.ActivitySet activitySet; - - private final int clientDensity = DisplayMetrics.DENSITY_DEFAULT; - - private final DisplayMetrics displayMetrics; - - private final List rootViews; - - private final Screenshot screenshot; - - RootViewsGenerator() { - displayMetrics = new DisplayMetrics(); - rootViews = new ArrayList<>(); - screenshot = new Screenshot(); - } - - @Override - public List call() { - rootViews.clear(); - - final Set activities = this.activitySet.getAll(); - - for (final Activity activity : activities) { - final String activityName = activity.getClass().getCanonicalName(); - final int orientation = activity.getResources().getConfiguration().orientation; - final View view = activity.getWindow().getDecorView().getRootView(); - activity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); - final RootView rootView = new RootView(activityName, view, orientation); - rootViews.add(rootView); - } - - final int viewCount = rootViews.size(); - for (int i = 0; i < viewCount; i++) { - final RootView rootView = rootViews.get(i); - takeScreenshot(rootView); - } - - return rootViews; - } - - void findInActivities(UIEditor.ActivitySet activitySet) { - this.activitySet = activitySet; - } - - private void takeScreenshot(final RootView root) { - final View rootView = root.rootView; - Bitmap bitmap = null; - - try { - @SuppressWarnings("JavaReflectionMemberAccess") @SuppressLint("PrivateApi") final Method - createSnapshot = View.class - .getDeclaredMethod("createSnapshot", Bitmap.Config.class, Integer.TYPE, Boolean.TYPE); - createSnapshot.setAccessible(true); - bitmap = (Bitmap) createSnapshot.invoke(rootView, Bitmap.Config.RGB_565, Color.WHITE, false); - } catch (final NoSuchMethodException e) { - Logger.v("Can't call createSnapshot, will use drawCache"); - } catch (final IllegalArgumentException e) { - Logger.v("Can't call createSnapshot with arguments"); - } catch (final InvocationTargetException e) { - Logger.v("Exception when calling createSnapshot", e.getLocalizedMessage()); - } catch (final IllegalAccessException e) { - Logger.v("Can't access createSnapshot, using drawCache"); - } catch (final ClassCastException e) { - Logger.v("createSnapshot didn't return a bitmap?", e.getLocalizedMessage()); - } - - Boolean originalCacheState = null; - try { - if (bitmap == null) { - originalCacheState = rootView.isDrawingCacheEnabled(); - rootView.setDrawingCacheEnabled(true); - rootView.buildDrawingCache(true); - bitmap = rootView.getDrawingCache(); - } - } catch (final RuntimeException e) { - Logger.v("Error taking a bitmap snapshot of view " + rootView + ", skipping", e); - } - - float scale = 1.0f; - if (bitmap != null) { - final int density = bitmap.getDensity(); - - if (density != Bitmap.DENSITY_NONE) { - scale = ((float) clientDensity) / density; - } - - final int rawWidth = bitmap.getWidth(); - final int rawHeight = bitmap.getHeight(); - final int destWidth = (int) ((bitmap.getWidth() * scale) + 0.5); - final int destHeight = (int) ((bitmap.getHeight() * scale) + 0.5); - - if (rawWidth > 0 && rawHeight > 0 && destWidth > 0 && destHeight > 0) { - screenshot.regenerate(destWidth, destHeight, clientDensity, bitmap); - } - } - - if (originalCacheState != null && !originalCacheState) { - rootView.setDrawingCacheEnabled(false); - } - root.scale = scale; - root.screenshot = screenshot; - } - } - - private static final int MAX_CLASS_CACHE_SIZE = 255; - - private static final Handler mainThreadHandler = new Handler(Looper.getMainLooper()); - - private static final RootViewsGenerator rootViewsGenerator = new RootViewsGenerator(); - - static private final ClassCache classCache = new ClassCache(MAX_CLASS_CACHE_SIZE); - - static void writeSnapshot(ViewSnapshotConfig snapshotConfig, UIEditor.ActivitySet liveActivities, - OutputStream out, CleverTapInstanceConfig config) throws IOException { - rootViewsGenerator.findInActivities(liveActivities); - final FutureTask> rootViewsFuture = new FutureTask<>(rootViewsGenerator); - mainThreadHandler.post(rootViewsFuture); - - final OutputStreamWriter writer = new OutputStreamWriter(out); - List rootViewList = Collections.emptyList(); - writer.write("["); - - try { - rootViewList = rootViewsFuture.get(1, TimeUnit.SECONDS); - } catch (final InterruptedException e) { - getConfigLogger(config).debug(getAccountId(config), "Screenshot interrupted.", e); - } catch (final TimeoutException e) { - getConfigLogger(config).debug(getAccountId(config), "Screenshot timed out.", e); - } catch (final ExecutionException e) { - getConfigLogger(config).verbose(getAccountId(config), "Screenshot error", e); - } - - final int viewCount = rootViewList.size(); - for (int i = 0; i < viewCount; i++) { - if (i > 0) { - writer.write(","); - } - final RootView rootView = rootViewList.get(i); - writer.write("{"); - writer.write("\"activity\":"); - writer.write(JSONObject.quote(rootView.activityName)); - writer.write(","); - writer.write("\"scale\":"); - writer.write(String.format("%s", rootView.scale)); - writer.write(","); - writer.write("\"orientation\":"); - writer.write(JSONObject.quote(rootView.orientation)); - writer.write(","); - writer.write("\"serialized_objects\":"); - { - final JsonWriter j = new JsonWriter(writer); - j.beginObject(); - j.name("rootObject").value(rootView.rootView.hashCode()); - j.name("objects"); - viewHierarchySnapshot(j, rootView.rootView, snapshotConfig); - j.endObject(); - j.flush(); - } - writer.write(","); - writer.write("\"screenshot\":"); - writer.flush(); - rootView.screenshot.writeJSON(Bitmap.CompressFormat.PNG, 100, out); - writer.write("}"); - } - - writer.write("]"); - writer.flush(); - } - - private static String getAccountId(CleverTapInstanceConfig config) { - return config.getAccountId(); - } - - private static Logger getConfigLogger(CleverTapInstanceConfig config) { - return config.getLogger(); - } - - private static void viewHierarchySnapshot(JsonWriter j, View rootView, ViewSnapshotConfig snapshotConfig) - throws IOException { - j.beginArray(); - viewSnapshot(j, rootView, snapshotConfig); - j.endArray(); - } - - private static void viewSnapshot(JsonWriter j, View view, ViewSnapshotConfig snapshotConfig) throws IOException { - final int viewId = view.getId(); - final String viewName; - if (viewId == -1) { - viewName = null; - } else { - viewName = snapshotConfig.resourceIds.nameForId(viewId); - } - - j.beginObject(); - j.name("hashCode").value(view.hashCode()); - j.name("id").value(viewId); - j.name("ct_id_name").value(viewName); - - final CharSequence contentDescription = view.getContentDescription(); - if (contentDescription == null) { - j.name("contentDescription").nullValue(); - } else { - j.name("contentDescription").value(contentDescription.toString()); - } - - final Object tag = view.getTag(); - if (tag == null) { - j.name("tag").nullValue(); - } else if (tag instanceof CharSequence) { - j.name("tag").value(tag.toString()); - } - - j.name("top").value(view.getTop()); - j.name("left").value(view.getLeft()); - j.name("width").value(view.getWidth()); - j.name("height").value(view.getHeight()); - j.name("scrollX").value(view.getScrollX()); - j.name("scrollY").value(view.getScrollY()); - j.name("visibility").value(view.getVisibility()); - - float transX; - float transY; - transX = view.getTranslationX(); - transY = view.getTranslationY(); - j.name("translationX").value(transX); - j.name("translationY").value(transY); - - j.name("classes"); - j.beginArray(); - Class klass = view.getClass(); - do { - j.value(classCache.get(klass)); - klass = klass.getSuperclass(); - } while (klass != Object.class && klass != null); - j.endArray(); - - writeViewProperties(j, view, snapshotConfig); - - ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); - if (layoutParams instanceof RelativeLayout.LayoutParams) { - RelativeLayout.LayoutParams relativeLayoutParams = (RelativeLayout.LayoutParams) layoutParams; - int[] rules = relativeLayoutParams.getRules(); - j.name("layoutRules"); - j.beginArray(); - for (int rule : rules) { - j.value(rule); - } - j.endArray(); - } - - j.name("subviews"); - j.beginArray(); - if (view instanceof ViewGroup) { - final ViewGroup group = (ViewGroup) view; - final int childCount = group.getChildCount(); - for (int i = 0; i < childCount; i++) { - final View child = group.getChildAt(i); - if (child != null) { - j.value(child.hashCode()); - } - } - } - j.endArray(); - j.endObject(); - - if (view instanceof ViewGroup) { - final ViewGroup group = (ViewGroup) view; - final int childCount = group.getChildCount(); - for (int i = 0; i < childCount; i++) { - final View child = group.getChildAt(i); - if (child != null) { - viewSnapshot(j, child, snapshotConfig); - } - } - } - } - - private static void writeViewProperties(JsonWriter j, View v, ViewSnapshotConfig snapshotConfig) - throws IOException { - final Class viewClass = v.getClass(); - for (final ViewProperty desc : snapshotConfig.propertyDescriptionList) { - if (desc.target.isAssignableFrom(viewClass) && null != desc.accessor) { - final Object value = desc.accessor.invokeMethod(v); - //noinspection StatementWithEmptyBody - if (null == value) { - // no-op - } else if (value instanceof Boolean) { - j.name(desc.name).value((Boolean) value); - } else if (value instanceof Number) { - j.name(desc.name).value((Number) value); - } else if (value instanceof ColorStateList) { - j.name(desc.name).value((Integer) ((ColorStateList) value).getDefaultColor()); - } else if (value instanceof Drawable) { - final Drawable drawable = (Drawable) value; - final Rect bounds = drawable.getBounds(); - j.name(desc.name); - j.beginObject(); - j.name("classes"); - j.beginArray(); - Class klass = drawable.getClass(); - while (klass != Object.class) { - if (klass != null) { - j.value(klass.getCanonicalName()); - klass = klass.getSuperclass(); - } - } - j.endArray(); - j.name("dimensions"); - j.beginObject(); - j.name("left").value(bounds.left); - j.name("right").value(bounds.right); - j.name("top").value(bounds.top); - j.name("bottom").value(bounds.bottom); - j.endObject(); - if (drawable instanceof ColorDrawable) { - final ColorDrawable colorDrawable = (ColorDrawable) drawable; - j.name("color").value(colorDrawable.getColor()); - } - j.endObject(); - } else { - j.name(desc.name).value(value.toString()); - } - } - } - } - -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/uieditor/UIEditor.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/uieditor/UIEditor.java deleted file mode 100644 index 3dfc0ee93..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/uieditor/UIEditor.java +++ /dev/null @@ -1,591 +0,0 @@ -package com.clevertap.android.sdk.ab_testing.uieditor; - -import android.app.Activity; -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.os.Handler; -import android.os.Looper; -import android.view.View; -import android.view.ViewTreeObserver; -import com.clevertap.android.sdk.CleverTapInstanceConfig; -import com.clevertap.android.sdk.ImageCache; -import com.clevertap.android.sdk.Logger; -import com.clevertap.android.sdk.Utils; -import com.clevertap.android.sdk.ab_testing.models.CTABVariant; -import java.io.OutputStream; -import java.lang.ref.WeakReference; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Deque; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -public class UIEditor { - - private static class UIChange { - - final List imageUrls; - - final ViewEdit viewEdit; - - private UIChange(ViewEdit viewEdit, List urls) { - this.viewEdit = viewEdit; - imageUrls = urls; - } - } - - private static class UIChangeBinding implements ViewTreeObserver.OnGlobalLayoutListener, Runnable { - - private boolean alive; - - private volatile boolean dying; - - private final Handler handler; - - private final ViewEdit viewEdit; - - private final WeakReference viewRoot; - - UIChangeBinding(View viewRoot, ViewEdit edit, Handler uiThreadHandler) { - viewEdit = edit; - this.viewRoot = new WeakReference<>(viewRoot); - handler = uiThreadHandler; - alive = true; - dying = false; - - final ViewTreeObserver observer = viewRoot.getViewTreeObserver(); - if (observer.isAlive()) { - observer.addOnGlobalLayoutListener(this); - } - run(); - } - - @Override - public void onGlobalLayout() { - run(); - } - - @Override - public void run() { - if (!alive) { - return; - } - final View viewRoot = this.viewRoot.get(); - if (null == viewRoot || dying) { - cleanUp(); - return; - } - viewEdit.run(viewRoot); - handler.removeCallbacks(this); - handler.postDelayed(this, 1000); - } - - private void cleanUp() { - if (alive) { - final View viewRoot = this.viewRoot.get(); - if (viewRoot != null) { - final ViewTreeObserver observer = viewRoot.getViewTreeObserver(); - if (observer.isAlive()) { - observer.removeGlobalOnLayoutListener(this); - } - } - viewEdit.cleanup(); - } - alive = false; - } - - private void kill() { - dying = true; - handler.post(this); - } - } - - class ActivitySet { - - private Set activitySet; - - ActivitySet() { - activitySet = new HashSet<>(); - } - - void add(Activity activity) { - checkThreadState(); - activitySet.add(activity); - } - - Set getAll() { - checkThreadState(); - return Collections.unmodifiableSet(activitySet); - } - - boolean isEmpty() { - checkThreadState(); - return activitySet.isEmpty(); - } - - void remove(Activity activity) { - checkThreadState(); - activitySet.remove(activity); - } - - private void checkThreadState() throws RuntimeException { - if (Thread.currentThread() != Looper.getMainLooper().getThread()) { - throw new RuntimeException("Can't access ActivitySet when not on the UI thread"); - } - } - } - - private static final Class[] EMPTY_PARAMS = new Class[0]; - - private static final List NEVER_MATCH_PATH = Collections.emptyList(); - - private ActivitySet activitySet; - - private CleverTapInstanceConfig config; - - private Context context; - - private final Deque currentEdits;//Need a LIFO structure to reverse changes - - private final ArrayList editorSessionImageUrls; - - private final Map> newEdits; - - private ResourceIds resourceIds; - - private SnapshotBuilder.ViewSnapshotConfig snapshotConfig; - - private final Handler uiThreadHandler; - - public UIEditor(Context context, CleverTapInstanceConfig config) { - String resourcePackageName = config.getPackageName(); - if (resourcePackageName == null) { - resourcePackageName = context.getPackageName(); - } - this.resourceIds = new ResourceIds(resourcePackageName); - this.config = config; - uiThreadHandler = new Handler(Looper.getMainLooper()); - newEdits = new HashMap<>(); - currentEdits = new ArrayDeque<>(); - activitySet = new ActivitySet(); - editorSessionImageUrls = new ArrayList<>(); - this.context = context; - } - - public void addActivity(Activity activity) { - activitySet.add(activity); - handleNewEditsOnUiThread(); - } - - public void applyVariants(Set variants, boolean isEditorSession) { - final Map> edits = new HashMap<>(); - for (CTABVariant variant : variants) { - for (CTABVariant.CTVariantAction action : variant.getActions()) { - final UIChange change = generateUIChange(action.getChange()); - if (change != null) { - if (isEditorSession) { - editorSessionImageUrls - .addAll(change.imageUrls); // Add all images to a list to be cleared when dashboard disconnects. - } - variant.addImageUrls(change.imageUrls); - - String name = action.getActivityName(); - ViewEdit viewEdit = change.viewEdit; - final List mapElement; - if (edits.containsKey(name)) { - mapElement = edits.get(name); - } else { - mapElement = new ArrayList<>(); - edits.put(name, mapElement); - } - if (mapElement != null) { - mapElement.add(viewEdit); - } - } - } - } - - clearEdits(); - - synchronized (newEdits) { - newEdits.clear(); - newEdits.putAll(edits); - } - handleNewEditsOnUiThread(); - } - - public boolean loadSnapshotConfig(JSONObject data) { - if (snapshotConfig == null) { - List properties = loadViewProperties(data); - if (properties != null) { - snapshotConfig = new SnapshotBuilder.ViewSnapshotConfig(properties, resourceIds); - } - } - return snapshotConfig != null; - } - - public void removeActivity(Activity activity) { - activitySet.remove(activity); - } - - public void stopVariants() { - clearEdits(); - for (final String assetUrl : editorSessionImageUrls) { - ImageCache.removeBitmap(assetUrl, true); - } - editorSessionImageUrls.clear(); - snapshotConfig = null; - } - - public void writeSnapshot(final OutputStream out) { - if (snapshotConfig == null) { - getConfigLogger().debug("UIEditor: Unable to write snapshot, snapshot config not set"); - return; - } - try { - SnapshotBuilder.writeSnapshot(snapshotConfig, activitySet, out, config); - } catch (Throwable t) { - getConfigLogger().debug("UIEditor: error writing snapshot", t); - } - } - - // Only call on UI Thread - private void applyEdits(View rootView, List viewEdits) { - synchronized (currentEdits) { - final int size = viewEdits.size(); - for (int i = 0; i < size; i++) { - final ViewEdit viewEdit = viewEdits.get(i); - final UIChangeBinding binding = new UIChangeBinding(rootView, viewEdit, uiThreadHandler); - currentEdits.add(binding); - } - } - } - - private Object castArgumentObject(Object jsonArgument, String type, List imageUrls) { - try { - if (type == null) { - return null; - } - switch (type) { - case "java.lang.CharSequence": - case "boolean": - case "java.lang.Boolean": - return jsonArgument; - case "int": - case "java.lang.Integer": - return ((Number) jsonArgument).intValue(); - case "float": - case "java.lang.Float": - return ((Number) jsonArgument).floatValue(); - case "android.graphics.drawable.Drawable": - case "android.graphics.drawable.BitmapDrawable": - return readBitmapDrawable((JSONObject) jsonArgument, imageUrls); - case "android.graphics.drawable.ColorDrawable": - int colorValue = ((Number) jsonArgument).intValue(); - return new ColorDrawable(colorValue); - default: - getConfigLogger().verbose(getAccountId(), "UIEditor: Unhandled argument object type: " + type); - return null; - } - } catch (final ClassCastException e) { - getConfigLogger().verbose(getAccountId(), - "UIEditor: Error casting class while converting argument - " + e.getLocalizedMessage()); - return null; - } - } - - private Integer checkIds(int explicitId, String idName, ResourceIds idNameToId) { - final int idFromName; - if (idName != null) { - if (idNameToId.knownIdName(idName)) { - idFromName = idNameToId.idFromName(idName); - } else { - getConfigLogger().debug(getAccountId(), - "UIEditor: Path element contains an id name not known to the system. No views will be matched.\n" - + - "Make sure that you're not stripping your packages R class out with proguard.\n" + - "id name was \"" + idName + "\"" - ); - return null; - } - } else { - idFromName = -1; - } - - if (idFromName != -1 && explicitId != -1 && idFromName != explicitId) { - getConfigLogger().debug(getAccountId(), - "UIEditor: Path contains both a named and an explicit id which don't match, can't match."); - return null; - } - - if (-1 != idFromName) { - return idFromName; - } - - return explicitId; - } - - private void clearEdits() { - synchronized (currentEdits) { - while (!currentEdits.isEmpty()) { - currentEdits.removeLast().kill();//removeLast() picks up the last change added to the Deque - } - } - } - - private List generatePath(JSONArray pathDesc, ResourceIds idNameToId) throws JSONException { - final List path = new ArrayList<>(); - for (int i = 0; i < pathDesc.length(); i++) { - final JSONObject targetView = pathDesc.getJSONObject(i); - final String prefixCode = Utils.optionalStringKey(targetView, "prefix"); - final String targetViewClass = Utils.optionalStringKey(targetView, "view_class"); - final int targetIndex = targetView.optInt("index", -1); - final String targetDescription = Utils.optionalStringKey(targetView, "contentDescription"); - final int targetExplicitId = targetView.optInt("id", -1); - final String targetIdName = Utils.optionalStringKey(targetView, "ct_id_name"); - final String targetTag = Utils.optionalStringKey(targetView, "tag"); - - final int prefix; - if (prefixCode == null) { - prefix = ViewEdit.PathElement.ZERO_LENGTH_PREFIX; - } else if (prefixCode.equals("shortest")) { - prefix = ViewEdit.PathElement.SHORTEST_PREFIX; - } else { - getConfigLogger().verbose(getAccountId(), - "UIEditor: Unrecognized prefix type \"" + prefixCode + "\". No views will be matched"); - return NEVER_MATCH_PATH; - } - final int targetId; - final Integer targetIdOrNull = checkIds(targetExplicitId, targetIdName, idNameToId); - if (targetIdOrNull == null) { - return NEVER_MATCH_PATH; - } else { - targetId = targetIdOrNull; - } - path.add(new ViewEdit.PathElement(prefix, targetViewClass, targetIndex, targetId, targetDescription, - targetTag)); - } - - return path; - } - - private UIChange generateUIChange(JSONObject data) { - final ViewEdit viewEdit; - final List imageUrls = new ArrayList<>(); - try { - final JSONArray pathDesc = data.getJSONArray("path"); - final List path = generatePath(pathDesc, resourceIds); - if (path.size() == 0) { - getConfigLogger().verbose("UIEditor: UI change path is empty: " + data.toString()); - return null; - } - if (data.getString("change_type").equals("property")) { - final JSONObject propertyDesc = data.getJSONObject("property"); - final String targetClassName = propertyDesc.getString("classname"); - if (targetClassName == null) { - getConfigLogger().verbose("UIEditor: UI change target classname is missing: " + data.toString()); - return null; - } - final Class targetClass; - try { - targetClass = Class.forName(targetClassName); - } catch (final ClassNotFoundException e) { - getConfigLogger().verbose(getAccountId(), - "UIEditor: Class not found while generating UI change - " + e.getLocalizedMessage()); - return null; - } - final ViewProperty prop = generateViewProperty(targetClass, data.getJSONObject("property")); - final JSONArray argsAndTypes = data.getJSONArray("args"); - final Object[] methodArgs = new Object[argsAndTypes.length()]; - for (int i = 0; i < argsAndTypes.length(); i++) { - final JSONArray argPlusType = argsAndTypes.getJSONArray(i); - final Object jsonArg = argPlusType.get(0); - final String argType = argPlusType.getString(1); - methodArgs[i] = castArgumentObject(jsonArg, argType, imageUrls); - } - - ViewCaller mutator = null; - if (prop != null) { - mutator = prop.createMutator(methodArgs); - } - if (mutator == null) { - getConfigLogger().verbose("UIEditor: UI change unable to create mutator: " + data.toString()); - return null; - } - viewEdit = new ViewEdit(path, mutator, prop.accessor, context); - } else { - getConfigLogger().verbose("UIEditor: UI change type is unknown: " + data.toString()); - return null; - } - } catch (final NoSuchMethodException e) { - getConfigLogger().verbose(getAccountId(), - "UIEditor: No such method found while generating UI change - " + e.getLocalizedMessage()); - return null; - } catch (final JSONException e) { - getConfigLogger().verbose(getAccountId(), - "UIEditor: Unable to parse JSON while generating UI change - " + e.getLocalizedMessage()); - return null; - } - return new UIChange(viewEdit, imageUrls); - } - - private ViewProperty generateViewProperty(Class targetClass, JSONObject property) { - try { - final String propName = property.getString("name"); - - ViewCaller accessor = null; - if (property.has("get")) { - final JSONObject accessorConfig = property.getJSONObject("get"); - final String accessorName = accessorConfig.getString("selector"); - final String accessorResultTypeName = accessorConfig.getJSONObject("result").getString("type"); - final Class accessorResultType = Class.forName(accessorResultTypeName); - accessor = new ViewCaller(targetClass, accessorName, EMPTY_PARAMS, accessorResultType); - } - - final String mutatorName; - if (property.has("set")) { - final JSONObject mutatorConfig = property.getJSONObject("set"); - mutatorName = mutatorConfig.getString("selector"); - } else { - mutatorName = null; - } - return new ViewProperty(propName, targetClass, accessor, mutatorName); - } catch (final NoSuchMethodException e) { - getConfigLogger().verbose("UIEditor: Error generating view property", e); - return null; - } catch (final JSONException e) { - getConfigLogger().verbose("UIEditor: Error generating view property", e); - return null; - } catch (final ClassNotFoundException e) { - getConfigLogger().verbose("UIEditor: Error generating view property", e); - return null; - } - } - - private String getAccountId() { - return config.getAccountId(); - } - - private Logger getConfigLogger() { - return config.getLogger(); - } - - private Bitmap getOrFetchBitmap(String key) { - initImageCache(); - return ImageCache.getOrFetchBitmap(key); - } - - // Ony call on UI Thread - private void handleNewEdits() { - for (final Activity activity : activitySet.getAll()) { - final String activityName = activity.getClass().getCanonicalName(); - final View rootView = activity.getWindow().getDecorView().getRootView(); - - final List specific; - final List wildcard; - synchronized (newEdits) { - specific = newEdits.get(activityName); - wildcard = newEdits.get(null); - } - if (specific != null) { - applyEdits(rootView, specific); - } - if (wildcard != null) { - applyEdits(rootView, wildcard); - } - } - } - - private void handleNewEditsOnUiThread() { - if (Thread.currentThread() == uiThreadHandler.getLooper().getThread()) { - handleNewEdits(); - } else { - uiThreadHandler.post(new Runnable() { - @Override - public void run() { - handleNewEdits(); - } - }); - } - } - - // only call off main thread as initWithPersistence touches the file system - private void initImageCache() { - ImageCache.initWithPersistence(context); - } - - private List loadViewProperties(JSONObject data) { - final List properties = new ArrayList<>(); - try { - final JSONObject config = data.getJSONObject("config"); - final JSONArray classes = config.getJSONArray("classes"); - for (int i = 0; i < classes.length(); i++) { - final JSONObject classDesc = classes.getJSONObject(i); - final String targetName = classDesc.getString("name"); - final Class targetClass = Class.forName(targetName); - final JSONArray props = classDesc.getJSONArray("properties"); - for (int j = 0; j < props.length(); j++) { - final JSONObject prop = props.getJSONObject(j); - final ViewProperty desc = generateViewProperty(targetClass, prop); - properties.add(desc); - } - } - } catch (JSONException e) { - getConfigLogger().verbose("UIEditor: Error loading view properties json: " + data.toString()); - return null; - } catch (final ClassNotFoundException e) { - getConfigLogger().verbose("UIEditor: Error loading view properties", e); - return null; - } - return properties; - } - - private Drawable readBitmapDrawable(JSONObject description, List imageUrls) { - try { - final String url = description.getString("url"); - final boolean useBounds; - final int left; - final int right; - final int top; - final int bottom; - if (description.isNull("dimensions")) { - left = right = top = bottom = 0; - useBounds = false; - } else { - final JSONObject dimensions = description.getJSONObject("dimensions"); - left = dimensions.getInt("left"); - right = dimensions.getInt("right"); - top = dimensions.getInt("top"); - bottom = dimensions.getInt("bottom"); - useBounds = true; - } - - final Bitmap image; - image = getOrFetchBitmap(url); - imageUrls.add(url); - - final Drawable ret = new BitmapDrawable(Resources.getSystem(), image); - if (useBounds) { - ret.setBounds(left, top, right, bottom); - } - - return ret; - } catch (JSONException e) { - getConfigLogger().verbose(getAccountId(), - "UIEditor: Unable to parse JSON while reading Bitmap from payload - " + e.getLocalizedMessage()); - return null; - } - } - -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/uieditor/ViewCaller.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/uieditor/ViewCaller.java deleted file mode 100644 index acc7e6995..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/uieditor/ViewCaller.java +++ /dev/null @@ -1,150 +0,0 @@ -package com.clevertap.android.sdk.ab_testing.uieditor; - -import android.view.View; -import com.clevertap.android.sdk.Logger; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -class ViewCaller { - - private final Object[] methodArgs; - - private final String methodName; - - private final Class methodResultType; - - private final Class targetClass; - - private final Method targetMethod; - - ViewCaller(Class targetClass, String methodName, Object[] methodArgs, Class resultType) - throws NoSuchMethodException { - this.methodName = methodName; - this.methodArgs = methodArgs; - this.methodResultType = resultType; - targetMethod = findMethod(targetClass); - if (null == targetMethod) { - throw new NoSuchMethodException("Method " + targetClass.getName() + "." + methodName + " doesn't exit"); - } - this.targetClass = targetMethod.getDeclaringClass(); - } - - boolean argsAreApplicable(Object[] proposedArgs) { - final Class[] paramTypes = targetMethod.getParameterTypes(); - if (proposedArgs.length != paramTypes.length) { - return false; - } - - for (int i = 0; i < proposedArgs.length; i++) { - final Class paramType = assignableArgType(paramTypes[i]); - if (proposedArgs[i] == null) { - if (paramType == byte.class || - paramType == short.class || - paramType == int.class || - paramType == long.class || - paramType == float.class || - paramType == double.class || - paramType == boolean.class || - paramType == char.class) { - return false; - } - } else { - final Class argumentType = assignableArgType(proposedArgs[i].getClass()); - if (argumentType.getCanonicalName() != null - && (argumentType.getCanonicalName().equals("android.content.res.ColorStateList") || - argumentType.getCanonicalName().equals("android.graphics.drawable.ColorDrawable") || - argumentType.getCanonicalName().equals("android.graphics.drawable.RippleDrawable"))) { - //no-op to skip - } else if (!paramType.isAssignableFrom(argumentType)) { - return false; - } - } - } - return true; - } - - Object[] getArgs() { - return methodArgs; - } - - String getMethodName() { - return methodName; - } - - Object invokeMethod(View target) { - return invokeMethodWithArgs(target, methodArgs); - } - - Object invokeMethodWithArgs(View target, Object[] arguments) { - final Class klass = target.getClass(); - if (targetClass.isAssignableFrom(klass)) { - try { - return targetMethod.invoke(target, arguments); - } catch (final IllegalAccessException e) { - Logger.v("Method " + targetMethod.getName() + " appears not to be public", e); - } catch (final IllegalArgumentException e) { - Logger.v("Method " + targetMethod.getName() + " called with arguments of the wrong type", e); - } catch (final InvocationTargetException e) { - Logger.v("Method " + targetMethod.getName() + " threw an exception", e); - } - } - return null; - } - - private Method findMethod(Class klass) { - final Class[] argumentTypes = new Class[methodArgs.length]; - for (int i = 0; i < methodArgs.length; i++) { - argumentTypes[i] = methodArgs[i].getClass(); - } - - for (final Method method : klass.getMethods()) { - final String foundName = method.getName(); - final Class[] params = method.getParameterTypes(); - - if (!foundName.equals(methodName) || params.length != methodArgs.length) { - continue; - } - - final Class assignType = assignableArgType(methodResultType); - final Class resultType = assignableArgType(method.getReturnType()); - if (!assignType.isAssignableFrom(resultType)) { - continue; - } - - boolean assignable = true; - for (int i = 0; i < params.length && assignable; i++) { - final Class argumentType = assignableArgType(argumentTypes[i]); - final Class paramType = assignableArgType(params[i]); - assignable = paramType.isAssignableFrom(argumentType); - } - - if (!assignable) { - continue; - } - - return method; - } - return null; - } - - private static Class assignableArgType(Class type) { - if (type == Byte.class) { - type = byte.class; - } else if (type == Short.class) { - type = short.class; - } else if (type == Integer.class) { - type = int.class; - } else if (type == Long.class) { - type = long.class; - } else if (type == Float.class) { - type = float.class; - } else if (type == Double.class) { - type = double.class; - } else if (type == Boolean.class) { - type = boolean.class; - } else if (type == Character.class) { - type = char.class; - } - return type; - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/uieditor/ViewEdit.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/uieditor/ViewEdit.java deleted file mode 100644 index ed42169da..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/uieditor/ViewEdit.java +++ /dev/null @@ -1,331 +0,0 @@ -package com.clevertap.android.sdk.ab_testing.uieditor; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import com.clevertap.android.sdk.Logger; -import java.util.List; -import java.util.Map; -import java.util.WeakHashMap; -import org.json.JSONException; -import org.json.JSONObject; - -class ViewEdit { - - static class PathElement { - - static final int ZERO_LENGTH_PREFIX = 0; - - static final int SHORTEST_PREFIX = 1; - - public final int index; - - final String contentDescription; - - final int prefix; - - final String tag; - - final String viewClassName; - - final int viewId; - - PathElement(int usePrefix, String className, int idx, int id, String cDesc, String vTag) { - prefix = usePrefix; - viewClassName = className; - index = idx; - viewId = id; - contentDescription = cDesc; - tag = vTag; - } - - @NonNull - @Override - public String toString() { - try { - final JSONObject s = new JSONObject(); - if (prefix == SHORTEST_PREFIX) { - s.put("prefix", "shortest"); - } - if (null != viewClassName) { - s.put("view_class", viewClassName); - } - if (index > -1) { - s.put("index", index); - } - if (viewId > -1) { - s.put("id", viewId); - } - if (null != contentDescription) { - s.put("contentDescription", contentDescription); - } - if (null != tag) { - s.put("tag", tag); - } - return s.toString(); - } catch (final JSONException e) { - throw new RuntimeException("Can't serialize PathElement to String", e); - } - } - } - - private class Pathfinder { - - private class IntStack { - - private static final int MAX_SIZE = 256; - - private final int[] stack; - - private int stackSize; - - IntStack() { - stack = new int[MAX_SIZE]; - stackSize = 0; - } - - public void free() { - stackSize--; - if (stackSize < 0) { - throw new ArrayIndexOutOfBoundsException(stackSize); - } - } - - public int read(int index) { - return stack[index]; - } - - int allocate() { - final int index = stackSize; - stackSize++; - stack[index] = 0; - return index; - } - - void increment(int index) { - stack[index]++; - } - - boolean isFull() { - return stack.length == stackSize; - } - } - - private final IntStack indexStack; - - Pathfinder() { - indexStack = new Pathfinder.IntStack(); - } - - void findTargetsInRoot(View givenRootView, List path, ViewEdit viewEdit) { - if (path.isEmpty()) { - return; - } - if (indexStack.isFull()) { - Logger.v( - "There appears to be a concurrency issue in the pathfinding code. Path will not be matched."); - return; - } - - final PathElement rootPathElement = path.get(0); - final List childPath = path.subList(1, path.size()); - - final int indexKey = indexStack.allocate(); - final View rootView = findMatch(rootPathElement, givenRootView, indexKey); - indexStack.free(); - - if (rootView != null) { - findTargetsInMatchedView(rootView, childPath, viewEdit); - } - } - - private View findMatch(PathElement pathElement, View target, int indexKey) { - final int currentIndex = indexStack.read(indexKey); - if (matches(pathElement, target)) { - indexStack.increment(indexKey); - if (pathElement.index == -1 || pathElement.index == currentIndex) { - return target; - } - } - if (pathElement.prefix == PathElement.SHORTEST_PREFIX && target instanceof ViewGroup) { - final ViewGroup group = (ViewGroup) target; - final int childCount = group.getChildCount(); - for (int i = 0; i < childCount; i++) { - final View child = group.getChildAt(i); - final View result = findMatch(pathElement, child, indexKey); - if (null != result) { - return result; - } - } - } - - return null; - } - - private void findTargetsInMatchedView(View alreadyMatched, List remainingPath, - ViewEdit viewEdit) { - if (remainingPath.isEmpty()) { - // found, apply the edit - viewEdit.apply(alreadyMatched); - return; - } - - if (!(alreadyMatched instanceof ViewGroup)) { - // Not possible as there are no children - return; - } - - if (indexStack.isFull()) { - Logger.v("Path too deep, will not match"); - return; - } - - final ViewGroup parent = (ViewGroup) alreadyMatched; - final PathElement pathElement = remainingPath.get(0); - final List next = remainingPath.subList(1, remainingPath.size()); - - final int childCount = parent.getChildCount(); - final int indexKey = indexStack.allocate(); - for (int i = 0; i < childCount; i++) { - final View givenChild = parent.getChildAt(i); - final View child = findMatch(pathElement, givenChild, indexKey); - if (child != null) { - findTargetsInMatchedView(child, next, viewEdit); - } - if (pathElement.index >= 0 && indexStack.read(indexKey) > pathElement.index) { - break; - } - } - indexStack.free(); - } - - private boolean hasClassName(Object o, String className) { - Class klass = o.getClass(); - while (true) { - //noinspection ConstantConditions - String klassCanonicalName = klass.getCanonicalName(); - return klassCanonicalName != null && klassCanonicalName.equals(className); - } - } - - private boolean matches(PathElement pathElement, View target) { - if (pathElement.viewClassName != null && !hasClassName(target, pathElement.viewClassName)) { - return false; - } - if (pathElement.viewId != -1 && (target.getId() != pathElement.viewId)) { - return false; - } - if (pathElement.contentDescription != null && !pathElement.contentDescription - .contentEquals(target.getContentDescription())) { - return false; - } - - final String matchTag = pathElement.tag; - if (pathElement.tag != null) { - final Object targetTag = target.getTag(); - return targetTag != null && matchTag.equals(target.getTag().toString()); - } - return true; - } - } - - private final ViewCaller accessor; - - private Context context; - - private final ViewCaller mutator; - - private final Object[] originalValueHolder; - - private final WeakHashMap originalValues; - - private final List path; - - private final Pathfinder pathFinder; - - ViewEdit(List path, ViewCaller mutator, ViewCaller accessor, Context context) { - this.path = path; - pathFinder = new Pathfinder(); - this.mutator = mutator; - this.accessor = accessor; - originalValueHolder = new Object[1]; - originalValues = new WeakHashMap<>(); - this.context = context; - } - - protected String name() { - return "Property Mutator"; - } - - void cleanup() { - for (Map.Entry original : originalValues.entrySet()) { - final View changedView = original.getKey(); - final Object originalValue = original.getValue(); - if (originalValue != null) { - if (originalValue instanceof ColorStateList) { - originalValueHolder[0] = ((ColorStateList) originalValue).getDefaultColor(); - } else { - originalValueHolder[0] = originalValue; - } - mutator.invokeMethodWithArgs(changedView, originalValueHolder); - } - } - } - - void run(View rootView) { - pathFinder.findTargetsInRoot(rootView, getPath(), this); - } - - private void apply(View targetView) { - if (accessor != null) { - final Object[] args = mutator.getArgs(); - if (args.length == 1) { - final Object targetValue = args[0]; - Object currentValue = accessor.invokeMethod(targetView); - - if (accessor.getMethodName().equals("getTextSize")) { - currentValue = (float) currentValue / context.getResources().getDisplayMetrics().scaledDensity; - } - - if (targetValue == currentValue) { - return; - } - - if (targetValue != null) { - if (targetValue instanceof Bitmap && currentValue instanceof Bitmap) { - final Bitmap targetBitmap = (Bitmap) targetValue; - final Bitmap currentBitmap = (Bitmap) currentValue; - if (targetBitmap.sameAs(currentBitmap)) { - return; - } - } else if (targetValue instanceof BitmapDrawable && currentValue instanceof BitmapDrawable) { - final Bitmap targetBitmap = ((BitmapDrawable) targetValue).getBitmap(); - final Bitmap currentBitmap = ((BitmapDrawable) currentValue).getBitmap(); - if (targetBitmap != null && targetBitmap.sameAs(currentBitmap)) { - return; - } - } else if (targetValue.equals(currentValue)) { - return; - } - } - if (!originalValues.containsKey(targetView)) { - originalValueHolder[0] = currentValue; - if (mutator.argsAreApplicable(originalValueHolder)) { - originalValues.put(targetView, currentValue); - } else { - originalValues.put(targetView, null); - } - } - } - } - mutator.invokeMethod(targetView); - } - - private List getPath() { - return this.path; - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/uieditor/ViewProperty.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/uieditor/ViewProperty.java deleted file mode 100644 index a774dc0b0..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/ab_testing/uieditor/ViewProperty.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.clevertap.android.sdk.ab_testing.uieditor; - -import androidx.annotation.NonNull; - -class ViewProperty { - - public final String name; - - final ViewCaller accessor; - - final Class target; - - private final String mutator; - - ViewProperty(String name, Class targetClass, ViewCaller accessor, String mutatorName) { - this.name = name; - this.target = targetClass; - this.accessor = accessor; - this.mutator = mutatorName; - } - - @NonNull - @Override - public String toString() { - return "ViewProperty " + name + "," + target + ", " + accessor + "/" + mutator; - } - - ViewCaller createMutator(Object[] methodArgs) throws NoSuchMethodException { - return mutator == null ? null : new ViewCaller(this.target, mutator, methodArgs, Void.TYPE); - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CloseImageView.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/customviews/CloseImageView.java similarity index 92% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CloseImageView.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/customviews/CloseImageView.java index 93886ea5e..30c044f4f 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CloseImageView.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/customviews/CloseImageView.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.customviews; import android.annotation.SuppressLint; import android.content.Context; @@ -9,11 +9,13 @@ import android.util.AttributeSet; import android.util.TypedValue; import androidx.appcompat.widget.AppCompatImageView; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.Logger; /** * Represents the close button. */ -final class CloseImageView extends AppCompatImageView { +public final class CloseImageView extends AppCompatImageView { private final int canvasSize = getScaledPixels(Constants.INAPP_CLOSE_IV_WIDTH); @@ -54,7 +56,6 @@ protected void onDraw(Canvas canvas) { } } catch (Throwable t) { Logger.v("Error displaying the inapp notif close button image:", t); - } } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/HorizontalSquareImageView.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/customviews/HorizontalSquareImageView.java similarity index 94% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/HorizontalSquareImageView.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/customviews/HorizontalSquareImageView.java index 4c0241a93..218305d22 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/HorizontalSquareImageView.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/customviews/HorizontalSquareImageView.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.customviews; import android.annotation.SuppressLint; import android.content.Context; diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/MediaPlayerRecyclerView.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/customviews/MediaPlayerRecyclerView.java similarity index 94% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/MediaPlayerRecyclerView.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/customviews/MediaPlayerRecyclerView.java index 7ca9dda06..d7cc16f29 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/MediaPlayerRecyclerView.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/customviews/MediaPlayerRecyclerView.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.customviews; import android.content.Context; import android.content.res.Configuration; @@ -10,9 +10,14 @@ import android.view.ViewGroup; import android.widget.AbsListView; import androidx.annotation.NonNull; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; import androidx.core.content.res.ResourcesCompat; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import com.clevertap.android.sdk.R; +import com.clevertap.android.sdk.inbox.CTInboxActivity; +import com.clevertap.android.sdk.inbox.CTInboxBaseMessageViewHolder; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; @@ -26,7 +31,7 @@ import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.PlayerView; -@SuppressWarnings("unused") +@RestrictTo(Scope.LIBRARY) public class MediaPlayerRecyclerView extends RecyclerView { SimpleExoPlayer player; @@ -226,11 +231,11 @@ public void onLoadingChanged(boolean isLoading) { } @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + public void onPlaybackParametersChanged(@NonNull PlaybackParameters playbackParameters) { } @Override - public void onPlayerError(ExoPlaybackException error) { + public void onPlayerError(@NonNull ExoPlaybackException error) { } @Override @@ -278,7 +283,7 @@ public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { } @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + public void onTracksChanged(@NonNull TrackGroupArray trackGroups, @NonNull TrackSelectionArray trackSelections) { } }); } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/RectangleImageView.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/customviews/RectangleImageView.java similarity index 95% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/RectangleImageView.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/customviews/RectangleImageView.java index 39f7f6f06..ae511bd66 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/RectangleImageView.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/customviews/RectangleImageView.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.customviews; import android.annotation.SuppressLint; import android.content.Context; diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/SquareImageView.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/customviews/SquareImageView.java similarity index 94% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/SquareImageView.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/customviews/SquareImageView.java index f82e136bf..108849f20 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/SquareImageView.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/customviews/SquareImageView.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.customviews; import android.annotation.SuppressLint; import android.content.Context; diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/VerticalSpaceItemDecoration.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/customviews/VerticalSpaceItemDecoration.java similarity index 71% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/VerticalSpaceItemDecoration.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/customviews/VerticalSpaceItemDecoration.java index dde88ad82..102cfd96d 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/VerticalSpaceItemDecoration.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/customviews/VerticalSpaceItemDecoration.java @@ -1,15 +1,18 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.customviews; import android.graphics.Rect; import android.view.View; import androidx.annotation.NonNull; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; import androidx.recyclerview.widget.RecyclerView; +@RestrictTo(Scope.LIBRARY) public class VerticalSpaceItemDecoration extends RecyclerView.ItemDecoration { private final int verticalSpaceHeight; - VerticalSpaceItemDecoration(int verticalSpaceHeight) { + public VerticalSpaceItemDecoration(int verticalSpaceHeight) { this.verticalSpaceHeight = verticalSpaceHeight; } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/db/BaseDatabaseManager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/db/BaseDatabaseManager.java new file mode 100644 index 000000000..6e88c9354 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/db/BaseDatabaseManager.java @@ -0,0 +1,35 @@ +package com.clevertap.android.sdk.db; + +import android.content.Context; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; +import com.clevertap.android.sdk.events.EventGroup; +import org.json.JSONObject; + +@RestrictTo(Scope.LIBRARY_GROUP) +public abstract class BaseDatabaseManager { + + public abstract void clearQueues(final Context context); + + public abstract QueueCursor getQueuedEvents(final Context context, final int batchSize, + final QueueCursor previousCursor, + final EventGroup eventGroup); + + public abstract void queueEventToDB(final Context context, final JSONObject event, final int type); + + abstract QueueCursor updateCursorForDBObject(JSONObject dbObject, QueueCursor cursor); + + public abstract DBAdapter loadDBAdapter(Context context); + + abstract QueueCursor getQueueCursor(final Context context, DBAdapter.Table table, final int batchSize, + final QueueCursor previousCursor); + + abstract QueueCursor getQueuedDBEvents(final Context context, final int batchSize, + final QueueCursor previousCursor); + + abstract QueueCursor getPushNotificationViewedQueuedEvents(final Context context, final int batchSize, + final QueueCursor previousCursor); + + public abstract void queuePushNotificationViewedEventToDB(final Context context, final JSONObject event); + +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/DBAdapter.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/db/DBAdapter.java similarity index 95% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/DBAdapter.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/db/DBAdapter.java index bfebaacb5..e6bbd48e4 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/DBAdapter.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/db/DBAdapter.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.db; import android.annotation.SuppressLint; import android.content.ContentValues; @@ -8,14 +8,20 @@ import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteStatement; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.Logger; +import com.clevertap.android.sdk.inbox.CTMessageDAO; import java.io.File; import java.util.ArrayList; import java.util.List; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; - -class DBAdapter { +@RestrictTo(Scope.LIBRARY) +public class DBAdapter { @SuppressWarnings("FieldCanBeLocal") private static class DatabaseHelper extends SQLiteOpenHelper { @@ -199,7 +205,7 @@ public String getName() { private static final String KEY_CREATED_AT = "created_at"; - private static final long DATA_EXPIRATION = 1000 * 60 * 60 * 24 * 5; + private static final long DATA_EXPIRATION = 1000L * 60 * 60 * 24 * 5; //Notification Inbox Messages Table fields private static final String _ID = "_id"; @@ -303,7 +309,7 @@ public String getName() { private boolean rtlDirtyFlag = true; - DBAdapter(Context context, CleverTapInstanceConfig config) { + public DBAdapter(Context context, CleverTapInstanceConfig config) { this(context, getDatabaseName(config)); this.config = config; @@ -355,7 +361,7 @@ synchronized void cleanupStaleEvents(Table table) { * @return boolean value based on success of operation */ @SuppressWarnings("UnusedReturnValue") - synchronized boolean deleteMessageForId(String messageId, String userId) { + public synchronized boolean deleteMessageForId(String messageId, String userId) { if (messageId == null || userId == null) { return false; } @@ -374,7 +380,7 @@ synchronized boolean deleteMessageForId(String messageId, String userId) { } } - synchronized boolean doesPushNotificationIdExist(String id) { + public synchronized boolean doesPushNotificationIdExist(String id) { return id.equals(fetchPushNotificationId(id)); } @@ -430,7 +436,7 @@ synchronized JSONObject fetchEvents(Table table, final int limit) { return null; } - synchronized String[] fetchPushNotificationIds() { + public synchronized String[] fetchPushNotificationIds() { if (!rtlDirtyFlag) { return new String[0]; } @@ -460,7 +466,7 @@ synchronized String[] fetchPushNotificationIds() { return pushIds.toArray(new String[0]); } - synchronized JSONObject fetchUserProfileById(final String id) { + public synchronized JSONObject fetchUserProfileById(final String id) { if (id == null) { return null; @@ -494,7 +500,7 @@ synchronized JSONObject fetchUserProfileById(final String id) { return profile; } - synchronized long getLastUninstallTimestamp() { + public synchronized long getLastUninstallTimestamp() { final String tName = Table.UNINSTALL_TS.getName(); Cursor cursor = null; long timestamp = 0; @@ -522,7 +528,7 @@ synchronized long getLastUninstallTimestamp() { * @param userId String userid * @return ArrayList of {@link CTMessageDAO} */ - synchronized ArrayList getMessages(String userId) { + public synchronized ArrayList getMessages(String userId) { final String tName = Table.INBOX_MESSAGES.getName(); Cursor cursor; ArrayList messageDAOArrayList = new ArrayList<>(); @@ -565,7 +571,7 @@ synchronized ArrayList getMessages(String userId) { * @return boolean value depending on success of operation */ @SuppressWarnings("UnusedReturnValue") - synchronized boolean markReadMessageForId(String messageId, String userId) { + public synchronized boolean markReadMessageForId(String messageId, String userId) { if (messageId == null || userId == null) { return false; } @@ -608,7 +614,7 @@ synchronized void removeEvents(Table table) { /** * remove the user profile with id from the db. */ - synchronized void removeUserProfile(String id) { + public synchronized void removeUserProfile(String id) { if (id == null) { return; @@ -662,7 +668,7 @@ synchronized int storeObject(JSONObject obj, Table table) { return (int) count; } - synchronized void storePushNotificationId(String id, long ttl) { + public synchronized void storePushNotificationId(String id, long ttl) { if (id == null) { return; @@ -698,7 +704,7 @@ synchronized void storePushNotificationId(String id, long ttl) { /** * Adds a String timestamp representing uninstall flag to the DB. */ - synchronized void storeUninstallTimestamp() { + public synchronized void storeUninstallTimestamp() { if (!this.belowMemThreshold()) { getConfigLogger().verbose("There is not enough space left on the device to store data, data discarded"); @@ -726,7 +732,7 @@ synchronized void storeUninstallTimestamp() { * @param obj the JSON to record * @return the number of rows in the table, or DB_OUT_OF_MEMORY_ERROR/DB_UPDATE_ERROR */ - synchronized long storeUserProfile(String id, JSONObject obj) { + public synchronized long storeUserProfile(String id, JSONObject obj) { if (id == null) { return DB_UPDATE_ERROR; @@ -756,7 +762,7 @@ synchronized long storeUserProfile(String id, JSONObject obj) { return ret; } - synchronized void updatePushNotificationIds(String[] ids) { + public synchronized void updatePushNotificationIds(String[] ids) { if (ids.length == 0) { return; } @@ -792,7 +798,7 @@ synchronized void updatePushNotificationIds(String[] ids) { * * @param inboxMessages ArrayList of type {@link CTMessageDAO} */ - synchronized void upsertMessages(ArrayList inboxMessages) { + public synchronized void upsertMessages(ArrayList inboxMessages) { if (!this.belowMemThreshold()) { Logger.v("There is not enough space left on the device to store data, data discarded"); return; diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/db/DBManager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/db/DBManager.java new file mode 100644 index 000000000..6ea3bf0e3 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/db/DBManager.java @@ -0,0 +1,186 @@ +package com.clevertap.android.sdk.db; + +import android.content.Context; +import android.content.SharedPreferences; +import com.clevertap.android.sdk.CTLockManager; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.StorageHelper; +import com.clevertap.android.sdk.db.DBAdapter.Table; +import com.clevertap.android.sdk.events.EventGroup; +import java.util.Iterator; +import org.json.JSONException; +import org.json.JSONObject; + +public class DBManager extends BaseDatabaseManager { + + private DBAdapter dbAdapter; + + private final CTLockManager ctLockManager; + + private final CleverTapInstanceConfig config; + + public DBManager(CleverTapInstanceConfig config, + CTLockManager ctLockManager) { + this.config = config; + this.ctLockManager = ctLockManager; + } + + @Override + public DBAdapter loadDBAdapter(final Context context) { + if (dbAdapter == null) { + dbAdapter = new DBAdapter(context, config); + dbAdapter.cleanupStaleEvents(DBAdapter.Table.EVENTS); + dbAdapter.cleanupStaleEvents(DBAdapter.Table.PROFILE_EVENTS); + dbAdapter.cleanupStaleEvents(DBAdapter.Table.PUSH_NOTIFICATION_VIEWED); + dbAdapter.cleanUpPushNotifications(); + } + return dbAdapter; + } + + /** + * Only call async + */ + @Override + public void clearQueues(final Context context) { + synchronized (ctLockManager.getEventLock()) { + + DBAdapter adapter = loadDBAdapter(context); + DBAdapter.Table tableName = DBAdapter.Table.EVENTS; + + adapter.removeEvents(tableName); + tableName = DBAdapter.Table.PROFILE_EVENTS; + adapter.removeEvents(tableName); + + clearUserContext(context); + } + } + + //Session + private void clearIJ(Context context) { + final SharedPreferences prefs = StorageHelper.getPreferences(context, Constants.NAMESPACE_IJ); + final SharedPreferences.Editor editor = prefs.edit(); + editor.clear(); + StorageHelper.persist(editor); + } + + //Session + private void clearLastRequestTimestamp(Context context) { + StorageHelper.putInt(context, StorageHelper.storageKeyWithSuffix(config, Constants.KEY_LAST_TS), 0); + } + + //Session + private void clearUserContext(final Context context) { + clearIJ(context); + clearFirstRequestTimestampIfNeeded(context); + clearLastRequestTimestamp(context); + } + //Session + private void clearFirstRequestTimestampIfNeeded(Context context) { + StorageHelper.putInt(context, StorageHelper.storageKeyWithSuffix(config, Constants.KEY_FIRST_TS), 0); + } + + // helper extracts the cursor data from the db object + + @Override + QueueCursor getPushNotificationViewedQueuedEvents(final Context context, final int batchSize, + final QueueCursor previousCursor) { + return getQueueCursor(context, DBAdapter.Table.PUSH_NOTIFICATION_VIEWED, batchSize, previousCursor); + } + + @Override + QueueCursor getQueueCursor(final Context context, final Table table, final int batchSize, + final QueueCursor previousCursor) { + synchronized (ctLockManager.getEventLock()) { + DBAdapter adapter = loadDBAdapter(context); + DBAdapter.Table tableName = (previousCursor != null) ? previousCursor.getTableName() : table; + + // if previousCursor that means the batch represented by the previous cursor was processed so remove those from the db + if (previousCursor != null) { + adapter.cleanupEventsFromLastId(previousCursor.getLastId(), previousCursor.getTableName()); + } + + // grab the new batch + QueueCursor newCursor = new QueueCursor(); + newCursor.setTableName(tableName); + JSONObject queuedDBEvents = adapter.fetchEvents(tableName, batchSize); + newCursor = updateCursorForDBObject(queuedDBEvents, newCursor); + + return newCursor; + } + } + + @Override + QueueCursor getQueuedDBEvents(final Context context, final int batchSize, final QueueCursor previousCursor) { + + synchronized (ctLockManager.getEventLock()) { + QueueCursor newCursor = getQueueCursor(context, DBAdapter.Table.EVENTS, batchSize, previousCursor); + + if (newCursor.isEmpty() && newCursor.getTableName().equals(DBAdapter.Table.EVENTS)) { + newCursor = getQueueCursor(context, DBAdapter.Table.PROFILE_EVENTS, batchSize, null); + } + + return newCursor.isEmpty() ? null : newCursor; + } + } + + @SuppressWarnings("SameParameterValue") + public QueueCursor getQueuedEvents(final Context context, final int batchSize, final QueueCursor previousCursor, + final EventGroup eventGroup) { + if (eventGroup == EventGroup.PUSH_NOTIFICATION_VIEWED) { + config.getLogger().verbose(config.getAccountId(), "Returning Queued Notification Viewed events"); + return getPushNotificationViewedQueuedEvents(context, batchSize, previousCursor); + } else { + config.getLogger().verbose(config.getAccountId(), "Returning Queued events"); + return getQueuedDBEvents(context, batchSize, previousCursor); + } + } + + //Event + @Override + public void queueEventToDB(final Context context, final JSONObject event, final int type) { + DBAdapter.Table table = (type == Constants.PROFILE_EVENT) ? DBAdapter.Table.PROFILE_EVENTS + : DBAdapter.Table.EVENTS; + queueEventInternal(context, event, table); + } + + @Override + public void queuePushNotificationViewedEventToDB(final Context context, final JSONObject event) { + queueEventInternal(context, event, DBAdapter.Table.PUSH_NOTIFICATION_VIEWED); + } + + @Override + QueueCursor updateCursorForDBObject(final JSONObject dbObject, final QueueCursor cursor) { + if (dbObject == null) { + return cursor; + } + + Iterator keys = dbObject.keys(); + if (keys.hasNext()) { + String key = keys.next(); + cursor.setLastId(key); + try { + cursor.setData(dbObject.getJSONArray(key)); + } catch (JSONException e) { + cursor.setLastId(null); + cursor.setData(null); + } + } + + return cursor; + } + + private void queueEventInternal(final Context context, final JSONObject event, DBAdapter.Table table) { + synchronized (ctLockManager.getEventLock()) { + DBAdapter adapter = loadDBAdapter(context); + int returnCode = adapter.storeObject(event, table); + + if (returnCode > 0) { + config.getLogger().debug(config.getAccountId(), "Queued event: " + event.toString()); + config.getLogger() + .verbose(config.getAccountId(), + "Queued event to DB table " + table + ": " + event.toString()); + } + } + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/QueueCursor.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/db/QueueCursor.java similarity index 89% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/QueueCursor.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/db/QueueCursor.java index 115673435..672ca8640 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/QueueCursor.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/db/QueueCursor.java @@ -1,9 +1,9 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.db; import org.json.JSONArray; @SuppressWarnings("unused") -final class QueueCursor { +public final class QueueCursor { private JSONArray data; // the db objects @@ -18,7 +18,7 @@ public String toString() { + data.toString(); } - JSONArray getData() { + public JSONArray getData() { return data; } @@ -42,7 +42,7 @@ void setTableName(DBAdapter.Table tableName) { this.tableName = tableName; } - Boolean isEmpty() { + public Boolean isEmpty() { return (lastId == null || data == null || data.length() <= 0); } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/displayunits/CTDisplayUnitController.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/displayunits/CTDisplayUnitController.java index 88d1c90ff..ac073336f 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/displayunits/CTDisplayUnitController.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/displayunits/CTDisplayUnitController.java @@ -15,7 +15,7 @@ */ public class CTDisplayUnitController { - private final HashMap items = new HashMap<>(); + final HashMap items = new HashMap<>(); /** * Getter for retrieving all the running Display Units in the cache. @@ -23,7 +23,7 @@ public class CTDisplayUnitController { * @return ArrayList - Could be null in case no Display Units are there in the cache */ @Nullable - public ArrayList getAllDisplayUnits() { + public synchronized ArrayList getAllDisplayUnits() { if (!items.isEmpty()) { return new ArrayList<>(items.values()); } else { @@ -39,7 +39,7 @@ public ArrayList getAllDisplayUnits() { * @return CleverTapDisplayUnit - Could be null in case no Display Units with the ID is found */ @Nullable - public CleverTapDisplayUnit getDisplayUnitForID(String unitId) { + public synchronized CleverTapDisplayUnit getDisplayUnitForID(String unitId) { if (!TextUtils.isEmpty(unitId)) { return items.get(unitId); } else { @@ -51,7 +51,7 @@ public CleverTapDisplayUnit getDisplayUnitForID(String unitId) { /** * clears the existing Display Units */ - public void reset() { + public synchronized void reset() { items.clear(); Logger.d(Constants.FEATURE_DISPLAY_UNIT, "Cleared Display Units Cache"); } @@ -63,7 +63,7 @@ public void reset() { * @return ArrayList - could be null in case of null/empty/invalid json array */ @Nullable - public ArrayList updateDisplayUnits(JSONArray messages) { + public synchronized ArrayList updateDisplayUnits(JSONArray messages) { //flush existing display units before updating with the new ones. reset(); diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/displayunits/CTDisplayUnitType.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/displayunits/CTDisplayUnitType.java index 2ff433f0f..0e3ddb9fe 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/displayunits/CTDisplayUnitType.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/displayunits/CTDisplayUnitType.java @@ -18,7 +18,7 @@ public enum CTDisplayUnitType { MESSAGE_WITH_ICON("message-icon"), CUSTOM_KEY_VALUE("custom-key-value"); - private final String type; + public final String type; /** * Returns the display type instance using the string value @@ -27,7 +27,7 @@ public enum CTDisplayUnitType { * @return CTDisplayUnitType */ @Nullable - public static CTDisplayUnitType type(String type) { + public static CTDisplayUnitType type(@NonNull String type) { if (!TextUtils.isEmpty(type)) { switch (type) { diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/displayunits/model/CleverTapDisplayUnit.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/displayunits/model/CleverTapDisplayUnit.java index 4d5cabed3..9b8e3a0f0 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/displayunits/model/CleverTapDisplayUnit.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/displayunits/model/CleverTapDisplayUnit.java @@ -290,7 +290,7 @@ public void writeToParcel(Parcel parcel, int i) { * * @param kvObj- Custom Key Values */ - private HashMap getKeyValues(JSONObject kvObj) { + HashMap getKeyValues(JSONObject kvObj) { try { if (kvObj != null) { Iterator keys = kvObj.keys(); diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/displayunits/model/CleverTapDisplayUnitContent.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/displayunits/model/CleverTapDisplayUnitContent.java index dbf6d3cf4..2c8d4a096 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/displayunits/model/CleverTapDisplayUnitContent.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/displayunits/model/CleverTapDisplayUnitContent.java @@ -44,7 +44,6 @@ public CleverTapDisplayUnitContent[] newArray(int size) { private String titleColor; - private CleverTapDisplayUnitContent(String title, String titleColor, String message, String messageColor, String icon, String media, String contentType, String posterUrl, String actionUrl, String error) { @@ -60,6 +59,7 @@ private CleverTapDisplayUnitContent(String title, String titleColor, String mess this.error = error; } + private CleverTapDisplayUnitContent(Parcel in) { title = in.readString(); titleColor = in.readString(); @@ -101,6 +101,10 @@ public String getContentType() { return contentType; } + void setContentType(final String contentType) { + this.contentType = contentType; + } + public String getError() { return error; } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/events/BaseEventQueueManager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/events/BaseEventQueueManager.java new file mode 100644 index 000000000..92ccc0ded --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/events/BaseEventQueueManager.java @@ -0,0 +1,24 @@ +package com.clevertap.android.sdk.events; + +import android.content.Context; +import java.util.concurrent.Future; +import org.json.JSONObject; + +public abstract class BaseEventQueueManager { + + public abstract Future queueEvent(final Context context, final JSONObject event, final int eventType); + + public abstract void addToQueue(final Context context, final JSONObject event, final int eventType); + + public abstract void flush(); + + public abstract void flushQueueAsync(final Context context, final EventGroup eventGroup); + + public abstract void pushBasicProfile(JSONObject baseProfile); + + public abstract void pushInitialEventsAsync(); + + public abstract void flushQueueSync(final Context context, final EventGroup eventGroup); + + public abstract void scheduleQueueFlush(final Context context); +} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/EventDetail.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/events/EventDetail.java similarity index 75% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/EventDetail.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/events/EventDetail.java index 53c351da5..a73a5a362 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/EventDetail.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/events/EventDetail.java @@ -1,11 +1,15 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.events; @SuppressWarnings({"unused", "WeakerAccess"}) public class EventDetail { - private int count, firstTime, lastTime; + private final int count; - private String name; + private final int firstTime; + + private final int lastTime; + + private final String name; public EventDetail(int count, int firstTime, int lastTime, String name) { this.count = count; diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/events/EventGroup.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/events/EventGroup.java new file mode 100644 index 000000000..717d4974b --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/events/EventGroup.java @@ -0,0 +1,13 @@ +package com.clevertap.android.sdk.events; + +public enum EventGroup { + + REGULAR(""), + PUSH_NOTIFICATION_VIEWED("-spiky"); + + public final String httpResource; + + EventGroup(String httpResource) { + this.httpResource = httpResource; + } +} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/events/EventMediator.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/events/EventMediator.java new file mode 100644 index 000000000..ce41af76c --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/events/EventMediator.java @@ -0,0 +1,74 @@ +package com.clevertap.android.sdk.events; + +import android.content.Context; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.CoreMetaData; +import com.clevertap.android.sdk.StorageHelper; +import java.util.Arrays; +import org.json.JSONException; +import org.json.JSONObject; + +public class EventMediator { + + private final CoreMetaData cleverTapMetaData; + + private final CleverTapInstanceConfig config; + + private final Context context; + + public EventMediator(Context context, CleverTapInstanceConfig config, CoreMetaData coreMetaData) { + this.context = context; + this.config = config; + cleverTapMetaData = coreMetaData; + } + + public boolean shouldDeferProcessingEvent(JSONObject event, int eventType) { + //noinspection SimplifiableIfStatement + if (config.isCreatedPostAppLaunch()) { + return false; + } + if (event.has("evtName")) { + try { + if (Arrays.asList(Constants.SYSTEM_EVENTS).contains(event.getString("evtName"))) { + return false; + } + } catch (JSONException e) { + //no-op + } + } + return (eventType == Constants.RAISED_EVENT && !cleverTapMetaData.isAppLaunchPushed()); + } + + public boolean shouldDropEvent(JSONObject event, int eventType) { + if (eventType == Constants.FETCH_EVENT) { + return false; + } + + if (cleverTapMetaData.isCurrentUserOptedOut()) { + String eventString = event == null ? "null" : event.toString(); + config.getLogger() + .debug(config.getAccountId(), "Current user is opted out dropping event: " + eventString); + return true; + } + + if (isMuted()) { + config.getLogger() + .verbose(config.getAccountId(), "CleverTap is muted, dropping event - " + event.toString()); + return true; + } + + return false; + } + + + /** + * @return true if the mute command was sent anytime between now and now - 24 hours. + */ + private boolean isMuted() { + final int now = (int) (System.currentTimeMillis() / 1000); + final int muteTS = StorageHelper.getIntFromPrefs(context, config, Constants.KEY_MUTED, 0); + + return now - muteTS < 24 * 60 * 60; + } +} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/events/EventQueueManager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/events/EventQueueManager.java new file mode 100644 index 000000000..609bbc51a --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/events/EventQueueManager.java @@ -0,0 +1,482 @@ +package com.clevertap.android.sdk.events; + +import static com.clevertap.android.sdk.utils.CTJsonConverter.getErrorObject; + +import android.content.Context; +import com.clevertap.android.sdk.BaseCallbackManager; +import com.clevertap.android.sdk.CTLockManager; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.CoreMetaData; +import com.clevertap.android.sdk.DeviceInfo; +import com.clevertap.android.sdk.FailureFlushListener; +import com.clevertap.android.sdk.LocalDataStore; +import com.clevertap.android.sdk.Logger; +import com.clevertap.android.sdk.SessionManager; +import com.clevertap.android.sdk.Utils; +import com.clevertap.android.sdk.db.BaseDatabaseManager; +import com.clevertap.android.sdk.login.IdentityRepo; +import com.clevertap.android.sdk.login.IdentityRepoFactory; +import com.clevertap.android.sdk.login.LoginInfoProvider; +import com.clevertap.android.sdk.network.BaseNetworkManager; +import com.clevertap.android.sdk.network.NetworkManager; +import com.clevertap.android.sdk.task.CTExecutorFactory; +import com.clevertap.android.sdk.task.MainLooperHandler; +import com.clevertap.android.sdk.task.Task; +import com.clevertap.android.sdk.validation.ValidationResult; +import com.clevertap.android.sdk.validation.ValidationResultStack; +import java.util.Iterator; +import java.util.TimeZone; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import org.json.JSONException; +import org.json.JSONObject; + +public class EventQueueManager extends BaseEventQueueManager implements FailureFlushListener { + + private Runnable commsRunnable = null; + + private final BaseDatabaseManager baseDatabaseManager; + + private final CoreMetaData cleverTapMetaData; + + private final CleverTapInstanceConfig config; + + private final Context context; + + private final CTLockManager ctLockManager; + + private final DeviceInfo deviceInfo; + + private final EventMediator eventMediator; + + private final LocalDataStore localDataStore; + + private final Logger logger; + + private LoginInfoProvider loginInfoProvider; + + private final MainLooperHandler mainLooperHandler; + + private final BaseNetworkManager networkManager; + + private final SessionManager sessionManager; + + private final ValidationResultStack validationResultStack; + + private Runnable pushNotificationViewedRunnable = null; + + public EventQueueManager(final BaseDatabaseManager baseDatabaseManager, + Context context, + CleverTapInstanceConfig config, + EventMediator eventMediator, + SessionManager sessionManager, + BaseCallbackManager callbackManager, + MainLooperHandler mainLooperHandler, + DeviceInfo deviceInfo, + ValidationResultStack validationResultStack, + NetworkManager networkManager, + CoreMetaData coreMetaData, + CTLockManager ctLockManager, + final LocalDataStore localDataStore) { + this.baseDatabaseManager = baseDatabaseManager; + this.context = context; + this.config = config; + this.eventMediator = eventMediator; + this.sessionManager = sessionManager; + this.mainLooperHandler = mainLooperHandler; + this.deviceInfo = deviceInfo; + this.validationResultStack = validationResultStack; + this.networkManager = networkManager; + this.localDataStore = localDataStore; + logger = this.config.getLogger(); + cleverTapMetaData = coreMetaData; + this.ctLockManager = ctLockManager; + + callbackManager.setFailureFlushListener(this); + } + + // only call async + @Override + public void addToQueue(final Context context, final JSONObject event, final int eventType) { + if (eventType == Constants.NV_EVENT) { + config.getLogger() + .verbose(config.getAccountId(), "Pushing Notification Viewed event onto separate queue"); + processPushNotificationViewedEvent(context, event); + } else { + processEvent(context, event, eventType); + } + } + + @Override + public void failureFlush(Context context) { + scheduleQueueFlush(context); + } + + @Override + public void flush() { + flushQueueAsync(context, EventGroup.REGULAR); + } + + @Override + public void flushQueueAsync(final Context context, final EventGroup eventGroup) { + Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask(); + task.execute("CommsManager#flushQueueAsync", new Callable() { + @Override + public Void call() { + if (eventGroup == EventGroup.PUSH_NOTIFICATION_VIEWED) { + logger.verbose(config.getAccountId(), + "Pushing Notification Viewed event onto queue flush sync"); + } else { + logger.verbose(config.getAccountId(), "Pushing event onto queue flush sync"); + } + flushQueueSync(context, eventGroup); + return null; + } + }); + } + + @Override + public void flushQueueSync(final Context context, final EventGroup eventGroup) { + if (!NetworkManager.isNetworkOnline(context)) { + logger.verbose(config.getAccountId(), "Network connectivity unavailable. Will retry later"); + return; + } + + if (cleverTapMetaData.isOffline()) { + logger.debug(config.getAccountId(), + "CleverTap Instance has been set to offline, won't send events queue"); + return; + } + + if (networkManager.needsHandshakeForDomain(eventGroup)) { + networkManager.initHandshake(eventGroup, new Runnable() { + @Override + public void run() { + networkManager.flushDBQueue(context, eventGroup); + } + }); + } else { + logger.verbose(config.getAccountId(), "Pushing Notification Viewed event onto queue DB flush"); + networkManager.flushDBQueue(context, eventGroup); + } + } + + public LoginInfoProvider getLoginInfoProvider() { + return loginInfoProvider; + } + + public void setLoginInfoProvider(final LoginInfoProvider loginInfoProvider) { + this.loginInfoProvider = loginInfoProvider; + } + + public int getNow() { + return (int) (System.currentTimeMillis() / 1000); + } + + public void processEvent(final Context context, final JSONObject event, final int eventType) { + synchronized (ctLockManager.getEventLock()) { + try { + if (CoreMetaData.getActivityCount() == 0) { + CoreMetaData.setActivityCount(1); + } + String type; + if (eventType == Constants.PAGE_EVENT) { + type = "page"; + } else if (eventType == Constants.PING_EVENT) { + type = "ping"; + attachMeta(event, context); + if (event.has("bk")) { + cleverTapMetaData.setBgPing(true); + event.remove("bk"); + } + + //Add a flag to denote, PING event is for geofences + if (cleverTapMetaData.isLocationForGeofence()) { + event.put("gf", true); + cleverTapMetaData.setLocationForGeofence(false); + event.put("gfSDKVersion", cleverTapMetaData.getGeofenceSDKVersion()); + cleverTapMetaData.setGeofenceSDKVersion(0); + } + } else if (eventType == Constants.PROFILE_EVENT) { + type = "profile"; + } else if (eventType == Constants.DATA_EVENT) { + type = "data"; + } else { + type = "event"; + } + + // Complete the received event with the other params + + String currentActivityName = cleverTapMetaData.getScreenName(); + if (currentActivityName != null) { + event.put("n", currentActivityName); + } + + int session = cleverTapMetaData.getCurrentSessionId(); + event.put("s", session); + event.put("pg", CoreMetaData.getActivityCount()); + event.put("type", type); + event.put("ep", getNow()); + event.put("f", cleverTapMetaData.isFirstSession()); + event.put("lsl", cleverTapMetaData.getLastSessionLength()); + attachPackageNameIfRequired(context, event); + + // Report any pending validation error + ValidationResult vr = validationResultStack.popValidationResult(); + if (vr != null) { + event.put(Constants.ERROR_KEY, getErrorObject(vr)); + } + localDataStore.setDataSyncFlag(event); + baseDatabaseManager.queueEventToDB(context, event, eventType); + updateLocalStore(context, event, eventType); + scheduleQueueFlush(context); + + } catch (Throwable e) { + config.getLogger().verbose(config.getAccountId(), "Failed to queue event: " + event.toString(), e); + } + } + } + + public void processPushNotificationViewedEvent(final Context context, final JSONObject event) { + synchronized (ctLockManager.getEventLock()) { + try { + int session = cleverTapMetaData.getCurrentSessionId(); + event.put("s", session); + event.put("type", "event"); + event.put("ep", getNow()); + // Report any pending validation error + ValidationResult vr = validationResultStack.popValidationResult(); + if (vr != null) { + event.put(Constants.ERROR_KEY, getErrorObject(vr)); + } + config.getLogger().verbose(config.getAccountId(), "Pushing Notification Viewed event onto DB"); + baseDatabaseManager.queuePushNotificationViewedEventToDB(context, event); + config.getLogger() + .verbose(config.getAccountId(), "Pushing Notification Viewed event onto queue flush"); + schedulePushNotificationViewedQueueFlush(context); + } catch (Throwable t) { + config.getLogger() + .verbose(config.getAccountId(), + "Failed to queue notification viewed event: " + event.toString(), t); + } + } + } + + //Profile + @Override + public void pushBasicProfile(JSONObject baseProfile) { + try { + String guid = getCleverTapID(); + + JSONObject profileEvent = new JSONObject(); + + if (baseProfile != null && baseProfile.length() > 0) { + Iterator i = baseProfile.keys(); + IdentityRepo iProfileHandler = IdentityRepoFactory + .getRepo(context, config, deviceInfo, validationResultStack); + setLoginInfoProvider(new LoginInfoProvider(context, config, deviceInfo)); + while (i.hasNext()) { + String next = i.next(); + + // need to handle command-based JSONObject props here now + Object value = null; + try { + value = baseProfile.getJSONObject(next); + } catch (Throwable t) { + try { + value = baseProfile.get(next); + } catch (JSONException e) { + //no-op + } + } + + if (value != null) { + profileEvent.put(next, value); + + // cache the valid identifier: guid pairs + boolean isProfileKey = iProfileHandler.hasIdentity(next); + if (isProfileKey) { + try { + getLoginInfoProvider().cacheGUIDForIdentifier(guid, next, value.toString()); + } catch (Throwable t) { + // no-op + } + } + } + } + } + + try { + String carrier = deviceInfo.getCarrier(); + if (carrier != null && !carrier.equals("")) { + profileEvent.put("Carrier", carrier); + } + + String cc = deviceInfo.getCountryCode(); + if (cc != null && !cc.equals("")) { + profileEvent.put("cc", cc); + } + + profileEvent.put("tz", TimeZone.getDefault().getID()); + + JSONObject event = new JSONObject(); + event.put("profile", profileEvent); + queueEvent(context, event, Constants.PROFILE_EVENT); + } catch (JSONException e) { + config.getLogger() + .verbose(config.getAccountId(), "FATAL: Creating basic profile update event failed!"); + } + } catch (Throwable t) { + config.getLogger().verbose(config.getAccountId(), "Basic profile sync", t); + } + } + + @Override + public void pushInitialEventsAsync() { + if (!cleverTapMetaData.inCurrentSession()) { + Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask(); + task.execute("CleverTapAPI#pushInitialEventsAsync", new Callable() { + @Override + public Void call() { + try { + config.getLogger().verbose(config.getAccountId(), "Queuing daily events"); + pushBasicProfile(null); + } catch (Throwable t) { + config.getLogger().verbose(config.getAccountId(), "Daily profile sync failed", t); + } + return null; + } + }); + } + } + + /** + * Adds a new event to the queue, to be sent later. + * + * @param context The Android context + * @param event The event to be queued + * @param eventType The type of event to be queued + */ + @Override + public Future queueEvent(final Context context, final JSONObject event, final int eventType) { + Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask(); + return task.submit("queueEvent", new Callable() { + @Override + public Void call() { + if (eventMediator.shouldDropEvent(event, eventType)) { + return null; + } + if (eventMediator.shouldDeferProcessingEvent(event, eventType)) { + config.getLogger().debug(config.getAccountId(), + "App Launched not yet processed, re-queuing event " + event + "after 2s"); + mainLooperHandler.postDelayed(new Runnable() { + @Override + public void run() { + Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask(); + task.execute("queueEventWithDelay", new Callable() { + @Override + public Void call() { + sessionManager.lazyCreateSession(context); + pushInitialEventsAsync(); + addToQueue(context, event, eventType); + return null; + } + }); + } + }, 2000); + } else { + if (eventType == Constants.FETCH_EVENT) { + addToQueue(context, event, eventType); + } else { + sessionManager.lazyCreateSession(context); + pushInitialEventsAsync(); + addToQueue(context, event, eventType); + } + } + return null; + } + }); + } + + @Override + public void scheduleQueueFlush(final Context context) { + if (commsRunnable == null) { + commsRunnable = new Runnable() { + @Override + public void run() { + flushQueueAsync(context, EventGroup.REGULAR); + flushQueueAsync(context, EventGroup.PUSH_NOTIFICATION_VIEWED); + } + }; + } + // Cancel any outstanding send runnables, and issue a new delayed one + mainLooperHandler.removeCallbacks(commsRunnable); + + mainLooperHandler.postDelayed(commsRunnable, networkManager.getDelayFrequency()); + + logger.verbose(config.getAccountId(), "Scheduling delayed queue flush on main event loop"); + } + + /** + * Attaches meta info about the current state of the device to an event. + * Typically, this meta is added only to the ping event. + */ + private void attachMeta(final JSONObject o, final Context context) { + // Memory consumption + try { + o.put("mc", Utils.getMemoryConsumption()); + } catch (Throwable t) { + // Ignore + } + + // Attach the network type + try { + o.put("nt", Utils.getCurrentNetworkType(context)); + } catch (Throwable t) { + // Ignore + } + } + + //Session + private void attachPackageNameIfRequired(final Context context, final JSONObject event) { + try { + final String type = event.getString("type"); + // Send it only for app launched events + if ("event".equals(type) && Constants.APP_LAUNCHED_EVENT.equals(event.getString("evtName"))) { + event.put("pai", context.getPackageName()); + } + } catch (Throwable t) { + // Ignore + } + } + + private String getCleverTapID() { + return deviceInfo.getDeviceID(); + } + + private void schedulePushNotificationViewedQueueFlush(final Context context) { + if (pushNotificationViewedRunnable == null) { + pushNotificationViewedRunnable = new Runnable() { + @Override + public void run() { + config.getLogger() + .verbose(config.getAccountId(), + "Pushing Notification Viewed event onto queue flush async"); + flushQueueAsync(context, EventGroup.PUSH_NOTIFICATION_VIEWED); + } + }; + } + mainLooperHandler.removeCallbacks(pushNotificationViewedRunnable); + mainLooperHandler.post(pushNotificationViewedRunnable); + } + + //Util + // only call async + private void updateLocalStore(final Context context, final JSONObject event, final int type) { + if (type == Constants.RAISED_EVENT) { + localDataStore.persistEvent(context, event, type); + } + } + +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/featureFlags/CTFeatureFlagsController.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/featureFlags/CTFeatureFlagsController.java index 723bb732a..57a49f958 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/featureFlags/CTFeatureFlagsController.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/featureFlags/CTFeatureFlagsController.java @@ -3,42 +3,47 @@ import static com.clevertap.android.sdk.product_config.CTProductConfigConstants.PRODUCT_CONFIG_JSON_KEY_FOR_KEY; import static com.clevertap.android.sdk.product_config.CTProductConfigConstants.PRODUCT_CONFIG_JSON_KEY_FOR_VALUE; -import android.content.Context; import android.text.TextUtils; +import com.clevertap.android.sdk.BaseAnalyticsManager; +import com.clevertap.android.sdk.BaseCallbackManager; import com.clevertap.android.sdk.CleverTapInstanceConfig; import com.clevertap.android.sdk.Constants; -import com.clevertap.android.sdk.FileUtils; import com.clevertap.android.sdk.Logger; -import com.clevertap.android.sdk.TaskManager; -import com.clevertap.android.sdk.Utils; -import java.lang.ref.WeakReference; +import com.clevertap.android.sdk.task.CTExecutorFactory; +import com.clevertap.android.sdk.task.OnSuccessListener; +import com.clevertap.android.sdk.task.Task; +import com.clevertap.android.sdk.utils.FileUtils; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.Callable; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; public class CTFeatureFlagsController { - private final CleverTapInstanceConfig config; + final CleverTapInstanceConfig config; - private String guid; + String guid; - private boolean isInitialized = false; + boolean isInitialized = false; - private final WeakReference listenerWeakReference; + final BaseAnalyticsManager mAnalyticsManager; - private final Context mContext; + final BaseCallbackManager mCallbackManager; - private final Map store = Collections.synchronizedMap(new HashMap()); + FileUtils mFileUtils; - public CTFeatureFlagsController(Context context, String guid, CleverTapInstanceConfig config, - FeatureFlagListener listener) { + private final Map store = Collections.synchronizedMap(new HashMap()); + + CTFeatureFlagsController(String guid, CleverTapInstanceConfig config, + BaseCallbackManager callbackManager, BaseAnalyticsManager analyticsManager, FileUtils fileUtils) { this.guid = guid; this.config = config; - listenerWeakReference = new WeakReference<>(listener); - this.mContext = context.getApplicationContext(); + mCallbackManager = callbackManager; + mAnalyticsManager = analyticsManager; + mFileUtils = fileUtils; init(); } @@ -51,20 +56,18 @@ public CTFeatureFlagsController(Context context, String guid, CleverTapInstanceC * Developers should not use this method */ public void fetchFeatureFlags() { - if (listenerWeakReference != null && listenerWeakReference.get() != null) { - Utils.runOnUiThread(new Runnable() { - @Override - public void run() { - try { - if (listenerWeakReference.get() != null) { - listenerWeakReference.get().fetchFeatureFlags(); - } - } catch (Exception e) { - getConfigLogger().verbose(getLogTag(), e.getLocalizedMessage()); - } + Task task = CTExecutorFactory.executors(config).mainTask(); + task.execute("fetchFeatureFlags", new Callable() { + @Override + public Void call() { + try { + mAnalyticsManager.fetchFeatureFlags(); + } catch (Exception e) { + getConfigLogger().verbose(getLogTag(), e.getLocalizedMessage()); } - }); - } + return null; + } + }); } /** @@ -84,7 +87,7 @@ public Boolean get(String key, boolean defaultValue) { "Getting feature flag with key - " + key + " and default value - " + defaultValue); Boolean value = store.get(key); if (value != null) { - return store.get(key); + return value; } else { getConfigLogger() .verbose(getLogTag(), "Feature flag not found, returning default value - " + defaultValue); @@ -119,6 +122,9 @@ public void resetWithGuid(String guid) { * Developers should not use this method */ public void setGuidAndInit(String cleverTapID) { + if (isInitialized) { + return; + } this.guid = cleverTapID; init(); } @@ -143,54 +149,38 @@ public synchronized void updateFeatureFlags(JSONObject jsonObject) throws JSONEx notifyFeatureFlagUpdate(); } - private synchronized void archiveData(JSONObject featureFlagRespObj) { - - if (featureFlagRespObj != null) { - try { - FileUtils.writeJsonToFile(mContext, config, getCachedDirName(), getCachedFileName(), - featureFlagRespObj); - getConfigLogger() - .verbose(getLogTag(), "Feature flags saved into file-[" + getCachedFullPath() + "]" + store); - } catch (Exception e) { - e.printStackTrace(); - getConfigLogger().verbose(getLogTag(), "ArchiveData failed - " + e.getLocalizedMessage()); - } - } - } - - private String getCachedDirName() { + String getCachedDirName() { return CTFeatureFlagConstants.DIR_FEATURE_FLAG + "_" + config.getAccountId() + "_" + guid; } - private String getCachedFileName() { + String getCachedFileName() { return CTFeatureFlagConstants.CACHED_FILE_NAME; } - private String getCachedFullPath() { + String getCachedFullPath() { return getCachedDirName() + "/" + getCachedFileName(); } - private Logger getConfigLogger() { - return config.getLogger(); - } - - private String getLogTag() { - return config.getAccountId() + "[Feature Flag]"; - } - - private void init() { + void init() { if (TextUtils.isEmpty(guid)) { return; } - TaskManager.getInstance().execute(new TaskManager.TaskListener() { + Task task = CTExecutorFactory.executors(config).ioTask(); + task.addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(final Boolean init) { + isInitialized = init; + } + }); + task.execute("initFeatureFlags", new Callable() { @Override - public Boolean doInBackground(Void aVoid) { + public Boolean call() { synchronized (this) { getConfigLogger().verbose(getLogTag(), "Feature flags init is called"); String fileName = getCachedFullPath(); try { store.clear(); - String content = FileUtils.readFromFile(mContext, config, fileName); + String content = mFileUtils.readFromFile(fileName); if (!TextUtils.isEmpty(content)) { JSONObject jsonObject = new JSONObject(content); @@ -223,27 +213,46 @@ public Boolean doInBackground(Void aVoid) { return true; } } + }); + } - @Override - public void onPostExecute(Boolean isInit) { - isInitialized = isInit; + private synchronized void archiveData(JSONObject featureFlagRespObj) { + + if (featureFlagRespObj != null) { + try { + mFileUtils.writeJsonToFile(getCachedDirName(), getCachedFileName(), + featureFlagRespObj); + getConfigLogger() + .verbose(getLogTag(), "Feature flags saved into file-[" + getCachedFullPath() + "]" + store); + } catch (Exception e) { + e.printStackTrace(); + getConfigLogger().verbose(getLogTag(), "ArchiveData failed - " + e.getLocalizedMessage()); } - }); + } + } + private Logger getConfigLogger() { + return config.getLogger(); + } + + private String getLogTag() { + return config.getAccountId() + "[Feature Flag]"; } private void notifyFeatureFlagUpdate() { - if (listenerWeakReference != null && listenerWeakReference.get() != null) { - Utils.runOnUiThread(new Runnable() { + if (mCallbackManager.getFeatureFlagListener() != null) { + Task task = CTExecutorFactory.executors(config).mainTask(); + task.execute("notifyFeatureFlagUpdate", new Callable() { @Override - public void run() { + public Void call() { try { - if (listenerWeakReference.get() != null) { - listenerWeakReference.get().featureFlagsDidUpdate(); + if (mCallbackManager.getFeatureFlagListener() != null) { + mCallbackManager.getFeatureFlagListener().featureFlagsUpdated(); } } catch (Exception e) { getConfigLogger().verbose(getLogTag(), e.getLocalizedMessage()); } + return null; } }); } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/featureFlags/CTFeatureFlagsFactory.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/featureFlags/CTFeatureFlagsFactory.java new file mode 100644 index 000000000..6bc68586f --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/featureFlags/CTFeatureFlagsFactory.java @@ -0,0 +1,19 @@ +package com.clevertap.android.sdk.featureFlags; + +import android.content.Context; +import com.clevertap.android.sdk.BaseAnalyticsManager; +import com.clevertap.android.sdk.BaseCallbackManager; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import com.clevertap.android.sdk.utils.FileUtils; + +/** + * Factory class to get {@link CTFeatureFlagsController} instance for a particular configuration + */ +public class CTFeatureFlagsFactory { + + public static CTFeatureFlagsController getInstance(Context context, String guid, CleverTapInstanceConfig config, + BaseCallbackManager callbackManager, BaseAnalyticsManager analyticsManager) { + FileUtils fileUtils = new FileUtils(context, config); + return new CTFeatureFlagsController(guid, config, callbackManager, analyticsManager, fileUtils); + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/featureFlags/FeatureFlagListener.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/featureFlags/FeatureFlagListener.java deleted file mode 100644 index 4ccccf601..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/featureFlags/FeatureFlagListener.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.clevertap.android.sdk.featureFlags; - -public interface FeatureFlagListener { - - void featureFlagsDidUpdate(); - - void fetchFeatureFlags(); -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/GifDecoder.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/gif/GifDecoder.java similarity index 99% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/GifDecoder.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/gif/GifDecoder.java index 28cc52fe2..c54ed76b9 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/GifDecoder.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/gif/GifDecoder.java @@ -17,13 +17,14 @@ * 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. */ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.gif; import android.annotation.TargetApi; import android.graphics.Bitmap; import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.clevertap.android.sdk.Logger; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -142,7 +143,7 @@ interface BitmapProvider { // Active color table. private int[] act; - private BitmapProvider bitmapProvider; + private final BitmapProvider bitmapProvider; // Raw data read working array. private byte[] block; diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/GifFrame.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/gif/GifFrame.java similarity index 97% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/GifFrame.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/gif/GifFrame.java index fbdb7f4b6..131f1b1a5 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/GifFrame.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/gif/GifFrame.java @@ -16,7 +16,7 @@ * 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. */ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.gif; class GifFrame { diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/GifHeader.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/gif/GifHeader.java similarity index 98% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/GifHeader.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/gif/GifHeader.java index e9c7c83ee..f79072650 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/GifHeader.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/gif/GifHeader.java @@ -16,7 +16,7 @@ * 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. */ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.gif; import java.util.ArrayList; import java.util.List; diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/GifHeaderParser.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/gif/GifHeaderParser.java similarity index 99% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/GifHeaderParser.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/gif/GifHeaderParser.java index 2a6be9afc..eeec1b1b3 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/GifHeaderParser.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/gif/GifHeaderParser.java @@ -16,8 +16,9 @@ * 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. */ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.gif; +import com.clevertap.android.sdk.Logger; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.ByteOrder; diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/GifImageView.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/gif/GifImageView.java similarity index 98% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/GifImageView.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/gif/GifImageView.java index 63d5ff8d2..aeb71f56c 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/GifImageView.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/gif/GifImageView.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.gif; import android.content.Context; import android.graphics.Bitmap; @@ -8,7 +8,7 @@ import androidx.appcompat.widget.AppCompatImageView; @SuppressWarnings({"unused"}) -class GifImageView extends AppCompatImageView implements Runnable { +public class GifImageView extends AppCompatImageView implements Runnable { public interface OnFrameAvailable { diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/SimpleBitmapProvider.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/gif/SimpleBitmapProvider.java similarity index 95% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/SimpleBitmapProvider.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/gif/SimpleBitmapProvider.java index 26471258b..d6848cf81 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/SimpleBitmapProvider.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/gif/SimpleBitmapProvider.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.gif; import android.graphics.Bitmap; import androidx.annotation.NonNull; diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppBaseFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragment.java similarity index 90% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppBaseFragment.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragment.java index edd5caaf2..519dda36d 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppBaseFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragment.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inapp; import android.content.Context; import android.content.Intent; @@ -8,6 +8,10 @@ import android.view.View; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.Utils; +import com.clevertap.android.sdk.customviews.CloseImageView; import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.Set; @@ -23,16 +27,6 @@ public void onClick(View view) { } } - interface InAppListener { - - void inAppNotificationDidClick(CTInAppNotification inAppNotification, Bundle formData, - HashMap keyValueMap); - - void inAppNotificationDidDismiss(Context context, CTInAppNotification inAppNotification, Bundle formData); - - void inAppNotificationDidShow(CTInAppNotification inAppNotification, Bundle formData); - } - CloseImageView closeImageView = null; CleverTapInstanceConfig config; @@ -45,7 +39,7 @@ void inAppNotificationDidClick(CTInAppNotification inAppNotification, Bundle for AtomicBoolean isCleanedUp = new AtomicBoolean(); - private WeakReference listenerWeakReference; + private WeakReference listenerWeakReference; @Override public void onAttach(Context context) { diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppBaseFullFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFullFragment.java similarity index 95% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppBaseFullFragment.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFullFragment.java index 2eda94f98..1152a4ce0 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppBaseFullFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFullFragment.java @@ -1,10 +1,15 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inapp; import android.os.Handler; import android.view.Gravity; import android.widget.FrameLayout; import android.widget.FrameLayout.LayoutParams; import android.widget.RelativeLayout; +import com.clevertap.android.sdk.InAppNotificationActivity; +import com.clevertap.android.sdk.Logger; +import com.clevertap.android.sdk.R; +import com.clevertap.android.sdk.Utils; +import com.clevertap.android.sdk.customviews.CloseImageView; public abstract class CTInAppBaseFullFragment extends CTInAppBaseFragment { @@ -25,7 +30,7 @@ void cleanup() {/* no-op */} @Override void generateListener() { if (context instanceof InAppNotificationActivity) { - setListener((CTInAppBaseFragment.InAppListener) context); + setListener((InAppListener) context); } } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppBaseFullHtmlFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFullHtmlFragment.java similarity index 95% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppBaseFullHtmlFragment.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFullHtmlFragment.java index e6898393f..565774384 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppBaseFullHtmlFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFullHtmlFragment.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inapp; import android.annotation.SuppressLint; import android.content.Context; @@ -15,6 +15,13 @@ import android.widget.RelativeLayout.LayoutParams; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.clevertap.android.sdk.CTWebInterface; +import com.clevertap.android.sdk.CleverTapAPI; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.Logger; +import com.clevertap.android.sdk.R; +import com.clevertap.android.sdk.customviews.CloseImageView; +import com.clevertap.android.sdk.utils.UriHelper; import java.net.URLDecoder; public abstract class CTInAppBaseFullHtmlFragment extends CTInAppBaseFullFragment { diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppBaseFullNativeFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFullNativeFragment.java similarity index 98% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppBaseFullNativeFragment.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFullNativeFragment.java index fc9876033..b7cf63521 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppBaseFullNativeFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFullNativeFragment.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inapp; import android.content.Context; import android.graphics.Color; diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppBasePartialFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBasePartialFragment.java similarity index 87% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppBasePartialFragment.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBasePartialFragment.java index db212bfbe..bff76d038 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppBasePartialFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBasePartialFragment.java @@ -1,7 +1,9 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inapp; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; +import com.clevertap.android.sdk.CleverTapAPI; +import com.clevertap.android.sdk.Utils; public abstract class CTInAppBasePartialFragment extends CTInAppBaseFragment { @@ -46,7 +48,7 @@ void cleanup() { @Override void generateListener() { if (config != null) { - setListener(CleverTapAPI.instanceWithConfig(this.context, config)); + setListener(CleverTapAPI.instanceWithConfig(this.context, config).getCoreState().getInAppController()); } } } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppBasePartialHtmlFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBasePartialHtmlFragment.java similarity index 97% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppBasePartialHtmlFragment.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBasePartialHtmlFragment.java index 52ec276c5..4a4f7ca8c 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppBasePartialHtmlFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBasePartialHtmlFragment.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inapp; import android.annotation.SuppressLint; import android.content.res.Configuration; @@ -16,6 +16,9 @@ import android.webkit.WebViewClient; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.Logger; +import com.clevertap.android.sdk.utils.UriHelper; import java.net.URLDecoder; public abstract class CTInAppBasePartialHtmlFragment extends CTInAppBasePartialFragment diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppBasePartialNativeFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBasePartialNativeFragment.java similarity index 99% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppBasePartialNativeFragment.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBasePartialNativeFragment.java index 272099f55..f8ec681cb 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppBasePartialNativeFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBasePartialNativeFragment.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inapp; import android.graphics.Color; import android.view.GestureDetector; diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppHtmlCoverFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlCoverFragment.java similarity index 90% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppHtmlCoverFragment.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlCoverFragment.java index a621b5cba..627f3703c 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppHtmlCoverFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlCoverFragment.java @@ -1,7 +1,8 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inapp; import android.widget.RelativeLayout; import android.widget.RelativeLayout.LayoutParams; +import com.clevertap.android.sdk.Constants; public class CTInAppHtmlCoverFragment extends CTInAppBaseFullHtmlFragment { diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppHtmlFooterFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlFooterFragment.java similarity index 85% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppHtmlFooterFragment.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlFooterFragment.java index 6eddae892..38c981942 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppHtmlFooterFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlFooterFragment.java @@ -1,8 +1,9 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inapp; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import com.clevertap.android.sdk.R; public class CTInAppHtmlFooterFragment extends CTInAppBasePartialHtmlFragment { diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppHtmlHalfInterstitialFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlHalfInterstitialFragment.java similarity index 68% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppHtmlHalfInterstitialFragment.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlHalfInterstitialFragment.java index 189c9056e..60dc6609d 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppHtmlHalfInterstitialFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlHalfInterstitialFragment.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inapp; public class CTInAppHtmlHalfInterstitialFragment extends CTInAppBaseFullHtmlFragment { diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppHtmlHeaderFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlHeaderFragment.java similarity index 85% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppHtmlHeaderFragment.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlHeaderFragment.java index b5fd99cda..b04d0ec54 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppHtmlHeaderFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlHeaderFragment.java @@ -1,8 +1,9 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inapp; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import com.clevertap.android.sdk.R; public class CTInAppHtmlHeaderFragment extends CTInAppBasePartialHtmlFragment { diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppHtmlInterstitialFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlInterstitialFragment.java similarity index 67% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppHtmlInterstitialFragment.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlInterstitialFragment.java index 649ecf877..4c2906ada 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppHtmlInterstitialFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlInterstitialFragment.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inapp; public class CTInAppHtmlInterstitialFragment extends CTInAppBaseFullHtmlFragment { diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNativeCoverFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeCoverFragment.java similarity index 96% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNativeCoverFragment.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeCoverFragment.java index 6abd68560..e68f5d976 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNativeCoverFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeCoverFragment.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inapp; import android.annotation.SuppressLint; import android.content.res.Configuration; @@ -14,6 +14,8 @@ import android.widget.RelativeLayout; import android.widget.TextView; import androidx.annotation.Nullable; +import com.clevertap.android.sdk.R; +import com.clevertap.android.sdk.customviews.CloseImageView; import java.util.ArrayList; public class CTInAppNativeCoverFragment extends CTInAppBaseFullNativeFragment { diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNativeCoverImageFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeCoverImageFragment.java similarity index 93% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNativeCoverImageFragment.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeCoverImageFragment.java index 9033eb9ac..4671ce8ee 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNativeCoverImageFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeCoverImageFragment.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inapp; import android.annotation.SuppressLint; import android.graphics.Color; @@ -10,6 +10,8 @@ import android.widget.ImageView; import android.widget.RelativeLayout; import androidx.annotation.Nullable; +import com.clevertap.android.sdk.R; +import com.clevertap.android.sdk.customviews.CloseImageView; public class CTInAppNativeCoverImageFragment extends CTInAppBaseFullFragment { diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNativeFooterFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeFooterFragment.java similarity index 98% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNativeFooterFragment.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeFooterFragment.java index da39ada8f..98bf6e2ed 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNativeFooterFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeFooterFragment.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inapp; import android.annotation.SuppressLint; import android.graphics.Bitmap; @@ -17,6 +17,7 @@ import android.widget.TextView; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import com.clevertap.android.sdk.R; import java.util.ArrayList; public class CTInAppNativeFooterFragment extends CTInAppBasePartialNativeFragment { diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNativeHalfInterstitialFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeHalfInterstitialFragment.java similarity index 98% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNativeHalfInterstitialFragment.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeHalfInterstitialFragment.java index 336ca86d4..81b341ddd 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNativeHalfInterstitialFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeHalfInterstitialFragment.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inapp; import android.annotation.SuppressLint; import android.content.res.Configuration; @@ -19,6 +19,8 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.clevertap.android.sdk.R; +import com.clevertap.android.sdk.customviews.CloseImageView; import java.util.ArrayList; public class CTInAppNativeHalfInterstitialFragment extends CTInAppBaseFullNativeFragment { diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNativeHalfInterstitialImageFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeHalfInterstitialImageFragment.java similarity index 98% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNativeHalfInterstitialImageFragment.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeHalfInterstitialImageFragment.java index 05e69f92f..0da408eff 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNativeHalfInterstitialImageFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeHalfInterstitialImageFragment.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inapp; import android.annotation.SuppressLint; import android.content.res.Configuration; @@ -16,6 +16,8 @@ import android.widget.RelativeLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.clevertap.android.sdk.R; +import com.clevertap.android.sdk.customviews.CloseImageView; public class CTInAppNativeHalfInterstitialImageFragment extends CTInAppBaseFullFragment { diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNativeHeaderFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeHeaderFragment.java similarity index 97% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNativeHeaderFragment.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeHeaderFragment.java index 8b3b0ab9c..75d9e9a0e 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNativeHeaderFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeHeaderFragment.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inapp; import android.annotation.SuppressLint; import android.graphics.Bitmap; @@ -17,6 +17,7 @@ import android.widget.TextView; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import com.clevertap.android.sdk.R; import java.util.ArrayList; public class CTInAppNativeHeaderFragment extends CTInAppBasePartialNativeFragment { diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNativeInterstitialFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeInterstitialFragment.java similarity index 99% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNativeInterstitialFragment.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeInterstitialFragment.java index a3c6bfd34..962f77f42 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNativeInterstitialFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeInterstitialFragment.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inapp; import static com.google.android.exoplayer2.ui.PlayerView.SHOW_BUFFERING_WHEN_PLAYING; @@ -29,6 +29,9 @@ import androidx.annotation.RequiresApi; import androidx.core.content.ContextCompat; import androidx.core.content.res.ResourcesCompat; +import com.clevertap.android.sdk.R; +import com.clevertap.android.sdk.customviews.CloseImageView; +import com.clevertap.android.sdk.gif.GifImageView; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.source.hls.HlsMediaSource; diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNativeInterstitialImageFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeInterstitialImageFragment.java similarity index 97% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNativeInterstitialImageFragment.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeInterstitialImageFragment.java index d7ba7f5a3..dea1dd7a3 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNativeInterstitialImageFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeInterstitialImageFragment.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inapp; import android.annotation.SuppressLint; import android.content.res.Configuration; @@ -14,6 +14,8 @@ import android.widget.RelativeLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.clevertap.android.sdk.R; +import com.clevertap.android.sdk.customviews.CloseImageView; public class CTInAppNativeInterstitialImageFragment extends CTInAppBaseFullFragment { diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNotification.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNotification.java similarity index 97% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNotification.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNotification.java index 0a6763646..17a9dddb4 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNotification.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNotification.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inapp; import android.content.res.Configuration; import android.graphics.Bitmap; @@ -6,13 +6,20 @@ import android.os.Parcel; import android.os.Parcelable; import android.util.LruCache; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.Logger; +import com.clevertap.android.sdk.Utils; +import com.clevertap.android.sdk.utils.ImageCache; import java.util.ArrayList; import java.util.Iterator; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -class CTInAppNotification implements Parcelable { +@RestrictTo(Scope.LIBRARY) +public class CTInAppNotification implements Parcelable { // intended to only hold an gif byte array reference for the life of the parent CTInAppNotification, in order to facilitate parceling private static class GifCache { @@ -279,6 +286,10 @@ public JSONObject getActionExtras() { return actionExtras; } + public String getId() { + return id; + } + @SuppressWarnings({"WeakerAccess"}) public CTInAppType getInAppType() { return inAppType; @@ -288,6 +299,10 @@ public long getTimeToLive() { return timeToLive; } + public boolean isExcludeFromCaps() { + return excludeFromCaps; + } + @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(id); @@ -356,11 +371,11 @@ int getButtonCount() { return buttonCount; } - ArrayList getButtons() { + public ArrayList getButtons() { return buttons; } - String getCampaignId() { + public String getCampaignId() { return campaignId; } @@ -392,10 +407,6 @@ String getHtml() { return html; } - String getId() { - return id; - } - Bitmap getImage(CTInAppNotificationMedia inAppMedia) { return ImageCache.getBitmap(inAppMedia.getCacheKey()); } @@ -411,11 +422,11 @@ CTInAppNotificationMedia getInAppMediaForOrientation(int orientation) { return returningMedia; } - JSONObject getJsonDescription() { + public JSONObject getJsonDescription() { return jsonDescription; } - int getMaxPerSession() { + public int getMaxPerSession() { return maxPerSession; } @@ -423,7 +434,7 @@ ArrayList getMediaList() { return mediaList; } - String getMessage() { + public String getMessage() { return message; } @@ -435,7 +446,7 @@ char getPosition() { return position; } - String getTitle() { + public String getTitle() { return title; } @@ -443,11 +454,11 @@ String getTitleColor() { return titleColor; } - int getTotalDailyCount() { + public int getTotalDailyCount() { return totalDailyCount; } - int getTotalLifetimeCount() { + public int getTotalLifetimeCount() { return totalLifetimeCount; } @@ -487,10 +498,6 @@ boolean isDarkenScreen() { return darkenScreen; } - boolean isExcludeFromCaps() { - return excludeFromCaps; - } - @SuppressWarnings("BooleanMethodIsAlwaysInverted") boolean isHideCloseButton() { return hideCloseButton; @@ -500,11 +507,11 @@ boolean isJsEnabled() { return jsEnabled; } - boolean isLandscape() { + public boolean isLandscape() { return isLandscape; } - boolean isPortrait() { + public boolean isPortrait() { return isPortrait; } @@ -740,7 +747,7 @@ private void legacyConfigureWithJson(JSONObject jsonObject) { private void removeImageOrGif() { for (CTInAppNotificationMedia inAppMedia : this.mediaList) { - if (inAppMedia.getMediaUrl() != null) { + if (inAppMedia.getMediaUrl() != null && inAppMedia.getCacheKey() != null) { if (!inAppMedia.getContentType().equals("image/gif")) { ImageCache.removeBitmap(inAppMedia.getCacheKey(), false); Logger.v("Deleted image - " + inAppMedia.getCacheKey()); diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNotificationButton.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNotificationButton.java similarity index 95% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNotificationButton.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNotificationButton.java index f6d7f8ac7..58276dfb0 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNotificationButton.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNotificationButton.java @@ -1,15 +1,18 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inapp; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; +import com.clevertap.android.sdk.Constants; import java.util.HashMap; import java.util.Iterator; import org.json.JSONException; import org.json.JSONObject; - -class CTInAppNotificationButton implements Parcelable { +@RestrictTo(Scope.LIBRARY) +public class CTInAppNotificationButton implements Parcelable { @SuppressWarnings("unused") public static final Parcelable.Creator CREATOR @@ -92,7 +95,7 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeMap(keyValues); } - String getActionUrl() { + public String getActionUrl() { return actionUrl; } @@ -145,7 +148,7 @@ void setJsonDescription(JSONObject jsonDescription) { this.jsonDescription = jsonDescription; } - String getText() { + public String getText() { return text; } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNotificationMedia.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNotificationMedia.java similarity index 96% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNotificationMedia.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNotificationMedia.java index 7e7571248..aa354f5ea 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppNotificationMedia.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNotificationMedia.java @@ -1,7 +1,9 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inapp; import android.os.Parcel; import android.os.Parcelable; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.Logger; import java.util.UUID; import org.json.JSONException; import org.json.JSONObject; diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppType.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppType.java similarity index 93% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppType.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppType.java index e95824102..34c1b4a30 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppType.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppType.java @@ -1,8 +1,11 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inapp; import androidx.annotation.NonNull; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; -enum CTInAppType { +@RestrictTo(Scope.LIBRARY) +public enum CTInAppType { CTInAppTypeHTML("html"), CTInAppTypeCoverHTML("coverHtml"), diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppWebView.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppWebView.java similarity index 98% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppWebView.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppWebView.java index aa9587d3e..d1480cb21 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInAppWebView.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppWebView.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inapp; import android.annotation.SuppressLint; import android.content.Context; diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/InAppController.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/InAppController.java new file mode 100644 index 000000000..d62203277 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/InAppController.java @@ -0,0 +1,536 @@ +package com.clevertap.android.sdk.inapp; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.Looper; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentTransaction; +import com.clevertap.android.sdk.AnalyticsManager; +import com.clevertap.android.sdk.BaseCallbackManager; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.ControllerManager; +import com.clevertap.android.sdk.CoreMetaData; +import com.clevertap.android.sdk.InAppNotificationActivity; +import com.clevertap.android.sdk.InAppNotificationListener; +import com.clevertap.android.sdk.Logger; +import com.clevertap.android.sdk.ManifestInfo; +import com.clevertap.android.sdk.StorageHelper; +import com.clevertap.android.sdk.Utils; +import com.clevertap.android.sdk.task.CTExecutorFactory; +import com.clevertap.android.sdk.task.MainLooperHandler; +import com.clevertap.android.sdk.task.Task; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.concurrent.Callable; +import org.json.JSONArray; +import org.json.JSONObject; + +public class InAppController implements CTInAppNotification.CTInAppNotificationListener, InAppListener { + + //InApp + private final class NotificationPrepareRunnable implements Runnable { + + private final WeakReference inAppControllerWeakReference; + + private final JSONObject jsonObject; + + private final boolean videoSupport = Utils.haveVideoPlayerSupport; + + NotificationPrepareRunnable(InAppController inAppController, JSONObject jsonObject) { + this.inAppControllerWeakReference = new WeakReference<>(inAppController); + this.jsonObject = jsonObject; + } + + @Override + public void run() { + final CTInAppNotification inAppNotification = new CTInAppNotification() + .initWithJSON(jsonObject, videoSupport); + if (inAppNotification.getError() != null) { + logger + .debug(config.getAccountId(), + "Unable to parse inapp notification " + inAppNotification.getError()); + return; + } + inAppNotification.listener = inAppControllerWeakReference.get(); + inAppNotification.prepareForDisplay(); + } + } + + private static CTInAppNotification currentlyDisplayingInApp = null; + + private static final List pendingNotifications = Collections + .synchronizedList(new ArrayList()); + + private HashSet inappActivityExclude = null; + + private final AnalyticsManager analyticsManager; + + private final BaseCallbackManager callbackManager; + + private final CleverTapInstanceConfig config; + + private final Context context; + + private final ControllerManager controllerManager; + + private final CoreMetaData coreMetaData; + + private final Logger logger; + + private final MainLooperHandler mainLooperHandler; + + public InAppController(Context context, + CleverTapInstanceConfig config, + MainLooperHandler mainLooperHandler, + ControllerManager controllerManager, + BaseCallbackManager callbackManager, + AnalyticsManager analyticsManager, + CoreMetaData coreMetaData) { + + this.context = context; + this.config = config; + logger = this.config.getLogger(); + this.mainLooperHandler = mainLooperHandler; + this.controllerManager = controllerManager; + this.callbackManager = callbackManager; + this.analyticsManager = analyticsManager; + this.coreMetaData = coreMetaData; + } + + public void checkExistingInAppNotifications(Activity activity) { + final boolean canShow = canShowInAppOnActivity(); + if (canShow) { + if (currentlyDisplayingInApp != null && ((System.currentTimeMillis() / 1000) < currentlyDisplayingInApp + .getTimeToLive())) { + Fragment inAppFragment = ((FragmentActivity) activity).getSupportFragmentManager() + .getFragment(new Bundle(), currentlyDisplayingInApp.getType()); + if (CoreMetaData.getCurrentActivity() != null && inAppFragment != null) { + FragmentTransaction fragmentTransaction = ((FragmentActivity) activity) + .getSupportFragmentManager() + .beginTransaction(); + Bundle bundle = new Bundle(); + bundle.putParcelable("inApp", currentlyDisplayingInApp); + bundle.putParcelable("config", config); + inAppFragment.setArguments(bundle); + fragmentTransaction.setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out); + fragmentTransaction.add(android.R.id.content, inAppFragment, currentlyDisplayingInApp.getType()); + Logger.v(config.getAccountId(), + "calling InAppFragment " + currentlyDisplayingInApp.getCampaignId()); + fragmentTransaction.commit(); + } + } + } + } + + public void checkPendingInAppNotifications(Activity activity) { + final boolean canShow = canShowInAppOnActivity(); + if (canShow) { + if (mainLooperHandler.getPendingRunnable() != null) { + logger.verbose(config.getAccountId(), "Found a pending inapp runnable. Scheduling it"); + mainLooperHandler.postDelayed(mainLooperHandler.getPendingRunnable(), 200); + mainLooperHandler.setPendingRunnable(null); + } else { + showNotificationIfAvailable(context); + } + } else { + Logger.d("In-app notifications will not be shown for this activity (" + + (activity != null ? activity.getLocalClassName() : "") + ")"); + } + } + + @Override + public void inAppNotificationDidClick(CTInAppNotification inAppNotification, Bundle formData, + HashMap keyValueMap) { + analyticsManager.pushInAppNotificationStateEvent(true, inAppNotification, formData); + if (keyValueMap != null && !keyValueMap.isEmpty()) { + if (callbackManager.getInAppNotificationButtonListener() != null) { + callbackManager.getInAppNotificationButtonListener().onInAppButtonClick(keyValueMap); + } + } + } + + @Override + public void inAppNotificationDidDismiss(final Context context, final CTInAppNotification inAppNotification, + final Bundle formData) { + inAppNotification.didDismiss(); + if (controllerManager.getInAppFCManager() != null) { + controllerManager.getInAppFCManager().didDismiss(inAppNotification); + logger.verbose(config.getAccountId(), "InApp Dismissed: " + inAppNotification.getCampaignId()); + } + try { + final InAppNotificationListener listener = callbackManager.getInAppNotificationListener(); + if (listener != null) { + final HashMap notifKVS; + + if (inAppNotification.getCustomExtras() != null) { + notifKVS = Utils.convertJSONObjectToHashMap(inAppNotification.getCustomExtras()); + } else { + notifKVS = new HashMap<>(); + } + + Logger.v("Calling the in-app listener on behalf of " + coreMetaData.getSource()); + + if (formData != null) { + listener.onDismissed(notifKVS, Utils.convertBundleObjectToHashMap(formData)); + } else { + listener.onDismissed(notifKVS, null); + } + } + } catch (Throwable t) { + logger.verbose(config.getAccountId(), "Failed to call the in-app notification listener", t); + } + + // Fire the next one, if any + Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask(Constants.TAG_FEATURE_IN_APPS); + task.execute("InappController#inAppNotificationDidDismiss", new Callable() { + @Override + public Void call() { + inAppDidDismiss(context, config, inAppNotification, InAppController.this); + _showNotificationIfAvailable(context); + return null; + } + }); + } + + //InApp + @Override + public void inAppNotificationDidShow(CTInAppNotification inAppNotification, Bundle formData) { + analyticsManager.pushInAppNotificationStateEvent(false, inAppNotification, formData); + } + + //InApp + @Override + public void notificationReady(final CTInAppNotification inAppNotification) { + if (Looper.myLooper() != Looper.getMainLooper()) { + mainLooperHandler.post(new Runnable() { + @Override + public void run() { + notificationReady(inAppNotification); + } + }); + return; + } + + if (inAppNotification.getError() != null) { + logger + .debug(config.getAccountId(), + "Unable to process inapp notification " + inAppNotification.getError()); + return; + } + logger.debug(config.getAccountId(), "Notification ready: " + inAppNotification.getJsonDescription()); + displayNotification(inAppNotification); + } + + //InApp + public void showNotificationIfAvailable(final Context context) { + if (!config.isAnalyticsOnly()) { + Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask(Constants.TAG_FEATURE_IN_APPS); + task.execute("InappController#showNotificationIfAvailable", new Callable() { + @Override + public Void call() { + _showNotificationIfAvailable(context); + return null; + } + }); + } + } + + //InApp + private void _showNotificationIfAvailable(Context context) { + SharedPreferences prefs = StorageHelper.getPreferences(context); + try { + if (!canShowInAppOnActivity()) { + Logger.v("Not showing notification on blacklisted activity"); + return; + } + + checkPendingNotifications(context, + config, this); // see if we have any pending notifications + + JSONArray inapps = new JSONArray( + StorageHelper.getStringFromPrefs(context, config, Constants.INAPP_KEY, "[]")); + if (inapps.length() < 1) { + return; + } + + JSONObject inapp = inapps.getJSONObject(0); + prepareNotificationForDisplay(inapp); + + // JSON array doesn't have the feature to remove a single element, + // so we have to copy over the entire array, but the first element + JSONArray inappsUpdated = new JSONArray(); + for (int i = 0; i < inapps.length(); i++) { + if (i == 0) { + continue; + } + inappsUpdated.put(inapps.get(i)); + } + SharedPreferences.Editor editor = prefs.edit() + .putString(StorageHelper.storageKeyWithSuffix(config, Constants.INAPP_KEY), + inappsUpdated.toString()); + StorageHelper.persist(editor); + } catch (Throwable t) { + // We won't get here + logger.verbose(config.getAccountId(), "InApp: Couldn't parse JSON array string from prefs", t); + } + } + + private boolean canShowInAppOnActivity() { + updateBlacklistedActivitySet(); + + for (String blacklistedActivity : inappActivityExclude) { + String currentActivityName = CoreMetaData.getCurrentActivityName(); + if (currentActivityName != null && currentActivityName.contains(blacklistedActivity)) { + return false; + } + } + + return true; + } + + //InApp + private void displayNotification(final CTInAppNotification inAppNotification) { + + if (Looper.myLooper() != Looper.getMainLooper()) { + mainLooperHandler.post(new Runnable() { + @Override + public void run() { + displayNotification(inAppNotification); + } + }); + return; + } + + if (controllerManager.getInAppFCManager() != null) { + if (!controllerManager.getInAppFCManager().canShow(inAppNotification)) { + logger.verbose(config.getAccountId(), + "InApp has been rejected by FC, not showing " + inAppNotification.getCampaignId()); + showInAppNotificationIfAny(); + return; + } + + controllerManager.getInAppFCManager().didShow(context, inAppNotification); + } else { + logger.verbose(config.getAccountId(), + "getCoreState().getInAppFCManager() is NULL, not showing " + inAppNotification.getCampaignId()); + return; + } + + final InAppNotificationListener listener = callbackManager.getInAppNotificationListener(); + + final boolean goFromListener; + + if (listener != null) { + final HashMap kvs; + + if (inAppNotification.getCustomExtras() != null) { + kvs = Utils.convertJSONObjectToHashMap(inAppNotification.getCustomExtras()); + } else { + kvs = new HashMap<>(); + } + + goFromListener = listener.beforeShow(kvs); + } else { + goFromListener = true; + } + + if (!goFromListener) { + logger.verbose(config.getAccountId(), + "Application has decided to not show this in-app notification: " + inAppNotification + .getCampaignId()); + showInAppNotificationIfAny(); + return; + } + showInApp(context, inAppNotification, config, this); + + } + + //InApp + private void prepareNotificationForDisplay(final JSONObject jsonObject) { + logger.debug(config.getAccountId(), "Preparing In-App for display: " + jsonObject.toString()); + Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask(Constants.TAG_FEATURE_IN_APPS); + task.execute("InappController#prepareNotificationForDisplay", new Callable() { + @Override + public Void call() { + new NotificationPrepareRunnable(InAppController.this, jsonObject).run(); + return null; + } + }); + } + + private void showInAppNotificationIfAny() { + if (!config.isAnalyticsOnly()) { + Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask(Constants.TAG_FEATURE_IN_APPS); + task.execute("InAppController#showInAppNotificationIfAny", new Callable() { + @Override + public Void call() { + _showNotificationIfAvailable(context); + return null; + } + }); + } + } + + private void updateBlacklistedActivitySet() { + if (inappActivityExclude == null) { + inappActivityExclude = new HashSet<>(); + try { + String activities = ManifestInfo.getInstance(context).getExcludedActivities(); + if (activities != null) { + String[] split = activities.split(","); + for (String a : split) { + inappActivityExclude.add(a.trim()); + } + } + } catch (Throwable t) { + // Ignore + } + logger.debug(config.getAccountId(), + "In-app notifications will not be shown on " + Arrays.toString(inappActivityExclude.toArray())); + } + } + + private static void checkPendingNotifications(final Context context, final CleverTapInstanceConfig config, + final InAppController inAppController) { + Logger.v(config.getAccountId(), "checking Pending Notifications"); + if (pendingNotifications != null && !pendingNotifications.isEmpty()) { + try { + final CTInAppNotification notification = pendingNotifications.get(0); + pendingNotifications.remove(0); + MainLooperHandler mainHandler = new MainLooperHandler(); + mainHandler.post(new Runnable() { + @Override + public void run() { + showInApp(context, notification, config, inAppController); + } + }); + } catch (Throwable t) { + // no-op + } + } + } + + //InApp + private static void inAppDidDismiss(Context context, CleverTapInstanceConfig config, + CTInAppNotification inAppNotification, InAppController inAppController) { + Logger.v(config.getAccountId(), "Running inAppDidDismiss"); + if (currentlyDisplayingInApp != null && (currentlyDisplayingInApp.getCampaignId() + .equals(inAppNotification.getCampaignId()))) { + currentlyDisplayingInApp = null; + checkPendingNotifications(context, config, inAppController); + } + } + + //InApp + private static void showInApp(Context context, final CTInAppNotification inAppNotification, + CleverTapInstanceConfig config, InAppController inAppController) { + + Logger.v(config.getAccountId(), "Attempting to show next In-App"); + + if (!CoreMetaData.isAppForeground()) { + pendingNotifications.add(inAppNotification); + Logger.v(config.getAccountId(), "Not in foreground, queueing this In App"); + return; + } + + if (currentlyDisplayingInApp != null) { + pendingNotifications.add(inAppNotification); + Logger.v(config.getAccountId(), "In App already displaying, queueing this In App"); + return; + } + + if ((System.currentTimeMillis() / 1000) > inAppNotification.getTimeToLive()) { + Logger.d("InApp has elapsed its time to live, not showing the InApp"); + return; + } + + currentlyDisplayingInApp = inAppNotification; + + CTInAppBaseFragment inAppFragment = null; + CTInAppType type = inAppNotification.getInAppType(); + switch (type) { + case CTInAppTypeCoverHTML: + case CTInAppTypeInterstitialHTML: + case CTInAppTypeHalfInterstitialHTML: + case CTInAppTypeCover: + case CTInAppTypeHalfInterstitial: + case CTInAppTypeInterstitial: + case CTInAppTypeAlert: + case CTInAppTypeInterstitialImageOnly: + case CTInAppTypeHalfInterstitialImageOnly: + case CTInAppTypeCoverImageOnly: + + Intent intent = new Intent(context, InAppNotificationActivity.class); + intent.putExtra("inApp", inAppNotification); + Bundle configBundle = new Bundle(); + configBundle.putParcelable("config", config); + intent.putExtra("configBundle", configBundle); + try { + Activity currentActivity = CoreMetaData.getCurrentActivity(); + if (currentActivity == null) { + throw new IllegalStateException("Current activity reference not found"); + } + config.getLogger().verbose(config.getAccountId(), + "calling InAppActivity for notification: " + inAppNotification.getJsonDescription()); + currentActivity.startActivity(intent); + Logger.d("Displaying In-App: " + inAppNotification.getJsonDescription()); + + } catch (Throwable t) { + Logger.v("Please verify the integration of your app." + + " It is not setup to support in-app notifications yet.", t); + } + break; + case CTInAppTypeFooterHTML: + inAppFragment = new CTInAppHtmlFooterFragment(); + break; + case CTInAppTypeHeaderHTML: + inAppFragment = new CTInAppHtmlHeaderFragment(); + break; + case CTInAppTypeFooter: + inAppFragment = new CTInAppNativeFooterFragment(); + break; + case CTInAppTypeHeader: + inAppFragment = new CTInAppNativeHeaderFragment(); + break; + default: + Logger.d(config.getAccountId(), "Unknown InApp Type found: " + type); + currentlyDisplayingInApp = null; + return; + } + + if (inAppFragment != null) { + Logger.d("Displaying In-App: " + inAppNotification.getJsonDescription()); + try { + //noinspection Constant Conditions + FragmentTransaction fragmentTransaction = ((FragmentActivity) CoreMetaData.getCurrentActivity()) + .getSupportFragmentManager() + .beginTransaction(); + Bundle bundle = new Bundle(); + bundle.putParcelable("inApp", inAppNotification); + bundle.putParcelable("config", config); + inAppFragment.setArguments(bundle); + fragmentTransaction.setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out); + fragmentTransaction.add(android.R.id.content, inAppFragment, inAppNotification.getType()); + Logger.v(config.getAccountId(), "calling InAppFragment " + inAppNotification.getCampaignId()); + fragmentTransaction.commit(); + + } catch (ClassCastException e) { + Logger.v(config.getAccountId(), + "Fragment not able to render, please ensure your Activity is an instance of AppCompatActivity" + + e.getMessage()); + } catch (Throwable t) { + Logger.v(config.getAccountId(), "Fragment not able to render", t); + } + } + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/InAppListener.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/InAppListener.java new file mode 100644 index 000000000..432099e8b --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/InAppListener.java @@ -0,0 +1,15 @@ +package com.clevertap.android.sdk.inapp; + +import android.content.Context; +import android.os.Bundle; +import java.util.HashMap; + +public interface InAppListener { + + void inAppNotificationDidClick(CTInAppNotification inAppNotification, Bundle formData, + HashMap keyValueMap); + + void inAppNotificationDidDismiss(Context context, CTInAppNotification inAppNotification, Bundle formData); + + void inAppNotificationDidShow(CTInAppNotification inAppNotification, Bundle formData); +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTCarouselImageViewHolder.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTCarouselImageViewHolder.java similarity index 91% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTCarouselImageViewHolder.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTCarouselImageViewHolder.java index c8c0b0e59..0b4f93f73 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTCarouselImageViewHolder.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTCarouselImageViewHolder.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inbox; import android.app.Activity; import android.content.Context; @@ -12,6 +12,7 @@ import androidx.annotation.NonNull; import androidx.core.content.res.ResourcesCompat; import androidx.viewpager.widget.ViewPager; +import com.clevertap.android.sdk.R; class CTCarouselImageViewHolder extends CTInboxBaseMessageViewHolder { @@ -21,13 +22,13 @@ class CTCarouselImageViewHolder extends CTInboxBaseMessageViewHolder { @SuppressWarnings("InnerClassMayBeStatic") class CarouselPageChangeListener implements ViewPager.OnPageChangeListener { - private Context context; + private final Context context; - private ImageView[] dots; + private final ImageView[] dots; - private CTInboxMessage inboxMessage; + private final CTInboxMessage inboxMessage; - private CTCarouselImageViewHolder viewHolder; + private final CTCarouselImageViewHolder viewHolder; CarouselPageChangeListener(Context context, CTCarouselImageViewHolder viewHolder, ImageView[] dots, CTInboxMessage inboxMessage) { @@ -59,15 +60,15 @@ public void onPageSelected(int position) { } } - private ImageView carouselReadDot; + private final ImageView carouselReadDot; - private TextView carouselTimestamp; + private final TextView carouselTimestamp; - private RelativeLayout clickLayout; + private final RelativeLayout clickLayout; - private CTCarouselViewPager imageViewPager; + private final CTCarouselViewPager imageViewPager; - private LinearLayout sliderDots; + private final LinearLayout sliderDots; CTCarouselImageViewHolder(@NonNull View itemView) { super(itemView); diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTCarouselMessageViewHolder.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTCarouselMessageViewHolder.java similarity index 91% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTCarouselMessageViewHolder.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTCarouselMessageViewHolder.java index 48c84697a..56067294d 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTCarouselMessageViewHolder.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTCarouselMessageViewHolder.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inbox; import android.app.Activity; import android.content.Context; @@ -12,6 +12,7 @@ import androidx.annotation.NonNull; import androidx.core.content.res.ResourcesCompat; import androidx.viewpager.widget.ViewPager; +import com.clevertap.android.sdk.R; /** * Viewholder class for Carousels @@ -23,13 +24,13 @@ class CTCarouselMessageViewHolder extends CTInboxBaseMessageViewHolder { */ class CarouselPageChangeListener implements ViewPager.OnPageChangeListener { - private Context context; + private final Context context; - private ImageView[] dots; + private final ImageView[] dots; - private CTInboxMessage inboxMessage; + private final CTInboxMessage inboxMessage; - private CTCarouselMessageViewHolder viewHolder; + private final CTCarouselMessageViewHolder viewHolder; CarouselPageChangeListener(Context context, CTCarouselMessageViewHolder viewHolder, ImageView[] dots, CTInboxMessage inboxMessage) { @@ -67,15 +68,23 @@ public void onPageSelected(int position) { } } - private RelativeLayout clickLayout; + private final RelativeLayout clickLayout; - private CTCarouselViewPager imageViewPager; + private final CTCarouselViewPager imageViewPager; - private ImageView readDot, carouselReadDot; + private final ImageView readDot; - private LinearLayout sliderDots; + private ImageView carouselReadDot; - private TextView title, message, timestamp, carouselTimestamp; + private final LinearLayout sliderDots; + + private final TextView title; + + private final TextView message; + + private final TextView timestamp; + + private TextView carouselTimestamp; CTCarouselMessageViewHolder(@NonNull View itemView) { super(itemView); diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTCarouselViewPager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTCarouselViewPager.java similarity index 93% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTCarouselViewPager.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTCarouselViewPager.java index b12ac3986..affb56004 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTCarouselViewPager.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTCarouselViewPager.java @@ -1,13 +1,16 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inbox; import android.content.Context; import android.util.AttributeSet; import android.view.View; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; import androidx.viewpager.widget.ViewPager; /** * Custom Viewpager class to handle orientation of images */ +@RestrictTo(Scope.LIBRARY) public class CTCarouselViewPager extends ViewPager { /** diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTCarouselViewPagerAdapter.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTCarouselViewPagerAdapter.java similarity index 84% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTCarouselViewPagerAdapter.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTCarouselViewPagerAdapter.java index 491084d0b..c34d86cda 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTCarouselViewPagerAdapter.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTCarouselViewPagerAdapter.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inbox; import android.content.Context; import android.view.LayoutInflater; @@ -7,31 +7,38 @@ import android.widget.ImageView; import android.widget.LinearLayout; import androidx.annotation.NonNull; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; import androidx.viewpager.widget.PagerAdapter; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.Logger; +import com.clevertap.android.sdk.R; +import com.clevertap.android.sdk.Utils; import java.lang.ref.WeakReference; import java.util.ArrayList; /** * Custom ViewPager Adapter to add views to the Carousel */ -@SuppressWarnings({"FieldCanBeLocal", "unused"}) +@SuppressWarnings({"FieldCanBeLocal"}) +@RestrictTo(Scope.LIBRARY) public class CTCarouselViewPagerAdapter extends PagerAdapter { - private ArrayList carouselImages; + private final ArrayList carouselImages; - private Context context; + private final Context context; - private CTInboxMessage inboxMessage; + private final CTInboxMessage inboxMessage; private LayoutInflater layoutInflater; - private LinearLayout.LayoutParams layoutParams; + private final LinearLayout.LayoutParams layoutParams; - private WeakReference parentWeakReference; + private final WeakReference parentWeakReference; - private int row; + private final int row; private View view; @@ -60,7 +67,7 @@ public int getCount() { @Override public Object instantiateItem(@NonNull final ViewGroup container, final int position) { layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - //noinspection ConstantConditions + //noinspection Constant Conditions view = layoutInflater.inflate(R.layout.inbox_carousel_image_layout, container, false); try { if (inboxMessage.getOrientation().equalsIgnoreCase("l")) { diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTIconMessageViewHolder.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTIconMessageViewHolder.java similarity index 97% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTIconMessageViewHolder.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTIconMessageViewHolder.java index 25a31d990..95e6c2724 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTIconMessageViewHolder.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTIconMessageViewHolder.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inbox; import android.app.Activity; import android.content.res.Configuration; @@ -14,6 +14,10 @@ import androidx.annotation.NonNull; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.Logger; +import com.clevertap.android.sdk.R; +import com.clevertap.android.sdk.Utils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -23,15 +27,25 @@ */ class CTIconMessageViewHolder extends CTInboxBaseMessageViewHolder { - private RelativeLayout clickLayout; + private final RelativeLayout clickLayout; - private Button cta1, cta2, cta3; + private final Button cta1; - private LinearLayout ctaLinearLayout; + private final Button cta2; - private ImageView readDot, iconImage; + private final Button cta3; - private TextView title, message, timestamp; + private final LinearLayout ctaLinearLayout; + + private final ImageView readDot; + + private final ImageView iconImage; + + private final TextView title; + + private final TextView message; + + private final TextView timestamp; CTIconMessageViewHolder(@NonNull View itemView) { super(itemView); diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxActivity.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxActivity.java similarity index 95% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxActivity.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxActivity.java index e001393be..e600844f9 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxActivity.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxActivity.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inbox; import android.content.Context; import android.graphics.Color; @@ -9,11 +9,18 @@ import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; import androidx.appcompat.widget.Toolbar; import androidx.core.content.res.ResourcesCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.viewpager.widget.ViewPager; +import com.clevertap.android.sdk.CTInboxStyleConfig; +import com.clevertap.android.sdk.CleverTapAPI; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import com.clevertap.android.sdk.Logger; +import com.clevertap.android.sdk.R; import com.google.android.material.tabs.TabLayout; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -23,9 +30,10 @@ /** * This activity shows the {@link CTInboxMessage} objects as per {@link CTInboxStyleConfig} style parameters */ +@RestrictTo(Scope.LIBRARY) public class CTInboxActivity extends FragmentActivity implements CTInboxListViewFragment.InboxListener { - interface InboxActivityListener { + public interface InboxActivityListener { void messageDidClick(CTInboxActivity ctInboxActivity, CTInboxMessage inboxMessage, Bundle data, HashMap keyValue); @@ -33,7 +41,7 @@ void messageDidClick(CTInboxActivity ctInboxActivity, CTInboxMessage inboxMessag void messageDidShow(CTInboxActivity ctInboxActivity, CTInboxMessage inboxMessage, Bundle data); } - static int orientation; + public static int orientation; CTInboxTabAdapter inboxTabAdapter; diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxBaseMessageViewHolder.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxBaseMessageViewHolder.java similarity index 95% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxBaseMessageViewHolder.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxBaseMessageViewHolder.java index 1f3141a38..a87745ac5 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxBaseMessageViewHolder.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxBaseMessageViewHolder.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inbox; import static com.google.android.exoplayer2.ui.PlayerView.SHOW_BUFFERING_NEVER; @@ -19,8 +19,11 @@ import android.widget.LinearLayout; import android.widget.RelativeLayout; import androidx.annotation.NonNull; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; import androidx.core.content.res.ResourcesCompat; import androidx.recyclerview.widget.RecyclerView; +import com.clevertap.android.sdk.R; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.ui.PlayerView; @@ -32,7 +35,8 @@ import java.text.SimpleDateFormat; import java.util.Date; -class CTInboxBaseMessageViewHolder extends RecyclerView.ViewHolder { +@RestrictTo(Scope.LIBRARY) +public class CTInboxBaseMessageViewHolder extends RecyclerView.ViewHolder { Context context; @@ -46,7 +50,6 @@ class CTInboxBaseMessageViewHolder extends RecyclerView.ViewHolder { FrameLayout progressBarFrameLayout; - @SuppressWarnings({"unused"}) RelativeLayout relativeLayout, clickLayout; private CTInboxMessageContent firstContentItem; @@ -63,7 +66,7 @@ class CTInboxBaseMessageViewHolder extends RecyclerView.ViewHolder { super(itemView); } - boolean addMediaPlayer(PlayerView videoSurfaceView) { + public boolean addMediaPlayer(PlayerView videoSurfaceView) { if (!requiresMediaPlayer) { return false; } @@ -242,17 +245,17 @@ void hideTwoButtons(Button mainButton, Button secondaryButton, Button tertiaryBu tertiaryButton.setLayoutParams(tertiaryLayoutParams); } - boolean needsMediaPlayer() { + public boolean needsMediaPlayer() { return requiresMediaPlayer; } - void playerBuffering() { + public void playerBuffering() { if (progressBarFrameLayout != null) { progressBarFrameLayout.setVisibility(View.VISIBLE); } } - void playerReady() { + public void playerReady() { FrameLayout frameLayout = getLayoutForMediaPlayer(); frameLayout.setVisibility(View.VISIBLE); if (muteIcon != null) { @@ -263,7 +266,7 @@ void playerReady() { } } - void playerRemoved() { + public void playerRemoved() { if (progressBarFrameLayout != null) { progressBarFrameLayout.setVisibility(View.GONE); } @@ -292,7 +295,7 @@ void setDots(ImageView[] dots, int dotsCount, Context appContext, LinearLayout s } } - boolean shouldAutoPlay() { + public boolean shouldAutoPlay() { return firstContentItem.mediaIsVideo(); } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxButtonClickListener.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxButtonClickListener.java similarity index 93% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxButtonClickListener.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxButtonClickListener.java index c4757f061..a74736138 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxButtonClickListener.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxButtonClickListener.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inbox; import android.content.ClipData; import android.content.ClipboardManager; @@ -6,6 +6,7 @@ import android.view.View; import android.widget.Toast; import androidx.viewpager.widget.ViewPager; +import com.clevertap.android.sdk.Constants; import java.util.HashMap; import org.json.JSONObject; @@ -16,13 +17,13 @@ class CTInboxButtonClickListener implements View.OnClickListener { private JSONObject buttonObject; - private String buttonText; + private final String buttonText; - private CTInboxListViewFragment fragment; + private final CTInboxListViewFragment fragment; - private CTInboxMessage inboxMessage; + private final CTInboxMessage inboxMessage; - private int position; + private final int position; private ViewPager viewPager; diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxController.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxController.java similarity index 58% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxController.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxController.java index b77923e59..2b74cd4b2 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxController.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxController.java @@ -1,46 +1,76 @@ -package com.clevertap.android.sdk; - +package com.clevertap.android.sdk.inbox; + +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; +import com.clevertap.android.sdk.BaseCallbackManager; +import com.clevertap.android.sdk.CTLockManager; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import com.clevertap.android.sdk.Logger; +import com.clevertap.android.sdk.db.DBAdapter; +import com.clevertap.android.sdk.task.CTExecutorFactory; +import com.clevertap.android.sdk.task.Task; import java.util.ArrayList; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.concurrent.Callable; import org.json.JSONArray; import org.json.JSONException; /** * Controller class which handles Users and Messages for the Notification Inbox */ -class CTInboxController { - - private static ExecutorService es; +@RestrictTo(Scope.LIBRARY) +public class CTInboxController { - private static long EXECUTOR_THREAD_ID = 0; - - private DBAdapter dbAdapter; + private final DBAdapter dbAdapter; private ArrayList messages; private final Object messagesLock = new Object(); - private String userId; + private final String userId; + + private final boolean videoSupported; - private boolean videoSupported; + private final CTLockManager ctLockManager; + + private final BaseCallbackManager callbackManager; + + private final CleverTapInstanceConfig config; // always call async - CTInboxController(String guid, DBAdapter adapter, boolean videoSupported) { + public CTInboxController(CleverTapInstanceConfig config, String guid, DBAdapter adapter, + CTLockManager ctLockManager, + BaseCallbackManager callbackManager, + boolean videoSupported) { this.userId = guid; this.dbAdapter = adapter; this.messages = this.dbAdapter.getMessages(this.userId); this.videoSupported = videoSupported; - if (es == null) { - es = Executors.newFixedThreadPool(1); - } + this.ctLockManager = ctLockManager; + this.callbackManager = callbackManager; + this.config = config; } - int count() { + public int count() { return getMessages().size(); } - boolean deleteMessageWithId(final String messageId) { + public void deleteInboxMessage(final CTInboxMessage message) { + Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask(); + task.execute("deleteInboxMessage", new Callable() { + @Override + public Void call() { + synchronized (ctLockManager.getInboxControllerLock()) { + boolean update = _deleteMessageWithId(message.getMessageId()); + if (update) { + callbackManager._notifyInboxMessagesDidUpdate(); + } + } + return null; + } + }); + } + + boolean _deleteMessageWithId(final String messageId) { CTMessageDAO messageDAO = findMessageById(messageId); if (messageDAO == null) { return false; @@ -48,27 +78,30 @@ boolean deleteMessageWithId(final String messageId) { synchronized (messagesLock) { this.messages.remove(messageDAO); } - postAsyncSafely("RunDeleteMessage", new Runnable() { + + Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask(); + task.execute("RunDeleteMessage",new Callable() { @Override - public void run() { + public Void call() { dbAdapter.deleteMessageForId(messageId, userId); + return null; } }); return true; } - CTMessageDAO getMessageForId(String messageId) { + public CTMessageDAO getMessageForId(String messageId) { return findMessageById(messageId); } - ArrayList getMessages() { + public ArrayList getMessages() { synchronized (messagesLock) { trimMessages(); return messages; } } - ArrayList getUnreadMessages() { + public ArrayList getUnreadMessages() { ArrayList unread = new ArrayList<>(); synchronized (messagesLock) { ArrayList messages = getMessages(); @@ -81,7 +114,23 @@ ArrayList getUnreadMessages() { return unread; } - boolean markReadForMessageWithId(final String messageId) { + public void markReadInboxMessage(final CTInboxMessage message) { + Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask(); + task.execute("markReadInboxMessage", new Callable() { + @Override + public Void call() { + synchronized (ctLockManager.getInboxControllerLock()) { + boolean read = _markReadForMessageWithId(message.getMessageId()); + if (read) { + callbackManager._notifyInboxMessagesDidUpdate(); + } + } + return null; + } + }); + } + + boolean _markReadForMessageWithId(final String messageId) { CTMessageDAO messageDAO = findMessageById(messageId); if (messageDAO == null) { return false; @@ -90,22 +139,23 @@ boolean markReadForMessageWithId(final String messageId) { synchronized (messagesLock) { messageDAO.setRead(1); } - - postAsyncSafely("RunMarkMessageRead", new Runnable() { + Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask(); + task.execute("RunMarkMessageRead", new Callable() { @Override - public void run() { + public Void call() { dbAdapter.markReadMessageForId(messageId, userId); + return null; } }); return true; } - int unreadCount() { + public int unreadCount() { return getUnreadMessages().size(); } // always call async - boolean updateMessages(final JSONArray inboxMessages) { + public boolean updateMessages(final JSONArray inboxMessages) { boolean haveUpdates = false; ArrayList newMessages = new ArrayList<>(); @@ -178,32 +228,8 @@ private void trimMessages() { } for (CTMessageDAO message : toDelete) { - deleteMessageWithId(message.getId()); - } - } - } - - private static void postAsyncSafely(final String name, final Runnable runnable) { - try { - final boolean executeSync = Thread.currentThread().getId() == EXECUTOR_THREAD_ID; - if (executeSync) { - runnable.run(); - } else { - es.submit(new Runnable() { - @Override - public void run() { - EXECUTOR_THREAD_ID = Thread.currentThread().getId(); - try { - Logger.v("CTInboxController Executor Service: Starting task - " + name); - runnable.run(); - } catch (Throwable t) { - Logger.v("CTInboxController Executor Service: Failed to complete the scheduled task", t); - } - } - }); + _deleteMessageWithId(message.getId()); } - } catch (Throwable t) { - Logger.v("Failed to submit task to the executor service", t); } } } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxListViewFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxListViewFragment.java similarity index 94% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxListViewFragment.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxListViewFragment.java index 05fa8d3de..e36963e92 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxListViewFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxListViewFragment.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inbox; import android.content.Context; import android.content.Intent; @@ -15,16 +15,28 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.DefaultItemAnimator; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import com.clevertap.android.sdk.CTInboxStyleConfig; +import com.clevertap.android.sdk.CleverTapAPI; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.Logger; +import com.clevertap.android.sdk.R; +import com.clevertap.android.sdk.Utils; +import com.clevertap.android.sdk.customviews.MediaPlayerRecyclerView; +import com.clevertap.android.sdk.customviews.VerticalSpaceItemDecoration; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import org.json.JSONObject; +@RestrictTo(Scope.LIBRARY) public class CTInboxListViewFragment extends Fragment { interface InboxListener { @@ -37,7 +49,7 @@ void messageDidClick(Context baseContext, CTInboxMessage inboxMessage, Bundle da CleverTapInstanceConfig config; - boolean haveVideoPlayerSupport = CleverTapAPI.haveVideoPlayerSupport; + boolean haveVideoPlayerSupport = Utils.haveVideoPlayerSupport; ArrayList inboxMessages = new ArrayList<>(); diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxMessage.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxMessage.java similarity index 97% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxMessage.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxMessage.java index 5e80d9010..9958dafc5 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxMessage.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxMessage.java @@ -1,7 +1,11 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inbox; import android.os.Parcel; import android.os.Parcelable; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.Logger; import java.util.ArrayList; import java.util.List; import org.json.JSONArray; @@ -66,7 +70,7 @@ public static Creator getCREATOR() { return CREATOR; } - CTInboxMessage(JSONObject jsonObject) { + public CTInboxMessage(JSONObject jsonObject) { this.data = jsonObject; try { this.messageId = jsonObject.has(Constants.KEY_ID) ? jsonObject.getString(Constants.KEY_ID) : "0"; diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxMessageAdapter.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxMessageAdapter.java similarity index 97% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxMessageAdapter.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxMessageAdapter.java index 65b579016..75b161792 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxMessageAdapter.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxMessageAdapter.java @@ -1,10 +1,11 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inbox; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; +import com.clevertap.android.sdk.R; import java.util.ArrayList; diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxMessageContent.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxMessageContent.java similarity index 98% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxMessageContent.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxMessageContent.java index b5d120e08..cdbf29871 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxMessageContent.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxMessageContent.java @@ -1,8 +1,12 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inbox; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.Logger; import java.util.HashMap; import java.util.Iterator; import org.json.JSONArray; @@ -13,6 +17,7 @@ * Public model class for the "msg" object from notification inbox payload */ @SuppressWarnings({"unused", "WeakerAccess"}) +@RestrictTo(Scope.LIBRARY) public class CTInboxMessageContent implements Parcelable { @SuppressWarnings("unused") diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxMessageType.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxMessageType.java similarity index 95% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxMessageType.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxMessageType.java index f60e511fa..9d28b08b3 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxMessageType.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxMessageType.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inbox; import androidx.annotation.NonNull; diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxTabAdapter.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxTabAdapter.java similarity index 90% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxTabAdapter.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxTabAdapter.java index 3e733bac8..ccfdada06 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTInboxTabAdapter.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxTabAdapter.java @@ -1,8 +1,10 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inbox; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; @@ -13,6 +15,7 @@ * Custom PagerAdapter for Notification Inbox tabs */ @SuppressWarnings({"unused", "WeakerAccess"}) +@RestrictTo(Scope.LIBRARY) public class CTInboxTabAdapter extends FragmentPagerAdapter { private final Fragment[] fragmentList; diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTMessageDAO.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTMessageDAO.java similarity index 83% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTMessageDAO.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTMessageDAO.java index 53d1020c7..8ebf2c571 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTMessageDAO.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTMessageDAO.java @@ -1,6 +1,10 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inbox; import android.text.TextUtils; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.Logger; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; @@ -12,7 +16,8 @@ /** * Message Data Access Object class interfacing with Database */ -class CTMessageDAO { +@RestrictTo(Scope.LIBRARY) +public class CTMessageDAO { private String campaignId; @@ -32,7 +37,7 @@ class CTMessageDAO { private JSONObject wzrkParams; - CTMessageDAO() { + public CTMessageDAO() { } private CTMessageDAO(String id, JSONObject jsonData, boolean read, long date, long expires, String userId, @@ -53,73 +58,73 @@ boolean containsVideoOrAudio() { return (content.mediaIsVideo() || content.mediaIsAudio()); } - String getCampaignId() { + public String getCampaignId() { return campaignId; } - void setCampaignId(String campaignId) { + public void setCampaignId(String campaignId) { this.campaignId = campaignId; } - long getDate() { + public long getDate() { return date; } - void setDate(long date) { + public void setDate(long date) { this.date = date; } - long getExpires() { + public long getExpires() { return expires; } - void setExpires(long expires) { + public void setExpires(long expires) { this.expires = expires; } - String getId() { + public String getId() { return id; } - void setId(String id) { + public void setId(String id) { this.id = id; } - JSONObject getJsonData() { + public JSONObject getJsonData() { return jsonData; } - void setJsonData(JSONObject jsonData) { + public void setJsonData(JSONObject jsonData) { this.jsonData = jsonData; } - String getTags() { + public String getTags() { return TextUtils.join(",", tags); } - void setTags(String tags) { + public void setTags(String tags) { String[] tagsArray = tags.split(","); this.tags.addAll(Arrays.asList(tagsArray)); } - String getUserId() { + public String getUserId() { return userId; } - void setUserId(String userId) { + public void setUserId(String userId) { this.userId = userId; } - JSONObject getWzrkParams() { + public JSONObject getWzrkParams() { return wzrkParams; } - void setWzrkParams(JSONObject wzrk_params) { + public void setWzrkParams(JSONObject wzrk_params) { this.wzrkParams = wzrk_params; } - int isRead() { + public int isRead() { if (read) { return 1; } else { @@ -127,11 +132,11 @@ int isRead() { } } - void setRead(int read) { + public void setRead(int read) { this.read = read == 1; } - JSONObject toJSON() { + public JSONObject toJSON() { JSONObject jsonObject = new JSONObject(); try { jsonObject.put("id", this.id); diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTSimpleMessageViewHolder.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTSimpleMessageViewHolder.java similarity index 98% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTSimpleMessageViewHolder.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTSimpleMessageViewHolder.java index d285c4035..d5ef29517 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTSimpleMessageViewHolder.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTSimpleMessageViewHolder.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.inbox; import android.app.Activity; import android.content.res.Configuration; @@ -13,6 +13,10 @@ import androidx.annotation.NonNull; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.Logger; +import com.clevertap.android.sdk.R; +import com.clevertap.android.sdk.Utils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -23,15 +27,19 @@ */ class CTSimpleMessageViewHolder extends CTInboxBaseMessageViewHolder { - private Button cta1, cta2, cta3; + private final Button cta1; - private TextView message; + private final Button cta2; - private ImageView readDot; + private final Button cta3; - private TextView timestamp; + private final TextView message; - private TextView title; + private final ImageView readDot; + + private final TextView timestamp; + + private final TextView title; CTSimpleMessageViewHolder(@NonNull View itemView) { super(itemView); diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/AbstractWebSocket.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/AbstractWebSocket.java deleted file mode 100755 index bc5d9db4c..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/AbstractWebSocket.java +++ /dev/null @@ -1,288 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket; - -import com.clevertap.android.sdk.java_websocket.framing.CloseFrame; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Timer; -import java.util.TimerTask; - - -/** - * Base class for additional implementations for the server as well as the client - */ -public abstract class AbstractWebSocket extends WebSocketAdapter { - - /** - * Attribute for the lost connection check interval - * - * @since 1.3.4 - */ - private int connectionLostTimeout = 60; - - /** - * Attribute for a timer allowing to check for lost connections - * - * @since 1.3.4 - */ - private Timer connectionLostTimer; - - /** - * Attribute for a timertask allowing to check for lost connections - * - * @since 1.3.4 - */ - private TimerTask connectionLostTimerTask; - - /** - * Attribute which allows you to enable/disable the SO_REUSEADDR socket option. - * - * @since 1.3.5 - */ - private boolean reuseAddr; - - /** - * Attribute to sync on - */ - private final Object syncConnectionLost = new Object(); - - /** - * Attribute which allows you to deactivate the Nagle's algorithm - * - * @since 1.3.3 - */ - private boolean tcpNoDelay; - - /** - * Attribute to keep track if the WebSocket Server/Client is running/connected - * - * @since 1.3.9 - */ - private boolean websocketRunning = false; - - /** - * Get the interval checking for lost connections - * Default is 60 seconds - * - * @return the interval - * @since 1.3.4 - */ - public int getConnectionLostTimeout() { - synchronized (syncConnectionLost) { - return connectionLostTimeout; - } - } - - /** - * Setter for the interval checking for lost connections - * A value lower or equal 0 results in the check to be deactivated - * - * @param connectionLostTimeout the interval in seconds - * @since 1.3.4 - */ - public void setConnectionLostTimeout(int connectionLostTimeout) { - synchronized (syncConnectionLost) { - this.connectionLostTimeout = connectionLostTimeout; - if (this.connectionLostTimeout <= 0) { - cancelConnectionLostTimer(); - return; - } - if (this.websocketRunning) { - //Reset all the pings - try { - ArrayList connections = new ArrayList(getConnections()); - WebSocketImpl webSocketImpl; - for (WebSocket conn : connections) { - if (conn instanceof WebSocketImpl) { - webSocketImpl = (WebSocketImpl) conn; - webSocketImpl.updateLastPong(); - } - } - } catch (Exception e) { - // no-op - } - restartConnectionLostTimer(); - } - } - } - - /** - * Tests Tests if SO_REUSEADDR is enabled. - * - * @return a boolean indicating whether or not SO_REUSEADDR is enabled. - * @since 1.3.5 - */ - public boolean isReuseAddr() { - return reuseAddr; - } - - /** - * Setter for soReuseAddr - *

    - * Enable/disable SO_REUSEADDR for the socket - * - * @param reuseAddr whether to enable or disable SO_REUSEADDR - * @since 1.3.5 - */ - public void setReuseAddr(boolean reuseAddr) { - this.reuseAddr = reuseAddr; - } - - /** - * Tests if TCP_NODELAY is enabled. - * - * @return a boolean indicating whether or not TCP_NODELAY is enabled for new connections. - * @since 1.3.3 - */ - public boolean isTcpNoDelay() { - return tcpNoDelay; - } - - /** - * Setter for tcpNoDelay - *

    - * Enable/disable TCP_NODELAY (disable/enable Nagle's algorithm) for new connections - * - * @param tcpNoDelay true to enable TCP_NODELAY, false to disable. - * @since 1.3.3 - */ - public void setTcpNoDelay(boolean tcpNoDelay) { - this.tcpNoDelay = tcpNoDelay; - } - - /** - * Getter to get all the currently available connections - * - * @return the currently available connections - * @since 1.3.4 - */ - protected abstract Collection getConnections(); - - /** - * Start the connection lost timer - * - * @since 1.3.4 - */ - protected void startConnectionLostTimer() { - synchronized (syncConnectionLost) { - if (this.connectionLostTimeout <= 0) { - return; - } - this.websocketRunning = true; - restartConnectionLostTimer(); - } - } - - /** - * Stop the connection lost timer - * - * @since 1.3.4 - */ - protected void stopConnectionLostTimer() { - synchronized (syncConnectionLost) { - if (connectionLostTimer != null || connectionLostTimerTask != null) { - this.websocketRunning = false; - cancelConnectionLostTimer(); - } - } - } - - /** - * Cancel any running timer for the connection lost detection - * - * @since 1.3.4 - */ - private void cancelConnectionLostTimer() { - if (connectionLostTimer != null) { - connectionLostTimer.cancel(); - connectionLostTimer = null; - } - if (connectionLostTimerTask != null) { - connectionLostTimerTask.cancel(); - connectionLostTimerTask = null; - } - } - - /** - * Send a ping to the endpoint or close the connection since the other endpoint did not respond with a ping - * - * @param webSocket the websocket instance - * @param current the current time in milliseconds - */ - private void executeConnectionLostDetection(WebSocket webSocket, long current) { - if (!(webSocket instanceof WebSocketImpl)) { - return; - } - WebSocketImpl webSocketImpl = (WebSocketImpl) webSocket; - if (webSocketImpl.getLastPong() < current) { - webSocketImpl.closeConnection(CloseFrame.ABNORMAL_CLOSE, - "The connection was closed because the other endpoint did not respond with a pong in time. For more information check: https://github.com/TooTallNate/Java-WebSocket/wiki/Lost-connection-detection"); - } else { - if (webSocketImpl.isOpen()) { - webSocketImpl.sendPing(); - } else { - // no-op - } - } - } - - /** - * This methods allows the reset of the connection lost timer in case of a changed parameter - * - * @since 1.3.4 - */ - private void restartConnectionLostTimer() { - cancelConnectionLostTimer(); - connectionLostTimer = new Timer("WebSocketTimer"); - connectionLostTimerTask = new TimerTask() { - - /** - * Keep the connections in a separate list to not cause deadlocks - */ - private ArrayList connections = new ArrayList(); - - @Override - public void run() { - connections.clear(); - try { - connections.addAll(getConnections()); - long current = (System.currentTimeMillis() - (connectionLostTimeout * 1500)); - for (WebSocket conn : connections) { - executeConnectionLostDetection(conn, current); - } - } catch (Exception e) { - //Ignore this exception - } - connections.clear(); - } - }; - connectionLostTimer.scheduleAtFixedRate(connectionLostTimerTask, 1000L * connectionLostTimeout, - 1000L * connectionLostTimeout); - - } - -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/LICENSE b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/LICENSE deleted file mode 100644 index 23dd51f0c..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/LICENSE +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Adapted from https://github.com/TooTallNate/Java-WebSocket, package renamed to prevent potential conflicts - * - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/SSLSocketChannel.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/SSLSocketChannel.java deleted file mode 100755 index 7947b1b56..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/SSLSocketChannel.java +++ /dev/null @@ -1,520 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket; - -import com.clevertap.android.sdk.java_websocket.util.ByteBufferUtils; -import java.io.IOException; -import java.nio.BufferOverflowException; -import java.nio.ByteBuffer; -import java.nio.channels.ByteChannel; -import java.nio.channels.SelectionKey; -import java.nio.channels.SocketChannel; -import java.util.concurrent.ExecutorService; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLEngineResult; -import javax.net.ssl.SSLEngineResult.HandshakeStatus; -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLSession; - - -/** - * A class that represents an SSL/TLS peer, and can be extended to create a client or a server. - *

    - * It makes use of the JSSE framework, and specifically the {@link SSLEngine} logic, which - * is described by Oracle as "an advanced API, not appropriate for casual use", since - * it requires the user to implement much of the communication establishment procedure himself. - * More information about it can be found here: http://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html#SSLEngine - *

    - * {@link SSLSocketChannel} implements the handshake protocol, required to establish a connection between two peers, - * which is common for both client and server and provides the abstract {@link SSLSocketChannel#read(ByteBuffer)} and - * {@link SSLSocketChannel#write(ByteBuffer)} (String)} methods, that need to be implemented by the specific SSL/TLS - * peer - * that is going to extend this class. - * - * @author Alex Karnezis - *

    - * Modified by marci4 to allow the usage as a ByteChannel - *

    - * Permission for usage recieved at May 25, 2017 by Alex Karnezis - */ -public class SSLSocketChannel implements WrappedByteChannel, ByteChannel { - - /** - * The engine which will be used for un-/wrapping of buffers - */ - private final SSLEngine engine; - - /** - * Will be used to execute tasks that may emerge during handshake in parallel with the server's main thread. - */ - private ExecutorService executor; - - /** - * Will contain this peer's application data in plaintext, that will be later encrypted - * using {@link SSLEngine#wrap(ByteBuffer, ByteBuffer)} and sent to the other peer. This buffer can typically - * be of any size, as long as it is large enough to contain this peer's outgoing messages. - * If this peer tries to send a message bigger than buffer's capacity a {@link BufferOverflowException} - * will be thrown. - */ - private ByteBuffer myAppData; - - /** - * Will contain this peer's encrypted data, that will be generated after {@link SSLEngine#wrap(ByteBuffer, - * ByteBuffer)} - * is applied on {@link SSLSocketChannel#myAppData}. It should be initialized using {@link - * SSLSession#getPacketBufferSize()}, - * which returns the size up to which, SSL/TLS packets will be generated from the engine under a session. - * All SSLEngine network buffers should be sized at least this large to avoid insufficient space problems when - * performing wrap and unwrap calls. - */ - private ByteBuffer myNetData; - - /** - * Will contain the other peer's (decrypted) application data. It must be large enough to hold the application - * data - * from any peer. Can be initialized with {@link SSLSession#getApplicationBufferSize()} for an estimation - * of the other peer's application data and should be enlarged if this size is not enough. - */ - private ByteBuffer peerAppData; - - /** - * Will contain the other peer's encrypted data. The SSL/TLS protocols specify that implementations should produce - * packets containing at most 16 KB of plaintext, - * so a buffer sized to this value should normally cause no capacity problems. However, some implementations - * violate the specification and generate large records up to 32 KB. - * If the {@link SSLEngine#unwrap(ByteBuffer, ByteBuffer)} detects large inbound packets, the buffer sizes - * returned by SSLSession will be updated dynamically, so the this peer - * should check for overflow conditions and enlarge the buffer using the session's (updated) buffer size. - */ - private ByteBuffer peerNetData; - - /** - * The underlaying socket channel - */ - private final SocketChannel socketChannel; - - - public SSLSocketChannel(SocketChannel inputSocketChannel, SSLEngine inputEngine, ExecutorService inputExecutor, - SelectionKey key) throws IOException { - if (inputSocketChannel == null || inputEngine == null || executor == inputExecutor) { - throw new IllegalArgumentException("parameter must not be null"); - } - - this.socketChannel = inputSocketChannel; - this.engine = inputEngine; - this.executor = inputExecutor; - myNetData = ByteBuffer.allocate(engine.getSession().getPacketBufferSize()); - peerNetData = ByteBuffer.allocate(engine.getSession().getPacketBufferSize()); - this.engine.beginHandshake(); - if (doHandshake()) { - if (key != null) { - key.interestOps(key.interestOps() | SelectionKey.OP_WRITE); - } - } else { - try { - socketChannel.close(); - } catch (IOException e) { - // no-op - } - } - } - - @Override - public void close() throws IOException { - closeConnection(); - } - - @Override - public boolean isBlocking() { - return socketChannel.isBlocking(); - } - - @Override - public boolean isNeedRead() { - return peerNetData.hasRemaining() || peerAppData.hasRemaining(); - } - - @Override - public boolean isNeedWrite() { - return false; - } - - @Override - public boolean isOpen() { - return socketChannel.isOpen(); - } - - @Override - public synchronized int read(ByteBuffer dst) throws IOException { - if (!dst.hasRemaining()) { - return 0; - } - if (peerAppData.hasRemaining()) { - peerAppData.flip(); - return ByteBufferUtils.transferByteBuffer(peerAppData, dst); - } - peerNetData.compact(); - - int bytesRead = socketChannel.read(peerNetData); - /* - * If bytesRead are 0 put we still have some data in peerNetData still to an unwrap (for testcase 1.1.6) - */ - if (bytesRead > 0 || peerNetData.hasRemaining()) { - peerNetData.flip(); - while (peerNetData.hasRemaining()) { - peerAppData.compact(); - SSLEngineResult result; - try { - result = engine.unwrap(peerNetData, peerAppData); - } catch (SSLException e) { - throw e; - } - switch (result.getStatus()) { - case OK: - peerAppData.flip(); - return ByteBufferUtils.transferByteBuffer(peerAppData, dst); - case BUFFER_UNDERFLOW: - peerAppData.flip(); - return ByteBufferUtils.transferByteBuffer(peerAppData, dst); - case BUFFER_OVERFLOW: - peerAppData = enlargeApplicationBuffer(peerAppData); - return read(dst); - case CLOSED: - closeConnection(); - dst.clear(); - return -1; - default: - throw new IllegalStateException("Invalid SSL status: " + result.getStatus()); - } - } - } else if (bytesRead < 0) { - handleEndOfStream(); - } - ByteBufferUtils.transferByteBuffer(peerAppData, dst); - return bytesRead; - } - - @Override - public int readMore(ByteBuffer dst) throws IOException { - return read(dst); - } - - @Override - public synchronized int write(ByteBuffer output) throws IOException { - int num = 0; - while (output.hasRemaining()) { - // The loop has a meaning for (outgoing) messages larger than 16KB. - // Every wrap call will remove 16KB from the original message and send it to the remote peer. - myNetData.clear(); - SSLEngineResult result = engine.wrap(output, myNetData); - switch (result.getStatus()) { - case OK: - myNetData.flip(); - while (myNetData.hasRemaining()) { - num += socketChannel.write(myNetData); - } - break; - case BUFFER_OVERFLOW: - myNetData = enlargePacketBuffer(myNetData); - break; - case BUFFER_UNDERFLOW: - throw new SSLException( - "Buffer underflow occured after a wrap. I don't think we should ever get here."); - case CLOSED: - closeConnection(); - return 0; - default: - throw new IllegalStateException("Invalid SSL status: " + result.getStatus()); - } - } - return num; - } - - @Override - public void writeMore() throws IOException { - //Nothing to do since we write out all the data in a while loop - } - - /** - * This method should be called when this peer wants to explicitly close the connection - * or when a close message has arrived from the other peer, in order to provide an orderly shutdown. - *

    - * It first calls {@link SSLEngine#closeOutbound()} which prepares this peer to send its own close message and - * sets {@link SSLEngine} to the NEED_WRAP state. Then, it delegates the exchange of close messages - * to the handshake method and finally, it closes socket channel. - * - * @throws IOException if an I/O error occurs to the socket channel. - */ - private void closeConnection() throws IOException { - engine.closeOutbound(); - try { - doHandshake(); - } catch (IOException e) { - //Just ignore this exception since we are closing the connection already - } - socketChannel.close(); - } - - /** - * Implements the handshake protocol between two peers, required for the establishment of the SSL/TLS connection. - * During the handshake, encryption configuration information - such as the list of available cipher suites - - * will - * be exchanged - * and if the handshake is successful will lead to an established SSL/TLS session. - *

    - *

    - * A typical handshake will usually contain the following steps: - *

    - *

      - *
    • 1. wrap: ClientHello
    • - *
    • 2. unwrap: ServerHello/Cert/ServerHelloDone
    • - *
    • 3. wrap: ClientKeyExchange
    • - *
    • 4. wrap: ChangeCipherSpec
    • - *
    • 5. wrap: Finished
    • - *
    • 6. unwrap: ChangeCipherSpec
    • - *
    • 7. unwrap: Finished
    • - *
    - *

    - * Handshake is also used during the end of the session, in order to properly close the connection between the two peers. - * A proper connection close will typically include the one peer sending a CLOSE message to another, and then wait for - * the other's CLOSE message to close the transport link. The other peer from his perspective would read a CLOSE message - * from his peer and then enter the handshake procedure to send his own CLOSE message as well. - * - * @return True if the connection handshake was successful or false if an error occurred. - * @throws IOException - if an error occurs during read/write to the socket channel. - */ - private boolean doHandshake() throws IOException { - SSLEngineResult result; - HandshakeStatus handshakeStatus; - - // NioSslPeer's fields myAppData and peerAppData are supposed to be large enough to hold all message data the peer - // will send and expects to receive from the other peer respectively. Since the messages to be exchanged will usually be less - // than 16KB long the capacity of these fields should also be smaller. Here we initialize these two local buffers - // to be used for the handshake, while keeping client's buffers at the same size. - int appBufferSize = engine.getSession().getApplicationBufferSize(); - myAppData = ByteBuffer.allocate(appBufferSize); - peerAppData = ByteBuffer.allocate(appBufferSize); - myNetData.clear(); - peerNetData.clear(); - - handshakeStatus = engine.getHandshakeStatus(); - boolean handshakeComplete = false; - while (!handshakeComplete) { - switch (handshakeStatus) { - case FINISHED: - handshakeComplete = !this.peerNetData.hasRemaining(); - if (handshakeComplete) { - return true; - } - socketChannel.write(this.peerNetData); - break; - case NEED_UNWRAP: - if (socketChannel.read(peerNetData) < 0) { - if (engine.isInboundDone() && engine.isOutboundDone()) { - return false; - } - try { - engine.closeInbound(); - } catch (SSLException e) { - //Ignore, cant do anything against this exception - } - engine.closeOutbound(); - // After closeOutbound the engine will be set to WRAP state, in order to try to send a close message to the client. - handshakeStatus = engine.getHandshakeStatus(); - break; - } - peerNetData.flip(); - try { - result = engine.unwrap(peerNetData, peerAppData); - peerNetData.compact(); - handshakeStatus = result.getHandshakeStatus(); - } catch (SSLException sslException) { - engine.closeOutbound(); - handshakeStatus = engine.getHandshakeStatus(); - break; - } - switch (result.getStatus()) { - case OK: - break; - case BUFFER_OVERFLOW: - // Will occur when peerAppData's capacity is smaller than the data derived from peerNetData's unwrap. - peerAppData = enlargeApplicationBuffer(peerAppData); - break; - case BUFFER_UNDERFLOW: - // Will occur either when no data was read from the peer or when the peerNetData buffer was too small to hold all peer's data. - peerNetData = handleBufferUnderflow(peerNetData); - break; - case CLOSED: - if (engine.isOutboundDone()) { - return false; - } else { - engine.closeOutbound(); - handshakeStatus = engine.getHandshakeStatus(); - break; - } - default: - throw new IllegalStateException("Invalid SSL status: " + result.getStatus()); - } - break; - case NEED_WRAP: - myNetData.clear(); - try { - result = engine.wrap(myAppData, myNetData); - handshakeStatus = result.getHandshakeStatus(); - } catch (SSLException sslException) { - engine.closeOutbound(); - handshakeStatus = engine.getHandshakeStatus(); - break; - } - switch (result.getStatus()) { - case OK: - myNetData.flip(); - while (myNetData.hasRemaining()) { - socketChannel.write(myNetData); - } - break; - case BUFFER_OVERFLOW: - // Will occur if there is not enough space in myNetData buffer to write all the data that would be generated by the method wrap. - // Since myNetData is set to session's packet size we should not get to this point because SSLEngine is supposed - // to produce messages smaller or equal to that, but a general handling would be the following: - myNetData = enlargePacketBuffer(myNetData); - break; - case BUFFER_UNDERFLOW: - throw new SSLException( - "Buffer underflow occured after a wrap. I don't think we should ever get here."); - case CLOSED: - try { - myNetData.flip(); - while (myNetData.hasRemaining()) { - socketChannel.write(myNetData); - } - // At this point the handshake status will probably be NEED_UNWRAP so we make sure that peerNetData is clear to read. - peerNetData.clear(); - } catch (Exception e) { - handshakeStatus = engine.getHandshakeStatus(); - } - break; - default: - throw new IllegalStateException("Invalid SSL status: " + result.getStatus()); - } - break; - case NEED_TASK: - Runnable task; - while ((task = engine.getDelegatedTask()) != null) { - executor.execute(task); - } - handshakeStatus = engine.getHandshakeStatus(); - break; - - case NOT_HANDSHAKING: - break; - default: - throw new IllegalStateException("Invalid SSL status: " + handshakeStatus); - } - } - - return true; - - } - - /** - * Enlarging a packet buffer (peerAppData or myAppData) - * - * @param buffer the buffer to enlarge - * @return the enlarged buffer - */ - private ByteBuffer enlargeApplicationBuffer(ByteBuffer buffer) { - return enlargeBuffer(buffer, engine.getSession().getApplicationBufferSize()); - } - - /** - * Compares sessionProposedCapacity with buffer's capacity. If buffer's capacity is smaller, - * returns a buffer with the proposed capacity. If it's equal or larger, returns a buffer - * with capacity twice the size of the initial one. - * - * @param buffer - the buffer to be enlarged. - * @param sessionProposedCapacity - the minimum size of the new buffer, proposed by {@link SSLSession}. - * @return A new buffer with a larger capacity. - */ - private ByteBuffer enlargeBuffer(ByteBuffer buffer, int sessionProposedCapacity) { - if (sessionProposedCapacity > buffer.capacity()) { - buffer = ByteBuffer.allocate(sessionProposedCapacity); - } else { - buffer = ByteBuffer.allocate(buffer.capacity() * 2); - } - return buffer; - } - - /** - * Enlarging a packet buffer (peerNetData or myNetData) - * - * @param buffer the buffer to enlarge - * @return the enlarged buffer - */ - private ByteBuffer enlargePacketBuffer(ByteBuffer buffer) { - return enlargeBuffer(buffer, engine.getSession().getPacketBufferSize()); - } - - /** - * Handles {@link SSLEngineResult.Status#BUFFER_UNDERFLOW}. Will check if the buffer is already filled, and if - * there is no space problem - * will return the same buffer, so the client tries to read again. If the buffer is already filled will try to - * enlarge the buffer either to - * session's proposed size or to a larger capacity. A buffer underflow can happen only after an unwrap, so the - * buffer will always be a - * peerNetData buffer. - * - * @param buffer - will always be peerNetData buffer. - * @return The same buffer if there is no space problem or a new buffer with the same data but more space. - */ - private ByteBuffer handleBufferUnderflow(ByteBuffer buffer) { - if (engine.getSession().getPacketBufferSize() < buffer.limit()) { - return buffer; - } else { - ByteBuffer replaceBuffer = enlargePacketBuffer(buffer); - buffer.flip(); - replaceBuffer.put(buffer); - return replaceBuffer; - } - } - - /** - * In addition to orderly shutdowns, an unorderly shutdown may occur, when the transport link (socket channel) - * is severed before close messages are exchanged. This may happen by getting an -1 or {@link IOException} - * when trying to read from the socket channel, or an {@link IOException} when trying to write to it. - * In both cases {@link SSLEngine#closeInbound()} should be called and then try to follow the standard procedure. - * - * @throws IOException if an I/O error occurs to the socket channel. - */ - private void handleEndOfStream() throws IOException { - try { - engine.closeInbound(); - } catch (Exception e) { - // no-op - } - closeConnection(); - } -} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/SSLSocketChannel2.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/SSLSocketChannel2.java deleted file mode 100755 index e902ec6c8..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/SSLSocketChannel2.java +++ /dev/null @@ -1,459 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ -package com.clevertap.android.sdk.java_websocket; - -import java.io.EOFException; -import java.io.IOException; -import java.net.Socket; -import java.net.SocketAddress; -import java.nio.ByteBuffer; -import java.nio.channels.ByteChannel; -import java.nio.channels.SelectableChannel; -import java.nio.channels.SelectionKey; -import java.nio.channels.SocketChannel; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLEngineResult; -import javax.net.ssl.SSLEngineResult.HandshakeStatus; -import javax.net.ssl.SSLEngineResult.Status; -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLSession; - -/** - * Implements the relevant portions of the SocketChannel interface with the SSLEngine wrapper. - */ -public class SSLSocketChannel2 implements ByteChannel, WrappedByteChannel { - - /** - * This object is used to feed the {@link SSLEngine}'s wrap and unwrap methods during the handshake phase. - **/ - protected static ByteBuffer emptybuffer = ByteBuffer.allocate(0); - - /** - * Should be used to count the buffer allocations. - * But because of #190 where HandshakeStatus.FINISHED is not properly returned by nio wrap/unwrap this variable is - * used to check whether {@link #createBuffers(SSLSession)} needs to be called. - **/ - protected int bufferallocations = 0; - - protected ExecutorService exec; - - /** - * encrypted data incoming - */ - protected ByteBuffer inCrypt; - - /** - * raw payload incomming - */ - protected ByteBuffer inData; - - /** - * encrypted data outgoing - */ - protected ByteBuffer outCrypt; - - protected SSLEngineResult readEngineResult; - - /** - * used to set interestOP SelectionKey.OP_WRITE for the underlying channel - */ - protected SelectionKey selectionKey; - - /** - * the underlying channel - */ - protected SocketChannel socketChannel; - - protected SSLEngine sslEngine; - - protected List> tasks; - - protected SSLEngineResult writeEngineResult; - - public SSLSocketChannel2(SocketChannel channel, SSLEngine sslEngine, ExecutorService exec, SelectionKey key) - throws IOException { - if (channel == null || sslEngine == null || exec == null) { - throw new IllegalArgumentException("parameter must not be null"); - } - - this.socketChannel = channel; - this.sslEngine = sslEngine; - this.exec = exec; - - readEngineResult = writeEngineResult = new SSLEngineResult(Status.BUFFER_UNDERFLOW, - sslEngine.getHandshakeStatus(), 0, 0); // init to prevent NPEs - - tasks = new ArrayList>(3); - if (key != null) { - key.interestOps(key.interestOps() | SelectionKey.OP_WRITE); - this.selectionKey = key; - } - createBuffers(sslEngine.getSession()); - // kick off handshake - socketChannel.write(wrap(emptybuffer));// initializes res - processHandshake(); - } - - public void close() throws IOException { - sslEngine.closeOutbound(); - sslEngine.getSession().invalidate(); - if (socketChannel.isOpen()) { - socketChannel.write(wrap(emptybuffer));// FIXME what if not all bytes can be written - } - socketChannel.close(); - } - - public SelectableChannel configureBlocking(boolean b) throws IOException { - return socketChannel.configureBlocking(b); - } - - public boolean connect(SocketAddress remote) throws IOException { - return socketChannel.connect(remote); - } - - public boolean finishConnect() throws IOException { - return socketChannel.finishConnect(); - } - - @Override - public boolean isBlocking() { - return socketChannel.isBlocking(); - } - - public boolean isConnected() { - return socketChannel.isConnected(); - } - - public boolean isInboundDone() { - return sslEngine.isInboundDone(); - } - - @Override - public boolean isNeedRead() { - return inData.hasRemaining() || (inCrypt.hasRemaining() - && readEngineResult.getStatus() != Status.BUFFER_UNDERFLOW - && readEngineResult.getStatus() != Status.CLOSED); - } - - @Override - public boolean isNeedWrite() { - return outCrypt.hasRemaining() - || !isHandShakeComplete(); // FIXME this condition can cause high cpu load during handshaking when network is slow - } - - @Override - public boolean isOpen() { - return socketChannel.isOpen(); - } - - /** - * Blocks when in blocking mode until at least one byte has been decoded.
    - * When not in blocking mode 0 may be returned. - * - * @return the number of bytes read. - **/ - public int read(ByteBuffer dst) throws IOException { - while (true) { - if (!dst.hasRemaining()) { - return 0; - } - if (!isHandShakeComplete()) { - if (isBlocking()) { - while (!isHandShakeComplete()) { - processHandshake(); - } - } else { - processHandshake(); - if (!isHandShakeComplete()) { - return 0; - } - } - } - // assert ( bufferallocations > 1 ); //see #190 - //if( bufferallocations <= 1 ) { - // createBuffers( sslEngine.getSession() ); - //} - /* 1. When "dst" is smaller than "inData" readRemaining will fill "dst" with data decoded in a previous read call. - * 2. When "inCrypt" contains more data than "inData" has remaining space, unwrap has to be called on more time(readRemaining) - */ - int purged = readRemaining(dst); - if (purged != 0) { - return purged; - } - - /* We only continue when we really need more data from the network. - * Thats the case if inData is empty or inCrypt holds to less data than necessary for decryption - */ - assert (inData.position() == 0); - inData.clear(); - - if (!inCrypt.hasRemaining()) { - inCrypt.clear(); - } else { - inCrypt.compact(); - } - - if (isBlocking() || readEngineResult.getStatus() == Status.BUFFER_UNDERFLOW) { - if (socketChannel.read(inCrypt) == -1) { - return -1; - } - } - inCrypt.flip(); - unwrap(); - - int transfered = transfereTo(inData, dst); - if (transfered == 0 && isBlocking()) { - continue; - } - return transfered; - } - } - - @Override - public int readMore(ByteBuffer dst) throws SSLException { - return readRemaining(dst); - } - - public Socket socket() { - return socketChannel.socket(); - } - - public int write(ByteBuffer src) throws IOException { - if (!isHandShakeComplete()) { - processHandshake(); - return 0; - } - // assert ( bufferallocations > 1 ); //see #190 - //if( bufferallocations <= 1 ) { - // createBuffers( sslEngine.getSession() ); - //} - int num = socketChannel.write(wrap(src)); - if (writeEngineResult.getStatus() == SSLEngineResult.Status.CLOSED) { - throw new EOFException("Connection is closed"); - } - return num; - - } - - @Override - public void writeMore() throws IOException { - write(outCrypt); - } - - protected void consumeDelegatedTasks() { - Runnable task; - while ((task = sslEngine.getDelegatedTask()) != null) { - tasks.add(exec.submit(task)); - // task.run(); - } - } - - protected void createBuffers(SSLSession session) { - int netBufferMax = session.getPacketBufferSize(); - int appBufferMax = Math.max(session.getApplicationBufferSize(), netBufferMax); - - if (inData == null) { - inData = ByteBuffer.allocate(appBufferMax); - outCrypt = ByteBuffer.allocate(netBufferMax); - inCrypt = ByteBuffer.allocate(netBufferMax); - } else { - if (inData.capacity() != appBufferMax) { - inData = ByteBuffer.allocate(appBufferMax); - } - if (outCrypt.capacity() != netBufferMax) { - outCrypt = ByteBuffer.allocate(netBufferMax); - } - if (inCrypt.capacity() != netBufferMax) { - inCrypt = ByteBuffer.allocate(netBufferMax); - } - } - if (inData.remaining() != 0) { - // no-op - } - inData.rewind(); - inData.flip(); - if (inCrypt.remaining() != 0) { - // no-op - } - inCrypt.rewind(); - inCrypt.flip(); - outCrypt.rewind(); - outCrypt.flip(); - bufferallocations++; - } - - private void consumeFutureUninterruptible(Future f) { - try { - while (true) { - try { - f.get(); - break; - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - } catch (ExecutionException e) { - throw new RuntimeException(e); - } - } - - private boolean isHandShakeComplete() { - HandshakeStatus status = sslEngine.getHandshakeStatus(); - return status == SSLEngineResult.HandshakeStatus.FINISHED - || status == SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING; - } - - /** - * This method will do whatever necessary to process the sslengine handshake. - * Thats why it's called both from the {@link #read(ByteBuffer)} and {@link #write(ByteBuffer)} - **/ - private synchronized void processHandshake() throws IOException { - if (sslEngine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING) { - return; // since this may be called either from a reading or a writing thread and because this method is synchronized it is necessary to double check if we are still handshaking. - } - if (!tasks.isEmpty()) { - Iterator> it = tasks.iterator(); - while (it.hasNext()) { - Future f = it.next(); - if (f.isDone()) { - it.remove(); - } else { - if (isBlocking()) { - consumeFutureUninterruptible(f); - } - return; - } - } - } - - if (sslEngine.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_UNWRAP) { - if (!isBlocking() || readEngineResult.getStatus() == Status.BUFFER_UNDERFLOW) { - inCrypt.compact(); - int read = socketChannel.read(inCrypt); - if (read == -1) { - throw new IOException("connection closed unexpectedly by peer"); - } - inCrypt.flip(); - } - inData.compact(); - unwrap(); - if (readEngineResult.getHandshakeStatus() == HandshakeStatus.FINISHED) { - createBuffers(sslEngine.getSession()); - return; - } - } - consumeDelegatedTasks(); - if (tasks.isEmpty() || sslEngine.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_WRAP) { - socketChannel.write(wrap(emptybuffer)); - if (writeEngineResult.getHandshakeStatus() == HandshakeStatus.FINISHED) { - createBuffers(sslEngine.getSession()); - return; - } - } - assert (sslEngine.getHandshakeStatus() - != HandshakeStatus.NOT_HANDSHAKING);// this function could only leave NOT_HANDSHAKING after createBuffers was called unless #190 occurs which means that nio wrap/unwrap never return HandshakeStatus.FINISHED - - bufferallocations - = 1; // look at variable declaration why this line exists and #190. Without this line buffers would not be be recreated when #190 AND a rehandshake occur. - } - - /** - * {@link #read(ByteBuffer)} may not be to leave all buffers(inData, inCrypt) - **/ - private int readRemaining(ByteBuffer dst) throws SSLException { - if (inData.hasRemaining()) { - return transfereTo(inData, dst); - } - if (!inData.hasRemaining()) { - inData.clear(); - } - // test if some bytes left from last read (e.g. BUFFER_UNDERFLOW) - if (inCrypt.hasRemaining()) { - unwrap(); - int amount = transfereTo(inData, dst); - if (readEngineResult.getStatus() == SSLEngineResult.Status.CLOSED) { - return -1; - } - if (amount > 0) { - return amount; - } - } - return 0; - } - - private int transfereTo(ByteBuffer from, ByteBuffer to) { - int fremain = from.remaining(); - int toremain = to.remaining(); - if (fremain > toremain) { - // FIXME there should be a more efficient transfer method - int limit = Math.min(fremain, toremain); - for (int i = 0; i < limit; i++) { - to.put(from.get()); - } - return limit; - } else { - to.put(from); - return fremain; - } - - } - - /** - * performs the unwrap operation by unwrapping from {@link #inCrypt} to {@link #inData} - **/ - private synchronized ByteBuffer unwrap() throws SSLException { - int rem; - //There are some ssl test suites, which get around the selector.select() call, which cause an infinite unwrap and 100% cpu usage (see #459 and #458) - if (readEngineResult.getStatus() == SSLEngineResult.Status.CLOSED - && sslEngine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING) { - try { - close(); - } catch (IOException e) { - //Not really interesting - } - } - do { - rem = inData.remaining(); - readEngineResult = sslEngine.unwrap(inCrypt, inData); - } while (readEngineResult.getStatus() == SSLEngineResult.Status.OK && (rem != inData.remaining() - || sslEngine.getHandshakeStatus() == HandshakeStatus.NEED_UNWRAP)); - inData.flip(); - return inData; - } - - private synchronized ByteBuffer wrap(ByteBuffer b) throws SSLException { - outCrypt.compact(); - writeEngineResult = sslEngine.wrap(b, outCrypt); - outCrypt.flip(); - return outCrypt; - } - -} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/SocketChannelIOHelper.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/SocketChannelIOHelper.java deleted file mode 100755 index 19ba64737..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/SocketChannelIOHelper.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket; - -import com.clevertap.android.sdk.java_websocket.enums.Role; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.ByteChannel; - -public class SocketChannelIOHelper { - - /** - * Returns whether the whole outQueue has been flushed - * - * @param ws The WebSocketImpl associated with the channels - * @param sockchannel The channel to write to - * @return returns Whether there is more data to write - * @throws IOException May be thrown by {@link WrappedByteChannel#writeMore()} - */ - public static boolean batch(WebSocketImpl ws, ByteChannel sockchannel) throws IOException { - if (ws == null) { - return false; - } - ByteBuffer buffer = ws.outQueue.peek(); - WrappedByteChannel c = null; - - if (buffer == null) { - if (sockchannel instanceof WrappedByteChannel) { - c = (WrappedByteChannel) sockchannel; - if (c.isNeedWrite()) { - c.writeMore(); - } - } - } else { - do {// FIXME writing as much as possible is unfair!! - /*int written = */ - sockchannel.write(buffer); - if (buffer.remaining() > 0) { - return false; - } else { - ws.outQueue.poll(); // Buffer finished. Remove it. - buffer = ws.outQueue.peek(); - } - } while (buffer != null); - } - - if (ws.outQueue.isEmpty() && ws.isFlushAndClose() && ws.getDraft() != null && ws.getDraft().getRole() != null - && ws.getDraft().getRole() == Role.SERVER) {// - ws.closeConnection(); - } - return c == null || !((WrappedByteChannel) sockchannel).isNeedWrite(); - } - - public static boolean read(final ByteBuffer buf, WebSocketImpl ws, ByteChannel channel) throws IOException { - buf.clear(); - int read = channel.read(buf); - buf.flip(); - - if (read == -1) { - ws.eot(); - return false; - } - return read != 0; - } - - /** - * @param buf The ByteBuffer to read from - * @param ws The WebSocketImpl associated with the channels - * @param channel The channel to read from - * @return returns Whether there is more data left which can be obtained via {@link - * WrappedByteChannel#readMore(ByteBuffer)} - * @throws IOException May be thrown by {@link WrappedByteChannel#readMore(ByteBuffer)}# - * @see WrappedByteChannel#readMore(ByteBuffer) - **/ - public static boolean readMore(final ByteBuffer buf, WebSocketImpl ws, WrappedByteChannel channel) - throws IOException { - buf.clear(); - int read = channel.readMore(buf); - buf.flip(); - - if (read == -1) { - ws.eot(); - return false; - } - return channel.isNeedRead(); - } - - private SocketChannelIOHelper() { - throw new IllegalStateException("Utility class"); - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/WebSocket.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/WebSocket.java deleted file mode 100755 index 512d3e492..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/WebSocket.java +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket; - -import com.clevertap.android.sdk.java_websocket.drafts.Draft; -import com.clevertap.android.sdk.java_websocket.enums.Opcode; -import com.clevertap.android.sdk.java_websocket.enums.ReadyState; -import com.clevertap.android.sdk.java_websocket.exceptions.WebsocketNotConnectedException; -import com.clevertap.android.sdk.java_websocket.framing.Framedata; -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; -import java.util.Collection; - -public interface WebSocket { - - /** - * sends the closing handshake. - * may be send in response to an other handshake. - * - * @param code the closing code - * @param message the closing message - */ - void close(int code, String message); - - /** - * sends the closing handshake. - * may be send in response to an other handshake. - * - * @param code the closing code - */ - void close(int code); - - /** - * Convenience function which behaves like close(CloseFrame.NORMAL) - */ - void close(); - - /** - * This will close the connection immediately without a proper close handshake. - * The code and the message therefore won't be transfered over the wire also they will be forwarded to - * onClose/onWebsocketClose. - * - * @param code the closing code - * @param message the closing message - **/ - void closeConnection(int code, String message); - - /** - * Getter for the connection attachment. - * - * @param The type of the attachment - * @return Returns the user attachment - * @since 1.3.7 - **/ - T getAttachment(); - - /** - * Setter for an attachment on the socket connection. - * The attachment may be of any type. - * - * @param attachment The object to be attached to the user - * @param The type of the attachment - * @since 1.3.7 - **/ - void setAttachment(T attachment); - - /** - * Getter for the draft - * - * @return the used draft - */ - Draft getDraft(); - - /** - * Returns the address of the endpoint this socket is bound to. - * - * @return never returns null - */ - InetSocketAddress getLocalSocketAddress(); - - /** - * Retrieve the WebSocket 'ReadyState'. - * This represents the state of the connection. - * It returns a numerical value, as per W3C WebSockets specs. - * - * @return Returns '0 = CONNECTING', '1 = OPEN', '2 = CLOSING' or '3 = CLOSED' - */ - ReadyState getReadyState(); - - /** - * Returns the address of the endpoint this socket is connected to, or{@code null} if it is unconnected. - * - * @return never returns null - */ - InetSocketAddress getRemoteSocketAddress(); - - /** - * Returns the HTTP Request-URI as defined by http://tools.ietf.org/html/rfc2616#section-5.1.2
    - * If the opening handshake has not yet happened it will return null. - * - * @return Returns the decoded path component of this URI. - **/ - String getResourceDescriptor(); - - /** - * Checks if the websocket has buffered data - * - * @return has the websocket buffered data - */ - boolean hasBufferedData(); - - /** - * Is the websocket in the state CLOSED - * - * @return state equals ReadyState.CLOSED - */ - boolean isClosed(); - - /** - * Is the websocket in the state CLOSING - * - * @return state equals ReadyState.CLOSING - */ - boolean isClosing(); - - /** - * Returns true when no further frames may be submitted
    - * This happens before the socket connection is closed. - * - * @return true when no further frames may be submitted - */ - boolean isFlushAndClose(); - - /** - * Is the websocket in the state OPEN - * - * @return state equals ReadyState.OPEN - */ - boolean isOpen(); - - /** - * Send Text data to the other end. - * - * @param text the text data to send - * @throws WebsocketNotConnectedException websocket is not yet connected - */ - void send(String text); - - /** - * Send Binary data (plain bytes) to the other end. - * - * @param bytes the binary data to send - * @throws IllegalArgumentException the data is null - * @throws WebsocketNotConnectedException websocket is not yet connected - */ - void send(ByteBuffer bytes); - - /** - * Send Binary data (plain bytes) to the other end. - * - * @param bytes the byte array to send - * @throws IllegalArgumentException the data is null - * @throws WebsocketNotConnectedException websocket is not yet connected - */ - void send(byte[] bytes); - - /** - * Allows to send continuous/fragmented frames conveniently.
    - * For more into on this frame type see http://tools.ietf.org/html/rfc6455#section-5.4
    - *

    - * If the first frame you send is also the last then it is not a fragmented frame and will received via onMessage - * instead of onFragmented even though it was send by this method. - * - * @param op This is only important for the first frame in the sequence. Opcode.TEXT, Opcode.BINARY are - * allowed. - * @param buffer The buffer which contains the payload. It may have no bytes remaining. - * @param fin true means the current frame is the last in the sequence. - **/ - void sendFragmentedFrame(Opcode op, ByteBuffer buffer, boolean fin); - - /** - * Send a frame to the other end - * - * @param framedata the frame to send to the other end - */ - void sendFrame(Framedata framedata); - - /** - * Send a collection of frames to the other end - * - * @param frames the frames to send to the other end - */ - void sendFrame(Collection frames); - - /** - * Send a ping to the other end - * - * @throws WebsocketNotConnectedException websocket is not yet connected - */ - void sendPing(); -} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/WebSocketAdapter.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/WebSocketAdapter.java deleted file mode 100755 index c161b8151..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/WebSocketAdapter.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket; - -import com.clevertap.android.sdk.java_websocket.drafts.Draft; -import com.clevertap.android.sdk.java_websocket.exceptions.InvalidDataException; -import com.clevertap.android.sdk.java_websocket.framing.Framedata; -import com.clevertap.android.sdk.java_websocket.framing.PingFrame; -import com.clevertap.android.sdk.java_websocket.framing.PongFrame; -import com.clevertap.android.sdk.java_websocket.handshake.ClientHandshake; -import com.clevertap.android.sdk.java_websocket.handshake.HandshakeImpl1Server; -import com.clevertap.android.sdk.java_websocket.handshake.ServerHandshake; -import com.clevertap.android.sdk.java_websocket.handshake.ServerHandshakeBuilder; - -/** - * This class default implements all methods of the WebSocketListener that can be overridden optionally when advances - * functionalities is needed.
    - **/ -public abstract class WebSocketAdapter implements WebSocketListener { - - @Override - public void onWebsocketHandshakeReceivedAsClient(WebSocket conn, ClientHandshake request, - ServerHandshake response) throws InvalidDataException { - //To overwrite - } - - /** - * This default implementation does not do anything. Go ahead and overwrite it. - * - * @see com.clevertap.android.sdk.java_websocket.WebSocketListener#onWebsocketHandshakeReceivedAsServer(WebSocket, - * Draft, ClientHandshake) - */ - @Override - public ServerHandshakeBuilder onWebsocketHandshakeReceivedAsServer(WebSocket conn, Draft draft, - ClientHandshake request) throws InvalidDataException { - return new HandshakeImpl1Server(); - } - - /** - * This default implementation does not do anything which will cause the connections to always progress. - * - * @see com.clevertap.android.sdk.java_websocket.WebSocketListener#onWebsocketHandshakeSentAsClient(WebSocket, - * ClientHandshake) - */ - @Override - public void onWebsocketHandshakeSentAsClient(WebSocket conn, ClientHandshake request) - throws InvalidDataException { - //To overwrite - } - - /** - * This default implementation will send a pong in response to the received ping. - * The pong frame will have the same payload as the ping frame. - * - * @see com.clevertap.android.sdk.java_websocket.WebSocketListener#onWebsocketPing(WebSocket, Framedata) - */ - @Override - public void onWebsocketPing(WebSocket conn, Framedata f) { - conn.sendFrame(new PongFrame((PingFrame) f)); - } - - /** - * This default implementation does not do anything. Go ahead and overwrite it. - * - * @see com.clevertap.android.sdk.java_websocket.WebSocketListener#onWebsocketPong(WebSocket, Framedata) - */ - @Override - public void onWebsocketPong(WebSocket conn, Framedata f) { - //To overwrite - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/WebSocketFactory.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/WebSocketFactory.java deleted file mode 100755 index 29922224e..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/WebSocketFactory.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket; - -import com.clevertap.android.sdk.java_websocket.drafts.Draft; -import java.util.List; - -public interface WebSocketFactory { - - /** - * Create a new Websocket with the provided listener, drafts and socket - * - * @param a The Listener for the WebsocketImpl - * @param d The draft which should be used - * @return A WebsocketImpl - */ - WebSocket createWebSocket(WebSocketAdapter a, Draft d); - - /** - * Create a new Websocket with the provided listener, drafts and socket - * - * @param a The Listener for the WebsocketImpl - * @param drafts The drafts which should be used - * @return A WebsocketImpl - */ - WebSocket createWebSocket(WebSocketAdapter a, List drafts); - -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/WebSocketImpl.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/WebSocketImpl.java deleted file mode 100755 index 4b9a02eb6..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/WebSocketImpl.java +++ /dev/null @@ -1,842 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket; - -import android.os.Build.VERSION_CODES; -import androidx.annotation.RequiresApi; -import com.clevertap.android.sdk.java_websocket.drafts.Draft; -import com.clevertap.android.sdk.java_websocket.drafts.Draft_6455; -import com.clevertap.android.sdk.java_websocket.enums.CloseHandshakeType; -import com.clevertap.android.sdk.java_websocket.enums.HandshakeState; -import com.clevertap.android.sdk.java_websocket.enums.Opcode; -import com.clevertap.android.sdk.java_websocket.enums.ReadyState; -import com.clevertap.android.sdk.java_websocket.enums.Role; -import com.clevertap.android.sdk.java_websocket.exceptions.IncompleteHandshakeException; -import com.clevertap.android.sdk.java_websocket.exceptions.InvalidDataException; -import com.clevertap.android.sdk.java_websocket.exceptions.InvalidHandshakeException; -import com.clevertap.android.sdk.java_websocket.exceptions.LimitExceededException; -import com.clevertap.android.sdk.java_websocket.exceptions.WebsocketNotConnectedException; -import com.clevertap.android.sdk.java_websocket.framing.CloseFrame; -import com.clevertap.android.sdk.java_websocket.framing.Framedata; -import com.clevertap.android.sdk.java_websocket.framing.PingFrame; -import com.clevertap.android.sdk.java_websocket.handshake.ClientHandshake; -import com.clevertap.android.sdk.java_websocket.handshake.ClientHandshakeBuilder; -import com.clevertap.android.sdk.java_websocket.handshake.Handshakedata; -import com.clevertap.android.sdk.java_websocket.handshake.ServerHandshake; -import com.clevertap.android.sdk.java_websocket.handshake.ServerHandshakeBuilder; -import com.clevertap.android.sdk.java_websocket.util.Charsetfunctions; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; -import java.nio.channels.ByteChannel; -import java.nio.channels.SelectionKey; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; - -/** - * Represents one end (client or server) of a single WebSocketImpl connection. - * Takes care of the "handshake" phase, then allows for easy sending of - * text frames, and receiving frames through an event-based model. - */ -public class WebSocketImpl implements WebSocket { - - /** - * The default port of WebSockets, as defined in the spec. If the nullary - * constructor is used, DEFAULT_PORT will be the port the WebSocketServer - * is binded to. Note that ports under 1024 usually require root permissions. - */ - public static final int DEFAULT_PORT = 80; - - /** - * The default wss port of WebSockets, as defined in the spec. If the nullary - * constructor is used, DEFAULT_WSS_PORT will be the port the WebSocketServer - * is binded to. Note that ports under 1024 usually require root permissions. - */ - public static final int DEFAULT_WSS_PORT = 443; - - /** - * Initial buffer size - */ - public static final int RCVBUF = 16384; - - /** - * Queue of buffers that need to be processed - */ - public final BlockingQueue inQueue; - - /** - * Queue of buffers that need to be sent to the client. - */ - public final BlockingQueue outQueue; - - /** - * Attribute to store connection attachment - * - * @since 1.3.7 - */ - private Object attachment; - - /** - * the possibly wrapped channel object whose selection is controlled by {@link #key} - */ - private ByteChannel channel; - - private Integer closecode = null; - - private Boolean closedremotely = null; - - private String closemessage = null; - - /** - * The draft which is used by this websocket - */ - private Draft draft = null; - - /** - * When true no further frames may be submitted to be sent - */ - private boolean flushandclosestate = false; - - /** - * stores the handshake sent by this websocket ( Role.CLIENT only ) - */ - private ClientHandshake handshakerequest = null; - - private SelectionKey key; - - /** - * A list of drafts available for this websocket - */ - private List knownDrafts; - - /** - * Attribute, when the last pong was recieved - */ - private long lastPong = System.currentTimeMillis(); - - /** - * Attribute to cache a ping frame - */ - private PingFrame pingFrame; - - /** - * The current state of the connection - */ - private volatile ReadyState readyState = ReadyState.NOT_YET_CONNECTED; - - private String resourceDescriptor = null; - - /** - * The role which this websocket takes in the connection - */ - private Role role; - - /** - * Attribut to synchronize the write - */ - private final Object synchronizeWriteObject = new Object(); - - /** - * the bytes of an incomplete received handshake - */ - private ByteBuffer tmpHandshakeBytes = ByteBuffer.allocate(0); - - /** - * The listener to notify of WebSocket events. - */ - private final WebSocketListener wsl; - - /** - * Creates a websocket with server role - * - * @param listener The listener for this instance - * @param drafts The drafts which should be used - */ - public WebSocketImpl(WebSocketListener listener, List drafts) { - this(listener, (Draft) null); - this.role = Role.SERVER; - // draft.copyInstance will be called when the draft is first needed - if (drafts == null || drafts.isEmpty()) { - knownDrafts = new ArrayList(); - knownDrafts.add(new Draft_6455()); - } else { - knownDrafts = drafts; - } - } - - /** - * creates a websocket with client role - * - * @param listener The listener for this instance - * @param draft The draft which should be used - */ - public WebSocketImpl(WebSocketListener listener, Draft draft) { - if (listener == null || (draft == null && role - == Role.SERVER))// socket can be null because we want do be able to create the object without already having a bound channel - { - throw new IllegalArgumentException("parameters must not be null"); - } - this.outQueue = new LinkedBlockingQueue(); - inQueue = new LinkedBlockingQueue(); - this.wsl = listener; - this.role = Role.CLIENT; - if (draft != null) { - this.draft = draft.copyInstance(); - } - } - - public synchronized void close(int code, String message, boolean remote) { - if (readyState != ReadyState.CLOSING && readyState != ReadyState.CLOSED) { - if (readyState == ReadyState.OPEN) { - if (code == CloseFrame.ABNORMAL_CLOSE) { - assert (!remote); - readyState = ReadyState.CLOSING; - flushAndClose(code, message, false); - return; - } - if (draft.getCloseHandshakeType() != CloseHandshakeType.NONE) { - try { - if (!remote) { - try { - wsl.onWebsocketCloseInitiated(this, code, message); - } catch (RuntimeException e) { - wsl.onWebsocketError(this, e); - } - } - if (isOpen()) { - CloseFrame closeFrame = new CloseFrame(); - closeFrame.setReason(message); - closeFrame.setCode(code); - closeFrame.isValid(); - sendFrame(closeFrame); - } - } catch (InvalidDataException e) { - wsl.onWebsocketError(this, e); - flushAndClose(CloseFrame.ABNORMAL_CLOSE, "generated frame is invalid", false); - } - } - flushAndClose(code, message, remote); - } else if (code == CloseFrame.FLASHPOLICY) { - assert (remote); - flushAndClose(CloseFrame.FLASHPOLICY, message, true); - } else if (code == CloseFrame.PROTOCOL_ERROR) { // this endpoint found a PROTOCOL_ERROR - flushAndClose(code, message, remote); - } else { - flushAndClose(CloseFrame.NEVER_CONNECTED, message, false); - } - readyState = ReadyState.CLOSING; - tmpHandshakeBytes = null; - return; - } - } - - @Override - public void close(int code, String message) { - close(code, message, false); - } - - @Override - public void close(int code) { - close(code, "", false); - } - - public void close(InvalidDataException e) { - close(e.getCloseCode(), e.getMessage(), false); - } - - @Override - public void close() { - close(CloseFrame.NORMAL); - } - - /** - * This will close the connection immediately without a proper close handshake. - * The code and the message therefore won't be transfered over the wire also they will be forwarded to - * onClose/onWebsocketClose. - * - * @param code the closing code - * @param message the closing message - * @param remote Indicates who "generated" code.
    - * true means that this endpoint received the code from the other - * endpoint.
    - * false means this endpoint decided to send the given code,
    - * remote may also be true if this endpoint started the closing handshake since the - * other endpoint may not simply echo the code but close the connection the same time - * this endpoint does do but with an other code.
    - **/ - public synchronized void closeConnection(int code, String message, boolean remote) { - if (readyState == ReadyState.CLOSED) { - return; - } - //Methods like eot() call this method without calling onClose(). Due to that reason we have to adjust the ReadyState manually - if (readyState == ReadyState.OPEN) { - if (code == CloseFrame.ABNORMAL_CLOSE) { - readyState = ReadyState.CLOSING; - } - } - if (key != null) { - // key.attach( null ); //see issue #114 - key.cancel(); - } - if (channel != null) { - try { - channel.close(); - } catch (IOException e) { - if (e.getMessage().equals("Broken pipe")) { - } else { - wsl.onWebsocketError(this, e); - } - } - } - try { - this.wsl.onWebsocketClose(this, code, message, remote); - } catch (RuntimeException e) { - - wsl.onWebsocketError(this, e); - } - if (draft != null) { - draft.reset(); - } - handshakerequest = null; - readyState = ReadyState.CLOSED; - } - - public void closeConnection() { - if (closedremotely == null) { - throw new IllegalStateException("this method must be used in conjunction with flushAndClose"); - } - closeConnection(closecode, closemessage, closedremotely); - } - - public void closeConnection(int code, String message) { - closeConnection(code, message, false); - } - - /** - * Method to decode the provided ByteBuffer - * - * @param socketBuffer the ByteBuffer to decode - */ - @RequiresApi(api = VERSION_CODES.KITKAT) - public void decode(ByteBuffer socketBuffer) { - assert (socketBuffer.hasRemaining()); - - if (readyState != ReadyState.NOT_YET_CONNECTED) { - if (readyState == ReadyState.OPEN) { - decodeFrames(socketBuffer); - } - } else { - if (decodeHandshake(socketBuffer) && (!isClosing() && !isClosed())) { - assert (tmpHandshakeBytes.hasRemaining() != socketBuffer.hasRemaining() || !socketBuffer - .hasRemaining()); // the buffers will never have remaining bytes at the same time - if (socketBuffer.hasRemaining()) { - decodeFrames(socketBuffer); - } else if (tmpHandshakeBytes.hasRemaining()) { - decodeFrames(tmpHandshakeBytes); - } - } - } - } - - public void eot() { - if (readyState == ReadyState.NOT_YET_CONNECTED) { - closeConnection(CloseFrame.NEVER_CONNECTED, true); - } else if (flushandclosestate) { - closeConnection(closecode, closemessage, closedremotely); - } else if (draft.getCloseHandshakeType() == CloseHandshakeType.NONE) { - closeConnection(CloseFrame.NORMAL, true); - } else if (draft.getCloseHandshakeType() == CloseHandshakeType.ONEWAY) { - if (role == Role.SERVER) { - closeConnection(CloseFrame.ABNORMAL_CLOSE, true); - } else { - closeConnection(CloseFrame.NORMAL, true); - } - } else { - closeConnection(CloseFrame.ABNORMAL_CLOSE, true); - } - } - - public synchronized void flushAndClose(int code, String message, boolean remote) { - if (flushandclosestate) { - return; - } - closecode = code; - closemessage = message; - closedremotely = remote; - - flushandclosestate = true; - - wsl.onWriteDemand(this); // ensures that all outgoing frames are flushed before closing the connection - try { - wsl.onWebsocketClosing(this, code, message, remote); - } catch (RuntimeException e) { - wsl.onWebsocketError(this, e); - } - if (draft != null) { - draft.reset(); - } - handshakerequest = null; - } - - @Override - @SuppressWarnings("unchecked") - public T getAttachment() { - return (T) attachment; - } - - @Override - public void setAttachment(T attachment) { - this.attachment = attachment; - } - - public ByteChannel getChannel() { - return channel; - } - - public void setChannel(ByteChannel channel) { - this.channel = channel; - } - - @Override - public Draft getDraft() { - return draft; - } - - @Override - public InetSocketAddress getLocalSocketAddress() { - return wsl.getLocalSocketAddress(this); - } - - @Override - public ReadyState getReadyState() { - return readyState; - } - - @Override - public InetSocketAddress getRemoteSocketAddress() { - return wsl.getRemoteSocketAddress(this); - } - - @Override - public String getResourceDescriptor() { - return resourceDescriptor; - } - - /** - * @return the selection key of this implementation - */ - public SelectionKey getSelectionKey() { - return key; - } - - /** - * @param key the selection key of this implementation - */ - public void setSelectionKey(SelectionKey key) { - this.key = key; - } - - /** - * Getter for the websocket listener - * - * @return the websocket listener associated with this instance - */ - public WebSocketListener getWebSocketListener() { - return wsl; - } - - @Override - public boolean hasBufferedData() { - return !this.outQueue.isEmpty(); - } - - @Override - public boolean isClosed() { - return readyState == ReadyState.CLOSED; - } - - @Override - public boolean isClosing() { - return readyState == ReadyState.CLOSING; - } - - @Override - public boolean isFlushAndClose() { - return flushandclosestate; - } - - @Override - public boolean isOpen() { - return readyState == ReadyState.OPEN; - } - - /** - * Send Text data to the other end. - * - * @throws WebsocketNotConnectedException websocket is not yet connected - */ - @Override - public void send(String text) { - if (text == null) { - throw new IllegalArgumentException("Cannot send 'null' data to a WebSocketImpl."); - } - send(draft.createFrames(text, role == Role.CLIENT)); - } - - /** - * Send Binary data (plain bytes) to the other end. - * - * @throws IllegalArgumentException the data is null - * @throws WebsocketNotConnectedException websocket is not yet connected - */ - @Override - public void send(ByteBuffer bytes) { - if (bytes == null) { - throw new IllegalArgumentException("Cannot send 'null' data to a WebSocketImpl."); - } - send(draft.createFrames(bytes, role == Role.CLIENT)); - } - - @Override - public void send(byte[] bytes) { - send(ByteBuffer.wrap(bytes)); - } - - @Override - public void sendFragmentedFrame(Opcode op, ByteBuffer buffer, boolean fin) { - send(draft.continuousFrame(op, buffer, fin)); - } - - @Override - public void sendFrame(Collection frames) { - send(frames); - } - - @Override - public void sendFrame(Framedata framedata) { - send(Collections.singletonList(framedata)); - } - - public void sendPing() { - if (pingFrame == null) { - pingFrame = new PingFrame(); - } - sendFrame(pingFrame); - } - - @RequiresApi(api = VERSION_CODES.KITKAT) - public void startHandshake(ClientHandshakeBuilder handshakedata) throws InvalidHandshakeException { - // Store the Handshake Request we are about to send - this.handshakerequest = draft.postProcessHandshakeRequestAsClient(handshakedata); - - resourceDescriptor = handshakedata.getResourceDescriptor(); - assert (resourceDescriptor != null); - - // Notify Listener - try { - wsl.onWebsocketHandshakeSentAsClient(this, this.handshakerequest); - } catch (InvalidDataException e) { - // Stop if the client code throws an exception - throw new InvalidHandshakeException("Handshake data rejected by client."); - } catch (RuntimeException e) { - wsl.onWebsocketError(this, e); - throw new InvalidHandshakeException("rejected because of " + e); - } - - // Send - write(draft.createHandshake(this.handshakerequest)); - } - - @Override - public String toString() { - return super.toString(); // its nice to be able to set breakpoints here - } - - /** - * Update the timestamp when the last pong was received - */ - public void updateLastPong() { - this.lastPong = System.currentTimeMillis(); - } - - protected void closeConnection(int code, boolean remote) { - closeConnection(code, "", remote); - } - - /** - * Getter for the last pong recieved - * - * @return the timestamp for the last recieved pong - */ - long getLastPong() { - return lastPong; - } - - /** - * Close the connection if there was a server error by a RuntimeException - * - * @param exception the RuntimeException causing this problem - */ - @RequiresApi(api = VERSION_CODES.KITKAT) - private void closeConnectionDueToInternalServerError(RuntimeException exception) { - write(generateHttpResponseDueToError(500)); - flushAndClose(CloseFrame.NEVER_CONNECTED, exception.getMessage(), false); - } - - /** - * Close the connection if the received handshake was not correct - * - * @param exception the InvalidDataException causing this problem - */ - @RequiresApi(api = VERSION_CODES.KITKAT) - private void closeConnectionDueToWrongHandshake(InvalidDataException exception) { - write(generateHttpResponseDueToError(404)); - flushAndClose(exception.getCloseCode(), exception.getMessage(), false); - } - - private void decodeFrames(ByteBuffer socketBuffer) { - List frames; - try { - frames = draft.translateFrame(socketBuffer); - for (Framedata f : frames) { - draft.processFrame(this, f); - } - } catch (LimitExceededException e) { - if (e.getLimit() == Integer.MAX_VALUE) { - wsl.onWebsocketError(this, e); - } - close(e); - } catch (InvalidDataException e) { - wsl.onWebsocketError(this, e); - close(e); - } - } - - /** - * Returns whether the handshake phase has is completed. - * In case of a broken handshake this will be never the case. - **/ - @RequiresApi(api = VERSION_CODES.KITKAT) - private boolean decodeHandshake(ByteBuffer socketBufferNew) { - ByteBuffer socketBuffer; - if (tmpHandshakeBytes.capacity() == 0) { - socketBuffer = socketBufferNew; - } else { - if (tmpHandshakeBytes.remaining() < socketBufferNew.remaining()) { - ByteBuffer buf = ByteBuffer.allocate(tmpHandshakeBytes.capacity() + socketBufferNew.remaining()); - tmpHandshakeBytes.flip(); - buf.put(tmpHandshakeBytes); - tmpHandshakeBytes = buf; - } - - tmpHandshakeBytes.put(socketBufferNew); - tmpHandshakeBytes.flip(); - socketBuffer = tmpHandshakeBytes; - } - socketBuffer.mark(); - try { - HandshakeState handshakestate; - try { - if (role == Role.SERVER) { - if (draft == null) { - for (Draft d : knownDrafts) { - d = d.copyInstance(); - try { - d.setParseMode(role); - socketBuffer.reset(); - Handshakedata tmphandshake = d.translateHandshake(socketBuffer); - if (!(tmphandshake instanceof ClientHandshake)) { - closeConnectionDueToWrongHandshake( - new InvalidDataException(CloseFrame.PROTOCOL_ERROR, - "wrong http function")); - return false; - } - ClientHandshake handshake = (ClientHandshake) tmphandshake; - handshakestate = d.acceptHandshakeAsServer(handshake); - if (handshakestate == HandshakeState.MATCHED) { - resourceDescriptor = handshake.getResourceDescriptor(); - ServerHandshakeBuilder response; - try { - response = wsl.onWebsocketHandshakeReceivedAsServer(this, d, handshake); - } catch (InvalidDataException e) { - closeConnectionDueToWrongHandshake(e); - return false; - } catch (RuntimeException e) { - wsl.onWebsocketError(this, e); - closeConnectionDueToInternalServerError(e); - return false; - } - write(d.createHandshake( - d.postProcessHandshakeResponseAsServer(handshake, response))); - draft = d; - open(handshake); - return true; - } - } catch (InvalidHandshakeException e) { - // go on with an other draft - } - } - if (draft == null) { - closeConnectionDueToWrongHandshake( - new InvalidDataException(CloseFrame.PROTOCOL_ERROR, "no draft matches")); - } - return false; - } else { - // special case for multiple step handshakes - Handshakedata tmphandshake = draft.translateHandshake(socketBuffer); - if (!(tmphandshake instanceof ClientHandshake)) { - flushAndClose(CloseFrame.PROTOCOL_ERROR, "wrong http function", false); - return false; - } - ClientHandshake handshake = (ClientHandshake) tmphandshake; - handshakestate = draft.acceptHandshakeAsServer(handshake); - - if (handshakestate == HandshakeState.MATCHED) { - open(handshake); - return true; - } else { - close(CloseFrame.PROTOCOL_ERROR, "the handshake did finally not match"); - } - return false; - } - } else if (role == Role.CLIENT) { - draft.setParseMode(role); - Handshakedata tmphandshake = draft.translateHandshake(socketBuffer); - if (!(tmphandshake instanceof ServerHandshake)) { - flushAndClose(CloseFrame.PROTOCOL_ERROR, "wrong http function", false); - return false; - } - ServerHandshake handshake = (ServerHandshake) tmphandshake; - handshakestate = draft.acceptHandshakeAsClient(handshakerequest, handshake); - if (handshakestate == HandshakeState.MATCHED) { - try { - wsl.onWebsocketHandshakeReceivedAsClient(this, handshakerequest, handshake); - } catch (InvalidDataException e) { - flushAndClose(e.getCloseCode(), e.getMessage(), false); - return false; - } catch (RuntimeException e) { - wsl.onWebsocketError(this, e); - flushAndClose(CloseFrame.NEVER_CONNECTED, e.getMessage(), false); - return false; - } - open(handshake); - return true; - } else { - close(CloseFrame.PROTOCOL_ERROR, "draft " + draft + " refuses handshake"); - } - } - } catch (InvalidHandshakeException e) { - close(e); - } - } catch (IncompleteHandshakeException e) { - if (tmpHandshakeBytes.capacity() == 0) { - socketBuffer.reset(); - int newsize = e.getPreferredSize(); - if (newsize == 0) { - newsize = socketBuffer.capacity() + 16; - } else { - assert (e.getPreferredSize() >= socketBuffer.remaining()); - } - tmpHandshakeBytes = ByteBuffer.allocate(newsize); - - tmpHandshakeBytes.put(socketBufferNew); - // tmpHandshakeBytes.flip(); - } else { - tmpHandshakeBytes.position(tmpHandshakeBytes.limit()); - tmpHandshakeBytes.limit(tmpHandshakeBytes.capacity()); - } - } - return false; - } - - /** - * Generate a simple response for the corresponding endpoint to indicate some error - * - * @param errorCode the http error code - * @return the complete response as ByteBuffer - */ - @RequiresApi(api = VERSION_CODES.KITKAT) - private ByteBuffer generateHttpResponseDueToError(int errorCode) { - String errorCodeDescription; - switch (errorCode) { - case 404: - errorCodeDescription = "404 WebSocket Upgrade Failure"; - break; - case 500: - default: - errorCodeDescription = "500 Internal Server Error"; - } - return ByteBuffer.wrap(Charsetfunctions.asciiBytes("HTTP/1.1 " + errorCodeDescription - + "\r\nContent-Type: text/html\nServer: TooTallNate Java-WebSocket\r\nContent-Length: " + (48 - + errorCodeDescription.length()) + "\r\n\r\n

    " + errorCodeDescription - + "

    ")); - } - - private void open(Handshakedata d) { - readyState = ReadyState.OPEN; - try { - wsl.onWebsocketOpen(this, d); - } catch (RuntimeException e) { - wsl.onWebsocketError(this, e); - } - } - - private void send(Collection frames) { - if (!isOpen()) { - throw new WebsocketNotConnectedException(); - } - if (frames == null) { - throw new IllegalArgumentException(); - } - ArrayList outgoingFrames = new ArrayList(); - for (Framedata f : frames) { - outgoingFrames.add(draft.createBinaryFrame(f)); - } - write(outgoingFrames); - } - - /** - * Write a list of bytebuffer (frames in binary form) into the outgoing queue - * - * @param bufs the list of bytebuffer - */ - private void write(List bufs) { - synchronized (synchronizeWriteObject) { - for (ByteBuffer b : bufs) { - write(b); - } - } - } - - private void write(ByteBuffer buf) { - outQueue.add(buf); - wsl.onWriteDemand(this); - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/WebSocketListener.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/WebSocketListener.java deleted file mode 100755 index 110feac39..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/WebSocketListener.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket; - -import com.clevertap.android.sdk.java_websocket.drafts.Draft; -import com.clevertap.android.sdk.java_websocket.exceptions.InvalidDataException; -import com.clevertap.android.sdk.java_websocket.framing.CloseFrame; -import com.clevertap.android.sdk.java_websocket.framing.Framedata; -import com.clevertap.android.sdk.java_websocket.handshake.ClientHandshake; -import com.clevertap.android.sdk.java_websocket.handshake.Handshakedata; -import com.clevertap.android.sdk.java_websocket.handshake.ServerHandshake; -import com.clevertap.android.sdk.java_websocket.handshake.ServerHandshakeBuilder; -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; - -/** - * Implemented by WebSocketClient and WebSocketServer. - * The methods within are called by WebSocket. - * Almost every method takes a first parameter conn which represents the source of the respective event. - */ -public interface WebSocketListener { - - /** - * @param conn The WebSocket instance this event is occuring on. - * @return Returns the address of the endpoint this socket is bound to. - * @see WebSocket#getLocalSocketAddress() - */ - InetSocketAddress getLocalSocketAddress(WebSocket conn); - - /** - * @param conn The WebSocket instance this event is occuring on. - * @return Returns the address of the endpoint this socket is connected to, or{@code null} if it is unconnected. - * @see WebSocket#getRemoteSocketAddress() - */ - InetSocketAddress getRemoteSocketAddress(WebSocket conn); - - /** - * Called after WebSocket#close is explicity called, or when the - * other end of the WebSocket connection is closed. - * - * @param ws The WebSocket instance this event is occuring on. - * @param code The codes can be looked up here: {@link CloseFrame} - * @param reason Additional information string - * @param remote Returns whether or not the closing of the connection was initiated by the remote host. - */ - void onWebsocketClose(WebSocket ws, int code, String reason, boolean remote); - - /** - * send when this peer sends a close handshake - * - * @param ws The WebSocket instance this event is occuring on. - * @param code The codes can be looked up here: {@link CloseFrame} - * @param reason Additional information string - */ - void onWebsocketCloseInitiated(WebSocket ws, int code, String reason); - - /** - * Called as soon as no further frames are accepted - * - * @param ws The WebSocket instance this event is occuring on. - * @param code The codes can be looked up here: {@link CloseFrame} - * @param reason Additional information string - * @param remote Returns whether or not the closing of the connection was initiated by the remote host. - */ - void onWebsocketClosing(WebSocket ws, int code, String reason, boolean remote); - - /** - * Called if an exception worth noting occurred. - * If an error causes the connection to fail onClose will be called additionally afterwards. - * - * @param conn The WebSocket instance this event is occuring on. - * @param ex The exception that occurred.
    - * Might be null if the exception is not related to any specific connection. For example if the server - * port could not be bound. - */ - void onWebsocketError(WebSocket conn, Exception ex); - - /** - * Called on the client side when the socket connection is first established, and the WebSocketImpl - * handshake response has been received. - * - * @param conn The WebSocket related to this event - * @param request The handshake initially send out to the server by this websocket. - * @param response The handshake the server sent in response to the request. - * @throws InvalidDataException Allows the client to reject the connection with the server in respect of its - * handshake response. - */ - void onWebsocketHandshakeReceivedAsClient(WebSocket conn, ClientHandshake request, ServerHandshake response) - throws InvalidDataException; - - /** - * Called on the server side when the socket connection is first established, and the WebSocket - * handshake has been received. This method allows to deny connections based on the received handshake.
    - * By default this method only requires protocol compliance. - * - * @param conn The WebSocket related to this event - * @param draft The protocol draft the client uses to connect - * @param request The opening http message send by the client. Can be used to access additional fields like - * cookies. - * @return Returns an incomplete handshake containing all optional fields - * @throws InvalidDataException Throwing this exception will cause this handshake to be rejected - */ - ServerHandshakeBuilder onWebsocketHandshakeReceivedAsServer(WebSocket conn, Draft draft, ClientHandshake request) - throws InvalidDataException; - - /** - * Called on the client side when the socket connection is first established, and the WebSocketImpl - * handshake has just been sent. - * - * @param conn The WebSocket related to this event - * @param request The handshake sent to the server by this websocket - * @throws InvalidDataException Allows the client to stop the connection from progressing - */ - void onWebsocketHandshakeSentAsClient(WebSocket conn, ClientHandshake request) throws InvalidDataException; - - /** - * Called when an entire text frame has been received. Do whatever you want - * here... - * - * @param conn The WebSocket instance this event is occurring on. - * @param message The UTF-8 decoded message that was received. - */ - void onWebsocketMessage(WebSocket conn, String message); - - /** - * Called when an entire binary frame has been received. Do whatever you want - * here... - * - * @param conn The WebSocket instance this event is occurring on. - * @param blob The binary message that was received. - */ - void onWebsocketMessage(WebSocket conn, ByteBuffer blob); - - /** - * Called after onHandshakeReceived returns true. - * Indicates that a complete WebSocket connection has been established, - * and we are ready to send/receive data. - * - * @param conn The WebSocket instance this event is occuring on. - * @param d The handshake of the websocket instance - */ - void onWebsocketOpen(WebSocket conn, Handshakedata d); - - /** - * Called a ping frame has been received. - * This method must send a corresponding pong by itself. - * - * @param conn The WebSocket instance this event is occuring on. - * @param f The ping frame. Control frames may contain payload. - */ - void onWebsocketPing(WebSocket conn, Framedata f); - - /** - * Called when a pong frame is received. - * - * @param conn The WebSocket instance this event is occuring on. - * @param f The pong frame. Control frames may contain payload. - **/ - void onWebsocketPong(WebSocket conn, Framedata f); - - /** - * This method is used to inform the selector thread that there is data queued to be written to the socket. - * - * @param conn The WebSocket instance this event is occuring on. - */ - void onWriteDemand(WebSocket conn); -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/WrappedByteChannel.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/WrappedByteChannel.java deleted file mode 100755 index 3134577e2..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/WrappedByteChannel.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.ByteChannel; - -public interface WrappedByteChannel extends ByteChannel { - - /** - * This function returns the blocking state of the channel - * - * @return is the channel blocking - */ - boolean isBlocking(); - - /** - * returns whether readMore should be called to fetch data which has been decoded but not yet been returned. - * - * @return is a additional read needed - * @see #read(ByteBuffer) - * @see #readMore(ByteBuffer) - **/ - boolean isNeedRead(); - - /** - * returns whether writeMore should be called write additional data. - * - * @return is a additional write needed - */ - boolean isNeedWrite(); - - /** - * This function does not read data from the underlying channel at all. It is just a way to fetch data which has - * already be received or decoded but was but was not yet returned to the user. - * This could be the case when the decoded data did not fit into the buffer the user passed to {@link - * #read(ByteBuffer)}. - * - * @param dst the destiny of the read - * @return the amount of remaining data - * @throws IOException when a error occurred during unwrapping - **/ - int readMore(ByteBuffer dst) throws IOException; - - /** - * Gets called when {@link #isNeedWrite()} ()} requires a additional rite - * - * @throws IOException may be thrown due to an error while writing - */ - void writeMore() throws IOException; -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/client/WebSocketClient.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/client/WebSocketClient.java deleted file mode 100755 index ba8db1510..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/client/WebSocketClient.java +++ /dev/null @@ -1,848 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.client; - -import android.os.Build.VERSION_CODES; -import androidx.annotation.RequiresApi; -import com.clevertap.android.sdk.java_websocket.AbstractWebSocket; -import com.clevertap.android.sdk.java_websocket.WebSocket; -import com.clevertap.android.sdk.java_websocket.WebSocketImpl; -import com.clevertap.android.sdk.java_websocket.drafts.Draft; -import com.clevertap.android.sdk.java_websocket.drafts.Draft_6455; -import com.clevertap.android.sdk.java_websocket.enums.Opcode; -import com.clevertap.android.sdk.java_websocket.enums.ReadyState; -import com.clevertap.android.sdk.java_websocket.exceptions.InvalidHandshakeException; -import com.clevertap.android.sdk.java_websocket.framing.CloseFrame; -import com.clevertap.android.sdk.java_websocket.framing.Framedata; -import com.clevertap.android.sdk.java_websocket.handshake.HandshakeImpl1Client; -import com.clevertap.android.sdk.java_websocket.handshake.Handshakedata; -import com.clevertap.android.sdk.java_websocket.handshake.ServerHandshake; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.InetSocketAddress; -import java.net.Proxy; -import java.net.Socket; -import java.net.URI; -import java.nio.ByteBuffer; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import javax.net.SocketFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLSocketFactory; - -/** - * A subclass must implement at least onOpen, onClose, and onMessage to be - * useful. At runtime the user is expected to establish a connection via {@link #connect()}, then receive events like - * {@link #onMessage(String)} via the overloaded methods and to {@link #send(String)} data to the server. - */ -public abstract class WebSocketClient extends AbstractWebSocket implements Runnable, WebSocket { - - private class WebsocketWriteThread implements Runnable { - - private final WebSocketClient webSocketClient; - - WebsocketWriteThread(WebSocketClient webSocketClient) { - this.webSocketClient = webSocketClient; - } - - @Override - public void run() { - Thread.currentThread().setName("WebSocketWriteThread-" + Thread.currentThread().getId()); - try { - runWriteData(); - } catch (IOException e) { - handleIOException(e); - } finally { - closeSocket(); - writeThread = null; - } - } - - /** - * Closing the socket - */ - private void closeSocket() { - try { - if (socket != null) { - socket.close(); - } - } catch (IOException ex) { - onWebsocketError(webSocketClient, ex); - } - } - - /** - * Write the data into the outstream - * - * @throws IOException if write or flush did not work - */ - private void runWriteData() throws IOException { - try { - while (!Thread.interrupted()) { - ByteBuffer buffer = engine.outQueue.take(); - ostream.write(buffer.array(), 0, buffer.limit()); - ostream.flush(); - } - } catch (InterruptedException e) { - for (ByteBuffer buffer : engine.outQueue) { - ostream.write(buffer.array(), 0, buffer.limit()); - ostream.flush(); - } - Thread.currentThread().interrupt(); - } - } - } - - /** - * The URI this channel is supposed to connect to. - */ - protected URI uri = null; - - /** - * The latch for closeBlocking() - */ - private CountDownLatch closeLatch = new CountDownLatch(1); - - /** - * The latch for connectBlocking() - */ - private CountDownLatch connectLatch = new CountDownLatch(1); - - /** - * The thread to connect and read message - */ - private Thread connectReadThread; - - /** - * The socket timeout value to be used in milliseconds. - */ - private int connectTimeout = 0; - - /** - * The draft to use - */ - private Draft draft; - - /** - * The underlying engine - */ - private WebSocketImpl engine = null; - - /** - * The additional headers to use - */ - private Map headers; - - /** - * The used OutputStream - */ - private OutputStream ostream; - - /** - * The used proxy, if any - */ - private Proxy proxy = Proxy.NO_PROXY; - - /** - * The socket for this WebSocketClient - */ - private Socket socket = null; - - /** - * The SocketFactory for this WebSocketClient - * - * @since 1.4.0 - */ - private SocketFactory socketFactory = null; - - /** - * The thread to write outgoing message - */ - private Thread writeThread; - - /** - * Constructs a WebSocketClient instance and sets it to the connect to the - * specified URI. The channel does not attampt to connect automatically. The connection - * will be established once you call connect. - * - * @param serverUri the server URI to connect to - */ - public WebSocketClient(URI serverUri) { - this(serverUri, new Draft_6455()); - } - - /** - * Constructs a WebSocketClient instance and sets it to the connect to the - * specified URI. The channel does not attampt to connect automatically. The connection - * will be established once you call connect. - * - * @param serverUri the server URI to connect to - * @param protocolDraft The draft which should be used for this connection - */ - public WebSocketClient(URI serverUri, Draft protocolDraft) { - this(serverUri, protocolDraft, null, 0); - } - - /** - * Constructs a WebSocketClient instance and sets it to the connect to the - * specified URI. The channel does not attampt to connect automatically. The connection - * will be established once you call connect. - * - * @param serverUri the server URI to connect to - * @param httpHeaders Additional HTTP-Headers - * @since 1.3.8 - */ - public WebSocketClient(URI serverUri, Map httpHeaders) { - this(serverUri, new Draft_6455(), httpHeaders); - } - - /** - * Constructs a WebSocketClient instance and sets it to the connect to the - * specified URI. The channel does not attampt to connect automatically. The connection - * will be established once you call connect. - * - * @param serverUri the server URI to connect to - * @param protocolDraft The draft which should be used for this connection - * @param httpHeaders Additional HTTP-Headers - * @since 1.3.8 - */ - public WebSocketClient(URI serverUri, Draft protocolDraft, Map httpHeaders) { - this(serverUri, protocolDraft, httpHeaders, 0); - } - - /** - * Constructs a WebSocketClient instance and sets it to the connect to the - * specified URI. The channel does not attampt to connect automatically. The connection - * will be established once you call connect. - * - * @param serverUri the server URI to connect to - * @param protocolDraft The draft which should be used for this connection - * @param httpHeaders Additional HTTP-Headers - * @param connectTimeout The Timeout for the connection - */ - public WebSocketClient(URI serverUri, Draft protocolDraft, Map httpHeaders, int connectTimeout) { - if (serverUri == null) { - throw new IllegalArgumentException(); - } else if (protocolDraft == null) { - throw new IllegalArgumentException("null as draft is permitted for `WebSocketServer` only!"); - } - this.uri = serverUri; - this.draft = protocolDraft; - this.headers = httpHeaders; - this.connectTimeout = connectTimeout; - setTcpNoDelay(false); - setReuseAddr(false); - this.engine = new WebSocketImpl(this, protocolDraft); - } - - /** - * Initiates the websocket close handshake. This method does not block
    - * In oder to make sure the connection is closed use closeBlocking - */ - public void close() { - if (writeThread != null) { - engine.close(CloseFrame.NORMAL); - } - } - - @Override - public void close(int code) { - engine.close(code); - } - - @Override - public void close(int code, String message) { - engine.close(code, message); - } - - /** - * Same as close but blocks until the websocket closed or failed to do so.
    - * - * @throws InterruptedException Thrown when the threads get interrupted - */ - public void closeBlocking() throws InterruptedException { - close(); - closeLatch.await(); - } - - @Override - public void closeConnection(int code, String message) { - engine.closeConnection(code, message); - } - - /** - * Initiates the websocket connection. This method does not block. - */ - public void connect() { - if (connectReadThread != null) { - throw new IllegalStateException("WebSocketClient objects are not reuseable"); - } - connectReadThread = new Thread(this); - connectReadThread.setName("WebSocketConnectReadThread-" + connectReadThread.getId()); - connectReadThread.start(); - } - - /** - * Same as connect but blocks until the websocket connected or failed to do so.
    - * - * @return Returns whether it succeeded or not. - * @throws InterruptedException Thrown when the threads get interrupted - */ - public boolean connectBlocking() throws InterruptedException { - connect(); - connectLatch.await(); - return engine.isOpen(); - } - - /** - * Same as connect but blocks with a timeout until the websocket connected or failed to do so.
    - * - * @param timeout The connect timeout - * @param timeUnit The timeout time unit - * @return Returns whether it succeeded or not. - * @throws InterruptedException Thrown when the threads get interrupted - */ - public boolean connectBlocking(long timeout, TimeUnit timeUnit) throws InterruptedException { - connect(); - return connectLatch.await(timeout, timeUnit) && engine.isOpen(); - } - - @Override - public T getAttachment() { - return engine.getAttachment(); - } - - @Override - public void setAttachment(T attachment) { - engine.setAttachment(attachment); - } - - /** - * Getter for the engine - * - * @return the engine - */ - public WebSocket getConnection() { - return engine; - } - - /** - * Returns the protocol version this channel uses.
    - * For more infos see https://github.com/TooTallNate/Java-WebSocket/wiki/Drafts - * - * @return The draft used for this client - */ - public Draft getDraft() { - return draft; - } - - @Override - public InetSocketAddress getLocalSocketAddress(WebSocket conn) { - if (socket != null) { - return (InetSocketAddress) socket.getLocalSocketAddress(); - } - return null; - } - - @Override - public InetSocketAddress getLocalSocketAddress() { - return engine.getLocalSocketAddress(); - } - - /** - * This represents the state of the connection. - */ - public ReadyState getReadyState() { - return engine.getReadyState(); - } - - @Override - public InetSocketAddress getRemoteSocketAddress(WebSocket conn) { - if (socket != null) { - return (InetSocketAddress) socket.getRemoteSocketAddress(); - } - return null; - } - - @Override - public InetSocketAddress getRemoteSocketAddress() { - return engine.getRemoteSocketAddress(); - } - - @Override - public String getResourceDescriptor() { - return uri.getPath(); - } - - /** - * Returns the socket to allow Hostname Verification - * - * @return the socket used for this connection - */ - public Socket getSocket() { - return socket; - } - - /** - * Accepts bound and unbound sockets.
    - * This method must be called before connect. - * If the given socket is not yet bound it will be bound to the uri specified in the constructor. - * - * @param socket The socket which should be used for the connection - * @deprecated use setSocketFactory - */ - @Deprecated - public void setSocket(Socket socket) { - if (this.socket != null) { - throw new IllegalStateException("socket has already been set"); - } - this.socket = socket; - } - - /** - * Returns the URI that this WebSocketClient is connected to. - * - * @return the URI connected to - */ - public URI getURI() { - return uri; - } - - @Override - public boolean hasBufferedData() { - return engine.hasBufferedData(); - } - - @Override - public boolean isClosed() { - return engine.isClosed(); - } - - @Override - public boolean isClosing() { - return engine.isClosing(); - } - - @Override - public boolean isFlushAndClose() { - return engine.isFlushAndClose(); - } - - @Override - public boolean isOpen() { - return engine.isOpen(); - } - - /** - * Called after the websocket connection has been closed. - * - * @param code The codes can be looked up here: {@link CloseFrame} - * @param reason Additional information string - * @param remote Returns whether or not the closing of the connection was initiated by the remote host. - **/ - public abstract void onClose(int code, String reason, boolean remote); - - /** - * Send when this peer sends a close handshake - * - * @param code The codes can be looked up here: {@link CloseFrame} - * @param reason Additional information string - */ - public void onCloseInitiated(int code, String reason) { - //To overwrite - } - - /** - * Called as soon as no further frames are accepted - * - * @param code The codes can be looked up here: {@link CloseFrame} - * @param reason Additional information string - * @param remote Returns whether or not the closing of the connection was initiated by the remote host. - */ - public void onClosing(int code, String reason, boolean remote) { - //To overwrite - } - - /** - * Called when errors occurs. If an error causes the websocket connection to fail {@link #onClose(int, String, - * boolean)} will be called additionally.
    - * This method will be called primarily because of IO or protocol errors.
    - * If the given exception is an RuntimeException that probably means that you encountered a bug.
    - * - * @param ex The exception causing this error - **/ - public abstract void onError(Exception ex); - - /** - * Callback for string messages received from the remote host - * - * @param message The UTF-8 decoded message that was received. - * @see #onMessage(ByteBuffer) - **/ - public abstract void onMessage(String message); - - /** - * Callback for binary messages received from the remote host - * - * @param bytes The binary message that was received. - * @see #onMessage(String) - **/ - public void onMessage(ByteBuffer bytes) { - //To overwrite - } - - /** - * Called after an opening handshake has been performed and the given websocket is ready to be written on. - * - * @param handshakedata The handshake of the websocket instance - */ - public abstract void onOpen(ServerHandshake handshakedata); - - // ABTRACT METHODS ///////////////////////////////////////////////////////// - - /** - * Calls subclass' implementation of onClose. - */ - @Override - public final void onWebsocketClose(WebSocket conn, int code, String reason, boolean remote) { - stopConnectionLostTimer(); - if (writeThread != null) { - writeThread.interrupt(); - } - onClose(code, reason, remote); - connectLatch.countDown(); - closeLatch.countDown(); - } - - @Override - public void onWebsocketCloseInitiated(WebSocket conn, int code, String reason) { - onCloseInitiated(code, reason); - } - - @Override - public void onWebsocketClosing(WebSocket conn, int code, String reason, boolean remote) { - onClosing(code, reason, remote); - } - - /** - * Calls subclass' implementation of onIOError. - */ - @Override - public final void onWebsocketError(WebSocket conn, Exception ex) { - onError(ex); - } - - /** - * Calls subclass' implementation of onMessage. - */ - @Override - public final void onWebsocketMessage(WebSocket conn, String message) { - onMessage(message); - } - - @Override - public final void onWebsocketMessage(WebSocket conn, ByteBuffer blob) { - onMessage(blob); - } - - /** - * Calls subclass' implementation of onOpen. - */ - @Override - public final void onWebsocketOpen(WebSocket conn, Handshakedata handshake) { - startConnectionLostTimer(); - onOpen((ServerHandshake) handshake); - connectLatch.countDown(); - } - - @Override - public final void onWriteDemand(WebSocket conn) { - // nothing to do - } - - /** - * Reinitiates the websocket connection. This method does not block. - * - * @since 1.3.8 - */ - public void reconnect() { - reset(); - connect(); - } - - /** - * Same as reconnect but blocks until the websocket reconnected or failed to do so.
    - * - * @return Returns whether it succeeded or not. - * @throws InterruptedException Thrown when the threads get interrupted - * @since 1.3.8 - */ - public boolean reconnectBlocking() throws InterruptedException { - reset(); - return connectBlocking(); - } - - @RequiresApi(api = VERSION_CODES.KITKAT) - public void run() { - InputStream istream; - try { - boolean isNewSocket = false; - if (socketFactory != null) { - socket = socketFactory.createSocket(); - } else if (socket == null) { - socket = new Socket(proxy); - isNewSocket = true; - - } else if (socket.isClosed()) { - throw new IOException(); - } - - socket.setTcpNoDelay(isTcpNoDelay()); - socket.setReuseAddress(isReuseAddr()); - - if (!socket.isBound()) { - socket.connect(new InetSocketAddress(uri.getHost(), getPort()), connectTimeout); - } - - // if the socket is set by others we don't apply any TLS wrapper - if (isNewSocket && "wss".equals(uri.getScheme())) { - - SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); - sslContext.init(null, null, null); - SSLSocketFactory factory = sslContext.getSocketFactory(); - socket = factory.createSocket(socket, uri.getHost(), getPort(), true); - } - - istream = socket.getInputStream(); - ostream = socket.getOutputStream(); - - sendHandshake(); - } catch ( /*IOException | SecurityException | UnresolvedAddressException | InvalidHandshakeException | ClosedByInterruptException | SocketTimeoutException */Exception e) { - onWebsocketError(engine, e); - engine.closeConnection(CloseFrame.NEVER_CONNECTED, e.getMessage()); - return; - } - - writeThread = new Thread(new WebsocketWriteThread(this)); - writeThread.start(); - - byte[] rawbuffer = new byte[WebSocketImpl.RCVBUF]; - int readBytes; - - try { - while (!isClosing() && !isClosed() && (readBytes = istream.read(rawbuffer)) != -1) { - engine.decode(ByteBuffer.wrap(rawbuffer, 0, readBytes)); - } - engine.eot(); - } catch (IOException e) { - handleIOException(e); - } catch (RuntimeException e) { - // this catch case covers internal errors only and indicates a bug in this websocket implementation - onError(e); - engine.closeConnection(CloseFrame.ABNORMAL_CLOSE, e.getMessage()); - } - connectReadThread = null; - } - - /** - * Sends text to the connected websocket server. - * - * @param text The string which will be transmitted. - */ - public void send(String text) { - engine.send(text); - } - - /** - * Sends binary data to the connected webSocket server. - * - * @param data The byte-Array of data to send to the WebSocket server. - */ - public void send(byte[] data) { - engine.send(data); - } - - @Override - public void send(ByteBuffer bytes) { - engine.send(bytes); - } - - @Override - public void sendFragmentedFrame(Opcode op, ByteBuffer buffer, boolean fin) { - engine.sendFragmentedFrame(op, buffer, fin); - } - - @Override - public void sendFrame(Framedata framedata) { - engine.sendFrame(framedata); - } - - @Override - public void sendFrame(Collection frames) { - engine.sendFrame(frames); - } - - @Override - public void sendPing() { - engine.sendPing(); - } - - /** - * Method to set a proxy for this connection - * - * @param proxy the proxy to use for this websocket client - */ - public void setProxy(Proxy proxy) { - if (proxy == null) { - throw new IllegalArgumentException(); - } - this.proxy = proxy; - } - - /** - * Accepts a SocketFactory.
    - * This method must be called before connect. - * The socket will be bound to the uri specified in the constructor. - * - * @param socketFactory The socket factory which should be used for the connection. - */ - public void setSocketFactory(SocketFactory socketFactory) { - this.socketFactory = socketFactory; - } - - @Override - protected Collection getConnections() { - return Collections.singletonList((WebSocket) engine); - } - - /** - * Extract the specified port - * - * @return the specified port or the default port for the specific scheme - */ - private int getPort() { - int port = uri.getPort(); - if (port == -1) { - String scheme = uri.getScheme(); - if ("wss".equals(scheme)) { - return WebSocketImpl.DEFAULT_WSS_PORT; - } else if ("ws".equals(scheme)) { - return WebSocketImpl.DEFAULT_PORT; - } else { - throw new IllegalArgumentException("unknown scheme: " + scheme); - } - } - return port; - } - - /** - * Method to give some additional info for specific IOExceptions - * - * @param e the IOException causing a eot. - */ - private void handleIOException(IOException e) { - if (e instanceof SSLException) { - onError(e); - } - engine.eot(); - } - - /** - * Reset everything relevant to allow a reconnect - * - * @since 1.3.8 - */ - private void reset() { - Thread current = Thread.currentThread(); - if (current == writeThread || current == connectReadThread) { - throw new IllegalStateException( - "You cannot initialize a reconnect out of the websocket thread. Use reconnect in another thread to insure a successful cleanup."); - } - try { - closeBlocking(); - if (writeThread != null) { - this.writeThread.interrupt(); - this.writeThread = null; - } - if (connectReadThread != null) { - this.connectReadThread.interrupt(); - this.connectReadThread = null; - } - this.draft.reset(); - if (this.socket != null) { - this.socket.close(); - this.socket = null; - } - } catch (Exception e) { - onError(e); - engine.closeConnection(CloseFrame.ABNORMAL_CLOSE, e.getMessage()); - return; - } - connectLatch = new CountDownLatch(1); - closeLatch = new CountDownLatch(1); - this.engine = new WebSocketImpl(this, this.draft); - } - - /** - * Create and send the handshake to the other endpoint - * - * @throws InvalidHandshakeException a invalid handshake was created - */ - @RequiresApi(api = VERSION_CODES.KITKAT) - private void sendHandshake() throws InvalidHandshakeException { - String path; - String part1 = uri.getRawPath(); - String part2 = uri.getRawQuery(); - if (part1 == null || part1.length() == 0) { - path = "/"; - } else { - path = part1; - } - if (part2 != null) { - path += '?' + part2; - } - int port = getPort(); - String host = uri.getHost() + ( - (port != WebSocketImpl.DEFAULT_PORT && port != WebSocketImpl.DEFAULT_WSS_PORT) - ? ":" + port - : ""); - - HandshakeImpl1Client handshake = new HandshakeImpl1Client(); - handshake.setResourceDescriptor(path); - handshake.put("Host", host); - if (headers != null) { - for (Map.Entry kv : headers.entrySet()) { - handshake.put(kv.getKey(), kv.getValue()); - } - } - engine.startHandshake(handshake); - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/drafts/Draft.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/drafts/Draft.java deleted file mode 100755 index db1cbf6a1..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/drafts/Draft.java +++ /dev/null @@ -1,356 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.drafts; - -import android.os.Build.VERSION_CODES; -import androidx.annotation.RequiresApi; -import com.clevertap.android.sdk.java_websocket.WebSocketImpl; -import com.clevertap.android.sdk.java_websocket.enums.CloseHandshakeType; -import com.clevertap.android.sdk.java_websocket.enums.HandshakeState; -import com.clevertap.android.sdk.java_websocket.enums.Opcode; -import com.clevertap.android.sdk.java_websocket.enums.Role; -import com.clevertap.android.sdk.java_websocket.exceptions.IncompleteHandshakeException; -import com.clevertap.android.sdk.java_websocket.exceptions.InvalidDataException; -import com.clevertap.android.sdk.java_websocket.exceptions.InvalidHandshakeException; -import com.clevertap.android.sdk.java_websocket.framing.BinaryFrame; -import com.clevertap.android.sdk.java_websocket.framing.CloseFrame; -import com.clevertap.android.sdk.java_websocket.framing.ContinuousFrame; -import com.clevertap.android.sdk.java_websocket.framing.DataFrame; -import com.clevertap.android.sdk.java_websocket.framing.Framedata; -import com.clevertap.android.sdk.java_websocket.framing.TextFrame; -import com.clevertap.android.sdk.java_websocket.handshake.ClientHandshake; -import com.clevertap.android.sdk.java_websocket.handshake.ClientHandshakeBuilder; -import com.clevertap.android.sdk.java_websocket.handshake.HandshakeBuilder; -import com.clevertap.android.sdk.java_websocket.handshake.HandshakeImpl1Client; -import com.clevertap.android.sdk.java_websocket.handshake.HandshakeImpl1Server; -import com.clevertap.android.sdk.java_websocket.handshake.Handshakedata; -import com.clevertap.android.sdk.java_websocket.handshake.ServerHandshake; -import com.clevertap.android.sdk.java_websocket.handshake.ServerHandshakeBuilder; -import com.clevertap.android.sdk.java_websocket.util.Charsetfunctions; -import java.nio.ByteBuffer; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; - -/** - * Base class for everything of a websocket specification which is not common such as the way the handshake is read or - * frames are transfered. - **/ -public abstract class Draft { - - protected Opcode continuousFrameType = null; - - /** - * In some cases the handshake will be parsed different depending on whether - */ - protected Role role = null; - - public static ByteBuffer readLine(ByteBuffer buf) { - ByteBuffer sbuf = ByteBuffer.allocate(buf.remaining()); - byte prev; - byte cur = '0'; - while (buf.hasRemaining()) { - prev = cur; - cur = buf.get(); - sbuf.put(cur); - if (prev == (byte) '\r' && cur == (byte) '\n') { - sbuf.limit(sbuf.position() - 2); - sbuf.position(0); - return sbuf; - - } - } - // ensure that there wont be any bytes skipped - buf.position(buf.position() - sbuf.position()); - return null; - } - - @RequiresApi(api = VERSION_CODES.KITKAT) - public static String readStringLine(ByteBuffer buf) { - ByteBuffer b = readLine(buf); - return b == null ? null : Charsetfunctions.stringAscii(b.array(), 0, b.limit()); - } - - @RequiresApi(api = VERSION_CODES.KITKAT) - public static HandshakeBuilder translateHandshakeHttp(ByteBuffer buf, Role role) - throws InvalidHandshakeException { - HandshakeBuilder handshake; - - String line = readStringLine(buf); - if (line == null) { - throw new IncompleteHandshakeException(buf.capacity() + 128); - } - - String[] firstLineTokens = line.split(" ", 3);// eg. HTTP/1.1 101 Switching the Protocols - if (firstLineTokens.length != 3) { - throw new InvalidHandshakeException(); - } - if (role == Role.CLIENT) { - handshake = translateHandshakeHttpClient(firstLineTokens, line); - } else { - handshake = translateHandshakeHttpServer(firstLineTokens, line); - } - line = readStringLine(buf); - while (line != null && line.length() > 0) { - String[] pair = line.split(":", 2); - if (pair.length != 2) { - throw new InvalidHandshakeException("not an http header"); - } - // If the handshake contains already a specific key, append the new value - if (handshake.hasFieldValue(pair[0])) { - handshake.put(pair[0], handshake.getFieldValue(pair[0]) + "; " + pair[1].replaceFirst("^ +", "")); - } else { - handshake.put(pair[0], pair[1].replaceFirst("^ +", "")); - } - line = readStringLine(buf); - } - if (line == null) { - throw new IncompleteHandshakeException(); - } - return handshake; - } - - public abstract HandshakeState acceptHandshakeAsClient(ClientHandshake request, ServerHandshake response) - throws InvalidHandshakeException; - - public abstract HandshakeState acceptHandshakeAsServer(ClientHandshake handshakedata) - throws InvalidHandshakeException; - - public int checkAlloc(int bytecount) throws InvalidDataException { - if (bytecount < 0) { - throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR, "Negative count"); - } - return bytecount; - } - - public List continuousFrame(Opcode op, ByteBuffer buffer, boolean fin) { - if (op != Opcode.BINARY && op != Opcode.TEXT) { - throw new IllegalArgumentException("Only Opcode.BINARY or Opcode.TEXT are allowed"); - } - DataFrame bui = null; - if (continuousFrameType != null) { - bui = new ContinuousFrame(); - } else { - continuousFrameType = op; - if (op == Opcode.BINARY) { - bui = new BinaryFrame(); - } else if (op == Opcode.TEXT) { - bui = new TextFrame(); - } - } - bui.setPayload(buffer); - bui.setFin(fin); - try { - bui.isValid(); - } catch (InvalidDataException e) { - throw new IllegalArgumentException(e); // can only happen when one builds close frames(Opcode.Close) - } - if (fin) { - continuousFrameType = null; - } else { - continuousFrameType = op; - } - return Collections.singletonList((Framedata) bui); - } - - /** - * Drafts must only be by one websocket at all. To prevent drafts to be used more than once the Websocket - * implementation should call this method in order to create a new usable version of a given draft instance.
    - * The copy can be safely used in conjunction with a new websocket connection. - * - * @return a copy of the draft - */ - public abstract Draft copyInstance(); - - public abstract ByteBuffer createBinaryFrame(Framedata framedata); - - public abstract List createFrames(ByteBuffer binary, boolean mask); - - public abstract List createFrames(String text, boolean mask); - - /** - * @deprecated use createHandshake without the role - */ - @RequiresApi(api = VERSION_CODES.KITKAT) - @Deprecated - public List createHandshake(Handshakedata handshakedata, Role ownrole) { - return createHandshake(handshakedata); - } - - @RequiresApi(api = VERSION_CODES.KITKAT) - public List createHandshake(Handshakedata handshakedata) { - return createHandshake(handshakedata, true); - } - - /** - * @deprecated use createHandshake without the role since it does not have any effect - */ - @RequiresApi(api = VERSION_CODES.KITKAT) - @Deprecated - public List createHandshake(Handshakedata handshakedata, Role ownrole, boolean withcontent) { - return createHandshake(handshakedata, withcontent); - } - - @RequiresApi(api = VERSION_CODES.KITKAT) - public List createHandshake(Handshakedata handshakedata, boolean withcontent) { - StringBuilder bui = new StringBuilder(100); - if (handshakedata instanceof ClientHandshake) { - bui.append("GET ").append(((ClientHandshake) handshakedata).getResourceDescriptor()).append(" HTTP/1.1"); - } else if (handshakedata instanceof ServerHandshake) { - bui.append("HTTP/1.1 101 ").append(((ServerHandshake) handshakedata).getHttpStatusMessage()); - } else { - throw new IllegalArgumentException("unknown role"); - } - bui.append("\r\n"); - Iterator it = handshakedata.iterateHttpFields(); - while (it.hasNext()) { - String fieldname = it.next(); - String fieldvalue = handshakedata.getFieldValue(fieldname); - bui.append(fieldname); - bui.append(": "); - bui.append(fieldvalue); - bui.append("\r\n"); - } - bui.append("\r\n"); - byte[] httpheader = Charsetfunctions.asciiBytes(bui.toString()); - - byte[] content = withcontent ? handshakedata.getContent() : null; - ByteBuffer bytebuffer = ByteBuffer.allocate((content == null ? 0 : content.length) + httpheader.length); - bytebuffer.put(httpheader); - if (content != null) { - bytebuffer.put(content); - } - bytebuffer.flip(); - return Collections.singletonList(bytebuffer); - } - - public abstract CloseHandshakeType getCloseHandshakeType(); - - public Role getRole() { - return role; - } - - public abstract ClientHandshakeBuilder postProcessHandshakeRequestAsClient(ClientHandshakeBuilder request) - throws InvalidHandshakeException; - - public abstract HandshakeBuilder postProcessHandshakeResponseAsServer(ClientHandshake request, - ServerHandshakeBuilder response) throws InvalidHandshakeException; - - /** - * Handle the frame specific to the draft - * - * @param webSocketImpl the websocketimpl used for this draft - * @param frame the frame which is supposed to be handled - * @throws InvalidDataException will be thrown on invalid data - */ - public abstract void processFrame(WebSocketImpl webSocketImpl, Framedata frame) throws InvalidDataException; - - public abstract void reset(); - - public void setParseMode(Role role) { - this.role = role; - } - - public String toString() { - return getClass().getSimpleName(); - } - - public abstract List translateFrame(ByteBuffer buffer) throws InvalidDataException; - - @RequiresApi(api = VERSION_CODES.KITKAT) - public Handshakedata translateHandshake(ByteBuffer buf) throws InvalidHandshakeException { - return translateHandshakeHttp(buf, role); - } - - protected boolean basicAccept(Handshakedata handshakedata) { - return handshakedata.getFieldValue("Upgrade").equalsIgnoreCase("websocket") && handshakedata - .getFieldValue("Connection").toLowerCase(Locale.ENGLISH).contains("upgrade"); - } - - int readVersion(Handshakedata handshakedata) { - String vers = handshakedata.getFieldValue("Sec-WebSocket-Version"); - if (vers.length() > 0) { - int v; - try { - v = new Integer(vers.trim()); - return v; - } catch (NumberFormatException e) { - return -1; - } - } - return -1; - } - - /** - * Checking the handshake for the role as client - * - * @param firstLineTokens the token of the first line split as as an string array - * @param line the whole line - * @return a handshake - */ - private static HandshakeBuilder translateHandshakeHttpClient(String[] firstLineTokens, String line) - throws InvalidHandshakeException { - // translating/parsing the response from the SERVER - if (!"101".equals(firstLineTokens[1])) { - throw new InvalidHandshakeException( - String.format("Invalid status code received: %s Status line: %s", firstLineTokens[1], line)); - } - if (!"HTTP/1.1".equalsIgnoreCase(firstLineTokens[0])) { - throw new InvalidHandshakeException( - String.format("Invalid status line received: %s Status line: %s", firstLineTokens[0], line)); - } - HandshakeBuilder handshake = new HandshakeImpl1Server(); - ServerHandshakeBuilder serverhandshake = (ServerHandshakeBuilder) handshake; - serverhandshake.setHttpStatus(Short.parseShort(firstLineTokens[1])); - serverhandshake.setHttpStatusMessage(firstLineTokens[2]); - return handshake; - } - - /** - * Checking the handshake for the role as server - * - * @param firstLineTokens the token of the first line split as as an string array - * @param line the whole line - * @return a handshake - */ - private static HandshakeBuilder translateHandshakeHttpServer(String[] firstLineTokens, String line) - throws InvalidHandshakeException { - // translating/parsing the request from the CLIENT - if (!"GET".equalsIgnoreCase(firstLineTokens[0])) { - throw new InvalidHandshakeException( - String.format("Invalid request method received: %s Status line: %s", firstLineTokens[0], line)); - } - if (!"HTTP/1.1".equalsIgnoreCase(firstLineTokens[2])) { - throw new InvalidHandshakeException( - String.format("Invalid status line received: %s Status line: %s", firstLineTokens[2], line)); - } - ClientHandshakeBuilder clienthandshake = new HandshakeImpl1Client(); - clienthandshake.setResourceDescriptor(firstLineTokens[1]); - return clienthandshake; - } - -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/drafts/Draft_6455.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/drafts/Draft_6455.java deleted file mode 100755 index f28d2a5de..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/drafts/Draft_6455.java +++ /dev/null @@ -1,1099 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.drafts; - -import com.clevertap.android.sdk.java_websocket.WebSocketImpl; -import com.clevertap.android.sdk.java_websocket.enums.CloseHandshakeType; -import com.clevertap.android.sdk.java_websocket.enums.HandshakeState; -import com.clevertap.android.sdk.java_websocket.enums.Opcode; -import com.clevertap.android.sdk.java_websocket.enums.ReadyState; -import com.clevertap.android.sdk.java_websocket.enums.Role; -import com.clevertap.android.sdk.java_websocket.exceptions.IncompleteException; -import com.clevertap.android.sdk.java_websocket.exceptions.InvalidDataException; -import com.clevertap.android.sdk.java_websocket.exceptions.InvalidFrameException; -import com.clevertap.android.sdk.java_websocket.exceptions.InvalidHandshakeException; -import com.clevertap.android.sdk.java_websocket.exceptions.LimitExceededException; -import com.clevertap.android.sdk.java_websocket.exceptions.NotSendableException; -import com.clevertap.android.sdk.java_websocket.extensions.DefaultExtension; -import com.clevertap.android.sdk.java_websocket.extensions.IExtension; -import com.clevertap.android.sdk.java_websocket.framing.BinaryFrame; -import com.clevertap.android.sdk.java_websocket.framing.CloseFrame; -import com.clevertap.android.sdk.java_websocket.framing.Framedata; -import com.clevertap.android.sdk.java_websocket.framing.FramedataImpl1; -import com.clevertap.android.sdk.java_websocket.framing.TextFrame; -import com.clevertap.android.sdk.java_websocket.handshake.ClientHandshake; -import com.clevertap.android.sdk.java_websocket.handshake.ClientHandshakeBuilder; -import com.clevertap.android.sdk.java_websocket.handshake.HandshakeBuilder; -import com.clevertap.android.sdk.java_websocket.handshake.ServerHandshake; -import com.clevertap.android.sdk.java_websocket.handshake.ServerHandshakeBuilder; -import com.clevertap.android.sdk.java_websocket.protocols.IProtocol; -import com.clevertap.android.sdk.java_websocket.protocols.Protocol; -import com.clevertap.android.sdk.java_websocket.util.Base64; -import com.clevertap.android.sdk.java_websocket.util.Charsetfunctions; -import java.math.BigInteger; -import java.nio.ByteBuffer; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.TimeZone; - -/** - * Implementation for the RFC 6455 websocket protocol - * This is the recommended class for your websocket connection - */ -public class Draft_6455 extends Draft { - - private class TranslatedPayloadMetaData { - - private int payloadLength; - - private int realPackageSize; - - TranslatedPayloadMetaData(int newPayloadLength, int newRealPackageSize) { - this.payloadLength = newPayloadLength; - this.realPackageSize = newRealPackageSize; - } - - private int getPayloadLength() { - return payloadLength; - } - - private int getRealPackageSize() { - return realPackageSize; - } - } - - /** - * Handshake specific field for the key - */ - private static final String SEC_WEB_SOCKET_KEY = "Sec-WebSocket-Key"; - - /** - * Handshake specific field for the protocol - */ - private static final String SEC_WEB_SOCKET_PROTOCOL = "Sec-WebSocket-Protocol"; - - /** - * Handshake specific field for the extension - */ - private static final String SEC_WEB_SOCKET_EXTENSIONS = "Sec-WebSocket-Extensions"; - - /** - * Handshake specific field for the accept - */ - private static final String SEC_WEB_SOCKET_ACCEPT = "Sec-WebSocket-Accept"; - - /** - * Handshake specific field for the upgrade - */ - private static final String UPGRADE = "Upgrade"; - - /** - * Handshake specific field for the connection - */ - private static final String CONNECTION = "Connection"; - - /** - * Attribute for the payload of the current continuous frame - */ - private final List byteBufferList; - - /** - * Attribute for the current continuous frame - */ - private Framedata currentContinuousFrame; - - /** - * Attribute for the used extension in this draft - */ - private IExtension extension = new DefaultExtension(); - - /** - * Attribute for the current incomplete frame - */ - private ByteBuffer incompleteframe; - - /** - * Attribute for all available extension in this draft - */ - private List knownExtensions; - - /** - * Attribute for all available protocols in this draft - */ - private List knownProtocols; - - /** - * Attribute for the maximum allowed size of a frame - * - * @since 1.4.0 - */ - private int maxFrameSize; - - /** - * Attribute for the used protocol in this draft - */ - private IProtocol protocol; - - /** - * Attribute for the reusable random instance - */ - private final SecureRandom reuseableRandom = new SecureRandom(); - - /** - * Constructor for the websocket protocol specified by RFC 6455 with default extensions - * - * @since 1.3.5 - */ - public Draft_6455() { - this(Collections.emptyList()); - } - - /** - * Constructor for the websocket protocol specified by RFC 6455 with custom extensions - * - * @param inputExtension the extension which should be used for this draft - * @since 1.3.5 - */ - public Draft_6455(IExtension inputExtension) { - this(Collections.singletonList(inputExtension)); - } - - /** - * Constructor for the websocket protocol specified by RFC 6455 with custom extensions - * - * @param inputExtensions the extensions which should be used for this draft - * @since 1.3.5 - */ - public Draft_6455(List inputExtensions) { - this(inputExtensions, Collections.singletonList(new Protocol(""))); - } - - /** - * Constructor for the websocket protocol specified by RFC 6455 with custom extensions and protocols - * - * @param inputExtensions the extensions which should be used for this draft - * @param inputProtocols the protocols which should be used for this draft - * @since 1.3.7 - */ - public Draft_6455(List inputExtensions, List inputProtocols) { - this(inputExtensions, inputProtocols, Integer.MAX_VALUE); - } - - /** - * Constructor for the websocket protocol specified by RFC 6455 with custom extensions and protocols - * - * @param inputExtensions the extensions which should be used for this draft - * @param inputMaxFrameSize the maximum allowed size of a frame (the real payload size, decoded frames can be - * bigger) - * @since 1.4.0 - */ - public Draft_6455(List inputExtensions, int inputMaxFrameSize) { - this(inputExtensions, Collections.singletonList(new Protocol("")), inputMaxFrameSize); - } - - /** - * Constructor for the websocket protocol specified by RFC 6455 with custom extensions and protocols - * - * @param inputExtensions the extensions which should be used for this draft - * @param inputProtocols the protocols which should be used for this draft - * @param inputMaxFrameSize the maximum allowed size of a frame (the real payload size, decoded frames can be - * bigger) - * @since 1.4.0 - */ - public Draft_6455(List inputExtensions, List inputProtocols, int inputMaxFrameSize) { - if (inputExtensions == null || inputProtocols == null || inputMaxFrameSize < 1) { - throw new IllegalArgumentException(); - } - knownExtensions = new ArrayList(inputExtensions.size()); - knownProtocols = new ArrayList(inputProtocols.size()); - boolean hasDefault = false; - byteBufferList = new ArrayList(); - for (IExtension inputExtension : inputExtensions) { - if (inputExtension.getClass().equals(DefaultExtension.class)) { - hasDefault = true; - } - } - knownExtensions.addAll(inputExtensions); - //We always add the DefaultExtension to implement the normal RFC 6455 specification - if (!hasDefault) { - knownExtensions.add(this.knownExtensions.size(), extension); - } - knownProtocols.addAll(inputProtocols); - maxFrameSize = inputMaxFrameSize; - } - - @Override - public HandshakeState acceptHandshakeAsClient(ClientHandshake request, ServerHandshake response) - throws InvalidHandshakeException { - if (!basicAccept(response)) { - return HandshakeState.NOT_MATCHED; - } - if (!request.hasFieldValue(SEC_WEB_SOCKET_KEY) || !response.hasFieldValue(SEC_WEB_SOCKET_ACCEPT)) { - return HandshakeState.NOT_MATCHED; - } - - String seckeyAnswer = response.getFieldValue(SEC_WEB_SOCKET_ACCEPT); - String seckeyChallenge = request.getFieldValue(SEC_WEB_SOCKET_KEY); - seckeyChallenge = generateFinalKey(seckeyChallenge); - - if (!seckeyChallenge.equals(seckeyAnswer)) { - return HandshakeState.NOT_MATCHED; - } - HandshakeState extensionState = HandshakeState.NOT_MATCHED; - String requestedExtension = response.getFieldValue(SEC_WEB_SOCKET_EXTENSIONS); - for (IExtension knownExtension : knownExtensions) { - if (knownExtension.acceptProvidedExtensionAsClient(requestedExtension)) { - extension = knownExtension; - extensionState = HandshakeState.MATCHED; - break; - } - } - HandshakeState protocolState = containsRequestedProtocol(response.getFieldValue(SEC_WEB_SOCKET_PROTOCOL)); - if (protocolState == HandshakeState.MATCHED && extensionState == HandshakeState.MATCHED) { - return HandshakeState.MATCHED; - } - return HandshakeState.NOT_MATCHED; - } - - @Override - public HandshakeState acceptHandshakeAsServer(ClientHandshake handshakedata) throws InvalidHandshakeException { - int v = readVersion(handshakedata); - if (v != 13) { - return HandshakeState.NOT_MATCHED; - } - HandshakeState extensionState = HandshakeState.NOT_MATCHED; - String requestedExtension = handshakedata.getFieldValue(SEC_WEB_SOCKET_EXTENSIONS); - for (IExtension knownExtension : knownExtensions) { - if (knownExtension.acceptProvidedExtensionAsServer(requestedExtension)) { - extension = knownExtension; - extensionState = HandshakeState.MATCHED; - break; - } - } - HandshakeState protocolState = containsRequestedProtocol( - handshakedata.getFieldValue(SEC_WEB_SOCKET_PROTOCOL)); - if (protocolState == HandshakeState.MATCHED && extensionState == HandshakeState.MATCHED) { - return HandshakeState.MATCHED; - } - return HandshakeState.NOT_MATCHED; - } - - @Override - public Draft copyInstance() { - ArrayList newExtensions = new ArrayList(); - for (IExtension iExtension : getKnownExtensions()) { - newExtensions.add(iExtension.copyInstance()); - } - ArrayList newProtocols = new ArrayList(); - for (IProtocol iProtocol : getKnownProtocols()) { - newProtocols.add(iProtocol.copyInstance()); - } - return new Draft_6455(newExtensions, newProtocols, maxFrameSize); - } - - @Override - public ByteBuffer createBinaryFrame(Framedata framedata) { - getExtension().encodeFrame(framedata); - return createByteBufferFromFramedata(framedata); - } - - @Override - public List createFrames(ByteBuffer binary, boolean mask) { - BinaryFrame curframe = new BinaryFrame(); - curframe.setPayload(binary); - curframe.setTransferemasked(mask); - try { - curframe.isValid(); - } catch (InvalidDataException e) { - throw new NotSendableException(e); - } - return Collections.singletonList((Framedata) curframe); - } - - @Override - public List createFrames(String text, boolean mask) { - TextFrame curframe = new TextFrame(); - curframe.setPayload(ByteBuffer.wrap(Charsetfunctions.utf8Bytes(text))); - curframe.setTransferemasked(mask); - try { - curframe.isValid(); - } catch (InvalidDataException e) { - throw new NotSendableException(e); - } - return Collections.singletonList((Framedata) curframe); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - Draft_6455 that = (Draft_6455) o; - - if (maxFrameSize != that.getMaxFrameSize()) { - return false; - } - if (extension != null ? !extension.equals(that.getExtension()) : that.getExtension() != null) { - return false; - } - return protocol != null ? protocol.equals(that.getProtocol()) : that.getProtocol() == null; - } - - @Override - public CloseHandshakeType getCloseHandshakeType() { - return CloseHandshakeType.TWOWAY; - } - - /** - * Getter for the extension which is used by this draft - * - * @return the extension which is used or null, if handshake is not yet done - */ - public IExtension getExtension() { - return extension; - } - - /** - * Getter for all available extensions for this draft - * - * @return the extensions which are enabled for this draft - */ - public List getKnownExtensions() { - return knownExtensions; - } - - /** - * Getter for all available protocols for this draft - * - * @return the protocols which are enabled for this draft - * @since 1.3.7 - */ - public List getKnownProtocols() { - return knownProtocols; - } - - /** - * Getter for the maximum allowed payload size which is used by this draft - * - * @return the size, which is allowed for the payload - * @since 1.4.0 - */ - public int getMaxFrameSize() { - return maxFrameSize; - } - - /** - * Getter for the protocol which is used by this draft - * - * @return the protocol which is used or null, if handshake is not yet done or no valid protocols - * @since 1.3.7 - */ - public IProtocol getProtocol() { - return protocol; - } - - @Override - public int hashCode() { - int result = extension != null ? extension.hashCode() : 0; - result = 31 * result + (protocol != null ? protocol.hashCode() : 0); - result = 31 * result + (maxFrameSize ^ (maxFrameSize >>> 32)); - return result; - } - - @Override - public ClientHandshakeBuilder postProcessHandshakeRequestAsClient(ClientHandshakeBuilder request) { - request.put(UPGRADE, "websocket"); - request.put(CONNECTION, UPGRADE); // to respond to a Connection keep alives - byte[] random = new byte[16]; - reuseableRandom.nextBytes(random); - request.put(SEC_WEB_SOCKET_KEY, Base64.encodeBytes(random)); - request.put("Sec-WebSocket-Version", "13");// overwriting the previous - StringBuilder requestedExtensions = new StringBuilder(); - for (IExtension knownExtension : knownExtensions) { - if (knownExtension.getProvidedExtensionAsClient() != null - && knownExtension.getProvidedExtensionAsClient().length() != 0) { - if (requestedExtensions.length() > 0) { - requestedExtensions.append(", "); - } - requestedExtensions.append(knownExtension.getProvidedExtensionAsClient()); - } - } - if (requestedExtensions.length() != 0) { - request.put(SEC_WEB_SOCKET_EXTENSIONS, requestedExtensions.toString()); - } - StringBuilder requestedProtocols = new StringBuilder(); - for (IProtocol knownProtocol : knownProtocols) { - if (knownProtocol.getProvidedProtocol().length() != 0) { - if (requestedProtocols.length() > 0) { - requestedProtocols.append(", "); - } - requestedProtocols.append(knownProtocol.getProvidedProtocol()); - } - } - if (requestedProtocols.length() != 0) { - request.put(SEC_WEB_SOCKET_PROTOCOL, requestedProtocols.toString()); - } - return request; - } - - @Override - public HandshakeBuilder postProcessHandshakeResponseAsServer(ClientHandshake request, - ServerHandshakeBuilder response) throws InvalidHandshakeException { - response.put(UPGRADE, "websocket"); - response.put(CONNECTION, request.getFieldValue(CONNECTION)); // to respond to a Connection keep alives - String seckey = request.getFieldValue(SEC_WEB_SOCKET_KEY); - if (seckey == null) { - throw new InvalidHandshakeException("missing Sec-WebSocket-Key"); - } - response.put(SEC_WEB_SOCKET_ACCEPT, generateFinalKey(seckey)); - if (getExtension().getProvidedExtensionAsServer().length() != 0) { - response.put(SEC_WEB_SOCKET_EXTENSIONS, getExtension().getProvidedExtensionAsServer()); - } - if (getProtocol() != null && getProtocol().getProvidedProtocol().length() != 0) { - response.put(SEC_WEB_SOCKET_PROTOCOL, getProtocol().getProvidedProtocol()); - } - response.setHttpStatusMessage("Web Socket Protocol Handshake"); - response.put("Server", "TooTallNate Java-WebSocket"); - response.put("Date", getServerTime()); - return response; - } - - @Override - public void processFrame(WebSocketImpl webSocketImpl, Framedata frame) throws InvalidDataException { - Opcode curop = frame.getOpcode(); - if (curop == Opcode.CLOSING) { - processFrameClosing(webSocketImpl, frame); - } else if (curop == Opcode.PING) { - webSocketImpl.getWebSocketListener().onWebsocketPing(webSocketImpl, frame); - } else if (curop == Opcode.PONG) { - webSocketImpl.updateLastPong(); - webSocketImpl.getWebSocketListener().onWebsocketPong(webSocketImpl, frame); - } else if (!frame.isFin() || curop == Opcode.CONTINUOUS) { - processFrameContinuousAndNonFin(webSocketImpl, frame, curop); - } else if (currentContinuousFrame != null) { - throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR, "Continuous frame sequence not completed."); - } else if (curop == Opcode.TEXT) { - processFrameText(webSocketImpl, frame); - } else if (curop == Opcode.BINARY) { - processFrameBinary(webSocketImpl, frame); - } else { - throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR, "non control or continious frame expected"); - } - } - - @Override - public void reset() { - incompleteframe = null; - if (extension != null) { - extension.reset(); - } - extension = new DefaultExtension(); - protocol = null; - } - - @Override - public String toString() { - String result = super.toString(); - if (getExtension() != null) { - result += " extension: " + getExtension().toString(); - } - if (getProtocol() != null) { - result += " protocol: " + getProtocol().toString(); - } - result += " max frame size: " + this.maxFrameSize; - return result; - } - - @Override - public List translateFrame(ByteBuffer buffer) throws InvalidDataException { - while (true) { - List frames = new LinkedList(); - Framedata cur; - if (incompleteframe != null) { - // complete an incomplete frame - try { - buffer.mark(); - int availableNextByteCount = buffer.remaining();// The number of bytes received - int expectedNextByteCount = incompleteframe - .remaining();// The number of bytes to complete the incomplete frame - - if (expectedNextByteCount > availableNextByteCount) { - // did not receive enough bytes to complete the frame - incompleteframe.put(buffer.array(), buffer.position(), availableNextByteCount); - buffer.position(buffer.position() + availableNextByteCount); - return Collections.emptyList(); - } - incompleteframe.put(buffer.array(), buffer.position(), expectedNextByteCount); - buffer.position(buffer.position() + expectedNextByteCount); - cur = translateSingleFrame((ByteBuffer) incompleteframe.duplicate().position(0)); - frames.add(cur); - incompleteframe = null; - } catch (IncompleteException e) { - // extending as much as suggested - ByteBuffer extendedframe = ByteBuffer.allocate(checkAlloc(e.getPreferredSize())); - assert (extendedframe.limit() > incompleteframe.limit()); - incompleteframe.rewind(); - extendedframe.put(incompleteframe); - incompleteframe = extendedframe; - continue; - } - } - - while (buffer.hasRemaining()) {// Read as much as possible full frames - buffer.mark(); - try { - cur = translateSingleFrame(buffer); - frames.add(cur); - } catch (IncompleteException e) { - // remember the incomplete data - buffer.reset(); - int pref = e.getPreferredSize(); - incompleteframe = ByteBuffer.allocate(checkAlloc(pref)); - incompleteframe.put(buffer); - break; - } - } - return frames; - } - } - - /** - * Add a payload to the current bytebuffer list - * - * @param payloadData the new payload - */ - private void addToBufferList(ByteBuffer payloadData) { - synchronized (byteBufferList) { - byteBufferList.add(payloadData); - } - } - - /** - * Check the current size of the buffer and throw an exception if the size is bigger than the max allowed frame - * size - * - * @throws LimitExceededException if the current size is bigger than the allowed size - */ - private void checkBufferLimit() throws LimitExceededException { - long totalSize = getByteBufferListSize(); - if (totalSize > maxFrameSize) { - clearBufferList(); - throw new LimitExceededException(maxFrameSize); - } - } - - /** - * Clear the current bytebuffer list - */ - private void clearBufferList() { - synchronized (byteBufferList) { - byteBufferList.clear(); - } - } - - /** - * Check if the requested protocol is part of this draft - * - * @param requestedProtocol the requested protocol - * @return MATCHED if it is matched, otherwise NOT_MATCHED - */ - private HandshakeState containsRequestedProtocol(String requestedProtocol) { - for (IProtocol knownProtocol : knownProtocols) { - if (knownProtocol.acceptProvidedProtocol(requestedProtocol)) { - protocol = knownProtocol; - return HandshakeState.MATCHED; - } - } - return HandshakeState.NOT_MATCHED; - } - - private ByteBuffer createByteBufferFromFramedata(Framedata framedata) { - ByteBuffer mes = framedata.getPayloadData(); - boolean mask = role == Role.CLIENT; - int sizebytes = getSizeBytes(mes); - ByteBuffer buf = ByteBuffer - .allocate(1 + (sizebytes > 1 ? sizebytes + 1 : sizebytes) + (mask ? 4 : 0) + mes.remaining()); - byte optcode = fromOpcode(framedata.getOpcode()); - byte one = (byte) (framedata.isFin() ? -128 : 0); - one |= optcode; - buf.put(one); - byte[] payloadlengthbytes = toByteArray(mes.remaining(), sizebytes); - assert (payloadlengthbytes.length == sizebytes); - - if (sizebytes == 1) { - buf.put((byte) (payloadlengthbytes[0] | getMaskByte(mask))); - } else if (sizebytes == 2) { - buf.put((byte) ((byte) 126 | getMaskByte(mask))); - buf.put(payloadlengthbytes); - } else if (sizebytes == 8) { - buf.put((byte) ((byte) 127 | getMaskByte(mask))); - buf.put(payloadlengthbytes); - } else { - throw new IllegalStateException("Size representation not supported/specified"); - } - if (mask) { - ByteBuffer maskkey = ByteBuffer.allocate(4); - maskkey.putInt(reuseableRandom.nextInt()); - buf.put(maskkey.array()); - for (int i = 0; mes.hasRemaining(); i++) { - buf.put((byte) (mes.get() ^ maskkey.get(i % 4))); - } - } else { - buf.put(mes); - //Reset the position of the bytebuffer e.g. for additional use - mes.flip(); - } - assert (buf.remaining() == 0) : buf.remaining(); - buf.flip(); - return buf; - } - - private byte fromOpcode(Opcode opcode) { - if (opcode == Opcode.CONTINUOUS) { - return 0; - } else if (opcode == Opcode.TEXT) { - return 1; - } else if (opcode == Opcode.BINARY) { - return 2; - } else if (opcode == Opcode.CLOSING) { - return 8; - } else if (opcode == Opcode.PING) { - return 9; - } else if (opcode == Opcode.PONG) { - return 10; - } - throw new IllegalArgumentException("Don't know how to handle " + opcode.toString()); - } - - /** - * Generate a final key from a input string - * - * @param in the input string - * @return a final key - */ - private String generateFinalKey(String in) { - String seckey = in.trim(); - String acc = seckey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - MessageDigest sh1; - try { - sh1 = MessageDigest.getInstance("SHA1"); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException(e); - } - return Base64.encodeBytes(sh1.digest(acc.getBytes())); - } - - /** - * Get the current size of the resulting bytebuffer in the bytebuffer list - * - * @return the size as long (to not get an integer overflow) - */ - private long getByteBufferListSize() { - long totalSize = 0; - synchronized (byteBufferList) { - for (ByteBuffer buffer : byteBufferList) { - totalSize += buffer.limit(); - } - } - return totalSize; - } - - /** - * Get the mask byte if existing - * - * @param mask is mask active or not - * @return -128 for true, 0 for false - */ - private byte getMaskByte(boolean mask) { - return mask ? (byte) -128 : 0; - } - - /** - * Method to generate a full bytebuffer out of all the fragmented frame payload - * - * @return a bytebuffer containing all the data - * @throws LimitExceededException will be thrown when the totalSize is bigger then Integer.MAX_VALUE due to not - * being able to allocate more - */ - private ByteBuffer getPayloadFromByteBufferList() throws LimitExceededException { - long totalSize = 0; - ByteBuffer resultingByteBuffer; - synchronized (byteBufferList) { - for (ByteBuffer buffer : byteBufferList) { - totalSize += buffer.limit(); - } - checkBufferLimit(); - resultingByteBuffer = ByteBuffer.allocate((int) totalSize); - for (ByteBuffer buffer : byteBufferList) { - resultingByteBuffer.put(buffer); - } - } - resultingByteBuffer.flip(); - return resultingByteBuffer; - } - - /** - * Generate a date for for the date-header - * - * @return the server time - */ - private String getServerTime() { - Calendar calendar = Calendar.getInstance(); - SimpleDateFormat dateFormat = new SimpleDateFormat( - "EEE, dd MMM yyyy HH:mm:ss z", Locale.US); - dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); - return dateFormat.format(calendar.getTime()); - } - - /** - * Get the size bytes for the byte buffer - * - * @param mes the current buffer - * @return the size bytes - */ - private int getSizeBytes(ByteBuffer mes) { - if (mes.remaining() <= 125) { - return 1; - } else if (mes.remaining() <= 65535) { - return 2; - } - return 8; - } - - /** - * Log the runtime exception to the specific WebSocketImpl - * - * @param webSocketImpl the implementation of the websocket - * @param e the runtime exception - */ - private void logRuntimeException(WebSocketImpl webSocketImpl, RuntimeException e) { - webSocketImpl.getWebSocketListener().onWebsocketError(webSocketImpl, e); - } - - /** - * Process the frame if it is a binary frame - * - * @param webSocketImpl the websocket impl - * @param frame the frame - */ - private void processFrameBinary(WebSocketImpl webSocketImpl, Framedata frame) { - try { - webSocketImpl.getWebSocketListener().onWebsocketMessage(webSocketImpl, frame.getPayloadData()); - } catch (RuntimeException e) { - logRuntimeException(webSocketImpl, e); - } - } - - /** - * Process the frame if it is a closing frame - * - * @param webSocketImpl the websocket impl - * @param frame the frame - */ - private void processFrameClosing(WebSocketImpl webSocketImpl, Framedata frame) { - int code = CloseFrame.NOCODE; - String reason = ""; - if (frame instanceof CloseFrame) { - CloseFrame cf = (CloseFrame) frame; - code = cf.getCloseCode(); - reason = cf.getMessage(); - } - if (webSocketImpl.getReadyState() == ReadyState.CLOSING) { - // complete the close handshake by disconnecting - webSocketImpl.closeConnection(code, reason, true); - } else { - // echo close handshake - if (getCloseHandshakeType() == CloseHandshakeType.TWOWAY) { - webSocketImpl.close(code, reason, true); - } else { - webSocketImpl.flushAndClose(code, reason, false); - } - } - } - - /** - * Process the frame if it is a continuous frame or the fin bit is not set - * - * @param webSocketImpl the websocket implementation to use - * @param frame the current frame - * @param curop the current Opcode - * @throws InvalidDataException if there is a protocol error - */ - private void processFrameContinuousAndNonFin(WebSocketImpl webSocketImpl, Framedata frame, Opcode curop) - throws InvalidDataException { - if (curop != Opcode.CONTINUOUS) { - processFrameIsNotFin(frame); - } else if (frame.isFin()) { - processFrameIsFin(webSocketImpl, frame); - } else if (currentContinuousFrame == null) { - throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR, "Continuous frame sequence was not started."); - } - //Check if the whole payload is valid utf8, when the opcode indicates a text - if (curop == Opcode.TEXT && !Charsetfunctions.isValidUTF8(frame.getPayloadData())) { - throw new InvalidDataException(CloseFrame.NO_UTF8); - } - //Checking if the current continuous frame contains a correct payload with the other frames combined - if (curop == Opcode.CONTINUOUS && currentContinuousFrame != null) { - addToBufferList(frame.getPayloadData()); - } - } - - /** - * Process the frame if it is the last frame - * - * @param webSocketImpl the websocket impl - * @param frame the frame - * @throws InvalidDataException if there is a protocol error - */ - private void processFrameIsFin(WebSocketImpl webSocketImpl, Framedata frame) throws InvalidDataException { - if (currentContinuousFrame == null) { - throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR, "Continuous frame sequence was not started."); - } - addToBufferList(frame.getPayloadData()); - checkBufferLimit(); - if (currentContinuousFrame.getOpcode() == Opcode.TEXT) { - ((FramedataImpl1) currentContinuousFrame).setPayload(getPayloadFromByteBufferList()); - ((FramedataImpl1) currentContinuousFrame).isValid(); - try { - webSocketImpl.getWebSocketListener().onWebsocketMessage(webSocketImpl, - Charsetfunctions.stringUtf8(currentContinuousFrame.getPayloadData())); - } catch (RuntimeException e) { - logRuntimeException(webSocketImpl, e); - } - } else if (currentContinuousFrame.getOpcode() == Opcode.BINARY) { - ((FramedataImpl1) currentContinuousFrame).setPayload(getPayloadFromByteBufferList()); - ((FramedataImpl1) currentContinuousFrame).isValid(); - try { - webSocketImpl.getWebSocketListener() - .onWebsocketMessage(webSocketImpl, currentContinuousFrame.getPayloadData()); - } catch (RuntimeException e) { - logRuntimeException(webSocketImpl, e); - } - } - currentContinuousFrame = null; - clearBufferList(); - } - - /** - * Process the frame if it is not the last frame - * - * @param frame the frame - * @throws InvalidDataException if there is a protocol error - */ - private void processFrameIsNotFin(Framedata frame) throws InvalidDataException { - if (currentContinuousFrame != null) { - throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR, - "Previous continuous frame sequence not completed."); - } - currentContinuousFrame = frame; - addToBufferList(frame.getPayloadData()); - checkBufferLimit(); - } - - /** - * Process the frame if it is a text frame - * - * @param webSocketImpl the websocket impl - * @param frame the frame - */ - private void processFrameText(WebSocketImpl webSocketImpl, Framedata frame) throws InvalidDataException { - try { - webSocketImpl.getWebSocketListener() - .onWebsocketMessage(webSocketImpl, Charsetfunctions.stringUtf8(frame.getPayloadData())); - } catch (RuntimeException e) { - logRuntimeException(webSocketImpl, e); - } - } - - private byte[] toByteArray(long val, int bytecount) { - byte[] buffer = new byte[bytecount]; - int highest = 8 * bytecount - 8; - for (int i = 0; i < bytecount; i++) { - buffer[i] = (byte) (val >>> (highest - 8 * i)); - } - return buffer; - } - - private Opcode toOpcode(byte opcode) throws InvalidFrameException { - switch (opcode) { - case 0: - return Opcode.CONTINUOUS; - case 1: - return Opcode.TEXT; - case 2: - return Opcode.BINARY; - // 3-7 are not yet defined - case 8: - return Opcode.CLOSING; - case 9: - return Opcode.PING; - case 10: - return Opcode.PONG; - // 11-15 are not yet defined - default: - throw new InvalidFrameException("Unknown opcode " + (short) opcode); - } - } - - private Framedata translateSingleFrame(ByteBuffer buffer) throws IncompleteException, InvalidDataException { - if (buffer == null) { - throw new IllegalArgumentException(); - } - int maxpacketsize = buffer.remaining(); - int realpacketsize = 2; - translateSingleFrameCheckPacketSize(maxpacketsize, realpacketsize); - byte b1 = buffer.get( /*0*/); - boolean fin = b1 >> 8 != 0; - boolean rsv1 = (b1 & 0x40) != 0; - boolean rsv2 = (b1 & 0x20) != 0; - boolean rsv3 = (b1 & 0x10) != 0; - byte b2 = buffer.get( /*1*/); - boolean mask = (b2 & -128) != 0; - int payloadlength = (byte) (b2 & ~(byte) 128); - Opcode optcode = toOpcode((byte) (b1 & 15)); - - if (!(payloadlength >= 0 && payloadlength <= 125)) { - TranslatedPayloadMetaData payloadData = translateSingleFramePayloadLength(buffer, optcode, payloadlength, - maxpacketsize, realpacketsize); - payloadlength = payloadData.getPayloadLength(); - realpacketsize = payloadData.getRealPackageSize(); - } - translateSingleFrameCheckLengthLimit(payloadlength); - realpacketsize += (mask ? 4 : 0); - realpacketsize += payloadlength; - translateSingleFrameCheckPacketSize(maxpacketsize, realpacketsize); - - ByteBuffer payload = ByteBuffer.allocate(checkAlloc(payloadlength)); - if (mask) { - byte[] maskskey = new byte[4]; - buffer.get(maskskey); - for (int i = 0; i < payloadlength; i++) { - payload.put((byte) (buffer.get( /*payloadstart + i*/) ^ maskskey[i % 4])); - } - } else { - payload.put(buffer.array(), buffer.position(), payload.limit()); - buffer.position(buffer.position() + payload.limit()); - } - - FramedataImpl1 frame = FramedataImpl1.get(optcode); - frame.setFin(fin); - frame.setRSV1(rsv1); - frame.setRSV2(rsv2); - frame.setRSV3(rsv3); - payload.flip(); - frame.setPayload(payload); - getExtension().isFrameValid(frame); - getExtension().decodeFrame(frame); - frame.isValid(); - return frame; - } - - /** - * Check if the frame size exceeds the allowed limit - * - * @param length the current payload length - * @throws LimitExceededException if the payload length is to big - */ - private void translateSingleFrameCheckLengthLimit(long length) throws LimitExceededException { - if (length > Integer.MAX_VALUE) { - throw new LimitExceededException("Payloadsize is to big..."); - } - if (length > maxFrameSize) { - throw new LimitExceededException("Payload limit reached.", maxFrameSize); - } - if (length < 0) { - throw new LimitExceededException("Payloadsize is to little..."); - } - } - - /** - * Check if the max packet size is smaller than the real packet size - * - * @param maxpacketsize the max packet size - * @param realpacketsize the real packet size - * @throws IncompleteException if the maxpacketsize is smaller than the realpackagesize - */ - private void translateSingleFrameCheckPacketSize(int maxpacketsize, int realpacketsize) - throws IncompleteException { - if (maxpacketsize < realpacketsize) { - throw new IncompleteException(realpacketsize); - } - } - - /** - * Translate the buffer depending when it has an extended payload length (126 or 127) - * - * @param buffer the buffer to read from - * @param optcode the decoded optcode - * @param oldPayloadlength the old payload length - * @param maxpacketsize the max packet size allowed - * @param oldRealpacketsize the real packet size - * @return the new payload data containing new payload length and new packet size - * @throws InvalidFrameException thrown if a control frame has an invalid length - * @throws IncompleteException if the maxpacketsize is smaller than the realpackagesize - * @throws LimitExceededException if the payload length is to big - */ - private TranslatedPayloadMetaData translateSingleFramePayloadLength(ByteBuffer buffer, Opcode optcode, - int oldPayloadlength, int maxpacketsize, int oldRealpacketsize) - throws InvalidFrameException, IncompleteException, LimitExceededException { - int payloadlength = oldPayloadlength, - realpacketsize = oldRealpacketsize; - if (optcode == Opcode.PING || optcode == Opcode.PONG || optcode == Opcode.CLOSING) { - throw new InvalidFrameException("more than 125 octets"); - } - if (payloadlength == 126) { - realpacketsize += 2; // additional length bytes - translateSingleFrameCheckPacketSize(maxpacketsize, realpacketsize); - byte[] sizebytes = new byte[3]; - sizebytes[1] = buffer.get( /*1 + 1*/); - sizebytes[2] = buffer.get( /*1 + 2*/); - payloadlength = new BigInteger(sizebytes).intValue(); - } else { - realpacketsize += 8; // additional length bytes - translateSingleFrameCheckPacketSize(maxpacketsize, realpacketsize); - byte[] bytes = new byte[8]; - for (int i = 0; i < 8; i++) { - bytes[i] = buffer.get( /*1 + i*/); - } - long length = new BigInteger(bytes).longValue(); - translateSingleFrameCheckLengthLimit(length); - payloadlength = (int) length; - } - return new TranslatedPayloadMetaData(payloadlength, realpacketsize); - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/enums/CloseHandshakeType.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/enums/CloseHandshakeType.java deleted file mode 100755 index 556efcf9f..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/enums/CloseHandshakeType.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.clevertap.android.sdk.java_websocket.enums; - -/** - * Enum which represents type of handshake is required for a close - */ -public enum CloseHandshakeType { - NONE, ONEWAY, TWOWAY -} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/enums/HandshakeState.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/enums/HandshakeState.java deleted file mode 100755 index 416402098..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/enums/HandshakeState.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.clevertap.android.sdk.java_websocket.enums; - -/** - * Enum which represents the states a handshake may be in - */ -public enum HandshakeState { - /** - * Handshake matched this Draft successfully - */ - MATCHED, - /** - * Handshake is does not match this Draft - */ - NOT_MATCHED -} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/enums/Opcode.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/enums/Opcode.java deleted file mode 100755 index dd470b4e3..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/enums/Opcode.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.clevertap.android.sdk.java_websocket.enums; - -/** - * Enum which contains the different valid opcodes - */ -public enum Opcode { - CONTINUOUS, TEXT, BINARY, PING, PONG, CLOSING - // more to come -} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/enums/ReadyState.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/enums/ReadyState.java deleted file mode 100755 index 6c7f5763e..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/enums/ReadyState.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.clevertap.android.sdk.java_websocket.enums; - -/** - * Enum which represents the state a websocket may be in - */ -public enum ReadyState { - NOT_YET_CONNECTED, OPEN, CLOSING, CLOSED -} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/enums/Role.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/enums/Role.java deleted file mode 100755 index 4d4a67bca..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/enums/Role.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.clevertap.android.sdk.java_websocket.enums; - -/** - * Enum which represents the states a websocket may be in - */ -public enum Role { - CLIENT, SERVER -} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/exceptions/IncompleteException.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/exceptions/IncompleteException.java deleted file mode 100755 index 4e3808cc5..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/exceptions/IncompleteException.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.exceptions; - -/** - * Exception which indicates that the frame is not yet complete - */ -public class IncompleteException extends Exception { - - /** - * It's Serializable. - */ - private static final long serialVersionUID = 7330519489840500997L; - - /** - * The preferred size - */ - private final int preferredSize; - - /** - * Constructor for the preferred size of a frame - * - * @param preferredSize the preferred size of a frame - */ - public IncompleteException(int preferredSize) { - this.preferredSize = preferredSize; - } - - /** - * Getter for the preferredSize - * - * @return the value of the preferred size - */ - public int getPreferredSize() { - return preferredSize; - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/exceptions/IncompleteHandshakeException.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/exceptions/IncompleteHandshakeException.java deleted file mode 100755 index db880abe7..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/exceptions/IncompleteHandshakeException.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.exceptions; - -/** - * exception which indicates that a incomplete handshake was recieved - */ -public class IncompleteHandshakeException extends RuntimeException { - - /** - * Serializable - */ - private static final long serialVersionUID = 7906596804233893092L; - - /** - * attribut which size of handshake would have been prefered - */ - private final int preferredSize; - - /** - * constructor for a IncompleteHandshakeException - *

    - * - * @param preferredSize the prefered size - */ - public IncompleteHandshakeException(int preferredSize) { - this.preferredSize = preferredSize; - } - - /** - * constructor for a IncompleteHandshakeException - *

    - * preferredSize will be 0 - */ - public IncompleteHandshakeException() { - this.preferredSize = 0; - } - - /** - * Getter preferredSize - * - * @return the preferredSize - */ - public int getPreferredSize() { - return preferredSize; - } - -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/exceptions/InvalidDataException.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/exceptions/InvalidDataException.java deleted file mode 100755 index debba429c..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/exceptions/InvalidDataException.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.exceptions; - -/** - * exception which indicates that a invalid data was recieved - */ -public class InvalidDataException extends Exception { - - /** - * Serializable - */ - private static final long serialVersionUID = 3731842424390998726L; - - /** - * attribut which closecode will be returned - */ - private final int closecode; - - /** - * constructor for a InvalidDataException - * - * @param closecode the closecode which will be returned - */ - public InvalidDataException(int closecode) { - this.closecode = closecode; - } - - /** - * constructor for a InvalidDataException. - * - * @param closecode the closecode which will be returned. - * @param s the detail message. - */ - public InvalidDataException(int closecode, String s) { - super(s); - this.closecode = closecode; - } - - /** - * constructor for a InvalidDataException. - * - * @param closecode the closecode which will be returned. - * @param t the throwable causing this exception. - */ - public InvalidDataException(int closecode, Throwable t) { - super(t); - this.closecode = closecode; - } - - /** - * constructor for a InvalidDataException. - * - * @param closecode the closecode which will be returned. - * @param s the detail message. - * @param t the throwable causing this exception. - */ - public InvalidDataException(int closecode, String s, Throwable t) { - super(s, t); - this.closecode = closecode; - } - - /** - * Getter closecode - * - * @return the closecode - */ - public int getCloseCode() { - return closecode; - } - -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/exceptions/InvalidEncodingException.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/exceptions/InvalidEncodingException.java deleted file mode 100755 index f1c76e8d6..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/exceptions/InvalidEncodingException.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.clevertap.android.sdk.java_websocket.exceptions; - -import java.io.UnsupportedEncodingException; - -/** - * The Character Encoding is not supported. - * - * @since 1.4.0 - */ -public class InvalidEncodingException extends RuntimeException { - - /** - * attribute for the encoding exception - */ - private final UnsupportedEncodingException encodingException; - - /** - * constructor for InvalidEncodingException - * - * @param encodingException the cause for this exception - */ - public InvalidEncodingException(UnsupportedEncodingException encodingException) { - if (encodingException == null) { - throw new IllegalArgumentException(); - } - this.encodingException = encodingException; - } - - /** - * Get the exception which includes more information on the unsupported encoding - * - * @return an UnsupportedEncodingException - */ - public UnsupportedEncodingException getEncodingException() { - return encodingException; - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/exceptions/InvalidFrameException.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/exceptions/InvalidFrameException.java deleted file mode 100755 index a11894163..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/exceptions/InvalidFrameException.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.exceptions; - -import com.clevertap.android.sdk.java_websocket.framing.CloseFrame; - -/** - * exception which indicates that a invalid frame was recieved (CloseFrame.PROTOCOL_ERROR) - */ -public class InvalidFrameException extends InvalidDataException { - - /** - * Serializable - */ - private static final long serialVersionUID = -9016496369828887591L; - - /** - * constructor for a InvalidFrameException - *

    - * calling InvalidDataException with closecode PROTOCOL_ERROR - */ - public InvalidFrameException() { - super(CloseFrame.PROTOCOL_ERROR); - } - - /** - * constructor for a InvalidFrameException - *

    - * calling InvalidDataException with closecode PROTOCOL_ERROR - * - * @param s the detail message. - */ - public InvalidFrameException(String s) { - super(CloseFrame.PROTOCOL_ERROR, s); - } - - /** - * constructor for a InvalidFrameException - *

    - * calling InvalidDataException with closecode PROTOCOL_ERROR - * - * @param t the throwable causing this exception. - */ - public InvalidFrameException(Throwable t) { - super(CloseFrame.PROTOCOL_ERROR, t); - } - - /** - * constructor for a InvalidFrameException - *

    - * calling InvalidDataException with closecode PROTOCOL_ERROR - * - * @param s the detail message. - * @param t the throwable causing this exception. - */ - public InvalidFrameException(String s, Throwable t) { - super(CloseFrame.PROTOCOL_ERROR, s, t); - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/exceptions/InvalidHandshakeException.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/exceptions/InvalidHandshakeException.java deleted file mode 100755 index e20013823..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/exceptions/InvalidHandshakeException.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.exceptions; - -import com.clevertap.android.sdk.java_websocket.framing.CloseFrame; - -/** - * exception which indicates that a invalid handshake was recieved (CloseFrame.PROTOCOL_ERROR) - */ -public class InvalidHandshakeException extends InvalidDataException { - - /** - * Serializable - */ - private static final long serialVersionUID = -1426533877490484964L; - - /** - * constructor for a InvalidHandshakeException - *

    - * calling InvalidDataException with closecode PROTOCOL_ERROR - */ - public InvalidHandshakeException() { - super(CloseFrame.PROTOCOL_ERROR); - } - - /** - * constructor for a InvalidHandshakeException - *

    - * calling InvalidDataException with closecode PROTOCOL_ERROR - * - * @param s the detail message. - * @param t the throwable causing this exception. - */ - public InvalidHandshakeException(String s, Throwable t) { - super(CloseFrame.PROTOCOL_ERROR, s, t); - } - - /** - * constructor for a InvalidHandshakeException - *

    - * calling InvalidDataException with closecode PROTOCOL_ERROR - * - * @param s the detail message. - */ - public InvalidHandshakeException(String s) { - super(CloseFrame.PROTOCOL_ERROR, s); - } - - /** - * constructor for a InvalidHandshakeException - *

    - * calling InvalidDataException with closecode PROTOCOL_ERROR - * - * @param t the throwable causing this exception. - */ - public InvalidHandshakeException(Throwable t) { - super(CloseFrame.PROTOCOL_ERROR, t); - } - -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/exceptions/LimitExceededException.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/exceptions/LimitExceededException.java deleted file mode 100755 index fe05134a8..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/exceptions/LimitExceededException.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.exceptions; - -import com.clevertap.android.sdk.java_websocket.framing.CloseFrame; - -/** - * exception which indicates that the message limited was exceeded (CloseFrame.TOOBIG) - */ -public class LimitExceededException extends InvalidDataException { - - /** - * Serializable - */ - private static final long serialVersionUID = 6908339749836826785L; - - /** - * A closer indication about the limit - */ - private final int limit; - - /** - * constructor for a LimitExceededException - *

    - * calling LimitExceededException with closecode TOOBIG - */ - public LimitExceededException() { - this(Integer.MAX_VALUE); - } - - /** - * constructor for a LimitExceededException - *

    - * calling InvalidDataException with closecode TOOBIG - */ - public LimitExceededException(int limit) { - super(CloseFrame.TOOBIG); - this.limit = limit; - } - - /** - * constructor for a LimitExceededException - *

    - * calling InvalidDataException with closecode TOOBIG - */ - public LimitExceededException(String s, int limit) { - super(CloseFrame.TOOBIG, s); - this.limit = limit; - } - - /** - * constructor for a LimitExceededException - *

    - * calling InvalidDataException with closecode TOOBIG - * - * @param s the detail message. - */ - public LimitExceededException(String s) { - this(s, Integer.MAX_VALUE); - } - - /** - * Get the limit which was hit so this exception was caused - * - * @return the limit as int - */ - public int getLimit() { - return limit; - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/exceptions/NotSendableException.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/exceptions/NotSendableException.java deleted file mode 100755 index 5cf7f7992..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/exceptions/NotSendableException.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.exceptions; - -/** - * exception which indicates the frame payload is not sendable - */ -public class NotSendableException extends RuntimeException { - - /** - * Serializable - */ - private static final long serialVersionUID = -6468967874576651628L; - - /** - * constructor for a NotSendableException - * - * @param s the detail message. - */ - public NotSendableException(String s) { - super(s); - } - - /** - * constructor for a NotSendableException - * - * @param t the throwable causing this exception. - */ - public NotSendableException(Throwable t) { - super(t); - } - - /** - * constructor for a NotSendableException - * - * @param s the detail message. - * @param t the throwable causing this exception. - */ - public NotSendableException(String s, Throwable t) { - super(s, t); - } - -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/exceptions/WebsocketNotConnectedException.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/exceptions/WebsocketNotConnectedException.java deleted file mode 100755 index b4eac45c0..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/exceptions/WebsocketNotConnectedException.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.exceptions; - -/** - * exception which indicates the websocket is not yet connected (ReadyState.OPEN) - */ -public class WebsocketNotConnectedException extends RuntimeException { - - /** - * Serializable - */ - private static final long serialVersionUID = -785314021592982715L; -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/extensions/CompressionExtension.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/extensions/CompressionExtension.java deleted file mode 100755 index acbddf65c..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/extensions/CompressionExtension.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.extensions; - -import com.clevertap.android.sdk.java_websocket.exceptions.InvalidDataException; -import com.clevertap.android.sdk.java_websocket.exceptions.InvalidFrameException; -import com.clevertap.android.sdk.java_websocket.framing.ControlFrame; -import com.clevertap.android.sdk.java_websocket.framing.DataFrame; -import com.clevertap.android.sdk.java_websocket.framing.Framedata; - -/** - * Implementation for a compression extension specified by https://tools.ietf.org/html/rfc7692 - * - * @since 1.3.5 - */ -public abstract class CompressionExtension extends DefaultExtension { - - @Override - public void isFrameValid(Framedata inputFrame) throws InvalidDataException { - if ((inputFrame instanceof DataFrame) && (inputFrame.isRSV2() || inputFrame.isRSV3())) { - throw new InvalidFrameException( - "bad rsv RSV1: " + inputFrame.isRSV1() + " RSV2: " + inputFrame.isRSV2() + " RSV3: " + inputFrame - .isRSV3()); - } - if ((inputFrame instanceof ControlFrame) && (inputFrame.isRSV1() || inputFrame.isRSV2() || inputFrame - .isRSV3())) { - throw new InvalidFrameException( - "bad rsv RSV1: " + inputFrame.isRSV1() + " RSV2: " + inputFrame.isRSV2() + " RSV3: " + inputFrame - .isRSV3()); - } - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/extensions/DefaultExtension.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/extensions/DefaultExtension.java deleted file mode 100755 index e852d195b..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/extensions/DefaultExtension.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.extensions; - -import com.clevertap.android.sdk.java_websocket.exceptions.InvalidDataException; -import com.clevertap.android.sdk.java_websocket.exceptions.InvalidFrameException; -import com.clevertap.android.sdk.java_websocket.framing.Framedata; - -/** - * Class which represents the normal websocket implementation specified by rfc6455. - *

    - * This is a fallback and will always be available for a Draft_6455 - * - * @since 1.3.5 - */ -public class DefaultExtension implements IExtension { - - @Override - public boolean acceptProvidedExtensionAsClient(String inputExtension) { - return true; - } - - @Override - public boolean acceptProvidedExtensionAsServer(String inputExtension) { - return true; - } - - @Override - public IExtension copyInstance() { - return new DefaultExtension(); - } - - @Override - public void decodeFrame(Framedata inputFrame) throws InvalidDataException { - //Nothing to do here - } - - @Override - public void encodeFrame(Framedata inputFrame) { - //Nothing to do here - } - - @Override - public boolean equals(Object o) { - return this == o || o != null && getClass() == o.getClass(); - } - - @Override - public String getProvidedExtensionAsClient() { - return ""; - } - - @Override - public String getProvidedExtensionAsServer() { - return ""; - } - - @Override - public int hashCode() { - return getClass().hashCode(); - } - - @Override - public void isFrameValid(Framedata inputFrame) throws InvalidDataException { - if (inputFrame.isRSV1() || inputFrame.isRSV2() || inputFrame.isRSV3()) { - throw new InvalidFrameException( - "bad rsv RSV1: " + inputFrame.isRSV1() + " RSV2: " + inputFrame.isRSV2() + " RSV3: " + inputFrame - .isRSV3()); - } - } - - public void reset() { - //Nothing to do here. No internal stats. - } - - @Override - public String toString() { - return getClass().getSimpleName(); - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/extensions/IExtension.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/extensions/IExtension.java deleted file mode 100755 index b782364aa..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/extensions/IExtension.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.extensions; - -import com.clevertap.android.sdk.java_websocket.exceptions.InvalidDataException; -import com.clevertap.android.sdk.java_websocket.framing.Framedata; - -/** - * Interface which specifies all required methods to develop a websocket extension. - * - * @since 1.3.5 - */ -public interface IExtension { - - /** - * Check if the received Sec-WebSocket-Extensions header field contains a offer for the specific extension if the - * endpoint is in the role of a client - * - * @param inputExtensionHeader the received Sec-WebSocket-Extensions header field offered by the other endpoint - * @return true, if the offer does fit to this specific extension - * @since 1.3.5 - */ - boolean acceptProvidedExtensionAsClient(String inputExtensionHeader); - - /** - * Check if the received Sec-WebSocket-Extensions header field contains a offer for the specific extension if the - * endpoint is in the role of a server - * - * @param inputExtensionHeader the received Sec-WebSocket-Extensions header field offered by the other endpoint - * @return true, if the offer does fit to this specific extension - * @since 1.3.5 - */ - boolean acceptProvidedExtensionAsServer(String inputExtensionHeader); - - /** - * Extensions must only be by one websocket at all. To prevent extensions to be used more than once the Websocket - * implementation should call this method in order to create a new usable version of a given extension - * instance.
    - * The copy can be safely used in conjunction with a new websocket connection. - * - * @return a copy of the extension - * @since 1.3.5 - */ - IExtension copyInstance(); - - /** - * Decode a frame with a extension specific algorithm. - * The algorithm is subject to be implemented by the specific extension. - * The resulting frame will be used in the application - * - * @param inputFrame the frame, which has do be decoded to be used in the application - * @throws InvalidDataException Throw InvalidDataException if the received frame is not correctly implemented by - * the other endpoint or there are other protocol errors/decoding errors - * @since 1.3.5 - */ - void decodeFrame(Framedata inputFrame) throws InvalidDataException; - - /** - * Encode a frame with a extension specific algorithm. - * The algorithm is subject to be implemented by the specific extension. - * The resulting frame will be send to the other endpoint. - * - * @param inputFrame the frame, which has do be encoded to be used on the other endpoint - * @since 1.3.5 - */ - void encodeFrame(Framedata inputFrame); - - /** - * Return the specific Sec-WebSocket-Extensions header offer for this extension if the endpoint is in the role of - * a client. - * If the extension returns an empty string (""), the offer will not be included in the handshake. - * - * @return the specific Sec-WebSocket-Extensions header for this extension - * @since 1.3.5 - */ - String getProvidedExtensionAsClient(); - - /** - * Return the specific Sec-WebSocket-Extensions header offer for this extension if the endpoint is in the role of - * a server. - * If the extension returns an empty string (""), the offer will not be included in the handshake. - * - * @return the specific Sec-WebSocket-Extensions header for this extension - * @since 1.3.5 - */ - String getProvidedExtensionAsServer(); - - /** - * Check if the received frame is correctly implemented by the other endpoint and there are no specification - * errors (like wrongly set RSV) - * - * @param inputFrame the received frame - * @throws InvalidDataException Throw InvalidDataException if the received frame is not correctly implementing the - * specification for the specific extension - * @since 1.3.5 - */ - void isFrameValid(Framedata inputFrame) throws InvalidDataException; - - /** - * Cleaning up internal stats when the draft gets reset. - * - * @since 1.3.5 - */ - void reset(); - - /** - * Return a string which should contain the class name as well as additional information about the current - * configurations for this extension (DEBUG purposes) - * - * @return a string containing the class name as well as additional information - * @since 1.3.5 - */ - String toString(); -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/BinaryFrame.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/BinaryFrame.java deleted file mode 100755 index 1e735330f..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/BinaryFrame.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.framing; - -import com.clevertap.android.sdk.java_websocket.enums.Opcode; - -/** - * Class to represent a binary frame - */ -public class BinaryFrame extends DataFrame { - - /** - * constructor which sets the opcode of this frame to binary - */ - public BinaryFrame() { - super(Opcode.BINARY); - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/CloseFrame.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/CloseFrame.java deleted file mode 100755 index 0e61c558c..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/CloseFrame.java +++ /dev/null @@ -1,366 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.framing; - -import com.clevertap.android.sdk.java_websocket.enums.Opcode; -import com.clevertap.android.sdk.java_websocket.exceptions.InvalidDataException; -import com.clevertap.android.sdk.java_websocket.exceptions.InvalidFrameException; -import com.clevertap.android.sdk.java_websocket.util.ByteBufferUtils; -import com.clevertap.android.sdk.java_websocket.util.Charsetfunctions; -import java.nio.ByteBuffer; - -/** - * Class to represent a close frame - */ -public class CloseFrame extends ControlFrame { - - /** - * indicates a normal closure, meaning whatever purpose the - * connection was established for has been fulfilled. - */ - public static final int NORMAL = 1000; - - /** - * 1001 indicates that an endpoint is "going away", such as a server - * going down, or a browser having navigated away from a page. - */ - public static final int GOING_AWAY = 1001; - - /** - * 1002 indicates that an endpoint is terminating the connection due - * to a protocol error. - */ - public static final int PROTOCOL_ERROR = 1002; - - /** - * 1003 indicates that an endpoint is terminating the connection - * because it has received a type of data it cannot accept (e.g. an - * endpoint that understands only text data MAY send this if it - * receives a binary message). - */ - public static final int REFUSE = 1003; - /*1004: Reserved. The specific meaning might be defined in the future.*/ - - /** - * 1005 is a reserved value and MUST NOT be set as a status code in a - * Close control frame by an endpoint. It is designated for use in - * applications expecting a status code to indicate that no status - * code was actually present. - */ - public static final int NOCODE = 1005; - - /** - * 1006 is a reserved value and MUST NOT be set as a status code in a - * Close control frame by an endpoint. It is designated for use in - * applications expecting a status code to indicate that the - * connection was closed abnormally, e.g. without sending or - * receiving a Close control frame. - */ - public static final int ABNORMAL_CLOSE = 1006; - - /** - * 1007 indicates that an endpoint is terminating the connection - * because it has received data within a message that was not - * consistent with the type of the message (e.g., non-UTF-8 [RFC3629] - * data within a text message). - */ - public static final int NO_UTF8 = 1007; - - /** - * 1008 indicates that an endpoint is terminating the connection - * because it has received a message that violates its policy. This - * is a generic status code that can be returned when there is no - * other more suitable status code (e.g. 1003 or 1009), or if there - * is a need to hide specific details about the policy. - */ - public static final int POLICY_VALIDATION = 1008; - - /** - * 1009 indicates that an endpoint is terminating the connection - * because it has received a message which is too big for it to - * process. - */ - public static final int TOOBIG = 1009; - - /** - * 1010 indicates that an endpoint (client) is terminating the - * connection because it has expected the server to negotiate one or - * more extension, but the server didn't return them in the response - * message of the WebSocket handshake. The list of extensions which - * are needed SHOULD appear in the /reason/ part of the Close frame. - * Note that this status code is not used by the server, because it - * can fail the WebSocket handshake instead. - */ - public static final int EXTENSION = 1010; - - /** - * 1011 indicates that a server is terminating the connection because - * it encountered an unexpected condition that prevented it from - * fulfilling the request. - **/ - public static final int UNEXPECTED_CONDITION = 1011; - - /** - * 1012 indicates that the service is restarted. - * A client may reconnect, and if it choses to do, should reconnect using a randomized delay of 5 - 30s. - * See https://www.ietf.org/mail-archive/web/hybi/current/msg09670.html for more information. - * - * @since 1.3.8 - **/ - public static final int SERVICE_RESTART = 1012; - - /** - * 1013 indicates that the service is experiencing overload. - * A client should only connect to a different IP (when there are multiple for the target) - * or reconnect to the same IP upon user action. - * See https://www.ietf.org/mail-archive/web/hybi/current/msg09670.html for more information. - * - * @since 1.3.8 - **/ - public static final int TRY_AGAIN_LATER = 1013; - - /** - * 1014 indicates that the server was acting as a gateway or proxy and received an - * invalid response from the upstream server. This is similar to 502 HTTP Status Code - * See https://www.ietf.org/mail-archive/web/hybi/current/msg10748.html fore more information. - * - * @since 1.3.8 - **/ - public static final int BAD_GATEWAY = 1014; - - /** - * 1015 is a reserved value and MUST NOT be set as a status code in a - * Close control frame by an endpoint. It is designated for use in - * applications expecting a status code to indicate that the - * connection was closed due to a failure to perform a TLS handshake - * (e.g., the server certificate can't be verified). - **/ - public static final int TLS_ERROR = 1015; - - /** - * The connection had never been established - */ - public static final int NEVER_CONNECTED = -1; - - /** - * The connection had a buggy close (this should not happen) - */ - public static final int BUGGYCLOSE = -2; - - /** - * The connection was flushed and closed - */ - public static final int FLASHPOLICY = -3; - - - /** - * The close code used in this close frame - */ - private int code; - - /** - * The close message used in this close frame - */ - private String reason; - - /** - * Constructor for a close frame - *

    - * Using opcode closing and fin = true - */ - public CloseFrame() { - super(Opcode.CLOSING); - setReason(""); - setCode(CloseFrame.NORMAL); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - if (!super.equals(o)) { - return false; - } - - CloseFrame that = (CloseFrame) o; - - if (code != that.code) { - return false; - } - return reason != null ? reason.equals(that.reason) : that.reason == null; - } - - /** - * Get the used close code - * - * @return the used close code - */ - public int getCloseCode() { - return code; - } - - /** - * Get the message that closeframe is containing - * - * @return the message in this frame - */ - public String getMessage() { - return reason; - } - - @Override - public ByteBuffer getPayloadData() { - if (code == NOCODE) { - return ByteBufferUtils.getEmptyByteBuffer(); - } - return super.getPayloadData(); - } - - @Override - public int hashCode() { - int result = super.hashCode(); - result = 31 * result + code; - result = 31 * result + (reason != null ? reason.hashCode() : 0); - return result; - } - - @Override - public void isValid() throws InvalidDataException { - super.isValid(); - if (code == CloseFrame.NO_UTF8 && reason.isEmpty()) { - throw new InvalidDataException(CloseFrame.NO_UTF8, "Received text is no valid utf8 string!"); - } - if (code == CloseFrame.NOCODE && 0 < reason.length()) { - throw new InvalidDataException(PROTOCOL_ERROR, "A close frame must have a closecode if it has a reason"); - } - //Intentional check for code != CloseFrame.TLS_ERROR just to make sure even if the code earlier changes - if ((code > CloseFrame.TLS_ERROR && code < 3000)) { - throw new InvalidDataException(PROTOCOL_ERROR, "Trying to send an illegal close code!"); - } - if (code == CloseFrame.ABNORMAL_CLOSE || code == CloseFrame.TLS_ERROR || code == CloseFrame.NOCODE - || code > 4999 || code < 1000 || code == 1004) { - throw new InvalidFrameException("closecode must not be sent over the wire: " + code); - } - } - - /** - * Set the close code for this close frame - * - * @param code the close code - */ - public void setCode(int code) { - this.code = code; - // CloseFrame.TLS_ERROR is not allowed to be transfered over the wire - if (code == CloseFrame.TLS_ERROR) { - this.code = CloseFrame.NOCODE; - this.reason = ""; - } - updatePayload(); - } - - @Override - public void setPayload(ByteBuffer payload) { - code = CloseFrame.NOCODE; - reason = ""; - payload.mark(); - if (payload.remaining() == 0) { - code = CloseFrame.NORMAL; - } else if (payload.remaining() == 1) { - code = CloseFrame.PROTOCOL_ERROR; - } else { - if (payload.remaining() >= 2) { - ByteBuffer bb = ByteBuffer.allocate(4); - bb.position(2); - bb.putShort(payload.getShort()); - bb.position(0); - code = bb.getInt(); - } - payload.reset(); - try { - int mark = payload.position();// because stringUtf8 also creates a mark - validateUtf8(payload, mark); - } catch (InvalidDataException e) { - code = CloseFrame.NO_UTF8; - reason = null; - } - } - } - - /** - * Set the close reason for this close frame - * - * @param reason the reason code - */ - public void setReason(String reason) { - if (reason == null) { - reason = ""; - } - this.reason = reason; - updatePayload(); - } - - @Override - public String toString() { - return super.toString() + "code: " + code; - } - - /** - * Update the payload to represent the close code and the reason - */ - private void updatePayload() { - byte[] by = Charsetfunctions.utf8Bytes(reason); - ByteBuffer buf = ByteBuffer.allocate(4); - buf.putInt(code); - buf.position(2); - ByteBuffer pay = ByteBuffer.allocate(2 + by.length); - pay.put(buf); - pay.put(by); - pay.rewind(); - super.setPayload(pay); - } - - /** - * Validate the payload to valid utf8 - * - * @param mark the current mark - * @param payload the current payload - * @throws InvalidDataException the current payload is not a valid utf8 - */ - private void validateUtf8(ByteBuffer payload, int mark) throws InvalidDataException { - try { - payload.position(payload.position() + 2); - reason = Charsetfunctions.stringUtf8(payload); - } catch (IllegalArgumentException e) { - throw new InvalidDataException(CloseFrame.NO_UTF8); - } finally { - payload.position(mark); - } - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/ContinuousFrame.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/ContinuousFrame.java deleted file mode 100755 index d73a6863e..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/ContinuousFrame.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.framing; - -import com.clevertap.android.sdk.java_websocket.enums.Opcode; - -/** - * Class to represent a continuous frame - */ -public class ContinuousFrame extends DataFrame { - - /** - * constructor which sets the opcode of this frame to continuous - */ - public ContinuousFrame() { - super(Opcode.CONTINUOUS); - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/ControlFrame.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/ControlFrame.java deleted file mode 100755 index 9627a31ed..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/ControlFrame.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.framing; - -import com.clevertap.android.sdk.java_websocket.enums.Opcode; -import com.clevertap.android.sdk.java_websocket.exceptions.InvalidDataException; -import com.clevertap.android.sdk.java_websocket.exceptions.InvalidFrameException; - -/** - * Abstract class to represent control frames - */ -public abstract class ControlFrame extends FramedataImpl1 { - - /** - * Class to represent a control frame - * - * @param opcode the opcode to use - */ - public ControlFrame(Opcode opcode) { - super(opcode); - } - - @Override - public void isValid() throws InvalidDataException { - if (!isFin()) { - throw new InvalidFrameException("Control frame cant have fin==false set"); - } - if (isRSV1()) { - throw new InvalidFrameException("Control frame cant have rsv1==true set"); - } - if (isRSV2()) { - throw new InvalidFrameException("Control frame cant have rsv2==true set"); - } - if (isRSV3()) { - throw new InvalidFrameException("Control frame cant have rsv3==true set"); - } - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/DataFrame.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/DataFrame.java deleted file mode 100755 index 9dc5adba3..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/DataFrame.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.framing; - -import com.clevertap.android.sdk.java_websocket.enums.Opcode; -import com.clevertap.android.sdk.java_websocket.exceptions.InvalidDataException; - -/** - * Abstract class to represent data frames - */ -public abstract class DataFrame extends FramedataImpl1 { - - /** - * Class to represent a data frame - * - * @param opcode the opcode to use - */ - public DataFrame(Opcode opcode) { - super(opcode); - } - - @Override - public void isValid() throws InvalidDataException { - //Nothing specific to check - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/Framedata.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/Framedata.java deleted file mode 100755 index b03205b99..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/Framedata.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.framing; - -import com.clevertap.android.sdk.java_websocket.enums.Opcode; -import java.nio.ByteBuffer; - -/** - * The interface for the frame - */ -public interface Framedata { - - /** - * Appends an additional frame to the current frame - *

    - * This methods does not override the opcode, but does override the fin - * - * @param nextframe the additional frame - */ - @SuppressWarnings("unused") - void append(Framedata nextframe); - - /** - * Defines the interpretation of the "Payload data". - * - * @return the interpretation as a Opcode - */ - Opcode getOpcode(); - - /** - * The "Payload data" which was sent in this frame - * - * @return the "Payload data" as ByteBuffer - */ - ByteBuffer getPayloadData(); - - /** - * Defines whether the "Payload data" is masked. - * - * @return true, "Payload data" is masked - */ - @SuppressWarnings("unused") - boolean getTransfereMasked(); - - /** - * Indicates that this is the final fragment in a message. The first fragment MAY also be the final fragment. - * - * @return true, if this frame is the final fragment - */ - boolean isFin(); - - /** - * Indicates that this frame has the rsv1 bit set. - * - * @return true, if this frame has the rsv1 bit set - */ - boolean isRSV1(); - - /** - * Indicates that this frame has the rsv2 bit set. - * - * @return true, if this frame has the rsv2 bit set - */ - boolean isRSV2(); - - /** - * Indicates that this frame has the rsv3 bit set. - * - * @return true, if this frame has the rsv3 bit set - */ - boolean isRSV3(); -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/FramedataImpl1.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/FramedataImpl1.java deleted file mode 100755 index 500eaa932..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/FramedataImpl1.java +++ /dev/null @@ -1,293 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.framing; - -import com.clevertap.android.sdk.java_websocket.enums.Opcode; -import com.clevertap.android.sdk.java_websocket.exceptions.InvalidDataException; -import com.clevertap.android.sdk.java_websocket.util.ByteBufferUtils; -import java.nio.ByteBuffer; - -/** - * Abstract implementation of a frame - */ -public abstract class FramedataImpl1 implements Framedata { - - /** - * Indicates that this is the final fragment in a message. - */ - private boolean fin; - - /** - * Defines the interpretation of the "Payload data". - */ - private Opcode optcode; - - /** - * Indicates that the rsv1 bit is set or not - */ - private boolean rsv1; - - /** - * Indicates that the rsv2 bit is set or not - */ - private boolean rsv2; - - /** - * Indicates that the rsv3 bit is set or not - */ - private boolean rsv3; - - /** - * Defines whether the "Payload data" is masked. - */ - private boolean transferemasked; - - /** - * The unmasked "Payload data" which was sent in this frame - */ - private ByteBuffer unmaskedpayload; - - /** - * Get a frame with a specific opcode - * - * @param opcode the opcode representing the frame - * @return the frame with a specific opcode - */ - public static FramedataImpl1 get(Opcode opcode) { - if (opcode == null) { - throw new IllegalArgumentException("Supplied opcode cannot be null"); - } - switch (opcode) { - case PING: - return new PingFrame(); - case PONG: - return new PongFrame(); - case TEXT: - return new TextFrame(); - case BINARY: - return new BinaryFrame(); - case CLOSING: - return new CloseFrame(); - case CONTINUOUS: - return new ContinuousFrame(); - default: - throw new IllegalArgumentException("Supplied opcode is invalid"); - } - } - - /** - * Constructor for a FramedataImpl without any attributes set apart from the opcode - * - * @param op the opcode to use - */ - public FramedataImpl1(Opcode op) { - optcode = op; - unmaskedpayload = ByteBufferUtils.getEmptyByteBuffer(); - fin = true; - transferemasked = false; - rsv1 = false; - rsv2 = false; - rsv3 = false; - } - - @Override - public void append(Framedata nextframe) { - ByteBuffer b = nextframe.getPayloadData(); - if (unmaskedpayload == null) { - unmaskedpayload = ByteBuffer.allocate(b.remaining()); - b.mark(); - unmaskedpayload.put(b); - b.reset(); - } else { - b.mark(); - unmaskedpayload.position(unmaskedpayload.limit()); - unmaskedpayload.limit(unmaskedpayload.capacity()); - - if (b.remaining() > unmaskedpayload.remaining()) { - ByteBuffer tmp = ByteBuffer.allocate(b.remaining() + unmaskedpayload.capacity()); - unmaskedpayload.flip(); - tmp.put(unmaskedpayload); - tmp.put(b); - unmaskedpayload = tmp; - - } else { - unmaskedpayload.put(b); - } - unmaskedpayload.rewind(); - b.reset(); - } - fin = nextframe.isFin(); - - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - FramedataImpl1 that = (FramedataImpl1) o; - - if (fin != that.fin) { - return false; - } - if (transferemasked != that.transferemasked) { - return false; - } - if (rsv1 != that.rsv1) { - return false; - } - if (rsv2 != that.rsv2) { - return false; - } - if (rsv3 != that.rsv3) { - return false; - } - if (optcode != that.optcode) { - return false; - } - return unmaskedpayload != null ? unmaskedpayload.equals(that.unmaskedpayload) : that.unmaskedpayload == null; - } - - @Override - public Opcode getOpcode() { - return optcode; - } - - @Override - public ByteBuffer getPayloadData() { - return unmaskedpayload; - } - - @Override - public boolean getTransfereMasked() { - return transferemasked; - } - - @Override - public int hashCode() { - int result = (fin ? 1 : 0); - result = 31 * result + optcode.hashCode(); - result = 31 * result + (unmaskedpayload != null ? unmaskedpayload.hashCode() : 0); - result = 31 * result + (transferemasked ? 1 : 0); - result = 31 * result + (rsv1 ? 1 : 0); - result = 31 * result + (rsv2 ? 1 : 0); - result = 31 * result + (rsv3 ? 1 : 0); - return result; - } - - @Override - public boolean isFin() { - return fin; - } - - /** - * Set the fin of this frame to the provided boolean - * - * @param fin true if fin has to be set - */ - public void setFin(boolean fin) { - this.fin = fin; - } - - @Override - public boolean isRSV1() { - return rsv1; - } - - /** - * Set the rsv1 of this frame to the provided boolean - * - * @param rsv1 true if fin has to be set - */ - public void setRSV1(boolean rsv1) { - this.rsv1 = rsv1; - } - - @Override - public boolean isRSV2() { - return rsv2; - } - - /** - * Set the rsv2 of this frame to the provided boolean - * - * @param rsv2 true if fin has to be set - */ - public void setRSV2(boolean rsv2) { - this.rsv2 = rsv2; - } - - @Override - public boolean isRSV3() { - return rsv3; - } - - /** - * Set the rsv3 of this frame to the provided boolean - * - * @param rsv3 true if fin has to be set - */ - public void setRSV3(boolean rsv3) { - this.rsv3 = rsv3; - } - - /** - * Check if the frame is valid due to specification - * - * @throws InvalidDataException thrown if the frame is not a valid frame - */ - public abstract void isValid() throws InvalidDataException; - - /** - * Set the payload of this frame to the provided payload - * - * @param payload the payload which is to set - */ - public void setPayload(ByteBuffer payload) { - this.unmaskedpayload = payload; - } - - /** - * Set the tranferemask of this frame to the provided boolean - * - * @param transferemasked true if transferemasked has to be set - */ - public void setTransferemasked(boolean transferemasked) { - this.transferemasked = transferemasked; - } - - @Override - public String toString() { - return "Framedata{ optcode:" + getOpcode() + ", fin:" + isFin() + ", rsv1:" + isRSV1() + ", rsv2:" + isRSV2() - + ", rsv3:" + isRSV3() + ", payloadlength:[pos:" + unmaskedpayload.position() + ", len:" - + unmaskedpayload.remaining() + "], payload:" + (unmaskedpayload.remaining() > 1000 - ? "(too big to display)" : new String(unmaskedpayload.array())) + '}'; - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/PingFrame.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/PingFrame.java deleted file mode 100755 index f068e4a64..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/PingFrame.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.framing; - -import com.clevertap.android.sdk.java_websocket.enums.Opcode; - -/** - * Class to represent a ping frame - */ -public class PingFrame extends ControlFrame { - - /** - * constructor which sets the opcode of this frame to ping - */ - public PingFrame() { - super(Opcode.PING); - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/PongFrame.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/PongFrame.java deleted file mode 100755 index cd5c94af4..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/PongFrame.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.framing; - -import com.clevertap.android.sdk.java_websocket.enums.Opcode; - -/** - * Class to represent a pong frame - */ -public class PongFrame extends ControlFrame { - - /** - * constructor which sets the opcode of this frame to pong - */ - public PongFrame() { - super(Opcode.PONG); - } - - /** - * constructor which sets the opcode of this frame to ping copying over the payload of the ping - * - * @param pingFrame the PingFrame which payload is to copy - */ - public PongFrame(PingFrame pingFrame) { - super(Opcode.PONG); - setPayload(pingFrame.getPayloadData()); - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/TextFrame.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/TextFrame.java deleted file mode 100755 index f958ddf28..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/framing/TextFrame.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.framing; - -import com.clevertap.android.sdk.java_websocket.enums.Opcode; -import com.clevertap.android.sdk.java_websocket.exceptions.InvalidDataException; -import com.clevertap.android.sdk.java_websocket.util.Charsetfunctions; - -/** - * Class to represent a text frames - */ -public class TextFrame extends DataFrame { - - /** - * constructor which sets the opcode of this frame to text - */ - public TextFrame() { - super(Opcode.TEXT); - } - - @Override - public void isValid() throws InvalidDataException { - super.isValid(); - if (!Charsetfunctions.isValidUTF8(getPayloadData())) { - throw new InvalidDataException(CloseFrame.NO_UTF8, "Received text is no valid utf8 string!"); - } - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/handshake/ClientHandshake.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/handshake/ClientHandshake.java deleted file mode 100755 index f754e9014..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/handshake/ClientHandshake.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.handshake; - -/** - * The interface for a client handshake - */ -public interface ClientHandshake extends Handshakedata { - - /** - * returns the HTTP Request-URI as defined by http://tools.ietf.org/html/rfc2616#section-5.1.2 - * - * @return the HTTP Request-URI - */ - String getResourceDescriptor(); -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/handshake/ClientHandshakeBuilder.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/handshake/ClientHandshakeBuilder.java deleted file mode 100755 index f5da41779..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/handshake/ClientHandshakeBuilder.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.handshake; - -/** - * The interface for building a handshake for the client - */ -public interface ClientHandshakeBuilder extends HandshakeBuilder, ClientHandshake { - - /** - * Set a specific resource descriptor - * - * @param resourceDescriptor the resource descriptior to set - */ - void setResourceDescriptor(String resourceDescriptor); -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/handshake/HandshakeBuilder.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/handshake/HandshakeBuilder.java deleted file mode 100755 index f8702d32c..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/handshake/HandshakeBuilder.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.handshake; - -/** - * The interface for building a handshake - */ -public interface HandshakeBuilder extends Handshakedata { - - /** - * Adding a specific field with a specific value - * - * @param name the http field - * @param value the value for this field - */ - void put(String name, String value); - - /** - * Setter for the content of the handshake - * - * @param content the content to set - */ - void setContent(byte[] content); -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/handshake/HandshakeImpl1Client.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/handshake/HandshakeImpl1Client.java deleted file mode 100755 index 2409bb4ab..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/handshake/HandshakeImpl1Client.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.handshake; - -/** - * Implementation for a client handshake - */ -public class HandshakeImpl1Client extends HandshakedataImpl1 implements ClientHandshakeBuilder { - - /** - * Attribute for the resource descriptor - */ - private String resourceDescriptor = "*"; - - @Override - public String getResourceDescriptor() { - return resourceDescriptor; - } - - @Override - public void setResourceDescriptor(String resourceDescriptor) { - if (resourceDescriptor == null) { - throw new IllegalArgumentException("http resource descriptor must not be null"); - } - this.resourceDescriptor = resourceDescriptor; - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/handshake/HandshakeImpl1Server.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/handshake/HandshakeImpl1Server.java deleted file mode 100755 index 223c71238..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/handshake/HandshakeImpl1Server.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.handshake; - -/** - * Implementation for a server handshake - */ -public class HandshakeImpl1Server extends HandshakedataImpl1 implements ServerHandshakeBuilder { - - /** - * Attribute for the http status - */ - private short httpstatus; - - /** - * Attribute for the http status message - */ - private String httpstatusmessage; - - @Override - public short getHttpStatus() { - return httpstatus; - } - - @Override - public void setHttpStatus(short status) { - httpstatus = status; - } - - @Override - public String getHttpStatusMessage() { - return httpstatusmessage; - } - - @Override - public void setHttpStatusMessage(String message) { - this.httpstatusmessage = message; - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/handshake/Handshakedata.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/handshake/Handshakedata.java deleted file mode 100755 index 2216bb238..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/handshake/Handshakedata.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.handshake; - -import java.util.Iterator; - -/** - * The interface for the data of a handshake - */ -public interface Handshakedata { - - /** - * Get the content of the handshake - * - * @return the content as byte-array - */ - byte[] getContent(); - - /** - * Gets the value of the field - * - * @param name The name of the field - * @return the value of the field or an empty String if not in the handshake - */ - String getFieldValue(String name); - - /** - * Checks if this handshake contains a specific field - * - * @param name The name of the field - * @return true, if it contains the field - */ - boolean hasFieldValue(String name); - - /** - * Iterator for the http fields - * - * @return the http fields - */ - Iterator iterateHttpFields(); -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/handshake/HandshakedataImpl1.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/handshake/HandshakedataImpl1.java deleted file mode 100755 index 7584b7a1f..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/handshake/HandshakedataImpl1.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.handshake; - -import java.util.Collections; -import java.util.Iterator; -import java.util.TreeMap; - -/** - * Implementation of a handshake builder - */ -public class HandshakedataImpl1 implements HandshakeBuilder { - - /** - * Attribute for the content of the handshake - */ - private byte[] content; - - /** - * Attribute for the http fields and values - */ - private TreeMap map; - - /** - * Constructor for handshake implementation - */ - public HandshakedataImpl1() { - map = new TreeMap(String.CASE_INSENSITIVE_ORDER); - } - - @Override - public byte[] getContent() { - return content; - } - - @Override - public void setContent(byte[] content) { - this.content = content; - } - - @Override - public String getFieldValue(String name) { - String s = map.get(name); - if (s == null) { - return ""; - } - return s; - } - - @Override - public boolean hasFieldValue(String name) { - return map.containsKey(name); - } - - @Override - public Iterator iterateHttpFields() { - return Collections.unmodifiableSet(map.keySet()).iterator();// Safety first - } - - @Override - public void put(String name, String value) { - map.put(name, value); - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/handshake/ServerHandshake.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/handshake/ServerHandshake.java deleted file mode 100755 index a7f199770..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/handshake/ServerHandshake.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.handshake; - -/** - * Interface for the server handshake - */ -public interface ServerHandshake extends Handshakedata { - - /** - * Get the http status code - * - * @return the http status code - */ - short getHttpStatus(); - - /** - * Get the http status message - * - * @return the http status message - */ - String getHttpStatusMessage(); -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/handshake/ServerHandshakeBuilder.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/handshake/ServerHandshakeBuilder.java deleted file mode 100755 index 8fde0194d..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/handshake/ServerHandshakeBuilder.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.handshake; - -/** - * The interface for building a handshake for the server - */ -public interface ServerHandshakeBuilder extends HandshakeBuilder, ServerHandshake { - - /** - * Setter for the http status code - * - * @param status the http status code - */ - void setHttpStatus(short status); - - /** - * Setter for the http status message - * - * @param message the http status message - */ - void setHttpStatusMessage(String message); -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/protocols/IProtocol.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/protocols/IProtocol.java deleted file mode 100755 index da4e23f77..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/protocols/IProtocol.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.protocols; - -/** - * Interface which specifies all required methods for a Sec-WebSocket-Protocol - * - * @since 1.3.7 - */ -public interface IProtocol { - - /** - * Check if the received Sec-WebSocket-Protocol header field contains a offer for the specific protocol - * - * @param inputProtocolHeader the received Sec-WebSocket-Protocol header field offered by the other endpoint - * @return true, if the offer does fit to this specific protocol - * @since 1.3.7 - */ - boolean acceptProvidedProtocol(String inputProtocolHeader); - - /** - * To prevent protocols to be used more than once the Websocket implementation should call this method in order to - * create a new usable version of a given protocol instance. - * - * @return a copy of the protocol - * @since 1.3.7 - */ - IProtocol copyInstance(); - - /** - * Return the specific Sec-WebSocket-protocol header offer for this protocol if the endpoint. - * If the extension returns an empty string (""), the offer will not be included in the handshake. - * - * @return the specific Sec-WebSocket-Protocol header for this protocol - * @since 1.3.7 - */ - String getProvidedProtocol(); - - /** - * Return a string which should contain the protocol name as well as additional information about the current - * configurations for this protocol (DEBUG purposes) - * - * @return a string containing the protocol name as well as additional information - * @since 1.3.7 - */ - String toString(); -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/protocols/Protocol.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/protocols/Protocol.java deleted file mode 100755 index 6f25f79e2..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/protocols/Protocol.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.protocols; - -import java.util.regex.Pattern; - -/** - * Class which represents the protocol used as Sec-WebSocket-Protocol - * - * @since 1.3.7 - */ -public class Protocol implements IProtocol { - - private static final Pattern patternSpace = Pattern.compile(" "); - - private static final Pattern patternComma = Pattern.compile(","); - - /** - * Attribute for the provided protocol - */ - private final String providedProtocol; - - /** - * Constructor for a Sec-Websocket-Protocol - * - * @param providedProtocol the protocol string - */ - public Protocol(String providedProtocol) { - if (providedProtocol == null) { - throw new IllegalArgumentException(); - } - this.providedProtocol = providedProtocol; - } - - @Override - public boolean acceptProvidedProtocol(String inputProtocolHeader) { - String protocolHeader = patternSpace.matcher(inputProtocolHeader).replaceAll(""); - String[] headers = patternComma.split(protocolHeader); - for (String header : headers) { - if (providedProtocol.equals(header)) { - return true; - } - } - return false; - } - - @Override - public IProtocol copyInstance() { - return new Protocol(getProvidedProtocol()); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - Protocol protocol = (Protocol) o; - - return providedProtocol.equals(protocol.providedProtocol); - } - - @Override - public String getProvidedProtocol() { - return this.providedProtocol; - } - - @Override - public int hashCode() { - return providedProtocol.hashCode(); - } - - @Override - public String toString() { - return getProvidedProtocol(); - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/util/Base64.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/util/Base64.java deleted file mode 100755 index 7e3c1b8d8..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/util/Base64.java +++ /dev/null @@ -1,1058 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.util; - -/** - *

    Encodes and decodes to and from Base64 notation.

    - *

    Homepage: http://iharder.net/base64.

    - * - *

    Example:

    - * - * String encoded = Base64.encode( myByteArray ); - *
    - * byte[] myByteArray = Base64.decode( encoded ); - * - *

    The options parameter, which appears in a few places, is used to pass - * several pieces of information to the encoder. In the "higher level" methods such as - * encodeBytes( bytes, options ) the options parameter can be used to indicate such - * things as first gzipping the bytes before encoding them, not inserting linefeeds, - * and encoding using the URL-safe and Ordered dialects.

    - * - *

    Note, according to RFC3548, - * Section 2.1, implementations should not add line feeds unless explicitly told - * to do so. I've got Base64 set to this behavior now, although earlier versions - * broke lines by default.

    - * - *

    The constants defined in Base64 can be OR-ed together to combine options, so you - * might make a call like this:

    - * - * String encoded = Base64.encodeBytes( mybytes, Base64.GZIP | Base64.DO_BREAK_LINES ); - *

    to compress the data before encoding it and then making the output have newline characters.

    - *

    Also...

    - * String encoded = Base64.encodeBytes( crazyString.getBytes() ); - * - * - * - *

    - * Change Log: - *

    - *
      - *
    • v2.3.7 - Fixed subtle bug when base 64 input stream contained the - * value 01111111, which is an invalid base 64 character but should not - * throw an ArrayIndexOutOfBoundsException either. Led to discovery of - * mishandling (or potential for better handling) of other bad input - * characters. You should now get an IOException if you try decoding - * something that has bad characters in it.
    • - *
    • v2.3.6 - Fixed bug when breaking lines and the final byte of the encoded - * string ended in the last column; the buffer was not properly shrunk and - * contained an extra (null) byte that made it into the string.
    • - *
    • v2.3.4 - Fixed bug when working with gzipped streams whereby flushing - * the Base64.OutputStream closed the Base64 encoding (by padding with equals - * signs) too soon. Also added an option to suppress the automatic decoding - * of gzipped streams. Also added experimental support for specifying a - * class loader when using the method.
    • - *
    • v2.3.3 - Changed default char encoding to US-ASCII which reduces the internal Java - * footprint with its CharEncoders and so forth. Fixed some javadocs that were - * inconsistent. Removed imports and specified things like java.io.IOException - * explicitly inline.
    • - *
    • v2.3.2 - Reduced memory footprint! Finally refined the "guessing" of how big the - * final encoded data will be so that the code doesn't have to create two output - * arrays: an oversized initial one and then a final, exact-sized one. Big win - * when using the family of methods (and not - * using the gzip options which uses a different mechanism with streams and stuff).
    • - *
    • v2.3.1 - Added {@link #encodeBytesToBytes(byte[], int, int, int)} and some - * similar helper methods to be more efficient with memory by not returning a - * String but just a byte array.
    • - *
    • v2.3 - This is not a drop-in replacement! This is two years of comments - * and bug fixes queued up and finally executed. Thanks to everyone who sent - * me stuff, and I'm sorry I wasn't able to distribute your fixes to everyone else. - * Much bad coding was cleaned up including throwing exceptions where necessary - * instead of returning null values or something similar. Here are some changes - * that may affect you: - *
        - *
      • Does not break lines, by default. This is to keep in compliance with - * RFC3548.
      • - *
      • Throws exceptions instead of returning null values. Because some operations - * (especially those that may permit the GZIP option) use IO streams, there - * is a possiblity of an java.io.IOException being thrown. After some discussion and - * thought, I've changed the behavior of the methods to throw java.io.IOExceptions - * rather than return null if ever there's an error. I think this is more - * appropriate, though it will require some changes to your code. Sorry, - * it should have been done this way to begin with.
      • - *
      • Removed all references to System.out, System.err, and the like. - * Shame on me. All I can say is sorry they were ever there.
      • - *
      • Throws IllegalArgumentExceptions as needed - * such as when passed arrays are null or offsets are invalid.
      • - *
      • Cleaned up as much javadoc as I could to avoid any javadoc warnings. - * This was especially annoying before for people who were thorough in their - * own projects and then had gobs of javadoc warnings on this file.
      • - *
      - *
    • v2.2.1 - Fixed bug using URL_SAFE and ORDERED encodings. Fixed bug - * when using very small files (~< 40 bytes).
    • - *
    • v2.2 - Added some helper methods for encoding/decoding directly from - * one file to the next. Also added a main() method to support command line - * encoding/decoding from one file to the next. Also added these Base64 dialects: - *
        - *
      1. The default is RFC3548 format.
      2. - *
      3. Calling Base64.setFormat(Base64.BASE64_FORMAT.URLSAFE_FORMAT) generates - * URL and file name friendly format as described in Section 4 of RFC3548. - * http://www.faqs.org/rfcs/rfc3548.html
      4. - *
      5. Calling Base64.setFormat(Base64.BASE64_FORMAT.ORDERED_FORMAT) generates - * URL and file name friendly format that preserves lexical ordering as described - * in http://www.faqs.org/qa/rfcc-1940.html
      6. - *
      - * Special thanks to Jim Kellerman at http://www.powerset.com/ - * for contributing the new Base64 dialects. - *
    • - * - *
    • v2.1 - Cleaned up javadoc comments and unused variables and methods. Added - * some convenience methods for reading and writing to and from files.
    • - *
    • v2.0.2 - Now specifies UTF-8 encoding in places where the code fails on systems - * with other encodings (like EBCDIC).
    • - *
    • v2.0.1 - Fixed an error when decoding a single byte, that is, when the - * encoded data was a single byte.
    • - *
    • v2.0 - I got rid of methods that used booleans to set options. - * Now everything is more consolidated and cleaner. The code now detects - * when data that's being decoded is gzip-compressed and will decompress it - * automatically. Generally things are cleaner. You'll probably have to - * change some method calls that you were making to support the new - * options format (ints that you "OR" together).
    • - *
    • v1.5.1 - Fixed bug when decompressing and decoding to a - * byte[] using decode( String s, boolean gzipCompressed ). - * Added the ability to "suspend" encoding in the Output Stream so - * you can turn on and off the encoding if you need to embed base64 - * data in an otherwise "normal" stream (like an XML file).
    • - *
    • v1.5 - Output stream pases on flush() command but doesn't do anything itself. - * This helps when using GZIP streams. - * Added the ability to GZip-compress objects before encoding them.
    • - *
    • v1.4 - Added helper methods to read/write files.
    • - *
    • v1.3.6 - Fixed OutputStream.flush() so that 'position' is reset.
    • - *
    • v1.3.5 - Added flag to turn on and off line breaks. Fixed bug in input stream - * where last buffer being read, if not completely full, was not returned.
    • - *
    • v1.3.4 - Fixed when "improperly padded stream" error was thrown at the wrong time.
    • - *
    • v1.3.3 - Fixed I/O streams which were totally messed up.
    • - *
    - * - *

    - * I am placing this code in the Public Domain. Do with it as you will. - * This software comes with no guarantees or warranties but with - * plenty of well-wishing instead! - * Please visit http://iharder.net/base64 - * periodically to check for updates or to contribute improvements. - *

    - * - * @author Robert Harder - * @author rob@iharder.net - * @version 2.3.7 - */ -public class Base64 { - - /* ******** P U B L I C F I E L D S ******** */ - - - /** - * A {@link Base64.OutputStream} will write data to another - * java.io.OutputStream, given in the constructor, - * and encode/decode to/from Base64 notation on the fly. - * - * @see Base64 - * @since 1.3 - */ - public static class OutputStream extends java.io.FilterOutputStream { - - private byte[] b4; // Scratch used in a few places - - private boolean breakLines; - - private byte[] buffer; - - private int bufferLength; - - private byte[] decodabet; // Local copies to avoid extra method calls - - private boolean encode; - - private int lineLength; - - private int options; // Record for later - - private int position; - - private boolean suspendEncoding; - - /** - * Constructs a {@link Base64.OutputStream} in ENCODE mode. - * - * @param out the java.io.OutputStream to which data will be written. - * @since 1.3 - */ - public OutputStream(java.io.OutputStream out) { - this(out, ENCODE); - } // end constructor - - - /** - * Constructs a {@link Base64.OutputStream} in - * either ENCODE or DECODE mode. - *

    - * Valid options:

    -         *   ENCODE or DECODE: Encode or Decode as data is read.
    -         *   DO_BREAK_LINES: don't break lines at 76 characters
    -         *     (only meaningful when encoding)
    -         * 
    - *

    - * Example: new Base64.OutputStream( out, Base64.ENCODE ) - * - * @param out the java.io.OutputStream to which data will be written. - * @param options Specified options. - * @see Base64#ENCODE - * @see Base64#DO_BREAK_LINES - * @since 1.3 - */ - public OutputStream(java.io.OutputStream out, int options) { - super(out); - this.breakLines = (options & DO_BREAK_LINES) != 0; - this.encode = (options & ENCODE) != 0; - this.bufferLength = encode ? 3 : 4; - this.buffer = new byte[bufferLength]; - this.position = 0; - this.lineLength = 0; - this.suspendEncoding = false; - this.b4 = new byte[4]; - this.options = options; - this.decodabet = getDecodabet(options); - } // end constructor - - /** - * Flushes and closes (I think, in the superclass) the stream. - * - * @since 1.3 - */ - @Override - public void close() throws java.io.IOException { - // 1. Ensure that pending characters are written - flushBase64(); - - // 2. Actually close the stream - // Base class both flushes and closes. - super.close(); - - buffer = null; - out = null; - } // end close - - /** - * Method added by PHIL. [Thanks, PHIL. -Rob] - * This pads the buffer without closing the stream. - * - * @throws java.io.IOException if there's an error. - */ - public void flushBase64() throws java.io.IOException { - if (position > 0) { - if (encode) { - out.write(encode3to4(b4, buffer, position, options)); - position = 0; - } // end if: encoding - else { - throw new java.io.IOException("Base64 input not properly padded."); - } // end else: decoding - } // end if: buffer partially full - - } // end flush - - /** - * Writes the byte to the output stream after - * converting to/from Base64 notation. - * When encoding, bytes are buffered three - * at a time before the output stream actually - * gets a write() call. - * When decoding, bytes are buffered four - * at a time. - * - * @param theByte the byte to write - * @since 1.3 - */ - @Override - public void write(int theByte) - throws java.io.IOException { - // Encoding suspended? - if (suspendEncoding) { - this.out.write(theByte); - return; - } // end if: supsended - - // Encode? - if (encode) { - buffer[position++] = (byte) theByte; - if (position >= bufferLength) { // Enough to encode. - - this.out.write(encode3to4(b4, buffer, bufferLength, options)); - - lineLength += 4; - if (breakLines && lineLength >= MAX_LINE_LENGTH) { - this.out.write(NEW_LINE); - lineLength = 0; - } // end if: end of line - - position = 0; - } // end if: enough to output - } // end if: encoding - - // Else, Decoding - else { - // Meaningful Base64 character? - if (decodabet[theByte & 0x7f] > WHITE_SPACE_ENC) { - buffer[position++] = (byte) theByte; - if (position >= bufferLength) { // Enough to output. - - int len = Base64.decode4to3(buffer, 0, b4, 0, options); - out.write(b4, 0, len); - position = 0; - } // end if: enough to output - } // end if: meaningful base64 character - else if (decodabet[theByte & 0x7f] != WHITE_SPACE_ENC) { - throw new java.io.IOException("Invalid character in Base64 data."); - } // end else: not white space either - } // end else: decoding - } // end write - - /** - * Calls {@link #write(int)} repeatedly until len - * bytes are written. - * - * @param theBytes array from which to read bytes - * @param off offset for array - * @param len max number of bytes to read into array - * @since 1.3 - */ - @Override - public void write(byte[] theBytes, int off, int len) - throws java.io.IOException { - // Encoding suspended? - if (suspendEncoding) { - this.out.write(theBytes, off, len); - return; - } // end if: supsended - - for (int i = 0; i < len; i++) { - write(theBytes[off + i]); - } // end for: each byte written - - } // end write - } // end inner class OutputStream - - /** - * No options specified. Value is zero. - */ - public final static int NO_OPTIONS = 0; - - /** - * Specify encoding in first bit. Value is one. - */ - public final static int ENCODE = 1; - - /** - * Specify that data should be gzip-compressed in second bit. Value is two. - */ - public final static int GZIP = 2; - - /** - * Do break lines when encoding. Value is 8. - */ - public final static int DO_BREAK_LINES = 8; - - /** - * Encode using Base64-like encoding that is URL- and Filename-safe as described - * in Section 4 of RFC3548: - * http://www.faqs.org/rfcs/rfc3548.html. - * It is important to note that data encoded this way is not officially valid Base64, - * or at the very least should not be called Base64 without also specifying that is - * was encoded using the URL- and Filename-safe dialect. - */ - public final static int URL_SAFE = 16; - - - /* ******** P R I V A T E F I E L D S ******** */ - - /** - * Encode using the special "ordered" dialect of Base64 described here: - * http://www.faqs.org/qa/rfcc-1940.html. - */ - public final static int ORDERED = 32; - - /** - * Maximum line length (76) of Base64 output. - */ - private final static int MAX_LINE_LENGTH = 76; - - /** - * The equals sign (=) as a byte. - */ - private final static byte EQUALS_SIGN = (byte) '='; - - /** - * The new line character (\n) as a byte. - */ - private final static byte NEW_LINE = (byte) '\n'; - - /** - * Preferred encoding. - */ - private final static String PREFERRED_ENCODING = "US-ASCII"; - - - /* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */ - - private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding - - /** - * The 64 valid Base64 values. - */ - /* Host platform me be something funny like EBCDIC, so we hardcode these values. */ - private final static byte[] _STANDARD_ALPHABET = { - (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', - (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', - (byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', - (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', - (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', - (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', - (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', - (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z', - (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', - (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) '+', (byte) '/' - }; - - - /* ******** U R L S A F E B A S E 6 4 A L P H A B E T ******** */ - - /** - * Translates a Base64 value to either its 6-bit reconstruction value - * or a negative number indicating some other meaning. - **/ - private final static byte[] _STANDARD_DECODABET = { - -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 - -5, -5, // Whitespace: Tab and Linefeed - -9, -9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 - -9, -9, -9, -9, -9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 - 62, // Plus sign at decimal 43 - -9, -9, -9, // Decimal 44 - 46 - 63, // Slash at decimal 47 - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine - -9, -9, -9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9, -9, -9, // Decimal 62 - 64 - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' - 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' - -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 - 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' - -9, -9, -9, -9, -9 // Decimal 123 - 127 - , -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 128 - 139 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 140 - 152 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 153 - 165 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 166 - 178 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 179 - 191 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 192 - 204 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 205 - 217 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 218 - 230 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 231 - 243 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9 // Decimal 244 - 255 - }; - - /** - * Used in the URL- and Filename-safe dialect described in Section 4 of RFC3548: - * http://www.faqs.org/rfcs/rfc3548.html. - * Notice that the last two bytes become "hyphen" and "underscore" instead of "plus" and "slash." - */ - private final static byte[] _URL_SAFE_ALPHABET = { - (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', - (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', - (byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', - (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', - (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', - (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', - (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', - (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z', - (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', - (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) '-', (byte) '_' - }; - - - - /* ******** O R D E R E D B A S E 6 4 A L P H A B E T ******** */ - - /** - * Used in decoding URL- and Filename-safe dialects of Base64. - */ - private final static byte[] _URL_SAFE_DECODABET = { - -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 - -5, -5, // Whitespace: Tab and Linefeed - -9, -9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 - -9, -9, -9, -9, -9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 - -9, // Plus sign at decimal 43 - -9, // Decimal 44 - 62, // Minus sign at decimal 45 - -9, // Decimal 46 - -9, // Slash at decimal 47 - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine - -9, -9, -9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9, -9, -9, // Decimal 62 - 64 - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' - 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' - -9, -9, -9, -9, // Decimal 91 - 94 - 63, // Underscore at decimal 95 - -9, // Decimal 96 - 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' - -9, -9, -9, -9, -9 // Decimal 123 - 127 - , -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 128 - 139 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 140 - 152 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 153 - 165 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 166 - 178 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 179 - 191 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 192 - 204 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 205 - 217 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 218 - 230 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 231 - 243 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9 // Decimal 244 - 255 - }; - - /** - * I don't get the point of this technique, but someone requested it, - * and it is described here: - * http://www.faqs.org/qa/rfcc-1940.html. - */ - private final static byte[] _ORDERED_ALPHABET = { - (byte) '-', - (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', - (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', - (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', - (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', - (byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', - (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', - (byte) '_', - (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', - (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', - (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', - (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z' - }; - - - /* ******** D E T E R M I N E W H I C H A L H A B E T ******** */ - - /** - * Used in decoding the "ordered" dialect of Base64. - */ - private final static byte[] _ORDERED_DECODABET = { - -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 - -5, -5, // Whitespace: Tab and Linefeed - -9, -9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 - -9, -9, -9, -9, -9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 - -9, // Plus sign at decimal 43 - -9, // Decimal 44 - 0, // Minus sign at decimal 45 - -9, // Decimal 46 - -9, // Slash at decimal 47 - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // Numbers zero through nine - -9, -9, -9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9, -9, -9, // Decimal 62 - 64 - 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, // Letters 'A' through 'M' - 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, // Letters 'N' through 'Z' - -9, -9, -9, -9, // Decimal 91 - 94 - 37, // Underscore at decimal 95 - -9, // Decimal 96 - 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, // Letters 'a' through 'm' - 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, // Letters 'n' through 'z' - -9, -9, -9, -9, -9 // Decimal 123 - 127 - , -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 128 - 139 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 140 - 152 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 153 - 165 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 166 - 178 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 179 - 191 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 192 - 204 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 205 - 217 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 218 - 230 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 231 - 243 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9 // Decimal 244 - 255 - }; - - /** - * Encodes a byte array into Base64 notation. - * Does not GZip-compress data. - * - * @param source The data to convert - * @return The data in Base64-encoded form - * @throws IllegalArgumentException if source array is null - * @since 1.4 - */ - public static String encodeBytes(byte[] source) { - // Since we're not going to have the GZIP encoding turned on, - // we're not going to have an java.io.IOException thrown, so - // we should not force the user to have to catch it. - String encoded = null; - try { - encoded = encodeBytes(source, 0, source.length, NO_OPTIONS); - } catch (java.io.IOException ex) { - assert false : ex.getMessage(); - } // end catch - assert encoded != null; - return encoded; - } // end encodeBytes - - /** - * Encodes a byte array into Base64 notation. - *

    - * Example options:

    -     *   GZIP: gzip-compresses object before encoding it.
    -     *   DO_BREAK_LINES: break lines at 76 characters
    -     *     Note: Technically, this makes your encoding non-compliant.
    -     * 
    - *

    - * Example: encodeBytes( myData, Base64.GZIP ) or - *

    - * Example: encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES ) - * - * - *

    As of v 2.3, if there is an error with the GZIP stream, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned a null value, but - * in retrospect that's a pretty poor way to handle it.

    - * - * @param source The data to convert - * @param off Offset in array where conversion should begin - * @param len Length of data to convert - * @param options Specified options - * @return The Base64-encoded data as a String - * @throws java.io.IOException if there is an error - * @throws IllegalArgumentException if source array is null, if source array, offset, or length are invalid - * @see Base64#GZIP - * @see Base64#DO_BREAK_LINES - * @since 2.0 - */ - public static String encodeBytes(byte[] source, int off, int len, int options) throws java.io.IOException { - byte[] encoded = encodeBytesToBytes(source, off, len, options); - - // Return value according to relevant encoding. - try { - return new String(encoded, PREFERRED_ENCODING); - } // end try - catch (java.io.UnsupportedEncodingException uue) { - return new String(encoded); - } // end catch - - } // end encodeBytes - - - - - /* ******** E N C O D I N G M E T H O D S ******** */ - - /** - * Similar to {@link #encodeBytes(byte[], int, int, int)} but returns - * a byte array instead of instantiating a String. This is more efficient - * if you're working with I/O streams and have large data sets to encode. - * - * @param source The data to convert - * @param off Offset in array where conversion should begin - * @param len Length of data to convert - * @param options Specified options - * @return The Base64-encoded data as a String - * @throws java.io.IOException if there is an error - * @throws IllegalArgumentException if source array is null, if source array, offset, or length are invalid - * @see Base64#GZIP - * @see Base64#DO_BREAK_LINES - * @since 2.3.1 - */ - public static byte[] encodeBytesToBytes(byte[] source, int off, int len, int options) throws java.io.IOException { - - if (source == null) { - throw new IllegalArgumentException("Cannot serialize a null array."); - } // end if: null - - if (off < 0) { - throw new IllegalArgumentException("Cannot have negative offset: " + off); - } // end if: off < 0 - - if (len < 0) { - throw new IllegalArgumentException("Cannot have length offset: " + len); - } // end if: len < 0 - - if (off + len > source.length) { - throw new IllegalArgumentException( - String.format("Cannot have offset of %d and length of %d with array of length %d", off, len, - source.length)); - } // end if: off < 0 - - // Compress? - if ((options & GZIP) != 0) { - java.io.ByteArrayOutputStream baos = null; - java.util.zip.GZIPOutputStream gzos = null; - Base64.OutputStream b64os = null; - - try { - // GZip -> Base64 -> ByteArray - baos = new java.io.ByteArrayOutputStream(); - b64os = new Base64.OutputStream(baos, ENCODE | options); - gzos = new java.util.zip.GZIPOutputStream(b64os); - - gzos.write(source, off, len); - gzos.close(); - } // end try - catch (java.io.IOException e) { - // Catch it and then throw it immediately so that - // the finally{} block is called for cleanup. - throw e; - } // end catch - finally { - try { - if (gzos != null) { - gzos.close(); - } - } catch (Exception e) { - } - try { - if (b64os != null) { - b64os.close(); - } - } catch (Exception e) { - } - try { - if (baos != null) { - baos.close(); - } - } catch (Exception e) { - } - } // end finally - - return baos.toByteArray(); - } // end if: compress - - // Else, don't compress. Better not to use streams at all then. - else { - boolean breakLines = (options & DO_BREAK_LINES) != 0; - - //int len43 = len * 4 / 3; - //byte[] outBuff = new byte[ ( len43 ) // Main 4:3 - // + ( (len % 3) > 0 ? 4 : 0 ) // Account for padding - // + (breakLines ? ( len43 / MAX_LINE_LENGTH ) : 0) ]; // New lines - // Try to determine more precisely how big the array needs to be. - // If we get it right, we don't have to do an array copy, and - // we save a bunch of memory. - int encLen = (len / 3) * 4 + (len % 3 > 0 ? 4 : 0); // Bytes needed for actual encoding - if (breakLines) { - encLen += encLen / MAX_LINE_LENGTH; // Plus extra newline characters - } - byte[] outBuff = new byte[encLen]; - - int d = 0; - int e = 0; - int len2 = len - 2; - int lineLength = 0; - for (; d < len2; d += 3, e += 4) { - encode3to4(source, d + off, 3, outBuff, e, options); - - lineLength += 4; - if (breakLines && lineLength >= MAX_LINE_LENGTH) { - outBuff[e + 4] = NEW_LINE; - e++; - lineLength = 0; - } // end if: end of line - } // en dfor: each piece of array - - if (d < len) { - encode3to4(source, d + off, len - d, outBuff, e, options); - e += 4; - } // end if: some padding needed - - // Only resize array if we didn't guess it right. - if (e <= outBuff.length - 1) { - // If breaking lines and the last byte falls right at - // the line length (76 bytes per line), there will be - // one extra byte, and the array will need to be resized. - // Not too bad of an estimate on array size, I'd say. - byte[] finalOut = new byte[e]; - System.arraycopy(outBuff, 0, finalOut, 0, e); - //System.err.println("Having to resize array from " + outBuff.length + " to " + e ); - return finalOut; - } else { - //System.err.println("No need to resize array."); - return outBuff; - } - - } // end else: don't compress - - } // end encodeBytesToBytes - - - /** - * Defeats instantiation. - */ - private Base64() { - } - - /** - * Decodes four bytes from array source - * and writes the resulting bytes (up to three of them) - * to destination. - * The source and destination arrays can be manipulated - * anywhere along their length by specifying - * srcOffset and destOffset. - * This method does not check to make sure your arrays - * are large enough to accomodate srcOffset + 4 for - * the source array or destOffset + 3 for - * the destination array. - * This method returns the actual number of bytes that - * were converted from the Base64 encoding. - *

    This is the lowest level of the decoding methods with - * all possible parameters.

    - * - * @param source the array to convert - * @param srcOffset the index where conversion begins - * @param destination the array to hold the conversion - * @param destOffset the index where output will be put - * @param options alphabet type is pulled from this (standard, url-safe, ordered) - * @return the number of decoded bytes converted - * @throws IllegalArgumentException if source or destination arrays are null, if srcOffset or destOffset are - * invalid - * or there is not enough room in the array. - * @since 1.3 - */ - private static int decode4to3( - byte[] source, int srcOffset, - byte[] destination, int destOffset, int options) { - - // Lots of error checking and exception throwing - if (source == null) { - throw new IllegalArgumentException("Source array was null."); - } // end if - if (destination == null) { - throw new IllegalArgumentException("Destination array was null."); - } // end if - if (srcOffset < 0 || srcOffset + 3 >= source.length) { - throw new IllegalArgumentException(String.format( - "Source array with length %d cannot have offset of %d and still process four bytes.", - source.length, srcOffset)); - } // end if - if (destOffset < 0 || destOffset + 2 >= destination.length) { - throw new IllegalArgumentException(String.format( - "Destination array with length %d cannot have offset of %d and still store three bytes.", - destination.length, destOffset)); - } // end if - - byte[] DECODABET = getDecodabet(options); - - // Example: Dk== - if (source[srcOffset + 2] == EQUALS_SIGN) { - // Two ways to do the same thing. Don't know which way I like best. - //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) - // | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 ); - int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) - | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12); - - destination[destOffset] = (byte) (outBuff >>> 16); - return 1; - } - - // Example: DkL= - else if (source[srcOffset + 3] == EQUALS_SIGN) { - // Two ways to do the same thing. Don't know which way I like best. - //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) - // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) - // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ); - int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) - | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12) - | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6); - - destination[destOffset] = (byte) (outBuff >>> 16); - destination[destOffset + 1] = (byte) (outBuff >>> 8); - return 2; - } - - // Example: DkLE - else { - // Two ways to do the same thing. Don't know which way I like best. - //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) - // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) - // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ) - // | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 ); - int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) - | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12) - | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6) - | ((DECODABET[source[srcOffset + 3]] & 0xFF)); - - destination[destOffset] = (byte) (outBuff >> 16); - destination[destOffset + 1] = (byte) (outBuff >> 8); - destination[destOffset + 2] = (byte) (outBuff); - - return 3; - } - } // end decodeToBytes - - /** - *

    Encodes up to three bytes of the array source - * and writes the resulting four Base64 bytes to destination. - * The source and destination arrays can be manipulated - * anywhere along their length by specifying - * srcOffset and destOffset. - * This method does not check to make sure your arrays - * are large enough to accomodate srcOffset + 3 for - * the source array or destOffset + 4 for - * the destination array. - * The actual number of significant bytes in your array is - * given by numSigBytes.

    - *

    This is the lowest level of the encoding methods with - * all possible parameters.

    - * - * @param source the array to convert - * @param srcOffset the index where conversion begins - * @param numSigBytes the number of significant bytes in your array - * @param destination the array to hold the conversion - * @param destOffset the index where output will be put - * @return the destination array - * @since 1.3 - */ - private static byte[] encode3to4( - byte[] source, int srcOffset, int numSigBytes, - byte[] destination, int destOffset, int options) { - - byte[] ALPHABET = getAlphabet(options); - - // 1 2 3 - // 01234567890123456789012345678901 Bit position - // --------000000001111111122222222 Array position from threeBytes - // --------| || || || | Six bit groups to index ALPHABET - // >>18 >>12 >> 6 >> 0 Right shift necessary - // 0x3f 0x3f 0x3f Additional AND - - // Create buffer with zero-padding if there are only one or two - // significant bytes passed in the array. - // We have to shift left 24 in order to flush out the 1's that appear - // when Java treats a value as negative that is cast from a byte to an int. - int inBuff = (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) - | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) - | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); - - switch (numSigBytes) { - case 3: - destination[destOffset] = ALPHABET[(inBuff >>> 18)]; - destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; - destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f]; - destination[destOffset + 3] = ALPHABET[(inBuff) & 0x3f]; - return destination; - - case 2: - destination[destOffset] = ALPHABET[(inBuff >>> 18)]; - destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; - destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f]; - destination[destOffset + 3] = EQUALS_SIGN; - return destination; - - case 1: - destination[destOffset] = ALPHABET[(inBuff >>> 18)]; - destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; - destination[destOffset + 2] = EQUALS_SIGN; - destination[destOffset + 3] = EQUALS_SIGN; - return destination; - - default: - return destination; - } // end switch - } // end encode3to4 - - /** - * Encodes up to the first three bytes of array threeBytes - * and returns a four-byte array in Base64 notation. - * The actual number of significant bytes in your array is - * given by numSigBytes. - * The array threeBytes needs only be as big as - * numSigBytes. - * Code can reuse a byte array by passing a four-byte array as b4. - * - * @param b4 A reusable byte array to reduce array instantiation - * @param threeBytes the array to convert - * @param numSigBytes the number of significant bytes in your array - * @return four byte array in Base64 notation. - * @since 1.5.1 - */ - private static byte[] encode3to4(byte[] b4, byte[] threeBytes, int numSigBytes, int options) { - encode3to4(threeBytes, 0, numSigBytes, b4, 0, options); - return b4; - } // end encode3to4 - - - - - - /* ******** D E C O D I N G M E T H O D S ******** */ - - /** - * Returns one of the _SOMETHING_ALPHABET byte arrays depending on - * the options specified. - * It's possible, though silly, to specify ORDERED and URLSAFE - * in which case one of them will be picked, though there is - * no guarantee as to which one will be picked. - */ - private final static byte[] getAlphabet(int options) { - if ((options & URL_SAFE) == URL_SAFE) { - return _URL_SAFE_ALPHABET; - } else if ((options & ORDERED) == ORDERED) { - return _ORDERED_ALPHABET; - } else { - return _STANDARD_ALPHABET; - } - } // end getAlphabet - - /** - * Returns one of the _SOMETHING_DECODABET byte arrays depending on - * the options specified. - * It's possible, though silly, to specify ORDERED and URL_SAFE - * in which case one of them will be picked, though there is - * no guarantee as to which one will be picked. - */ - private final static byte[] getDecodabet(int options) { - if ((options & URL_SAFE) == URL_SAFE) { - return _URL_SAFE_DECODABET; - } else if ((options & ORDERED) == ORDERED) { - return _ORDERED_DECODABET; - } else { - return _STANDARD_DECODABET; - } - } // end getAlphabet -} // end class Base64 diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/util/ByteBufferUtils.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/util/ByteBufferUtils.java deleted file mode 100755 index 86dd69cc5..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/util/ByteBufferUtils.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.util; - -import java.nio.ByteBuffer; - -/** - * Utility class for ByteBuffers - */ -public class ByteBufferUtils { - - /** - * Get a ByteBuffer with zero capacity - * - * @return empty ByteBuffer - */ - public static ByteBuffer getEmptyByteBuffer() { - return ByteBuffer.allocate(0); - } - - /** - * Transfer from one ByteBuffer to another ByteBuffer - * - * @param source the ByteBuffer to copy from - * @param dest the ByteBuffer to copy to - * @return the number of transferred bytes - */ - public static int transferByteBuffer(ByteBuffer source, ByteBuffer dest) { - if (source == null || dest == null) { - throw new IllegalArgumentException(); - } - int fremain = source.remaining(); - int toremain = dest.remaining(); - if (fremain > toremain) { - int limit = Math.min(fremain, toremain); - source.limit(limit); - dest.put(source); - return limit; - } else { - dest.put(source); - return fremain; - } - } - - /** - * Private constructor for static class - */ - private ByteBufferUtils() { - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/util/Charsetfunctions.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/util/Charsetfunctions.java deleted file mode 100755 index f4d44d09c..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/java_websocket/util/Charsetfunctions.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright (c) 2010-2019 Nathan Rajlich - * - * 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. - */ - -package com.clevertap.android.sdk.java_websocket.util; - -import android.os.Build.VERSION_CODES; -import androidx.annotation.RequiresApi; -import com.clevertap.android.sdk.java_websocket.exceptions.InvalidDataException; -import com.clevertap.android.sdk.java_websocket.framing.CloseFrame; -import java.nio.ByteBuffer; -import java.nio.charset.CharacterCodingException; -import java.nio.charset.CharsetDecoder; -import java.nio.charset.CodingErrorAction; -import java.nio.charset.StandardCharsets; - -public class Charsetfunctions { - - private static final CodingErrorAction codingErrorAction = CodingErrorAction.REPORT; - - /** - * Implementation of the "Flexible and Economical UTF-8 Decoder" algorithm - * by Björn Höhrmann (http://bjoern.hoehrmann.de/utf-8/decoder/dfa/) - */ - private static final int[] utf8d = { - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 00..1f - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 20..3f - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 40..5f - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 60..7f - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, // 80..9f - 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, // a0..bf - 8, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // c0..df - 0xa, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x4, 0x3, 0x3, // e0..ef - 0xb, 0x6, 0x6, 0x6, 0x5, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, // f0..ff - 0x0, 0x1, 0x2, 0x3, 0x5, 0x8, 0x7, 0x1, 0x1, 0x1, 0x4, 0x6, 0x1, 0x1, 0x1, 0x1, // s0..s0 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, // s1..s2 - 1, 2, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, // s3..s4 - 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, // s5..s6 - 1, 3, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // s7..s8 - }; - - /* - * @return ASCII encoding in bytes - */ - @RequiresApi(api = VERSION_CODES.KITKAT) - public static byte[] asciiBytes(String s) { - return s.getBytes(StandardCharsets.US_ASCII); - } - - /** - * Check if the provided BytebBuffer contains a valid utf8 encoded string. - *

    - * Using the algorithm "Flexible and Economical UTF-8 Decoder" by Björn Höhrmann - * (http://bjoern.hoehrmann.de/utf-8/decoder/dfa/) - * - * @param data the ByteBuffer - * @param off offset (for performance reasons) - * @return does the ByteBuffer contain a valid utf8 encoded string - */ - public static boolean isValidUTF8(ByteBuffer data, int off) { - int len = data.remaining(); - if (len < off) { - return false; - } - int state = 0; - for (int i = off; i < len; ++i) { - state = utf8d[256 + (state << 4) + utf8d[(0xff & data.get(i))]]; - if (state == 1) { - return false; - } - } - return true; - } - - /** - * Calling isValidUTF8 with offset 0 - * - * @param data the ByteBuffer - * @return does the ByteBuffer contain a valid utf8 encoded string - */ - public static boolean isValidUTF8(ByteBuffer data) { - return isValidUTF8(data, 0); - } - - @RequiresApi(api = VERSION_CODES.KITKAT) - public static String stringAscii(byte[] bytes) { - return stringAscii(bytes, 0, bytes.length); - } - - @RequiresApi(api = VERSION_CODES.KITKAT) - public static String stringAscii(byte[] bytes, int offset, int length) { - return new String(bytes, offset, length, StandardCharsets.US_ASCII); - } - - @RequiresApi(api = VERSION_CODES.KITKAT) - public static String stringUtf8(byte[] bytes) throws InvalidDataException { - return stringUtf8(ByteBuffer.wrap(bytes)); - } - - @RequiresApi(api = VERSION_CODES.KITKAT) - public static String stringUtf8(ByteBuffer bytes) throws InvalidDataException { - CharsetDecoder decode = StandardCharsets.UTF_8.newDecoder(); - decode.onMalformedInput(codingErrorAction); - decode.onUnmappableCharacter(codingErrorAction); - String s; - try { - bytes.mark(); - s = decode.decode(bytes).toString(); - bytes.reset(); - } catch (CharacterCodingException e) { - throw new InvalidDataException(CloseFrame.NO_UTF8, e); - } - return s; - } - - /* - * @return UTF-8 encoding in bytes - */ - @RequiresApi(api = VERSION_CODES.KITKAT) - public static byte[] utf8Bytes(String s) { - return s.getBytes(StandardCharsets.UTF_8); - } - - /** - * Private constructor for real static class - */ - private Charsetfunctions() { - } - -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/login/ConfigurableIdentityRepo.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/login/ConfigurableIdentityRepo.java index 9c80303b7..ce41a8f2c 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/login/ConfigurableIdentityRepo.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/login/ConfigurableIdentityRepo.java @@ -1,25 +1,32 @@ package com.clevertap.android.sdk.login; -import static com.clevertap.android.sdk.LogConstants.LOG_TAG_ON_USER_LOGIN; +import static com.clevertap.android.sdk.login.LoginConstants.LOG_TAG_ON_USER_LOGIN; +import android.content.Context; import androidx.annotation.NonNull; -import com.clevertap.android.sdk.BaseCTApiListener; -import com.clevertap.android.sdk.ValidationResult; -import com.clevertap.android.sdk.ValidationResultFactory; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import com.clevertap.android.sdk.DeviceInfo; +import com.clevertap.android.sdk.validation.ValidationResult; +import com.clevertap.android.sdk.validation.ValidationResultFactory; +import com.clevertap.android.sdk.validation.ValidationResultStack; public class ConfigurableIdentityRepo implements IdentityRepo { private static final String TAG = "ConfigurableIdentityRepo"; - private final BaseCTApiListener ctApiListener; - private IdentitySet identitySet; private final LoginInfoProvider infoProvider; - public ConfigurableIdentityRepo(BaseCTApiListener ctApiListener) { - this.ctApiListener = ctApiListener; - this.infoProvider = new LoginInfoProvider(ctApiListener); + private final CleverTapInstanceConfig config; + + private final ValidationResultStack validationResultStack; + + public ConfigurableIdentityRepo(Context context, CleverTapInstanceConfig config, DeviceInfo deviceInfo, + ValidationResultStack mValidationResultStack) { + this.config = config; + this.infoProvider = new LoginInfoProvider(context, config, deviceInfo); + this.validationResultStack = mValidationResultStack; loadIdentitySet(); } @@ -31,7 +38,7 @@ public IdentitySet getIdentitySet() { @Override public boolean hasIdentity(@NonNull String Key) { boolean hasIdentity = identitySet.contains(Key); - ctApiListener.config().log(LOG_TAG_ON_USER_LOGIN, + config.log(LOG_TAG_ON_USER_LOGIN, TAG + "isIdentity [Key: " + Key + " , Value: " + hasIdentity + "]"); return hasIdentity; } @@ -44,7 +51,7 @@ void loadIdentitySet() { // Read from Pref IdentitySet prefKeySet = IdentitySet.from(infoProvider.getCachedIdentityKeysForAccount()); - ctApiListener.config().log(LOG_TAG_ON_USER_LOGIN, + config.log(LOG_TAG_ON_USER_LOGIN, TAG + "PrefIdentitySet [" + prefKeySet + "]"); /* ---------------------------------------------------------------- @@ -52,9 +59,9 @@ void loadIdentitySet() { * For Multi Instance - Get Identity Set configured via the setter * ---------------------------------------------------------------- */ IdentitySet configKeySet = IdentitySet - .from(ctApiListener.config().getIdentityKeys()); + .from(config.getIdentityKeys()); - ctApiListener.config().log(LOG_TAG_ON_USER_LOGIN, + config.log(LOG_TAG_ON_USER_LOGIN, TAG + "ConfigIdentitySet [" + configKeySet + "]"); /* --------------------------------------------------- @@ -70,15 +77,15 @@ void loadIdentitySet() { * --------------------------------------------------- */ if (prefKeySet.isValid()) { identitySet = prefKeySet; - ctApiListener.config().log(LOG_TAG_ON_USER_LOGIN, + config.log(LOG_TAG_ON_USER_LOGIN, TAG + "Identity Set activated from Pref[" + identitySet + "]"); } else if (configKeySet.isValid()) { identitySet = configKeySet; - ctApiListener.config().log(LOG_TAG_ON_USER_LOGIN, + config.log(LOG_TAG_ON_USER_LOGIN, TAG + "Identity Set activated from Config[" + identitySet + "]"); } else { identitySet = IdentitySet.getDefault(); - ctApiListener.config().log(LOG_TAG_ON_USER_LOGIN, + config.log(LOG_TAG_ON_USER_LOGIN, TAG + "Identity Set activated from Default[" + identitySet + "]"); } boolean isSavedInPref = prefKeySet.isValid(); @@ -88,7 +95,7 @@ void loadIdentitySet() { * ------------------------------------------------------------------------ */ String storedValue = identitySet.toString(); infoProvider.saveIdentityKeysForAccount(storedValue); - ctApiListener.config().log(LOG_TAG_ON_USER_LOGIN, + config.log(LOG_TAG_ON_USER_LOGIN, TAG + "Saving Identity Keys in Pref[" + storedValue + "]"); } } @@ -103,11 +110,11 @@ void loadIdentitySet() { private void handleError(final IdentitySet prefKeySet, final IdentitySet configKeySet) { if (prefKeySet.isValid() && configKeySet.isValid() && !prefKeySet.equals(configKeySet)) { ValidationResult error = ValidationResultFactory.create(531); - ctApiListener.remoteErrorLogger().pushValidationResult(error); - ctApiListener.config().log(LOG_TAG_ON_USER_LOGIN, + validationResultStack.pushValidationResult(error); + config.log(LOG_TAG_ON_USER_LOGIN, TAG + "pushing error due to mismatch [Pref:" + prefKeySet + "], [Config:" + configKeySet + "]"); } else { - ctApiListener.config().log(LOG_TAG_ON_USER_LOGIN, + config.log(LOG_TAG_ON_USER_LOGIN, TAG + "No error found while comparing [Pref:" + prefKeySet + "], [Config:" + configKeySet + "]"); } } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/login/IdentityRepoFactory.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/login/IdentityRepoFactory.java index 719b3f293..4703ca09c 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/login/IdentityRepoFactory.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/login/IdentityRepoFactory.java @@ -1,11 +1,13 @@ package com.clevertap.android.sdk.login; -import static com.clevertap.android.sdk.LogConstants.LOG_TAG_ON_USER_LOGIN; +import static com.clevertap.android.sdk.login.LoginConstants.LOG_TAG_ON_USER_LOGIN; -import androidx.annotation.NonNull; +import android.content.Context; import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; -import com.clevertap.android.sdk.BaseCTApiListener; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import com.clevertap.android.sdk.DeviceInfo; +import com.clevertap.android.sdk.validation.ValidationResultStack; /** * Provides Repo instance for an account @@ -16,23 +18,23 @@ public class IdentityRepoFactory { /** * Creates repo provider based on login state & config details. * - * @param ctApiListener - CleverTapAPI instance * @return - repo provider */ - public static IdentityRepo getRepo(@NonNull BaseCTApiListener ctApiListener) { - final LoginInfoProvider infoProvider = new LoginInfoProvider(ctApiListener); + public static IdentityRepo getRepo(Context context, CleverTapInstanceConfig config, DeviceInfo deviceInfo, + ValidationResultStack validationResultStack) { + final LoginInfoProvider infoProvider = new LoginInfoProvider(context, config, deviceInfo); final IdentityRepo repo; if (infoProvider.isLegacyProfileLoggedIn()) { repo = new LegacyIdentityRepo( - ctApiListener);// case 1: Migration (cached guid but no newly saved profile pref) + config);// case 1: Migration (cached guid but no newly saved profile pref) } else { /* ---------------------------------------------------- * case 2: Not logged in & using default config * case 3: Not logged in & using multi instance config * -----------------------------------------------------*/ - repo = new ConfigurableIdentityRepo(ctApiListener); + repo = new ConfigurableIdentityRepo(context, config, deviceInfo, validationResultStack); } - ctApiListener.config().log(LOG_TAG_ON_USER_LOGIN, + config.log(LOG_TAG_ON_USER_LOGIN, "Repo provider: " + repo.getClass().getSimpleName()); return repo; } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/login/IdentitySet.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/login/IdentitySet.java index 955f03ce8..41d57fcd7 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/login/IdentitySet.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/login/IdentitySet.java @@ -1,5 +1,6 @@ package com.clevertap.android.sdk.login; +import androidx.annotation.NonNull; import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; import com.clevertap.android.sdk.Constants; @@ -41,6 +42,11 @@ public boolean equals(final Object thatObj) { return identities.equals(that.identities); } + @Override + public int hashCode() { + return super.hashCode(); + } + /** * Stringifies the identity set in comma separated string value. * Set String @@ -48,6 +54,7 @@ public boolean equals(final Object thatObj) { * * @return String value of the identity set. */ + @NonNull @Override public String toString() { StringBuilder stringBuilder = new StringBuilder(); diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/login/LegacyIdentityRepo.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/login/LegacyIdentityRepo.java index 30769e6e4..b512442ca 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/login/LegacyIdentityRepo.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/login/LegacyIdentityRepo.java @@ -1,9 +1,9 @@ package com.clevertap.android.sdk.login; -import static com.clevertap.android.sdk.LogConstants.LOG_TAG_ON_USER_LOGIN; +import static com.clevertap.android.sdk.login.LoginConstants.LOG_TAG_ON_USER_LOGIN; import androidx.annotation.NonNull; -import com.clevertap.android.sdk.BaseCTApiListener; +import com.clevertap.android.sdk.CleverTapInstanceConfig; /** * Legacy class which handles old static identity logic. @@ -13,19 +13,13 @@ public class LegacyIdentityRepo implements IdentityRepo { private static final String TAG = "LegacyIdentityRepo"; - private final BaseCTApiListener mCTApiListener; - private IdentitySet identities; - public LegacyIdentityRepo(final BaseCTApiListener ctApiListener) { - this.mCTApiListener = ctApiListener; - loadIdentitySet(); - } + private final CleverTapInstanceConfig config; - private void loadIdentitySet() { - this.identities = IdentitySet.getDefault(); - mCTApiListener.config().log(LOG_TAG_ON_USER_LOGIN, - TAG + " Setting the default IdentitySet[" + identities + "]"); + public LegacyIdentityRepo(final CleverTapInstanceConfig config) { + this.config = config; + loadIdentitySet(); } @Override @@ -36,8 +30,14 @@ public IdentitySet getIdentitySet() { @Override public boolean hasIdentity(@NonNull final String Key) { boolean hasIdentity = identities.contains(Key); - mCTApiListener.config().log(LOG_TAG_ON_USER_LOGIN, + config.log(LOG_TAG_ON_USER_LOGIN, "isIdentity [Key: " + Key + " , Value: " + hasIdentity + "]"); return hasIdentity; } + + private void loadIdentitySet() { + this.identities = IdentitySet.getDefault(); + config.log(LOG_TAG_ON_USER_LOGIN, + TAG + " Setting the default IdentitySet[" + identities + "]"); + } } \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/login/LoginConstants.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/login/LoginConstants.java new file mode 100644 index 000000000..4dc669617 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/login/LoginConstants.java @@ -0,0 +1,7 @@ +package com.clevertap.android.sdk.login; + +public interface LoginConstants { + + String LOG_TAG_ON_USER_LOGIN = "ON_USER_LOGIN"; + +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/login/LoginController.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/login/LoginController.java new file mode 100644 index 000000000..e4bfd4fae --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/login/LoginController.java @@ -0,0 +1,312 @@ +package com.clevertap.android.sdk.login; + +import android.content.Context; +import com.clevertap.android.sdk.AnalyticsManager; +import com.clevertap.android.sdk.BaseCallbackManager; +import com.clevertap.android.sdk.CTLockManager; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.ControllerManager; +import com.clevertap.android.sdk.CoreMetaData; +import com.clevertap.android.sdk.DeviceInfo; +import com.clevertap.android.sdk.LocalDataStore; +import com.clevertap.android.sdk.Logger; +import com.clevertap.android.sdk.SessionManager; +import com.clevertap.android.sdk.db.BaseDatabaseManager; +import com.clevertap.android.sdk.db.DBManager; +import com.clevertap.android.sdk.events.BaseEventQueueManager; +import com.clevertap.android.sdk.events.EventGroup; +import com.clevertap.android.sdk.product_config.CTProductConfigController; +import com.clevertap.android.sdk.product_config.CTProductConfigFactory; +import com.clevertap.android.sdk.pushnotification.PushProviders; +import com.clevertap.android.sdk.task.CTExecutorFactory; +import com.clevertap.android.sdk.task.Task; +import com.clevertap.android.sdk.validation.ValidationResult; +import com.clevertap.android.sdk.validation.ValidationResultStack; +import java.util.Map; +import java.util.concurrent.Callable; + +public class LoginController { + + private String cachedGUID = null; + + private final AnalyticsManager analyticsManager; + + private final BaseEventQueueManager baseEventQueueManager; + + private final CTLockManager ctLockManager; + + private final BaseCallbackManager callbackManager; + + private final CleverTapInstanceConfig config; + + private final Context context; + + private final ControllerManager controllerManager; + + private final CoreMetaData coreMetaData; + + private final BaseDatabaseManager dbManager; + + private final DeviceInfo deviceInfo; + + private final LocalDataStore localDataStore; + + private final PushProviders pushProviders; + + private final SessionManager sessionManager; + + private final ValidationResultStack validationResultStack; + + private String processingUserLoginIdentifier = null; + + private static final Object processingUserLoginLock = new Object(); + + public LoginController(Context context, + CleverTapInstanceConfig config, + DeviceInfo deviceInfo, + ValidationResultStack validationResultStack, + BaseEventQueueManager eventQueueManager, + AnalyticsManager analyticsManager, + CoreMetaData coreMetaData, + ControllerManager controllerManager, + SessionManager sessionManager, + LocalDataStore localDataStore, + BaseCallbackManager callbackManager, + DBManager dbManager, + CTLockManager ctLockManager) { + this.config = config; + this.context = context; + this.deviceInfo = deviceInfo; + this.validationResultStack = validationResultStack; + baseEventQueueManager = eventQueueManager; + this.analyticsManager = analyticsManager; + this.coreMetaData = coreMetaData; + pushProviders = controllerManager.getPushProviders(); + this.sessionManager = sessionManager; + this.localDataStore = localDataStore; + this.callbackManager = callbackManager; + this.dbManager = dbManager; + this.controllerManager = controllerManager; + this.ctLockManager = ctLockManager; + } + + public void asyncProfileSwitchUser(final Map profile, final String cacheGuid, + final String cleverTapID) { + Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask(); + task.execute("resetProfile", new Callable() { + @Override + public Void call() { + try { + config.getLogger().verbose(config.getAccountId(), "asyncProfileSwitchUser:[profile " + profile + + " with Cached GUID " + ((cacheGuid != null) ? cachedGUID + : "NULL" + " and cleverTapID " + cleverTapID)); + //set optOut to false on the current user to unregister the device token + coreMetaData.setCurrentUserOptedOut(false); + // unregister the device token on the current user + pushProviders.forcePushDeviceToken(false); + + // try and flush and then reset the queues + baseEventQueueManager.flushQueueSync(context, EventGroup.REGULAR); + baseEventQueueManager.flushQueueSync(context, EventGroup.PUSH_NOTIFICATION_VIEWED); + dbManager.clearQueues(context); + + // clear out the old data + localDataStore.changeUser(); + CoreMetaData.setActivityCount(1); + sessionManager.destroySession(); + + // either force restore the cached GUID or generate a new one + if (cacheGuid != null) { + deviceInfo.forceUpdateDeviceId(cacheGuid); + callbackManager.notifyUserProfileInitialized(cacheGuid); + } else if (config.getEnableCustomCleverTapId()) { + deviceInfo.forceUpdateCustomCleverTapID(cleverTapID); + } else { + deviceInfo.forceNewDeviceID(); + } + callbackManager.notifyUserProfileInitialized(deviceInfo.getDeviceID()); + deviceInfo + .setCurrentUserOptOutStateFromStorage(); // be sure to call this after the guid is updated + analyticsManager.forcePushAppLaunchedEvent(); + if (profile != null) { + analyticsManager.pushProfile(profile); + } + pushProviders.forcePushDeviceToken(true); + synchronized (processingUserLoginLock) { + processingUserLoginIdentifier = null; + } + resetInbox(); + resetFeatureFlags(); + resetProductConfigs(); + recordDeviceIDErrors(); + resetDisplayUnits(); + controllerManager.getInAppFCManager().changeUser(deviceInfo.getDeviceID()); + } catch (Throwable t) { + config.getLogger().verbose(config.getAccountId(), "Reset Profile error", t); + } + return null; + } + }); + } + + @SuppressWarnings({"unused", "WeakerAccess"}) + public void onUserLogin(final Map profile, final String cleverTapID) { + if (config.getEnableCustomCleverTapId()) { + if (cleverTapID == null) { + Logger.i( + "CLEVERTAP_USE_CUSTOM_ID has been specified in the AndroidManifest.xml Please call onUserlogin() and pass a custom CleverTap ID"); + } + } else { + if (cleverTapID != null) { + Logger.i( + "CLEVERTAP_USE_CUSTOM_ID has not been specified in the AndroidManifest.xml Please call CleverTapAPI.defaultInstance() without a custom CleverTap ID"); + } + } + _onUserLogin(profile, cleverTapID); + } + + public void recordDeviceIDErrors() { + for (ValidationResult validationResult : deviceInfo.getValidationResults()) { + validationResultStack.pushValidationResult(validationResult); + } + } + + private void _onUserLogin(final Map profile, final String cleverTapID) { + if (profile == null) { + return; + } + + try { + final String currentGUID = deviceInfo.getDeviceID(); + if (currentGUID == null) { + return; + } + + boolean haveIdentifier = false; + LoginInfoProvider loginInfoProvider = new LoginInfoProvider(context, + config, deviceInfo); + // check for valid identifier keys + // use the first one we find + IdentityRepo iProfileHandler = IdentityRepoFactory + .getRepo(context, config, deviceInfo, + validationResultStack); + for (String key : profile.keySet()) { + Object value = profile.get(key); + boolean isProfileKey = iProfileHandler.hasIdentity(key); + if (isProfileKey) { + try { + String identifier = null; + if (value != null) { + identifier = value.toString(); + } + if (identifier != null && identifier.length() > 0) { + haveIdentifier = true; + cachedGUID = loginInfoProvider.getGUIDForIdentifier(key, identifier); + if (cachedGUID != null) { + break; + } + } + } catch (Throwable t) { + // no-op + } + } + } + + // if no valid identifier provided or there are no identified users on the device; just push on the current profile + if (!deviceInfo.isErrorDeviceId()) { + if (!haveIdentifier || loginInfoProvider.isAnonymousDevice()) { + config.getLogger().debug(config.getAccountId(), + "onUserLogin: no identifier provided or device is anonymous, pushing on current user profile"); + analyticsManager.pushProfile(profile); + return; + } + } + + // if identifier maps to current guid, push on current profile + if (cachedGUID != null && cachedGUID.equals(currentGUID)) { + config.getLogger().debug(config.getAccountId(), + "onUserLogin: " + profile.toString() + " maps to current device id " + currentGUID + + " pushing on current profile"); + analyticsManager.pushProfile(profile); + return; + } + + // stringify profile to use as dupe blocker + String profileToString = profile.toString(); + + // as processing happens async block concurrent onUserLogin requests with the same profile, as our cache is set async + if (isProcessUserLoginWithIdentifier(profileToString)) { + config.getLogger() + .debug(config.getAccountId(), "Already processing onUserLogin for " + profileToString); + return; + } + + // create new guid if necessary and reset + // block any concurrent onUserLogin call for the same profile + synchronized (processingUserLoginLock) { + processingUserLoginIdentifier = profileToString; + } + + config.getLogger() + .verbose(config.getAccountId(), "onUserLogin: queuing reset profile for " + profileToString + + " with Cached GUID " + ((cachedGUID != null) ? cachedGUID : "NULL")); + + asyncProfileSwitchUser(profile, cachedGUID, cleverTapID); + + } catch (Throwable t) { + config.getLogger().verbose(config.getAccountId(), "onUserLogin failed", t); + } + } + + private boolean isProcessUserLoginWithIdentifier(String identifier) { + synchronized (processingUserLoginLock) { + return processingUserLoginIdentifier != null && processingUserLoginIdentifier.equals(identifier); + } + } + + /** + * Resets the Display Units in the cache + */ + private void resetDisplayUnits() { + if (controllerManager.getCTDisplayUnitController() != null) { + controllerManager.getCTDisplayUnitController().reset(); + } else { + config.getLogger().verbose(config.getAccountId(), + Constants.FEATURE_DISPLAY_UNIT + "Can't reset Display Units, DisplayUnitcontroller is null"); + } + } + + private void resetFeatureFlags() { + if (controllerManager.getCTFeatureFlagsController() != null && controllerManager + .getCTFeatureFlagsController() + .isInitialized()) { + controllerManager.getCTFeatureFlagsController().resetWithGuid(deviceInfo.getDeviceID()); + controllerManager.getCTFeatureFlagsController().fetchFeatureFlags(); + } + } + + // always call async + private void resetInbox() { + synchronized (ctLockManager.getInboxControllerLock()) { + controllerManager.setCTInboxController(null); + } + controllerManager.initializeInbox(); + } +//Session + + private void resetProductConfigs() { + if (config.isAnalyticsOnly()) { + config.getLogger().debug(config.getAccountId(), "Product Config is not enabled for this instance"); + return; + } + if (controllerManager.getCTProductConfigController() != null) { + controllerManager.getCTProductConfigController().resetSettings(); + } + CTProductConfigController ctProductConfigController = + CTProductConfigFactory.getInstance(context, deviceInfo, config, analyticsManager, coreMetaData, + callbackManager); + controllerManager.setCTProductConfigController(ctProductConfigController); + config.getLogger().verbose(config.getAccountId(), "Product Config reset"); + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/login/LoginInfoProvider.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/login/LoginInfoProvider.java index b0faca734..7d0d595c3 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/login/LoginInfoProvider.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/login/LoginInfoProvider.java @@ -4,13 +4,11 @@ import android.text.TextUtils; import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; -import com.clevertap.android.sdk.BaseCTApiListener; -import com.clevertap.android.sdk.CTJsonConverter; import com.clevertap.android.sdk.CleverTapInstanceConfig; import com.clevertap.android.sdk.Constants; import com.clevertap.android.sdk.DeviceInfo; -import com.clevertap.android.sdk.LogConstants; import com.clevertap.android.sdk.StorageHelper; +import com.clevertap.android.sdk.utils.CTJsonConverter; import org.json.JSONObject; /** @@ -23,12 +21,12 @@ public class LoginInfoProvider { private final Context context; - private final DeviceInfo mDeviceInfo; + private final DeviceInfo deviceInfo; - public LoginInfoProvider(BaseCTApiListener ctApiListener) { - context = ctApiListener.context(); - config = ctApiListener.config(); - mDeviceInfo = ctApiListener.deviceInfo(); + public LoginInfoProvider(Context context, CleverTapInstanceConfig config, DeviceInfo deviceInfo) { + this.context = context; + this.config = config; + this.deviceInfo = deviceInfo; } //Profile @@ -60,7 +58,7 @@ public void cacheGUIDForIdentifier(String guid, String key, String identifier) { public boolean deviceIsMultiUser() { JSONObject cachedGUIDs = getCachedGUIDs(); boolean deviceIsMultiUser = cachedGUIDs.length() > 1; - config.log(LogConstants.LOG_TAG_ON_USER_LOGIN, + config.log(LoginConstants.LOG_TAG_ON_USER_LOGIN, "deviceIsMultiUser:[" + deviceIsMultiUser + "]"); return deviceIsMultiUser; } @@ -70,18 +68,38 @@ public boolean deviceIsMultiUser() { */ public JSONObject getCachedGUIDs() { String json = StorageHelper.getStringFromPrefs(context, config, Constants.CACHED_GUIDS_KEY, null); - config.log(LogConstants.LOG_TAG_ON_USER_LOGIN, + config.log(LoginConstants.LOG_TAG_ON_USER_LOGIN, "getCachedGUIDs:[" + json + "]"); return CTJsonConverter.toJsonObject(json, config.getLogger(), config.getAccountId()); } + /** + * Caches the pairs for this account + * + * @param cachedGUIDs - jsonObject of the Pairs + */ + public void setCachedGUIDs(JSONObject cachedGUIDs) { + if (cachedGUIDs == null) { + return; + } + try { + String cachedGuid = cachedGUIDs.toString(); + StorageHelper.putString(context, StorageHelper.storageKeyWithSuffix(config, Constants.CACHED_GUIDS_KEY), + cachedGuid); + config.log(LoginConstants.LOG_TAG_ON_USER_LOGIN, + "setCachedGUIDs:[" + cachedGuid + "]"); + } catch (Throwable t) { + config.getLogger().verbose(config.getAccountId(), "Error persisting guid cache: " + t.toString()); + } + } + /** * @return - Cached Identity Keys for the account */ public String getCachedIdentityKeysForAccount() { String cachedKeys = StorageHelper .getStringFromPrefs(context, config, Constants.SP_KEY_PROFILE_IDENTITIES, ""); - config.log(LogConstants.LOG_TAG_ON_USER_LOGIN, "getCachedIdentityKeysForAccount:" + cachedKeys); + config.log(LoginConstants.LOG_TAG_ON_USER_LOGIN, "getCachedIdentityKeysForAccount:" + cachedKeys); return cachedKeys; } @@ -101,7 +119,7 @@ public String getGUIDForIdentifier(String key, String identifier) { JSONObject cache = getCachedGUIDs(); try { String cachedGuid = cache.getString(cacheKey); - config.log(LogConstants.LOG_TAG_ON_USER_LOGIN, + config.log(LoginConstants.LOG_TAG_ON_USER_LOGIN, "getGUIDForIdentifier:[Key:" + key + ", value:" + cachedGuid + "]"); return cachedGuid; } catch (Throwable t) { @@ -113,31 +131,11 @@ public String getGUIDForIdentifier(String key, String identifier) { public boolean isAnonymousDevice() { JSONObject cachedGUIDs = getCachedGUIDs(); boolean isAnonymousDevice = cachedGUIDs.length() <= 0; - config.log(LogConstants.LOG_TAG_ON_USER_LOGIN, + config.log(LoginConstants.LOG_TAG_ON_USER_LOGIN, "isAnonymousDevice:[" + isAnonymousDevice + "]"); return isAnonymousDevice; } - /** - * Caches the pairs for this account - * - * @param cachedGUIDs - jsonObject of the Pairs - */ - public void setCachedGUIDs(JSONObject cachedGUIDs) { - if (cachedGUIDs == null) { - return; - } - try { - String cachedGuid = cachedGUIDs.toString(); - StorageHelper.putString(context, StorageHelper.storageKeyWithSuffix(config, Constants.CACHED_GUIDS_KEY), - cachedGuid); - config.log(LogConstants.LOG_TAG_ON_USER_LOGIN, - "setCachedGUIDs:[" + cachedGuid + "]"); - } catch (Throwable t) { - config.getLogger().verbose(config.getAccountId(), "Error persisting guid cache: " + t.toString()); - } - } - /** * Checks if any user was previously logged in using the legacy identity set{@link * Constants#LEGACY_IDENTITY_KEYS}for the @@ -147,7 +145,7 @@ public boolean isLegacyProfileLoggedIn() { JSONObject jsonObject = getCachedGUIDs(); boolean isLoggedIn = jsonObject != null && jsonObject.length() > 0 && TextUtils .isEmpty(getCachedIdentityKeysForAccount()); - config.log(LogConstants.LOG_TAG_ON_USER_LOGIN, "isLegacyProfileLoggedIn:" + isLoggedIn); + config.log(LoginConstants.LOG_TAG_ON_USER_LOGIN, "isLegacyProfileLoggedIn:" + isLoggedIn); return isLoggedIn; } @@ -159,12 +157,12 @@ public boolean isLegacyProfileLoggedIn() { public void saveIdentityKeysForAccount(final String valueCommaSeparated) { StorageHelper.putString(context, config, Constants.SP_KEY_PROFILE_IDENTITIES, valueCommaSeparated); - config.log(LogConstants.LOG_TAG_ON_USER_LOGIN, "saveIdentityKeysForAccount:" + valueCommaSeparated); + config.log(LoginConstants.LOG_TAG_ON_USER_LOGIN, "saveIdentityKeysForAccount:" + valueCommaSeparated); } private boolean isErrorDeviceId() { - boolean isErrorDeviceId = mDeviceInfo.isErrorDeviceId(); - config.log(LogConstants.LOG_TAG_ON_USER_LOGIN, + boolean isErrorDeviceId = deviceInfo.isErrorDeviceId(); + config.log(LoginConstants.LOG_TAG_ON_USER_LOGIN, "isErrorDeviceId:[" + isErrorDeviceId + "]"); return isErrorDeviceId; } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/network/BaseNetworkManager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/network/BaseNetworkManager.java new file mode 100644 index 000000000..2df032367 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/network/BaseNetworkManager.java @@ -0,0 +1,20 @@ +package com.clevertap.android.sdk.network; + +import android.content.Context; +import com.clevertap.android.sdk.events.EventGroup; +import org.json.JSONArray; + +public abstract class BaseNetworkManager { + + public abstract void flushDBQueue(final Context context, final EventGroup eventGroup); + + public abstract int getDelayFrequency(); + + public abstract void initHandshake(final EventGroup eventGroup, + final Runnable handshakeSuccessCallback); + + public abstract boolean needsHandshakeForDomain(final EventGroup eventGroup); + + abstract boolean sendQueue(final Context context, final EventGroup eventGroup, final JSONArray queue); + +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.java new file mode 100644 index 000000000..3cc1f972d --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.java @@ -0,0 +1,847 @@ +package com.clevertap.android.sdk.network; + +import static com.clevertap.android.sdk.utils.CTJsonConverter.getRenderedTargetList; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.SharedPreferences; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; +import com.clevertap.android.sdk.BaseCallbackManager; +import com.clevertap.android.sdk.CTLockManager; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.ControllerManager; +import com.clevertap.android.sdk.CoreMetaData; +import com.clevertap.android.sdk.DeviceInfo; +import com.clevertap.android.sdk.LocalDataStore; +import com.clevertap.android.sdk.Logger; +import com.clevertap.android.sdk.StorageHelper; +import com.clevertap.android.sdk.db.BaseDatabaseManager; +import com.clevertap.android.sdk.db.QueueCursor; +import com.clevertap.android.sdk.events.EventGroup; +import com.clevertap.android.sdk.login.IdentityRepoFactory; +import com.clevertap.android.sdk.response.ARPResponse; +import com.clevertap.android.sdk.response.BaseResponse; +import com.clevertap.android.sdk.response.CleverTapResponse; +import com.clevertap.android.sdk.response.CleverTapResponseHelper; +import com.clevertap.android.sdk.response.ConsoleResponse; +import com.clevertap.android.sdk.response.DisplayUnitResponse; +import com.clevertap.android.sdk.response.FeatureFlagResponse; +import com.clevertap.android.sdk.response.GeofenceResponse; +import com.clevertap.android.sdk.response.InAppResponse; +import com.clevertap.android.sdk.response.InboxResponse; +import com.clevertap.android.sdk.response.MetadataResponse; +import com.clevertap.android.sdk.response.ProductConfigResponse; +import com.clevertap.android.sdk.response.PushAmpResponse; +import com.clevertap.android.sdk.task.CTExecutorFactory; +import com.clevertap.android.sdk.task.Task; +import com.clevertap.android.sdk.validation.ValidationResultStack; +import com.clevertap.android.sdk.validation.Validator; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.security.SecureRandom; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Random; +import java.util.concurrent.Callable; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import org.json.JSONArray; +import org.json.JSONObject; + +@RestrictTo(Scope.LIBRARY) +public class NetworkManager extends BaseNetworkManager { + + private static SSLSocketFactory sslSocketFactory; + + private static SSLContext sslContext; + + private final BaseCallbackManager callbackManager; + + private CleverTapResponse cleverTapResponse; + + private final CleverTapInstanceConfig config; + + private final Context context; + + private final ControllerManager controllerManager; + + private final CoreMetaData coreMetaData; + + private int currentRequestTimestamp = 0; + + private final BaseDatabaseManager databaseManager; + + private final DeviceInfo deviceInfo; + + private final Logger logger; + + private int networkRetryCount = 0; + + private final ValidationResultStack validationResultStack; + + private int responseFailureCount = 0; + + public static boolean isNetworkOnline(Context context) { + + try { + ConnectivityManager cm = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + if (cm == null) { + // lets be optimistic, if we are truly offline we handle the exception + return true; + } + @SuppressLint("MissingPermission") NetworkInfo netInfo = cm.getActiveNetworkInfo(); + return netInfo != null && netInfo.isConnected(); + } catch (Throwable ignore) { + // lets be optimistic, if we are truly offline we handle the exception + return true; + } + } + + public NetworkManager( + Context context, + CleverTapInstanceConfig config, + DeviceInfo deviceInfo, + CoreMetaData coreMetaData, + ValidationResultStack validationResultStack, + ControllerManager controllerManager, + BaseDatabaseManager baseDatabaseManager, + final BaseCallbackManager callbackManager, + CTLockManager ctLockManager, + Validator validator, + LocalDataStore localDataStore) { + this.context = context; + this.config = config; + this.deviceInfo = deviceInfo; + this.callbackManager = callbackManager; + logger = this.config.getLogger(); + + this.coreMetaData = coreMetaData; + this.validationResultStack = validationResultStack; + this.controllerManager = controllerManager; + databaseManager = baseDatabaseManager; + // maintain order + CleverTapResponse cleverTapResponse = new CleverTapResponseHelper(); + + cleverTapResponse = new GeofenceResponse(cleverTapResponse, config, callbackManager); + cleverTapResponse = new ProductConfigResponse(cleverTapResponse, config, coreMetaData, controllerManager); + cleverTapResponse = new FeatureFlagResponse(cleverTapResponse, config, controllerManager); + cleverTapResponse = new DisplayUnitResponse(cleverTapResponse, config, + callbackManager, controllerManager); + cleverTapResponse = new PushAmpResponse(cleverTapResponse, context, config, + baseDatabaseManager, callbackManager, controllerManager); + cleverTapResponse = new InboxResponse(cleverTapResponse, config, ctLockManager, + callbackManager, controllerManager); + + cleverTapResponse = new ConsoleResponse(cleverTapResponse, config); + cleverTapResponse = new ARPResponse(cleverTapResponse, config, this, validator, controllerManager); + cleverTapResponse = new MetadataResponse(cleverTapResponse, config, deviceInfo, this); + cleverTapResponse = new InAppResponse(cleverTapResponse, config, controllerManager, false); + + cleverTapResponse = new BaseResponse(context, config, deviceInfo, this, localDataStore, cleverTapResponse); + + setCleverTapResponse(cleverTapResponse); + + } + + @Override + public void flushDBQueue(final Context context, final EventGroup eventGroup) { + config.getLogger() + .verbose(config.getAccountId(), "Somebody has invoked me to send the queue to CleverTap servers"); + + QueueCursor cursor; + QueueCursor previousCursor = null; + boolean loadMore = true; + + while (loadMore) { + + cursor = databaseManager.getQueuedEvents(context, 50, previousCursor, eventGroup); + + if (cursor == null || cursor.isEmpty()) { + config.getLogger().verbose(config.getAccountId(), "No events in the queue, failing"); + break; + } + + previousCursor = cursor; + JSONArray queue = cursor.getData(); + + if (queue == null || queue.length() <= 0) { + config.getLogger().verbose(config.getAccountId(), "No events in the queue, failing"); + break; + } + + loadMore = sendQueue(context, eventGroup, queue); + } + } + + //gives delay frequency based on region + //randomly adds delay to 1s delay in case of non-EU regions + @Override + public int getDelayFrequency() { + + int minDelayFrequency = 0; + + logger.debug(config.getAccountId(), "Network retry #" + networkRetryCount); + + //Retry with delay as 1s for first 10 retries + if (networkRetryCount < 10) { + logger.debug(config.getAccountId(), + "Failure count is " + networkRetryCount + ". Setting delay frequency to 1s"); + minDelayFrequency = Constants.PUSH_DELAY_MS; //reset minimum delay to 1s + return minDelayFrequency; + } + + if (config.getAccountRegion() == null) { + //Retry with delay as 1s if region is null in case of eu1 + logger.debug(config.getAccountId(), "Setting delay frequency to 1s"); + return Constants.PUSH_DELAY_MS; + } else { + //Retry with delay as minimum delay frequency and add random number of seconds to scatter traffic + SecureRandom randomGen = new SecureRandom(); + int randomDelay = (randomGen.nextInt(10) + 1) * 1000; + minDelayFrequency += randomDelay; + if (minDelayFrequency < Constants.MAX_DELAY_FREQUENCY) { + logger.debug(config.getAccountId(), "Setting delay frequency to " + minDelayFrequency); + return minDelayFrequency; + } else { + minDelayFrequency = Constants.PUSH_DELAY_MS; + } + logger.debug(config.getAccountId(), "Setting delay frequency to " + minDelayFrequency); + return minDelayFrequency; + } + } + + //New namespace for ARP Shared Prefs + public String getNewNamespaceARPKey() { + + final String accountId = config.getAccountId(); + if (accountId == null) { + return null; + } + + logger.verbose(config.getAccountId(), "New ARP Key = ARP:" + accountId + ":" + deviceInfo.getDeviceID()); + return "ARP:" + accountId + ":" + deviceInfo.getDeviceID(); + } + + public void incrementResponseFailureCount() { + responseFailureCount++; + } + + @Override + public void initHandshake(final EventGroup eventGroup, final Runnable handshakeSuccessCallback) { + responseFailureCount = 0; + setDomain(context, null); + performHandshakeForDomain(context, eventGroup, handshakeSuccessCallback); + } + + @Override + public boolean needsHandshakeForDomain(final EventGroup eventGroup) { + final String domain = getDomainFromPrefsOrMetadata(eventGroup); + return domain == null || responseFailureCount > 5; + } + + @SuppressLint("CommitPrefEdits") + public void setI(Context context, long i) { + final SharedPreferences prefs = StorageHelper.getPreferences(context, Constants.NAMESPACE_IJ); + final SharedPreferences.Editor editor = prefs.edit(); + editor.putLong(StorageHelper.storageKeyWithSuffix(config, Constants.KEY_I), i); + StorageHelper.persist(editor); + } + + @SuppressLint("CommitPrefEdits") + public void setJ(Context context, long j) { + final SharedPreferences prefs = StorageHelper.getPreferences(context, Constants.NAMESPACE_IJ); + final SharedPreferences.Editor editor = prefs.edit(); + editor.putLong(StorageHelper.storageKeyWithSuffix(config, Constants.KEY_J), j); + StorageHelper.persist(editor); + } + + HttpsURLConnection buildHttpsURLConnection(final String endpoint) + throws IOException { + URL url = new URL(endpoint); + HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); + conn.setConnectTimeout(10000); + conn.setReadTimeout(10000); + conn.setRequestProperty("Content-Type", "application/json; charset=utf-8"); + conn.setRequestProperty("X-CleverTap-Account-ID", config.getAccountId()); + conn.setRequestProperty("X-CleverTap-Token", config.getAccountToken()); + conn.setInstanceFollowRedirects(false); + if (config.isSslPinningEnabled()) { + SSLContext _sslContext = getSSLContext(); + if (_sslContext != null) { + conn.setSSLSocketFactory(getPinnedCertsSslSocketfactory(_sslContext)); + } + } + return conn; + } + + CleverTapResponse getCleverTapResponse() { + return cleverTapResponse; + } + + void setCleverTapResponse(final CleverTapResponse cleverTapResponse) { + this.cleverTapResponse = cleverTapResponse; + } + + int getCurrentRequestTimestamp() { + return currentRequestTimestamp; + } + + void setCurrentRequestTimestamp(final int currentRequestTimestamp) { + this.currentRequestTimestamp = currentRequestTimestamp; + } + + String getDomain(boolean defaultToHandshakeURL, final EventGroup eventGroup) { + String domain = getDomainFromPrefsOrMetadata(eventGroup); + + final boolean emptyDomain = domain == null || domain.trim().length() == 0; + if (emptyDomain && !defaultToHandshakeURL) { + return null; + } + + if (emptyDomain) { + domain = Constants.PRIMARY_DOMAIN + "/hello"; + } else { + domain += "/a1"; + } + + return domain; + } + + String getDomainFromPrefsOrMetadata(final EventGroup eventGroup) { + + try { + final String region = config.getAccountRegion(); + if (region != null && region.trim().length() > 0) { + // Always set this to 0 so that the handshake is not performed during a HTTP failure + setResponseFailureCount(0); + if (eventGroup.equals(EventGroup.PUSH_NOTIFICATION_VIEWED)) { + return region.trim().toLowerCase() + eventGroup.httpResource + "." + Constants.PRIMARY_DOMAIN; + } else { + return region.trim().toLowerCase() + "." + Constants.PRIMARY_DOMAIN; + } + } + } catch (Throwable t) { + // Ignore + } + if (eventGroup.equals(EventGroup.PUSH_NOTIFICATION_VIEWED)) { + return StorageHelper.getStringFromPrefs(context, config, Constants.SPIKY_KEY_DOMAIN_NAME, null); + } else { + return StorageHelper.getStringFromPrefs(context, config, Constants.KEY_DOMAIN_NAME, null); + } + + } + + String getEndpoint(final boolean defaultToHandshakeURL, final EventGroup eventGroup) { + String domain = getDomain(defaultToHandshakeURL, eventGroup); + if (domain == null) { + logger.verbose(config.getAccountId(), "Unable to configure endpoint, domain is null"); + return null; + } + + final String accountId = config.getAccountId(); + + if (accountId == null) { + logger.verbose(config.getAccountId(), "Unable to configure endpoint, accountID is null"); + return null; + } + + String endpoint = "https://" + domain + "?os=Android&t=" + deviceInfo.getSdkVersion(); + endpoint += "&z=" + accountId; + + final boolean needsHandshake = needsHandshakeForDomain(eventGroup); + // Don't attach ts if its handshake + if (needsHandshake) { + return endpoint; + } + + currentRequestTimestamp = (int) (System.currentTimeMillis() / 1000); + endpoint += "&ts=" + getCurrentRequestTimestamp(); + + return endpoint; + } + + int getFirstRequestTimestamp() { + return StorageHelper.getIntFromPrefs(context, config, Constants.KEY_FIRST_TS, 0); + } + + int getLastRequestTimestamp() { + return StorageHelper.getIntFromPrefs(context, config, Constants.KEY_LAST_TS, 0); + } + + void setLastRequestTimestamp(int ts) { + StorageHelper.putInt(context, StorageHelper.storageKeyWithSuffix(config, Constants.KEY_LAST_TS), ts); + } + + int getResponseFailureCount() { + return responseFailureCount; + } + + void setResponseFailureCount(final int responseFailureCount) { + this.responseFailureCount = responseFailureCount; + } + + //gives delay frequency based on region + //randomly adds delay to 1s delay in case of non-EU regions + + boolean hasDomainChanged(final String newDomain) { + final String oldDomain = StorageHelper.getStringFromPrefs(context, config, Constants.KEY_DOMAIN_NAME, null); + return !newDomain.equals(oldDomain); + } + + String insertHeader(Context context, JSONArray arr) { + try { + final JSONObject header = new JSONObject(); + + String deviceId = deviceInfo.getDeviceID(); + if (deviceId != null && !deviceId.equals("")) { + header.put("g", deviceId); + } else { + logger.verbose(config.getAccountId(), + "CRITICAL: Couldn't finalise on a device ID! Using error device ID instead!"); + } + + header.put("type", "meta"); + + JSONObject appFields = deviceInfo.getAppLaunchedFields(); + header.put("af", appFields); + + long i = getI(); + if (i > 0) { + header.put("_i", i); + } + + long j = getJ(); + if (j > 0) { + header.put("_j", j); + } + + String accountId = config.getAccountId(); + String token = config.getAccountToken(); + + if (accountId == null || token == null) { + logger + .debug(config.getAccountId(), + "Account ID/token not found, unable to configure queue request"); + return null; + } + + header.put("id", accountId); + header.put("tk", token); + header.put("l_ts", getLastRequestTimestamp()); + header.put("f_ts", getFirstRequestTimestamp()); + header.put("ct_pi", IdentityRepoFactory + .getRepo(this.context, config, deviceInfo, + validationResultStack).getIdentitySet().toString()); + header.put("ddnd", + !(deviceInfo.getNotificationsEnabledForUser() && (controllerManager.getPushProviders() + .isNotificationSupported()))); + if (coreMetaData.isBgPing()) { + header.put("bk", 1); + coreMetaData.setBgPing(false); + } + header.put("rtl", getRenderedTargetList(databaseManager.loadDBAdapter(this.context))); + if (!coreMetaData.isInstallReferrerDataSent()) { + header.put("rct", coreMetaData.getReferrerClickTime()); + header.put("ait", coreMetaData.getAppInstallTime()); + } + header.put("frs", coreMetaData.isFirstRequestInSession()); + coreMetaData.setFirstRequestInSession(false); + + // Attach ARP + try { + final JSONObject arp = getARP(); + if (arp != null && arp.length() > 0) { + header.put("arp", arp); + } + } catch (Throwable t) { + logger.verbose(config.getAccountId(), "Failed to attach ARP", t); + } + + JSONObject ref = new JSONObject(); + try { + + String utmSource = coreMetaData.getSource(); + if (utmSource != null) { + ref.put("us", utmSource); + } + + String utmMedium = coreMetaData.getMedium(); + if (utmMedium != null) { + ref.put("um", utmMedium); + } + + String utmCampaign = coreMetaData.getCampaign(); + if (utmCampaign != null) { + ref.put("uc", utmCampaign); + } + + if (ref.length() > 0) { + header.put("ref", ref); + } + + } catch (Throwable t) { + logger.verbose(config.getAccountId(), "Failed to attach ref", t); + } + + JSONObject wzrkParams = coreMetaData.getWzrkParams(); + if (wzrkParams != null && wzrkParams.length() > 0) { + header.put("wzrk_ref", wzrkParams); + } + + if (controllerManager.getInAppFCManager() != null) { + Logger.v("Attaching InAppFC to Header"); + controllerManager.getInAppFCManager().attachToHeader(context, header); + } + + // Resort to string concat for backward compatibility + return "[" + header.toString() + ", " + arr.toString().substring(1); + } catch (Throwable t) { + logger.verbose(config.getAccountId(), "CommsManager: Failed to attach header", t); + return arr.toString(); + } + } + + void performHandshakeForDomain(final Context context, final EventGroup eventGroup, + final Runnable handshakeSuccessCallback) { + final String endpoint = getEndpoint(true, eventGroup); + if (endpoint == null) { + logger.verbose(config.getAccountId(), "Unable to perform handshake, endpoint is null"); + } + logger.verbose(config.getAccountId(), "Performing handshake with " + endpoint); + + HttpsURLConnection conn = null; + try { + conn = buildHttpsURLConnection(endpoint); + final int responseCode = conn.getResponseCode(); + if (responseCode != 200) { + logger + .verbose(config.getAccountId(), + "Invalid HTTP status code received for handshake - " + responseCode); + return; + } + + logger.verbose(config.getAccountId(), "Received success from handshake :)"); + + if (processIncomingHeaders(context, conn)) { + logger.verbose(config.getAccountId(), "We are not muted"); + // We have a new domain, run the callback + handshakeSuccessCallback.run(); + } + } catch (Throwable t) { + logger.verbose(config.getAccountId(), "Failed to perform handshake!", t); + } finally { + if (conn != null) { + try { + conn.getInputStream().close(); + conn.disconnect(); + } catch (Throwable t) { + // Ignore + } + } + } + } + + /** + * Processes the incoming response headers for a change in domain and/or mute. + * + * @return True to continue sending requests, false otherwise. + */ + boolean processIncomingHeaders(final Context context, final HttpsURLConnection conn) { + final String muteCommand = conn.getHeaderField(Constants.HEADER_MUTE); + if (muteCommand != null && muteCommand.trim().length() > 0) { + if (muteCommand.equals("true")) { + setMuted(context, true); + return false; + } else { + setMuted(context, false); + } + } + + final String domainName = conn.getHeaderField(Constants.HEADER_DOMAIN_NAME); + Logger.v("Getting domain from header - " + domainName); + if (domainName == null || domainName.trim().length() == 0) { + return true; + } + + final String spikyDomainName = conn.getHeaderField(Constants.SPIKY_HEADER_DOMAIN_NAME); + Logger.v("Getting spiky domain from header - " + spikyDomainName); + + setMuted(context, false); + setDomain(context, domainName); + Logger.v("Setting spiky domain from header as -" + spikyDomainName); + if (spikyDomainName == null) { + setSpikyDomain(context, domainName); + } else { + setSpikyDomain(context, spikyDomainName); + } + return true; + } + + /** + * @return true if the network request succeeded. Anything non 200 results in a false. + */ + @Override + boolean sendQueue(final Context context, final EventGroup eventGroup, final JSONArray queue) { + if (queue == null || queue.length() <= 0) { + return false; + } + + if (deviceInfo.getDeviceID() == null) { + logger.debug(config.getAccountId(), "CleverTap Id not finalized, unable to send queue"); + return false; + } + + HttpsURLConnection conn = null; + try { + final String endpoint = getEndpoint(false, eventGroup); + + // This is just a safety check, which would only arise + // if upstream didn't adhere to the protocol (sent nothing during the initial handshake) + if (endpoint == null) { + logger.debug(config.getAccountId(), "Problem configuring queue endpoint, unable to send queue"); + return false; + } + + conn = buildHttpsURLConnection(endpoint); + + final String body; + final String req = insertHeader(context, queue); + if (req == null) { + logger.debug(config.getAccountId(), "Problem configuring queue request, unable to send queue"); + return false; + } + + logger.debug(config.getAccountId(), "Send queue contains " + queue.length() + " items: " + req); + logger.debug(config.getAccountId(), "Sending queue to: " + endpoint); + conn.setDoOutput(true); + // noinspection all + conn.getOutputStream().write(req.getBytes("UTF-8")); + + final int responseCode = conn.getResponseCode(); + + // Always check for a 200 OK + if (responseCode != 200) { + throw new IOException("Response code is not 200. It is " + responseCode); + } + + // Check for a change in domain + final String newDomain = conn.getHeaderField(Constants.HEADER_DOMAIN_NAME); + if (newDomain != null && newDomain.trim().length() > 0) { + if (hasDomainChanged(newDomain)) { + // The domain has changed. Return a status of -1 so that the caller retries + setDomain(context, newDomain); + logger.debug(config.getAccountId(), + "The domain has changed to " + newDomain + ". The request will be retried shortly."); + return false; + } + } + + if (processIncomingHeaders(context, conn)) { + // noinspection all + BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8")); + + StringBuilder sb = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + sb.append(line); + } + body = sb.toString(); + getCleverTapResponse().processResponse(null, body, this.context); + } + + setLastRequestTimestamp(getCurrentRequestTimestamp()); + setFirstRequestTimestampIfNeeded(getCurrentRequestTimestamp()); + + logger.debug(config.getAccountId(), "Queue sent successfully"); + + responseFailureCount = 0; + networkRetryCount = 0; //reset retry count when queue is sent successfully + return true; + } catch (Throwable e) { + logger.debug(config.getAccountId(), + "An exception occurred while sending the queue, will retry: ", e); + responseFailureCount++; + networkRetryCount++; + callbackManager.getFailureFlushListener().failureFlush(context); + return false; + } finally { + if (conn != null) { + try { + conn.getInputStream().close(); + conn.disconnect(); + } catch (Throwable t) { + // Ignore + } + } + } + } + + void setDomain(final Context context, String domainName) { + logger.verbose(config.getAccountId(), "Setting domain to " + domainName); + StorageHelper.putString(context, StorageHelper.storageKeyWithSuffix(config, Constants.KEY_DOMAIN_NAME), + domainName); + } + + void setFirstRequestTimestampIfNeeded(int ts) { + if (getFirstRequestTimestamp() > 0) { + return; + } + StorageHelper.putInt(context, StorageHelper.storageKeyWithSuffix(config, Constants.KEY_FIRST_TS), ts); + } + + void setSpikyDomain(final Context context, String spikyDomainName) { + logger.verbose(config.getAccountId(), "Setting spiky domain to " + spikyDomainName); + StorageHelper.putString(context, StorageHelper.storageKeyWithSuffix(config, Constants.SPIKY_KEY_DOMAIN_NAME), + spikyDomainName); + } + + /** + * The ARP is additional request parameters, which must be sent once + * received after any HTTP call. This is sort of a proxy for cookies. + * + * @return A JSON object containing the ARP key/values. Can be null. + */ + private JSONObject getARP() { + try { + final String nameSpaceKey = getNewNamespaceARPKey(); + if (nameSpaceKey == null) { + return null; + } + + SharedPreferences prefs; + + //checking whether new namespace is empty or not + //if not empty, using prefs of new namespace to send ARP + //if empty, checking for old prefs + if (!StorageHelper.getPreferences(context, nameSpaceKey).getAll().isEmpty()) { + //prefs point to new namespace + prefs = StorageHelper.getPreferences(context, nameSpaceKey); + } else { + //prefs point to new namespace migrated from old namespace + prefs = migrateARPToNewNameSpace(nameSpaceKey, getNamespaceARPKey()); + } + + final Map all = prefs.getAll(); + final Iterator> iter = all.entrySet().iterator(); + + while (iter.hasNext()) { + final Map.Entry kv = iter.next(); + final Object o = kv.getValue(); + if (o instanceof Number && ((Number) o).intValue() == -1) { + iter.remove(); + } + } + final JSONObject ret = new JSONObject(all); + logger.verbose(config.getAccountId(), + "Fetched ARP for namespace key: " + nameSpaceKey + " values: " + all.toString()); + return ret; + } catch (Throwable t) { + logger.verbose(config.getAccountId(), "Failed to construct ARP object", t); + return null; + } + } + + private long getI() { + return StorageHelper.getLongFromPrefs(context, config, Constants.KEY_I, 0, Constants.NAMESPACE_IJ); + } + + private long getJ() { + return StorageHelper.getLongFromPrefs(context, config, Constants.KEY_J, 0, Constants.NAMESPACE_IJ); + } + + //Session + //Old namespace for ARP Shared Prefs + private String getNamespaceARPKey() { + + final String accountId = config.getAccountId(); + if (accountId == null) { + return null; + } + + logger.verbose(config.getAccountId(), "Old ARP Key = ARP:" + accountId); + return "ARP:" + accountId; + } + + private SharedPreferences migrateARPToNewNameSpace(String newKey, String oldKey) { + SharedPreferences oldPrefs = StorageHelper.getPreferences(context, oldKey); + SharedPreferences newPrefs = StorageHelper.getPreferences(context, newKey); + SharedPreferences.Editor editor = newPrefs.edit(); + Map all = oldPrefs.getAll(); + + for (Map.Entry kv : all.entrySet()) { + final Object o = kv.getValue(); + if (o instanceof Number) { + final int update = ((Number) o).intValue(); + editor.putInt(kv.getKey(), update); + } else if (o instanceof String) { + if (((String) o).length() < 100) { + editor.putString(kv.getKey(), (String) o); + } else { + logger.verbose(config.getAccountId(), + "ARP update for key " + kv.getKey() + " rejected (string value too long)"); + } + } else if (o instanceof Boolean) { + editor.putBoolean(kv.getKey(), (Boolean) o); + } else { + logger.verbose(config.getAccountId(), + "ARP update for key " + kv.getKey() + " rejected (invalid data type)"); + } + } + logger.verbose(config.getAccountId(), "Completed ARP update for namespace key: " + newKey + ""); + StorageHelper.persist(editor); + oldPrefs.edit().clear().apply(); + return newPrefs; + } + + private void setMuted(final Context context, boolean mute) { + if (mute) { + final int now = (int) (System.currentTimeMillis() / 1000); + StorageHelper.putInt(context, StorageHelper.storageKeyWithSuffix(config, Constants.KEY_MUTED), now); + setDomain(context, null); + + // Clear all the queues + Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask(); + task.execute("CommsManager#setMuted", new Callable() { + @Override + public Void call() { + databaseManager.clearQueues(context); + return null; + } + }); + } else { + StorageHelper.putInt(context, StorageHelper.storageKeyWithSuffix(config, Constants.KEY_MUTED), 0); + } + } + + private static SSLSocketFactory getPinnedCertsSslSocketfactory(SSLContext sslContext) { + if (sslContext == null) { + return null; + } + + if (sslSocketFactory == null) { + try { + sslSocketFactory = sslContext.getSocketFactory(); + Logger.d("Pinning SSL session to DigiCertGlobalRoot CA certificate"); + } catch (Throwable e) { + Logger.d("Issue in pinning SSL,", e); + } + } + return sslSocketFactory; + } + + private static synchronized SSLContext getSSLContext() { + if (sslContext == null) { + sslContext = new SSLContextBuilder().build(); + } + return sslContext; + } +} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/SSLContextBuilder.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/network/SSLContextBuilder.java similarity index 96% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/SSLContextBuilder.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/network/SSLContextBuilder.java index 796977f5f..3e4fa8a93 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/SSLContextBuilder.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/network/SSLContextBuilder.java @@ -1,6 +1,7 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.network; +import com.clevertap.android.sdk.Logger; import java.io.BufferedInputStream; import java.io.InputStream; import java.security.KeyStore; diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/product_config/CTProductConfigController.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/product_config/CTProductConfigController.java index ea57f4c21..392b51467 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/product_config/CTProductConfigController.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/product_config/CTProductConfigController.java @@ -9,19 +9,26 @@ import android.content.Context; import android.text.TextUtils; +import androidx.annotation.NonNull; +import com.clevertap.android.sdk.BaseAnalyticsManager; +import com.clevertap.android.sdk.BaseCallbackManager; import com.clevertap.android.sdk.CleverTapInstanceConfig; import com.clevertap.android.sdk.Constants; -import com.clevertap.android.sdk.FileUtils; -import com.clevertap.android.sdk.TaskManager; -import com.clevertap.android.sdk.Utils; +import com.clevertap.android.sdk.CoreMetaData; +import com.clevertap.android.sdk.task.CTExecutorFactory; +import com.clevertap.android.sdk.task.OnSuccessListener; +import com.clevertap.android.sdk.task.Task; +import com.clevertap.android.sdk.utils.FileUtils; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.json.JSONArray; +import org.json.JSONException; import org.json.JSONObject; public class CTProductConfigController { @@ -32,37 +39,45 @@ private enum PROCESSING_STATE { ACTIVATED } - private final Map activatedConfigs = Collections.synchronizedMap(new HashMap()); + //use lock for synchronization for read write + final Map activatedConfigs = Collections.synchronizedMap(new HashMap()); - private final CleverTapInstanceConfig config; + //use lock for synchronization for read write + final Map defaultConfigs = Collections.synchronizedMap(new HashMap()); - private final Context context; + AtomicBoolean isInitialized = new AtomicBoolean(false); - private final Map defaultConfigs = Collections.synchronizedMap(new HashMap()); + final FileUtils fileUtils; - private String guid; + private final CleverTapInstanceConfig config; + + private final Context context; private final AtomicBoolean isFetchAndActivating = new AtomicBoolean(false); - private boolean isInitialized = false; + private final BaseAnalyticsManager analyticsManager; + + private final BaseCallbackManager callbackManager; - private final CTProductConfigControllerListener listener; + private final CoreMetaData coreMetaData; private final ProductConfigSettings settings; - private final Map waitingTobeActivatedConfig = Collections.synchronizedMap(new HashMap()); + //use lock for synchronization for read write + private final Map waitingTobeActivatedConfig = Collections + .synchronizedMap(new HashMap()); - // -----------------------------------------------------------------------// - // ******** Public API *****// - // -----------------------------------------------------------------------// - - public CTProductConfigController(Context context, String guid, CleverTapInstanceConfig config, - CTProductConfigControllerListener listener) { + CTProductConfigController(Context context, CleverTapInstanceConfig config, + final BaseAnalyticsManager analyticsManager, final CoreMetaData coreMetaData, + final BaseCallbackManager callbackManager, ProductConfigSettings productConfigSettings, + FileUtils fileUtils) { this.context = context; - this.guid = guid; this.config = config; - this.listener = listener; - this.settings = new ProductConfigSettings(context, guid, config); + this.coreMetaData = coreMetaData; + this.callbackManager = callbackManager; + this.analyticsManager = analyticsManager; + settings = productConfigSettings; + this.fileUtils = fileUtils; initAsync(); } @@ -71,15 +86,22 @@ public CTProductConfigController(Context context, String guid, CleverTapInstance */ @SuppressWarnings("WeakerAccess") public void activate() { - if (TextUtils.isEmpty(guid)) { + if (TextUtils.isEmpty(settings.getGuid())) { return; } - TaskManager.getInstance().execute(new TaskManager.TaskListener() { + Task task = CTExecutorFactory.executors(config).ioTask(); + task.addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(final Void result) { + sendCallback(PROCESSING_STATE.ACTIVATED); + } + }).execute("activateProductConfigs", new Callable() { @Override - public Void doInBackground(Void params) { + public Void call() { synchronized (this) { try { //read fetched info + HashMap toWriteValues = new HashMap<>(); if (!waitingTobeActivatedConfig.isEmpty()) { toWriteValues.putAll(waitingTobeActivatedConfig); @@ -104,12 +126,8 @@ public Void doInBackground(Void params) { return null; } } - - @Override - public void onPostExecute(Void isSuccess) { - sendCallback(PROCESSING_STATE.ACTIVATED); - } }); + } /** @@ -127,7 +145,7 @@ public void fetch() { @SuppressWarnings("WeakerAccess") public void fetch(long minimumFetchIntervalInSeconds) { if (canRequest(minimumFetchIntervalInSeconds)) { - listener.fetchProductConfig(); + fetchProductConfig(); } } @@ -139,6 +157,27 @@ public void fetchAndActivate() { isFetchAndActivating.set(true); } + /** + * This method is internal to CleverTap SDK. + * Developers should not use this method manually. + */ + + public void fetchProductConfig() { + JSONObject event = new JSONObject(); + JSONObject notif = new JSONObject(); + try { + notif.put("t", Constants.FETCH_TYPE_PC); + event.put("evtName", Constants.WZRK_FETCH); + event.put("evtData", notif); + } catch (JSONException e) { + e.printStackTrace(); + } + analyticsManager.sendFetchEvent(event); + coreMetaData.setProductConfigRequested(true); + config.getLogger() + .verbose(config.getAccountId(), Constants.LOG_TAG_PRODUCT_CONFIG + "Fetching product config"); + } + /** * Returns the parameter value for the given key as a boolean. * @@ -147,8 +186,9 @@ public void fetchAndActivate() { * CTProductConfigConstants#DEFAULT_VALUE_FOR_BOOLEAN} */ public Boolean getBoolean(String Key) { - if (isInitialized && !TextUtils.isEmpty(Key)) { - String value = activatedConfigs.get(Key); + if (isInitialized.get() && !TextUtils.isEmpty(Key)) { + String value; + value = activatedConfigs.get(Key); if (!TextUtils.isEmpty(value)) { return Boolean.parseBoolean(value); } @@ -164,9 +204,10 @@ public Boolean getBoolean(String Key) { * CTProductConfigConstants#DEFAULT_VALUE_FOR_DOUBLE} */ public Double getDouble(String Key) { - if (isInitialized && !TextUtils.isEmpty(Key)) { + if (isInitialized.get() && !TextUtils.isEmpty(Key)) { try { - String value = activatedConfigs.get(Key); + String value; + value = activatedConfigs.get(Key); if (!TextUtils.isEmpty(value)) { return Double.parseDouble(value); } @@ -196,9 +237,10 @@ public long getLastFetchTimeStampInMillis() { * CTProductConfigConstants#DEFAULT_VALUE_FOR_LONG} */ public Long getLong(String Key) { - if (isInitialized && !TextUtils.isEmpty(Key)) { + if (isInitialized.get() && !TextUtils.isEmpty(Key)) { try { - String value = activatedConfigs.get(Key); + String value; + value = activatedConfigs.get(Key); if (!TextUtils.isEmpty(value)) { return Long.parseLong(value); } @@ -211,6 +253,10 @@ public Long getLong(String Key) { return DEFAULT_VALUE_FOR_LONG; } + // -----------------------------------------------------------------------// + // ******** Public API *****// + // -----------------------------------------------------------------------// + /** * Returns the parameter value for the given key as a String. * @@ -219,8 +265,9 @@ public Long getLong(String Key) { * CTProductConfigConstants#DEFAULT_VALUE_FOR_STRING} */ public String getString(String Key) { - if (isInitialized && !TextUtils.isEmpty(Key)) { - String value = activatedConfigs.get(Key); + if (isInitialized.get() && !TextUtils.isEmpty(Key)) { + String value; + value = activatedConfigs.get(Key); if (!TextUtils.isEmpty(value)) { return value; } @@ -229,7 +276,7 @@ public String getString(String Key) { } public boolean isInitialized() { - return isInitialized; + return isInitialized.get(); } /** @@ -237,9 +284,7 @@ public boolean isInitialized() { * Developers should not use this method manually. */ public void onFetchFailed() { - if (isFetchAndActivating.get()) { - isFetchAndActivating.set(false); - } + isFetchAndActivating.compareAndSet(true, false); config.getLogger().verbose(ProductConfigUtil.getLogTag(config), "Fetch Failed"); } @@ -248,37 +293,38 @@ public void onFetchFailed() { * Developers should not use this method manually. */ public void onFetchSuccess(JSONObject kvResponse) { - if (TextUtils.isEmpty(guid)) { + if (TextUtils.isEmpty(settings.getGuid())) { return; } synchronized (this) { if (kvResponse != null) { try { parseFetchedResponse(kvResponse); - FileUtils.writeJsonToFile(context, config, getProductConfigDirName(), - CTProductConfigConstants.FILE_NAME_ACTIVATED, new JSONObject(waitingTobeActivatedConfig)); + fileUtils.writeJsonToFile(getProductConfigDirName(), + CTProductConfigConstants.FILE_NAME_ACTIVATED, + new JSONObject(waitingTobeActivatedConfig)); config.getLogger() .verbose(ProductConfigUtil.getLogTag(config), "Fetch file-[" + getActivatedFullPath() + "] write success: " + waitingTobeActivatedConfig); - Utils.runOnUiThread(new Runnable() { + Task task = CTExecutorFactory.executors(config).mainTask(); + task.execute("sendPCFetchSuccessCallback", new Callable() { @Override - public void run() { + public Void call() { config.getLogger() .verbose(ProductConfigUtil.getLogTag(config), "Product Config: fetch Success"); sendCallback(PROCESSING_STATE.FETCHED); + return null; } }); - if (isFetchAndActivating.get()) { - isFetchAndActivating.set(false); + if (isFetchAndActivating.getAndSet(false)) { activate(); } } catch (Exception e) { e.printStackTrace(); config.getLogger().verbose(ProductConfigUtil.getLogTag(config), "Product Config: fetch Failed"); sendCallback(PROCESSING_STATE.FETCHED); - if (isFetchAndActivating.get()) { - isFetchAndActivating.set(false);// set fetchAndActivating flag to false if fetch fails. - } + // set fetchAndActivating flag to false if fetch fails. + isFetchAndActivating.compareAndSet(true, false); } } } @@ -291,39 +337,11 @@ public void reset() { defaultConfigs.clear(); activatedConfigs.clear(); settings.initDefaults(); - - TaskManager.getInstance().execute(new TaskManager.TaskListener() { - @Override - public Void doInBackground(Void aVoid) { - synchronized (this) { - - try { - String dirName = getProductConfigDirName(); - FileUtils.deleteDirectory(context, config, dirName); - config.getLogger() - .verbose(ProductConfigUtil.getLogTag(config), "Reset Deleted Dir: " + dirName); - } catch (Exception e) { - e.printStackTrace(); - config.getLogger().verbose(ProductConfigUtil.getLogTag(config), - "Reset failed: " + e.getLocalizedMessage()); - } - return null; - } - } - - @Override - public void onPostExecute(Void aVoid) { - - } - }); + eraseStoredConfigFiles(); } - // -----------------------------------------------------------------------// - // ******** Internal API *****// - // -----------------------------------------------------------------------// - public void resetSettings() { - settings.reset(); + settings.reset(fileUtils); } /** @@ -340,22 +358,7 @@ public void setArpValue(JSONObject arp) { * @param resourceID - resource Id of the XML. */ public void setDefaults(final int resourceID) { - TaskManager.getInstance().execute(new TaskManager.TaskListener() { - @Override - public Void doInBackground(Void aVoid) { - synchronized (this) { - defaultConfigs.putAll(DefaultXmlParser.getDefaultsFromXml(context, resourceID)); - config.getLogger().verbose(ProductConfigUtil.getLogTag(config), - "Product Config: setDefaults Completed with: " + defaultConfigs); - return null; - } - } - - @Override - public void onPostExecute(Void aVoid) { - initAsync(); - } - }); + setDefaultsWithXmlParser(resourceID, new DefaultXmlParser()); } /** @@ -364,11 +367,18 @@ public void onPostExecute(Void aVoid) { * @param map - HashMap of the default configs */ public void setDefaults(final HashMap map) { - TaskManager.getInstance().execute(new TaskManager.TaskListener() { + Task task = CTExecutorFactory.executors(config).ioTask(); + task.addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(final Void aVoid) { + initAsync(); + } + }).execute("ProductConfig#setDefaultsUsingHashMap", new Callable() { @Override - public Void doInBackground(Void aVoid) { + public Void call() { synchronized (this) { if (map != null && !map.isEmpty()) { + for (Map.Entry entry : map.entrySet()) { if (entry != null) { String key = entry.getKey(); @@ -390,11 +400,6 @@ public Void doInBackground(Void aVoid) { return null; } } - - @Override - public void onPostExecute(Void aVoid) { - initAsync(); - } }); } @@ -403,10 +408,10 @@ public void onPostExecute(Void aVoid) { * Developers should not use this method manually. */ public void setGuidAndInit(String cleverTapID) { - if (TextUtils.isEmpty(guid)) { + if (isInitialized() || TextUtils.isEmpty(cleverTapID)) { return; } - this.guid = cleverTapID; + settings.setGuid(cleverTapID); initAsync(); } @@ -420,8 +425,127 @@ public void setMinimumFetchIntervalInSeconds(long fetchIntervalInSeconds) { settings.setMinimumFetchIntervalInSeconds(fetchIntervalInSeconds); } + void eraseStoredConfigFiles() { + Task task = CTExecutorFactory.executors(config).ioTask(); + task.execute("eraseStoredConfigs", new Callable() { + @Override + public Void call() { + synchronized (this) { + try { + String dirName = getProductConfigDirName(); + fileUtils.deleteDirectory(dirName); + config.getLogger() + .verbose(ProductConfigUtil.getLogTag(config), "Reset Deleted Dir: " + dirName); + } catch (Exception e) { + e.printStackTrace(); + config.getLogger().verbose(ProductConfigUtil.getLogTag(config), + "Reset failed: " + e.getLocalizedMessage()); + } + + return null; + } + } + }); + } + + String getActivatedFullPath() { + return getProductConfigDirName() + "/" + CTProductConfigConstants.FILE_NAME_ACTIVATED; + } + + BaseAnalyticsManager getAnalyticsManager() { + return analyticsManager; + } + + BaseCallbackManager getCallbackManager() { + return callbackManager; + } + + // -----------------------------------------------------------------------// + // ******** Internal API *****// + // -----------------------------------------------------------------------// + + CleverTapInstanceConfig getConfig() { + return config; + } + + CoreMetaData getCoreMetaData() { + return coreMetaData; + } + + String getProductConfigDirName() { + return CTProductConfigConstants.DIR_PRODUCT_CONFIG + "_" + config.getAccountId() + "_" + settings.getGuid(); + } + + ProductConfigSettings getSettings() { + return settings; + } + + void initAsync() { + if (TextUtils.isEmpty(settings.getGuid())) { + return; + } + Task task = CTExecutorFactory.executors(config).ioTask(); + task.addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(final Boolean aVoid) { + sendCallback(PROCESSING_STATE.INIT); + } + }).execute("ProductConfig#initAsync", new Callable() { + @Override + public Boolean call() { + synchronized (this) { + try { + //apply defaults + if (!defaultConfigs.isEmpty()) { + activatedConfigs.putAll(defaultConfigs); + } + HashMap storedConfig = getStoredValues(getActivatedFullPath()); + if (!storedConfig.isEmpty()) { + waitingTobeActivatedConfig.putAll(storedConfig); + } + config.getLogger().verbose(ProductConfigUtil.getLogTag(config), + "Loaded configs ready to be applied: " + waitingTobeActivatedConfig); + settings.loadSettings(fileUtils); + isInitialized.set(true); + + } catch (Exception e) { + e.printStackTrace(); + config.getLogger().verbose(ProductConfigUtil.getLogTag(config), + "InitAsync failed - " + e.getLocalizedMessage()); + return false; + } + return true; + } + } + }); + } + + boolean isFetchAndActivating() { + return isFetchAndActivating.get(); + } + + void setDefaultsWithXmlParser(final int resourceID, @NonNull final DefaultXmlParser xmlParser) { + Task task = CTExecutorFactory.executors(config).ioTask(); + task.addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(final Void aVoid) { + initAsync(); + } + }).execute("PCController#setDefaultsWithXmlParser", new Callable() { + @Override + public Void call() { + synchronized (this) { + defaultConfigs.putAll(xmlParser.getDefaultsFromXml(context, resourceID)); + config.getLogger().verbose(ProductConfigUtil.getLogTag(config), + "Product Config: setDefaults Completed with: " + defaultConfigs); + return null; + } + } + }); + } + private boolean canRequest(long minimumFetchIntervalInSeconds) { - boolean validGuid = !TextUtils.isEmpty(guid); + boolean validGuid = !TextUtils.isEmpty(settings.getGuid()); if (!validGuid) { config.getLogger() @@ -449,7 +573,7 @@ private HashMap convertServerJsonToMap(JSONObject jsonObject) { JSONArray kvArray; try { kvArray = jsonObject.getJSONArray(Constants.KEY_KV); - } catch (Exception e) { + } catch (JSONException e) { e.printStackTrace(); config.getLogger().verbose(ProductConfigUtil.getLogTag(config), "ConvertServerJsonToMap failed - " + e.getLocalizedMessage()); @@ -478,19 +602,11 @@ private HashMap convertServerJsonToMap(JSONObject jsonObject) { return map; } - private String getActivatedFullPath() { - return getProductConfigDirName() + "/" + CTProductConfigConstants.FILE_NAME_ACTIVATED; - } - - private String getProductConfigDirName() { - return CTProductConfigConstants.DIR_PRODUCT_CONFIG + "_" + config.getAccountId() + "_" + guid; - } - private HashMap getStoredValues(String fullFilePath) { HashMap map = new HashMap<>(); String content; try { - content = FileUtils.readFromFile(context, config, fullFilePath); + content = fileUtils.readFromFile(fullFilePath); config.getLogger().verbose(ProductConfigUtil.getLogTag(config), "GetStoredValues reading file success:[ " + fullFilePath + "]--[Content]" + content); } catch (Exception e) { @@ -531,42 +647,25 @@ private HashMap getStoredValues(String fullFilePath) { return map; } - private void initAsync() { - if (TextUtils.isEmpty(guid)) { - return; + private void onActivated() { + if (callbackManager.getProductConfigListener() != null) { + callbackManager.getProductConfigListener().onActivated(); } - TaskManager.getInstance().execute(new TaskManager.TaskListener() { - @Override - public Boolean doInBackground(Void params) { - synchronized (this) { - try { - //apply defaults - if (!defaultConfigs.isEmpty()) { - activatedConfigs.putAll(defaultConfigs); - } - HashMap storedConfig = getStoredValues(getActivatedFullPath()); - if (!storedConfig.isEmpty()) { - waitingTobeActivatedConfig.putAll(storedConfig); - } - config.getLogger().verbose(ProductConfigUtil.getLogTag(config), - "Loaded configs ready to be applied: " + waitingTobeActivatedConfig); - settings.loadSettings(); - isInitialized = true; - } catch (Exception e) { - e.printStackTrace(); - config.getLogger().verbose(ProductConfigUtil.getLogTag(config), - "InitAsync failed - " + e.getLocalizedMessage()); - return false; - } - return true; - } - } + } - @Override - public void onPostExecute(Boolean isInitSuccess) { - sendCallback(PROCESSING_STATE.INIT); - } - }); + //Event + + private void onFetched() { + if (callbackManager.getProductConfigListener() != null) { + callbackManager.getProductConfigListener().onFetched(); + } + } + + private void onInit() { + if (callbackManager.getProductConfigListener() != null) { + config.getLogger().verbose(config.getAccountId(), "Product Config initialized"); + callbackManager.getProductConfigListener().onInit(); + } } private synchronized void parseFetchedResponse(JSONObject jsonObject) { @@ -592,13 +691,13 @@ private void sendCallback(PROCESSING_STATE state) { if (state != null) { switch (state) { case INIT: - listener.onInit(); + onInit(); break; case FETCHED: - listener.onFetched(); + onFetched(); break; case ACTIVATED: - listener.onActivated(); + onActivated(); break; } } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/product_config/CTProductConfigControllerListener.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/product_config/CTProductConfigControllerListener.java deleted file mode 100644 index e8fb16da1..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/product_config/CTProductConfigControllerListener.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.clevertap.android.sdk.product_config; - -/** - * This interface is internal to CleverTap SDK - * Developers should not use this. - */ -public interface CTProductConfigControllerListener extends CTProductConfigListener { - - void fetchProductConfig(); -} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/product_config/CTProductConfigFactory.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/product_config/CTProductConfigFactory.java new file mode 100644 index 000000000..8c1c35842 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/product_config/CTProductConfigFactory.java @@ -0,0 +1,25 @@ +package com.clevertap.android.sdk.product_config; + +import android.content.Context; +import com.clevertap.android.sdk.BaseAnalyticsManager; +import com.clevertap.android.sdk.BaseCallbackManager; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import com.clevertap.android.sdk.CoreMetaData; +import com.clevertap.android.sdk.DeviceInfo; +import com.clevertap.android.sdk.utils.FileUtils; + +/** + * Factory class to get {@link CTProductConfigController} instance for a particular configuration + */ +public class CTProductConfigFactory { + + public static CTProductConfigController getInstance(Context context, DeviceInfo deviceInfo, + CleverTapInstanceConfig config, BaseAnalyticsManager baseAnalyticsManager, CoreMetaData coreMetaData, + BaseCallbackManager callbackManager) { + final String guid = deviceInfo.getDeviceID(); + FileUtils fileUtils = new FileUtils(context, config); + ProductConfigSettings configSettings = new ProductConfigSettings(guid, config, fileUtils); + return new CTProductConfigController(context, config, baseAnalyticsManager, coreMetaData, callbackManager, + configSettings, fileUtils); + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/product_config/DefaultXmlParser.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/product_config/DefaultXmlParser.java index 0d9df1dc6..0f43b2727 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/product_config/DefaultXmlParser.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/product_config/DefaultXmlParser.java @@ -11,7 +11,7 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; -public class DefaultXmlParser { +class DefaultXmlParser { private static final String XML_TAG_ENTRY = "entry"; @@ -23,72 +23,80 @@ public class DefaultXmlParser { private static final int XML_TAG_TYPE_VALUE = 1; - public DefaultXmlParser() { + + DefaultXmlParser() { } - static HashMap getDefaultsFromXml(Context context, int resourceId) { - HashMap defaultsMap = new HashMap<>(); + HashMap getDefaultsFromXml(Context context, int resourceId) { + HashMap hashMap = new HashMap<>(); + getDefaultsFromXml(context.getResources(), resourceId, hashMap); + return hashMap; + } - try { - Resources resources = context.getResources(); - if (resources == null) { - Log.e("ProductConfig", - "Could not find the resources of the current context while trying to set defaults from an XML."); - return defaultsMap; - } + void getDefaultsFromXml(final Resources resources, final int resourceId, + final HashMap hashMap) { + if (resources == null) { + Log.e("ProductConfig", + "Could not find the resources of the current context while trying to set defaults from an XML."); + return; + } + try { XmlResourceParser xmlParser = resources.getXml(resourceId); - String curTag = null; - String key = null; - String value = null; - - for (int eventType = xmlParser.getEventType(); eventType != XmlPullParser.END_DOCUMENT; - eventType = xmlParser.next()) { - if (eventType == XmlPullParser.START_TAG) { - curTag = xmlParser.getName(); - } else if (eventType != XmlPullParser.END_TAG) { - if (eventType == XmlPullParser.TEXT && curTag != null) { - byte tagType = -1; - switch (curTag) { - case XML_TAG_KEY: - tagType = XML_TAG_TYPE_KEY; - break; - case XML_TAG_VALUE: - tagType = XML_TAG_TYPE_VALUE; - } - - switch (tagType) { - case XML_TAG_TYPE_KEY: - key = xmlParser.getText(); - break; - case XML_TAG_TYPE_VALUE: - value = xmlParser.getText(); - break; - default: - Log.w(LOG_TAG_PRODUCT_CONFIG, - "Encountered an unexpected tag while parsing the defaults XML."); - } + getDefaultsFromXmlParser(xmlParser, hashMap); + } catch (Exception e) { + Log.e("ProductConfig", "Encountered an error while parsing the defaults XML file.", e); + } + } + + void getDefaultsFromXmlParser(final XmlResourceParser xmlParser, + final HashMap defaultsMap) throws XmlPullParserException, IOException { + String curTag = null; + String key = null; + String value = null; + + for (int eventType = xmlParser.getEventType(); eventType != XmlPullParser.END_DOCUMENT; + eventType = xmlParser.next()) { + if (eventType == XmlPullParser.START_TAG) { + curTag = xmlParser.getName(); + } else if (eventType != XmlPullParser.END_TAG) { + if (eventType == XmlPullParser.TEXT && curTag != null) { + byte tagType = -1; + switch (curTag) { + case XML_TAG_KEY: + tagType = XML_TAG_TYPE_KEY; + break; + case XML_TAG_VALUE: + tagType = XML_TAG_TYPE_VALUE; } - } else { - if (xmlParser.getName().equals(XML_TAG_ENTRY)) { - if (key != null && value != null) { - defaultsMap.put(key, value); - } else { - Log.w(LOG_TAG_PRODUCT_CONFIG, - "An entry in the defaults XML has an invalid key and/or value tag."); - } - key = null; - value = null; + switch (tagType) { + case XML_TAG_TYPE_KEY: + key = xmlParser.getText(); + break; + case XML_TAG_TYPE_VALUE: + value = xmlParser.getText(); + break; + default: + Log.w(LOG_TAG_PRODUCT_CONFIG, + "Encountered an unexpected tag while parsing the defaults XML."); + } + } + } else { + if (xmlParser.getName().equals(XML_TAG_ENTRY)) { + if (key != null && value != null) { + defaultsMap.put(key, value); + } else { + Log.w(LOG_TAG_PRODUCT_CONFIG, + "An entry in the defaults XML has an invalid key and/or value tag."); } - curTag = null; + key = null; + value = null; } + + curTag = null; } - } catch (IOException | XmlPullParserException var11) { - Log.e("ProductConfig", "Encountered an error while parsing the defaults XML file.", var11); } - - return defaultsMap; } } \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/product_config/ProductConfigSettings.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/product_config/ProductConfigSettings.java index 44838a6d9..2e2a848fc 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/product_config/ProductConfigSettings.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/product_config/ProductConfigSettings.java @@ -1,52 +1,60 @@ package com.clevertap.android.sdk.product_config; -import static com.clevertap.android.sdk.product_config.CTProductConfigConstants.DEFAULT_MIN_FETCH_INTERVAL_SECONDS; -import static com.clevertap.android.sdk.product_config.CTProductConfigConstants.DEFAULT_NO_OF_CALLS; -import static com.clevertap.android.sdk.product_config.CTProductConfigConstants.DEFAULT_WINDOW_LENGTH_MINS; -import static com.clevertap.android.sdk.product_config.CTProductConfigConstants.KEY_LAST_FETCHED_TIMESTAMP; -import static com.clevertap.android.sdk.product_config.CTProductConfigConstants.PRODUCT_CONFIG_MIN_INTERVAL_IN_SECONDS; -import static com.clevertap.android.sdk.product_config.CTProductConfigConstants.PRODUCT_CONFIG_NO_OF_CALLS; -import static com.clevertap.android.sdk.product_config.CTProductConfigConstants.PRODUCT_CONFIG_WINDOW_LENGTH_MINS; - -import android.content.Context; import android.text.TextUtils; + import com.clevertap.android.sdk.CleverTapInstanceConfig; -import com.clevertap.android.sdk.FileUtils; -import com.clevertap.android.sdk.TaskManager; +import com.clevertap.android.sdk.task.CTExecutorFactory; +import com.clevertap.android.sdk.task.OnSuccessListener; +import com.clevertap.android.sdk.task.Task; +import com.clevertap.android.sdk.utils.FileUtils; + +import org.json.JSONException; +import org.json.JSONObject; + import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; -import org.json.JSONException; -import org.json.JSONObject; + +import static com.clevertap.android.sdk.product_config.CTProductConfigConstants.DEFAULT_MIN_FETCH_INTERVAL_SECONDS; +import static com.clevertap.android.sdk.product_config.CTProductConfigConstants.DEFAULT_NO_OF_CALLS; +import static com.clevertap.android.sdk.product_config.CTProductConfigConstants.DEFAULT_WINDOW_LENGTH_MINS; +import static com.clevertap.android.sdk.product_config.CTProductConfigConstants.KEY_LAST_FETCHED_TIMESTAMP; +import static com.clevertap.android.sdk.product_config.CTProductConfigConstants.PRODUCT_CONFIG_MIN_INTERVAL_IN_SECONDS; +import static com.clevertap.android.sdk.product_config.CTProductConfigConstants.PRODUCT_CONFIG_NO_OF_CALLS; +import static com.clevertap.android.sdk.product_config.CTProductConfigConstants.PRODUCT_CONFIG_WINDOW_LENGTH_MINS; class ProductConfigSettings { private final CleverTapInstanceConfig config; - private final Context context; + private String guid; - private final String guid; + private final FileUtils fileUtils; private final Map settingsMap = Collections.synchronizedMap(new HashMap()); - ProductConfigSettings(Context context, String guid, CleverTapInstanceConfig config) { - this.context = context.getApplicationContext(); + ProductConfigSettings(String guid, CleverTapInstanceConfig config, FileUtils fileUtils) { this.guid = guid; this.config = config; + this.fileUtils = fileUtils; initDefaults(); } - public void reset() { - initDefaults(); - TaskManager.getInstance().execute(new TaskManager.TaskListener() { + void eraseStoredSettingsFile(final FileUtils fileUtils) { + if (fileUtils == null) { + throw new IllegalArgumentException("FileUtils can't be null"); + } + Task task = CTExecutorFactory.executors(config).ioTask(); + task.execute("ProductConfigSettings#eraseStoredSettingsFile", new Callable() { @Override - public Void doInBackground(Void aVoid) { + public Void call() { synchronized (this) { try { String fileName = getFullPath(); - FileUtils.deleteFile(context, config, fileName); + fileUtils.deleteFile(fileName); config.getLogger() .verbose(ProductConfigUtil.getLogTag(config), "Deleted settings file" + fileName); } catch (Exception e) { @@ -57,15 +65,39 @@ public Void doInBackground(Void aVoid) { return null; } } + }); + } - @Override - public void onPostExecute(Void aVoid) { + String getDirName() { + return CTProductConfigConstants.DIR_PRODUCT_CONFIG + "_" + config.getAccountId() + "_" + guid; + } + + String getFullPath() { + return getDirName() + "/" + CTProductConfigConstants.FILE_NAME_CONFIG_SETTINGS; + } + + String getGuid() { + return guid; + } + + void setGuid(final String guid) { + this.guid = guid; + } + JSONObject getJsonObject(final String content) { + if (!TextUtils.isEmpty(content)) { + try { + return new JSONObject(content); + } catch (JSONException e) { + e.printStackTrace(); + config.getLogger().verbose(ProductConfigUtil.getLogTag(config), + "LoadSettings failed: " + e.getLocalizedMessage()); } - }); + } + return null; } - long getLastFetchTimeStampInMillis() { + synchronized long getLastFetchTimeStampInMillis() { long lastFetchedTimeStamp = 0L; String value = settingsMap.get(KEY_LAST_FETCHED_TIMESTAMP); try { @@ -100,61 +132,60 @@ void initDefaults() { settingsMap.put(PRODUCT_CONFIG_WINDOW_LENGTH_MINS, String.valueOf(DEFAULT_WINDOW_LENGTH_MINS)); settingsMap.put(KEY_LAST_FETCHED_TIMESTAMP, String.valueOf(0)); settingsMap.put(PRODUCT_CONFIG_MIN_INTERVAL_IN_SECONDS, String.valueOf(DEFAULT_MIN_FETCH_INTERVAL_SECONDS)); - synchronized (this) { - config.getLogger() - .verbose(ProductConfigUtil.getLogTag(config), - "Settings loaded with default values: " + settingsMap); - } + config.getLogger() + .verbose(ProductConfigUtil.getLogTag(config), + "Settings loaded with default values: " + settingsMap); } /** * loads settings from file. * It's a sync call, please make sure to call this from a background thread */ - synchronized void loadSettings() { - String content; + synchronized void loadSettings(FileUtils fileUtils) { + if (fileUtils == null) { + throw new IllegalArgumentException("fileutils can't be null"); + } try { - content = FileUtils.readFromFile(context, config, getFullPath()); + String content = fileUtils.readFromFile(getFullPath()); + JSONObject jsonObject = getJsonObject(content); + populateMapWithJson(jsonObject); } catch (Exception e) { e.printStackTrace(); config.getLogger().verbose(ProductConfigUtil.getLogTag(config), "LoadSettings failed while reading file: " + e.getLocalizedMessage()); + } + } + + synchronized void populateMapWithJson(final JSONObject jsonObject) { + if (jsonObject == null) { return; } - if (!TextUtils.isEmpty(content)) { - JSONObject jsonObject = null; - try { - jsonObject = new JSONObject(content); - } catch (JSONException e) { - e.printStackTrace(); - config.getLogger().verbose(ProductConfigUtil.getLogTag(config), - "LoadSettings failed: " + e.getLocalizedMessage()); - return; - } - Iterator iterator = jsonObject.keys(); - while (iterator.hasNext()) { - String key = iterator.next(); - if (!TextUtils.isEmpty(key)) { - String value = null; - try { - Object obj = jsonObject.get(key); - if (obj != null) { - value = String.valueOf(obj); - } - } catch (Exception e) { - e.printStackTrace(); - config.getLogger().verbose(ProductConfigUtil.getLogTag(config), - "Failed loading setting for key " + key + " Error: " + e.getLocalizedMessage()); - continue; - } - if (!TextUtils.isEmpty(value)) { - settingsMap.put(key, value); - } + Iterator iterator = jsonObject.keys(); + while (iterator.hasNext()) { + String key = iterator.next(); + if (!TextUtils.isEmpty(key)) { + String value; + try { + Object obj = jsonObject.get(key); + value = String.valueOf(obj); + } catch (Exception e) { + e.printStackTrace(); + config.getLogger().verbose(ProductConfigUtil.getLogTag(config), + "Failed loading setting for key " + key + " Error: " + e.getLocalizedMessage()); + continue; + } + if (!TextUtils.isEmpty(value)) { + settingsMap.put(key, value); } } - config.getLogger().verbose(ProductConfigUtil.getLogTag(config), - "LoadSettings completed with settings: " + settingsMap); } + config.getLogger().verbose(ProductConfigUtil.getLogTag(config), + "LoadSettings completed with settings: " + settingsMap); + } + + void reset(final FileUtils fileUtils) { + initDefaults(); + eraseStoredSettingsFile(fileUtils); } void setARPValue(JSONObject arp) { @@ -191,14 +222,6 @@ synchronized void setMinimumFetchIntervalInSeconds(long intervalInSeconds) { } } - private String getDirName() { - return CTProductConfigConstants.DIR_PRODUCT_CONFIG + "_" + config.getAccountId() + "_" + guid; - } - - private String getFullPath() { - return getDirName() + "/" + CTProductConfigConstants.FILE_NAME_CONFIG_SETTINGS; - } - private long getMinFetchIntervalInSeconds() { long minInterVal = DEFAULT_MIN_FETCH_INTERVAL_SECONDS; String value = settingsMap.get(PRODUCT_CONFIG_MIN_INTERVAL_IN_SECONDS); @@ -214,7 +237,7 @@ private long getMinFetchIntervalInSeconds() { return minInterVal; } - private int getNoOfCallsInAllowedWindow() { + synchronized private int getNoOfCallsInAllowedWindow() { int noCallsAllowedInWindow = DEFAULT_NO_OF_CALLS; String value = settingsMap.get(PRODUCT_CONFIG_NO_OF_CALLS); try { @@ -237,7 +260,7 @@ private synchronized void setNoOfCallsInAllowedWindow(int callsInAllowedWindow) } } - private int getWindowIntervalInMinutes() { + synchronized private int getWindowIntervalInMinutes() { int windowIntervalInMinutes = DEFAULT_WINDOW_LENGTH_MINS; String value = settingsMap.get(PRODUCT_CONFIG_WINDOW_LENGTH_MINS); try { @@ -271,37 +294,37 @@ private void setProductConfigValuesFromARP(String key, int value) { } } - private void updateConfigToFile() { - TaskManager.getInstance().execute(new TaskManager.TaskListener() { - @Override - public Boolean doInBackground(Void aVoid) { - synchronized (this) { - try { - //Ensure that we are not saving min interval in seconds - HashMap toWriteMap = new HashMap<>(settingsMap); - toWriteMap.remove(PRODUCT_CONFIG_MIN_INTERVAL_IN_SECONDS); - - FileUtils.writeJsonToFile(context, config, getDirName(), - CTProductConfigConstants.FILE_NAME_CONFIG_SETTINGS, new JSONObject(toWriteMap)); - } catch (Exception e) { - e.printStackTrace(); - config.getLogger().verbose(ProductConfigUtil.getLogTag(config), - "UpdateConfigToFile failed: " + e.getLocalizedMessage()); - return false; - } - return true; - } - } - + private synchronized void updateConfigToFile() { + Task task = CTExecutorFactory.executors(config).ioTask(); + task.addOnSuccessListener(new OnSuccessListener() { @Override - public void onPostExecute(Boolean isSuccess) { + public void onSuccess(final Boolean isSuccess) { if (isSuccess) { config.getLogger().verbose(ProductConfigUtil.getLogTag(config), "Product Config settings: writing Success " + settingsMap); } else { config.getLogger() - .verbose(ProductConfigUtil.getLogTag(config), "Product Config settings: writing Failed"); + .verbose(ProductConfigUtil.getLogTag(config), + "Product Config settings: writing Failed"); + } + } + }).execute("ProductConfigSettings#updateConfigToFile", new Callable() { + @Override + public Boolean call() { + try { + //Ensure that we are not saving min interval in seconds + HashMap toWriteMap = new HashMap<>(settingsMap); + toWriteMap.remove(PRODUCT_CONFIG_MIN_INTERVAL_IN_SECONDS); + + fileUtils.writeJsonToFile(getDirName(), + CTProductConfigConstants.FILE_NAME_CONFIG_SETTINGS, new JSONObject(toWriteMap)); + } catch (Exception e) { + e.printStackTrace(); + config.getLogger().verbose(ProductConfigUtil.getLogTag(config), + "UpdateConfigToFile failed: " + e.getLocalizedMessage()); + return false; } + return true; } }); } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/CTApiPushListener.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/CTApiPushListener.java deleted file mode 100644 index e9b4d6cb4..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/CTApiPushListener.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.clevertap.android.sdk.pushnotification; - -import androidx.annotation.RestrictTo; -import com.clevertap.android.sdk.BaseCTApiListener; - -/** - * Interface to call notification related methods of ClevertapAPI - */ -@RestrictTo(RestrictTo.Scope.LIBRARY) -public interface CTApiPushListener extends BaseCTApiListener { - - void onNewToken(String freshToken, PushConstants.PushType pushType); - - void pushDeviceTokenEvent(String token, boolean register, PushConstants.PushType pushType); -} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/CTPushProviderListener.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/CTPushProviderListener.java index 8ff616e41..31fee0edb 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/CTPushProviderListener.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/CTPushProviderListener.java @@ -1,8 +1,6 @@ package com.clevertap.android.sdk.pushnotification; -import com.clevertap.android.sdk.BaseCTApiListener; - -public interface CTPushProviderListener extends BaseCTApiListener { +public interface CTPushProviderListener { void onNewToken(String token, PushConstants.PushType pushType); diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/NotificationInfo.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/NotificationInfo.java index b33ca1ecb..e490166bb 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/NotificationInfo.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/NotificationInfo.java @@ -22,7 +22,7 @@ public final class NotificationInfo { *

    * True if and only if this notification is from CleverTap, and it should be parsed for information. */ - private final boolean shouldRender; + public final boolean shouldRender; @RestrictTo(RestrictTo.Scope.LIBRARY) public NotificationInfo(boolean fromCleverTap, boolean shouldRender) { diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/PushProviders.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/PushProviders.java index 73982eb0b..ab492b984 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/PushProviders.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/PushProviders.java @@ -1,20 +1,69 @@ package com.clevertap.android.sdk.pushnotification; +import static android.content.Context.JOB_SCHEDULER_SERVICE; +import static android.content.Context.NOTIFICATION_SERVICE; import static com.clevertap.android.sdk.BuildConfig.VERSION_CODE; import static com.clevertap.android.sdk.pushnotification.PushNotificationUtil.getPushTypes; +import android.annotation.SuppressLint; +import android.app.AlarmManager; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.content.ComponentName; +import android.content.ContentResolver; import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.SystemClock; import android.text.TextUtils; import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; -import com.clevertap.android.sdk.CTExecutors; +import androidx.annotation.RestrictTo.Scope; +import androidx.core.app.NotificationCompat; +import com.clevertap.android.sdk.AnalyticsManager; +import com.clevertap.android.sdk.CleverTapAPI.DevicePushTokenRefreshListener; import com.clevertap.android.sdk.CleverTapInstanceConfig; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.ControllerManager; import com.clevertap.android.sdk.DeviceInfo; +import com.clevertap.android.sdk.Logger; +import com.clevertap.android.sdk.ManifestInfo; import com.clevertap.android.sdk.StorageHelper; -import com.clevertap.android.sdk.ValidationResultStack; +import com.clevertap.android.sdk.Utils; +import com.clevertap.android.sdk.db.BaseDatabaseManager; +import com.clevertap.android.sdk.db.DBAdapter; +import com.clevertap.android.sdk.pushnotification.PushConstants.PushType; +import com.clevertap.android.sdk.pushnotification.amp.CTBackgroundIntentService; +import com.clevertap.android.sdk.pushnotification.amp.CTBackgroundJobService; +import com.clevertap.android.sdk.task.CTExecutorFactory; +import com.clevertap.android.sdk.task.Task; +import com.clevertap.android.sdk.validation.ValidationResult; +import com.clevertap.android.sdk.validation.ValidationResultFactory; +import com.clevertap.android.sdk.validation.ValidationResultStack; import java.lang.reflect.Constructor; +import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; import java.util.List; +import java.util.Locale; +import java.util.concurrent.Callable; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; /** * Single point of contact to load & support all types of Notification messaging services viz. FCM, XPS, HMS etc. @@ -27,24 +76,126 @@ public class PushProviders implements CTPushProviderListener { private final ArrayList availableCTPushProviders = new ArrayList<>(); - private final CTApiPushListener ctApiPushListener; - private final ArrayList customEnabledPushTypes = new ArrayList<>(); + private final AnalyticsManager analyticsManager; + + private final BaseDatabaseManager baseDatabaseManager; + + private final CleverTapInstanceConfig config; + + private final Context context; + + private final ValidationResultStack validationResultStack; + + private final Object tokenLock = new Object(); + + private DevicePushTokenRefreshListener tokenRefreshListener; + /** * Factory method to load push providers. * * @return A PushProviders class with the loaded providers. */ @NonNull - public static PushProviders load(CTApiPushListener ctApiPushListener) { - PushProviders providers = new PushProviders(ctApiPushListener); + public static PushProviders load(Context context, + CleverTapInstanceConfig config, + BaseDatabaseManager baseDatabaseManager, + ValidationResultStack validationResultStack, + AnalyticsManager analyticsManager, ControllerManager controllerManager) { + PushProviders providers = new PushProviders(context, config, baseDatabaseManager, validationResultStack, + analyticsManager); providers.init(); + controllerManager.setPushProviders(providers); return providers; } - private PushProviders(CTApiPushListener ctApiPushListener) { - this.ctApiPushListener = ctApiPushListener; + private PushProviders( + Context context, + CleverTapInstanceConfig config, + BaseDatabaseManager baseDatabaseManager, + ValidationResultStack validationResultStack, + AnalyticsManager analyticsManager) { + this.context = context; + this.config = config; + this.baseDatabaseManager = baseDatabaseManager; + this.validationResultStack = validationResultStack; + this.analyticsManager = analyticsManager; + initPushAmp(); + } + + /** + * Launches an asynchronous task to download the notification icon from CleverTap, + * and create the Android notification. + *

    + * If your app is using CleverTap SDK's built in FCM message handling, + * this method does not need to be called explicitly. + *

    + * Use this method when implementing your own FCM handling mechanism. Refer to the + * SDK documentation for usage scenarios and examples. + * + * @param context A reference to an Android context + * @param extras The {@link Bundle} object received by the broadcast receiver + * @param notificationId A custom id to build a notification + */ + public void _createNotification(final Context context, final Bundle extras, final int notificationId) { + if (extras == null || extras.get(Constants.NOTIFICATION_TAG) == null) { + return; + } + + if (config.isAnalyticsOnly()) { + config.getLogger() + .debug(config.getAccountId(), "Instance is set for Analytics only, cannot create notification"); + return; + } + + try { + Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask(); + task.execute("CleverTapAPI#_createNotification", new Callable() { + @Override + public Void call() { + try { + config.getLogger() + .debug(config.getAccountId(), "Handling notification: " + extras.toString()); + if (extras.getString(Constants.WZRK_PUSH_ID) != null) { + if (baseDatabaseManager.loadDBAdapter(context) + .doesPushNotificationIdExist(extras.getString(Constants.WZRK_PUSH_ID))) { + config.getLogger().debug(config.getAccountId(), + "Push Notification already rendered, not showing again"); + return null; + } + } + String notifMessage = extras.getString(Constants.NOTIF_MSG); + notifMessage = (notifMessage != null) ? notifMessage : ""; + if (notifMessage.isEmpty()) { + //silent notification + config.getLogger() + .verbose(config.getAccountId(), + "Push notification message is empty, not rendering"); + baseDatabaseManager.loadDBAdapter(context) + .storeUninstallTimestamp(); + String pingFreq = extras.getString("pf", ""); + if (!TextUtils.isEmpty(pingFreq)) { + updatePingFrequencyIfNeeded(context, Integer.parseInt(pingFreq)); + } + return null; + } + String notifTitle = extras.getString(Constants.NOTIF_TITLE, ""); + notifTitle = notifTitle.isEmpty() ? context.getApplicationInfo().name : notifTitle; + triggerNotification(context, extras, notifMessage, notifTitle, notificationId); + } catch (Throwable t) { + // Occurs if the notification image was null + // Let's return, as we couldn't get a handle on the app's icon + // Some devices throw a PackageManager* exception too + config.getLogger() + .debug(config.getAccountId(), "Couldn't render notification: ", t); + } + return null; + } + }); + } catch (Throwable t) { + config.getLogger().debug(config.getAccountId(), "Failed to process push notification", t); + } } /** @@ -57,42 +208,64 @@ public void cacheToken(final String token, final PushConstants.PushType pushType if (TextUtils.isEmpty(token) || pushType == null) { return; } - +// try { - CTExecutors.getInstance().diskIO().execute(new Runnable() { + Task task = CTExecutorFactory.executors(config).ioTask(); + task.execute("PushProviders#cacheToken", new Callable() { @Override - public void run() { + public Void call() { if (alreadyHaveToken(token, pushType)) { - return; + return null; } @PushConstants.RegKeyType String key = pushType.getTokenPrefKey(); if (TextUtils.isEmpty(key)) { - return; + return null; } StorageHelper - .putStringImmediate(context(), StorageHelper.storageKeyWithSuffix(config(), key), token); - config().log(PushConstants.LOG_TAG, pushType + "Cached New Token successfully " + token); + .putStringImmediate(context, StorageHelper.storageKeyWithSuffix(config, key), token); + config.log(PushConstants.LOG_TAG, pushType + "Cached New Token successfully " + token); + return null; } }); } catch (Throwable t) { - config().log(PushConstants.LOG_TAG, pushType + "Unable to cache token " + token, t); + config.log(PushConstants.LOG_TAG, pushType + "Unable to cache token " + token, t); } } - @Override - public CleverTapInstanceConfig config() { - return ctApiPushListener.config(); + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public void doTokenRefresh(String token, PushType pushType) { + if (TextUtils.isEmpty(token) || pushType == null) { + return; + } + switch (pushType) { + case FCM: + handleToken(token, PushType.FCM, true); + break; + case XPS: + handleToken(token, PushType.XPS, true); + break; + case HPS: + handleToken(token, PushType.HPS, true); + break; + case BPS: + handleToken(token, PushType.BPS, true); + break; + case ADM: + handleToken(token, PushType.ADM, true); + break; + } } - @Override - public Context context() { - return ctApiPushListener.context(); - } + /** + * push the device token outside of the normal course + */ + @RestrictTo(Scope.LIBRARY) + public void forcePushDeviceToken(final boolean register) { - @Override - public DeviceInfo deviceInfo() { - return ctApiPushListener.deviceInfo(); + for (PushType pushType : allEnabledPushTypes) { + pushDeviceTokenEvent(null, register, pushType); + } } /** @@ -115,17 +288,25 @@ public String getCachedToken(PushConstants.PushType pushType) { if (pushType != null) { @PushConstants.RegKeyType String key = pushType.getTokenPrefKey(); if (!TextUtils.isEmpty(key)) { - String cachedToken = StorageHelper.getStringFromPrefs(context(), config(), key, null); - config().log(PushConstants.LOG_TAG, pushType + "getting Cached Token - " + cachedToken); + String cachedToken = StorageHelper.getStringFromPrefs(context, config, key, null); + config.log(PushConstants.LOG_TAG, pushType + "getting Cached Token - " + cachedToken); return cachedToken; } } if (pushType != null) { - config().log(PushConstants.LOG_TAG, pushType + " Unable to find cached Token for type "); + config.log(PushConstants.LOG_TAG, pushType + " Unable to find cached Token for type "); } return null; } + public DevicePushTokenRefreshListener getDevicePushTokenRefreshListener() { + return tokenRefreshListener; + } + + public void setDevicePushTokenRefreshListener(final DevicePushTokenRefreshListener tokenRefreshListener) { + this.tokenRefreshListener = tokenRefreshListener; + } + /** * Direct Method to send tokens to Clevertap's server * Call this method when Clients are handling the Messaging services on their own @@ -156,29 +337,121 @@ public boolean isNotificationSupported() { } @Override - public void onNewToken(String token, PushConstants.PushType pushType) { - ctApiPushListener.onNewToken(token, pushType); + public void onNewToken(String freshToken, PushConstants.PushType pushType) { + if (!TextUtils.isEmpty(freshToken)) { + doTokenRefresh(freshToken, pushType); + deviceTokenDidRefresh(freshToken, pushType); + } + } + + //Push + public void onTokenRefresh() { + refreshAllTokens(); } /** - * Fetches latest tokens from various providers and send to Clevertap's server + * Stores silent push notification in DB for smooth working of Push Amplification + * Background Job Service and also stores wzrk_pid to the DB to avoid duplication of Push + * Notifications from Push Amplification. + * + * @param extras - Bundle */ - public void refreshAllTokens() { - CTExecutors.getInstance().diskIO().execute(new Runnable() { + public void processCustomPushNotification(final Bundle extras) { + Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask(); + task.execute("customHandlePushAmplification", new Callable() { @Override - public void run() { - // refresh tokens of Push Providers - refreshCTProviderTokens(); - - // refresh tokens of custom Providers - refreshCustomProviderTokens(); + public Void call() { + String notifMessage = extras.getString(Constants.NOTIF_MSG); + notifMessage = (notifMessage != null) ? notifMessage : ""; + if (notifMessage.isEmpty()) { + //silent notification + config.getLogger() + .verbose(config.getAccountId(), "Push notification message is empty, not rendering"); + baseDatabaseManager.loadDBAdapter(context).storeUninstallTimestamp(); + String pingFreq = extras.getString("pf", ""); + if (!TextUtils.isEmpty(pingFreq)) { + updatePingFrequencyIfNeeded(context, Integer.parseInt(pingFreq)); + } + } else { + String wzrk_pid = extras.getString(Constants.WZRK_PUSH_ID); + String ttl = extras.getString(Constants.WZRK_TIME_TO_LIVE, + (System.currentTimeMillis() + Constants.DEFAULT_PUSH_TTL) / 1000 + ""); + long wzrk_ttl = Long.parseLong(ttl); + DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); + config.getLogger().verbose("Storing Push Notification..." + wzrk_pid + " - with ttl - " + ttl); + dbAdapter.storePushNotificationId(wzrk_pid, wzrk_ttl); + } + return null; } }); } - @Override - public ValidationResultStack remoteErrorLogger() { - return ctApiPushListener.remoteErrorLogger(); + public void runInstanceJobWork(final Context context, final JobParameters parameters) { + Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask(); + task.execute("runningJobService", new Callable() { + @Override + public Void call() { + if (isNotificationSupported()) { + Logger.v(config.getAccountId(), "Token is not present, not running the Job"); + return null; + } + + Calendar now = Calendar.getInstance(); + + int hour = now.get(Calendar.HOUR_OF_DAY); // Get hour in 24 hour format + int minute = now.get(Calendar.MINUTE); + + Date currentTime = parseTimeToDate(hour + ":" + minute); + Date startTime = parseTimeToDate(Constants.DND_START); + Date endTime = parseTimeToDate(Constants.DND_STOP); + + if (isTimeBetweenDNDTime(startTime, endTime, currentTime)) { + Logger.v(config.getAccountId(), "Job Service won't run in default DND hours"); + return null; + } + + long lastTS = baseDatabaseManager.loadDBAdapter(context).getLastUninstallTimestamp(); + + if (lastTS == 0 || lastTS > System.currentTimeMillis() - 24 * 60 * 60 * 1000) { + try { + JSONObject eventObject = new JSONObject(); + eventObject.put("bk", 1); + analyticsManager.sendPingEvent(eventObject); + + if (parameters == null) { + int pingFrequency = getPingFrequency(context); + AlarmManager alarmManager = (AlarmManager) context + .getSystemService(Context.ALARM_SERVICE); + Intent cancelIntent = new Intent(CTBackgroundIntentService.MAIN_ACTION); + cancelIntent.setPackage(context.getPackageName()); + PendingIntent alarmPendingIntent = PendingIntent + .getService(context, config.getAccountId().hashCode(), cancelIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + if (alarmManager != null) { + alarmManager.cancel(alarmPendingIntent); + } + Intent alarmIntent = new Intent(CTBackgroundIntentService.MAIN_ACTION); + alarmIntent.setPackage(context.getPackageName()); + PendingIntent alarmServicePendingIntent = PendingIntent + .getService(context, config.getAccountId().hashCode(), alarmIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + if (alarmManager != null) { + if (pingFrequency != -1) { + alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + (pingFrequency + * Constants.ONE_MIN_IN_MILLIS), + Constants.ONE_MIN_IN_MILLIS * pingFrequency, alarmServicePendingIntent); + } + } + } + } catch (JSONException e) { + Logger.v("Unable to raise background Ping event"); + } + + } + return null; + } + }); } /** @@ -189,18 +462,131 @@ public ValidationResultStack remoteErrorLogger() { * @param pushType - pushtype Ref:{@link PushConstants.PushType} */ public void unregisterToken(String token, PushConstants.PushType pushType) { - ctApiPushListener.pushDeviceTokenEvent(token, false, pushType); + pushDeviceTokenEvent(token, false, pushType); + } + + /** + * updates the ping frequency if there is a change & reschedules existing ping tasks. + */ + public void updatePingFrequencyIfNeeded(final Context context, int frequency) { + config.getLogger().verbose("Ping frequency received - " + frequency); + config.getLogger().verbose("Stored Ping Frequency - " + getPingFrequency(context)); + if (frequency != getPingFrequency(context)) { + setPingFrequency(context, frequency); + if (config.isBackgroundSync() && !config.isAnalyticsOnly()) { + Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask(); + task.execute("createOrResetJobScheduler", new Callable() { + @Override + public Void call() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + config.getLogger().verbose("Creating job"); + createOrResetJobScheduler(context); + } else { + config.getLogger().verbose("Resetting alarm"); + resetAlarmScheduler(context); + } + return null; + } + }); + } + } } private boolean alreadyHaveToken(String newToken, PushConstants.PushType pushType) { boolean alreadyAvailable = !TextUtils.isEmpty(newToken) && pushType != null && newToken .equalsIgnoreCase(getCachedToken(pushType)); if (pushType != null) { - config().log(PushConstants.LOG_TAG, pushType + "Token Already available value: " + alreadyAvailable); + config.log(PushConstants.LOG_TAG, pushType + "Token Already available value: " + alreadyAvailable); } return alreadyAvailable; } + private void createAlarmScheduler(Context context) { + int pingFrequency = getPingFrequency(context); + if (pingFrequency > 0) { + AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + Intent intent = new Intent(CTBackgroundIntentService.MAIN_ACTION); + intent.setPackage(context.getPackageName()); + PendingIntent alarmPendingIntent = PendingIntent + .getService(context, config.getAccountId().hashCode(), intent, + PendingIntent.FLAG_UPDATE_CURRENT); + if (alarmManager != null) { + alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), + Constants.ONE_MIN_IN_MILLIS * pingFrequency, alarmPendingIntent); + } + } + } + + @SuppressLint("MissingPermission") + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + private void createOrResetJobScheduler(Context context) { + + int existingJobId = StorageHelper.getInt(context, Constants.PF_JOB_ID, -1); + JobScheduler jobScheduler = (JobScheduler) context.getSystemService(JOB_SCHEDULER_SERVICE); + + //Disable push amp for devices below Api 26 + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + if (existingJobId >= 0) {//cancel already running job + jobScheduler.cancel(existingJobId); + StorageHelper.putInt(context, Constants.PF_JOB_ID, -1); + } + + config.getLogger() + .debug(config.getAccountId(), "Push Amplification feature is not supported below Oreo"); + return; + } + + if (jobScheduler == null) { + return; + } + int pingFrequency = getPingFrequency(context); + + if (existingJobId < 0 && pingFrequency < 0) { + return; //no running job and nothing to create + } + + if (pingFrequency < 0) { //running job but hard cancel + jobScheduler.cancel(existingJobId); + StorageHelper.putInt(context, Constants.PF_JOB_ID, -1); + return; + } + + ComponentName componentName = new ComponentName(context, CTBackgroundJobService.class); + boolean needsCreate = (existingJobId < 0 && pingFrequency > 0); + + //running job, no hard cancel so check for diff in ping frequency and recreate if needed + JobInfo existingJobInfo = getJobInfo(existingJobId, jobScheduler); + if (existingJobInfo != null + && existingJobInfo.getIntervalMillis() != pingFrequency * Constants.ONE_MIN_IN_MILLIS) { + jobScheduler.cancel(existingJobId); + StorageHelper.putInt(context, Constants.PF_JOB_ID, -1); + needsCreate = true; + } + + if (needsCreate) { + int jobid = config.getAccountId().hashCode(); + JobInfo.Builder builder = new JobInfo.Builder(jobid, componentName); + builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); + builder.setRequiresCharging(false); + + builder.setPeriodic(pingFrequency * Constants.ONE_MIN_IN_MILLIS, 5 * Constants.ONE_MIN_IN_MILLIS); + builder.setRequiresBatteryNotLow(true); + + if (Utils.hasPermission(context, "android.permission.RECEIVE_BOOT_COMPLETED")) { + builder.setPersisted(true); + } + + JobInfo jobInfo = builder.build(); + int resultCode = jobScheduler.schedule(jobInfo); + if (resultCode == JobScheduler.RESULT_SUCCESS) { + Logger.d(config.getAccountId(), "Job scheduled - " + jobid); + StorageHelper.putInt(context, Constants.PF_JOB_ID, jobid); + } else { + Logger.d(config.getAccountId(), "Job not scheduled - " + jobid); + } + } + } + /** * Creates the list of push providers. * @@ -215,17 +601,18 @@ private List createProviders() { CTPushProvider pushProvider = null; try { Class providerClass = Class.forName(className); - Constructor constructor = providerClass.getConstructor(CTPushProviderListener.class); - pushProvider = (CTPushProvider) constructor.newInstance(this); - config().log(PushConstants.LOG_TAG, "Found provider:" + className); + Constructor constructor = providerClass + .getConstructor(CTPushProviderListener.class, Context.class, CleverTapInstanceConfig.class); + pushProvider = (CTPushProvider) constructor.newInstance(this, context, config); + config.log(PushConstants.LOG_TAG, "Found provider:" + className); } catch (InstantiationException e) { - config().log(PushConstants.LOG_TAG, "Unable to create provider InstantiationException" + className); + config.log(PushConstants.LOG_TAG, "Unable to create provider InstantiationException" + className); } catch (IllegalAccessException e) { - config().log(PushConstants.LOG_TAG, "Unable to create provider IllegalAccessException" + className); + config.log(PushConstants.LOG_TAG, "Unable to create provider IllegalAccessException" + className); } catch (ClassNotFoundException e) { - config().log(PushConstants.LOG_TAG, "Unable to create provider ClassNotFoundException" + className); + config.log(PushConstants.LOG_TAG, "Unable to create provider ClassNotFoundException" + className); } catch (Exception e) { - config().log(PushConstants.LOG_TAG, + config.log(PushConstants.LOG_TAG, "Unable to create provider " + className + " Exception:" + e.getClass().getName()); } @@ -239,29 +626,38 @@ private List createProviders() { return providers; } + //Push + @SuppressWarnings("SameParameterValue") + private void deviceTokenDidRefresh(String token, PushType type) { + if (tokenRefreshListener != null) { + config.getLogger().debug(config.getAccountId(), "Notifying devicePushTokenDidRefresh: " + token); + tokenRefreshListener.devicePushTokenDidRefresh(token, type); + } + } + private void findCTPushProviders(List providers) { if (providers.isEmpty()) { - config().log(PushConstants.LOG_TAG, + config.log(PushConstants.LOG_TAG, "No push providers found!. Make sure to install at least one push provider"); return; } for (CTPushProvider provider : providers) { if (!isValid(provider)) { - config().log(PushConstants.LOG_TAG, "Invalid Provider: " + provider.getClass()); + config.log(PushConstants.LOG_TAG, "Invalid Provider: " + provider.getClass()); continue; } if (!provider.isSupported()) { - config().log(PushConstants.LOG_TAG, "Unsupported Provider: " + provider.getClass()); + config.log(PushConstants.LOG_TAG, "Unsupported Provider: " + provider.getClass()); continue; } if (provider.isAvailable()) { - config().log(PushConstants.LOG_TAG, "Available Provider: " + provider.getClass()); + config.log(PushConstants.LOG_TAG, "Available Provider: " + provider.getClass()); availableCTPushProviders.add(provider); } else { - config().log(PushConstants.LOG_TAG, "Unavailable Provider: " + provider.getClass()); + config.log(PushConstants.LOG_TAG, "Unavailable Provider: " + provider.getClass()); } } } @@ -273,20 +669,27 @@ private void findCustomEnabledPushTypes() { } } + //Session + private void findEnabledPushTypes() { - for (PushConstants.PushType pushType : getPushTypes(config().getAllowedPushTypes())) { + for (PushConstants.PushType pushType : getPushTypes(config.getAllowedPushTypes())) { String className = pushType.getMessagingSDKClassName(); try { Class.forName(className); allEnabledPushTypes.add(pushType); - config().log(PushConstants.LOG_TAG, "SDK Class Available :" + className); + config.log(PushConstants.LOG_TAG, "SDK Class Available :" + className); } catch (Exception e) { - config().log(PushConstants.LOG_TAG, + config.log(PushConstants.LOG_TAG, "SDK class Not available " + className + " Exception:" + e.getClass().getName()); } } } + private int getPingFrequency(Context context) { + return StorageHelper.getInt(context, Constants.PING_FREQUENCY, + Constants.PING_FREQUENCY_VALUE); //intentional global key because only one Job is running + } + /** * Loads all the plugins that are currently supported by the device. */ @@ -301,10 +704,74 @@ private void init() { findCustomEnabledPushTypes(); } + private void initPushAmp() { + if (config.isBackgroundSync() && !config + .isAnalyticsOnly()) { + Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask(); + task.execute("createOrResetJobScheduler", new Callable() { + @Override + public Void call() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + createOrResetJobScheduler(context); + } else { + createAlarmScheduler(context); + } + return null; + } + }); + } + } + + @SuppressWarnings("SameParameterValue") + private boolean isServiceAvailable(Context context, Class clazz) { + if (clazz == null) { + return false; + } + + PackageManager pm = context.getPackageManager(); + String packageName = context.getPackageName(); + + PackageInfo packageInfo; + try { + packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SERVICES); + ServiceInfo[] services = packageInfo.services; + for (ServiceInfo serviceInfo : services) { + if (serviceInfo.name.equals(clazz.getName())) { + Logger.v("Service " + serviceInfo.name + " found"); + return true; + } + } + } catch (PackageManager.NameNotFoundException e) { + Logger.d("Intent Service name not found exception - " + e.getLocalizedMessage()); + } + return false; + } + + private boolean isTimeBetweenDNDTime(Date startTime, Date stopTime, Date currentTime) { + //Start Time + Calendar startTimeCalendar = Calendar.getInstance(); + startTimeCalendar.setTime(startTime); + //Current Time + Calendar currentTimeCalendar = Calendar.getInstance(); + currentTimeCalendar.setTime(currentTime); + //Stop Time + Calendar stopTimeCalendar = Calendar.getInstance(); + stopTimeCalendar.setTime(stopTime); + + if (stopTime.compareTo(startTime) < 0) { + if (currentTimeCalendar.compareTo(stopTimeCalendar) < 0) { + currentTimeCalendar.add(Calendar.DATE, 1); + } + stopTimeCalendar.add(Calendar.DATE, 1); + } + return currentTimeCalendar.compareTo(startTimeCalendar) >= 0 + && currentTimeCalendar.compareTo(stopTimeCalendar) < 0; + } + private boolean isValid(CTPushProvider provider) { if (VERSION_CODE < provider.minSDKSupportVersionCode()) { - config().log(PushConstants.LOG_TAG, + config.log(PushConstants.LOG_TAG, "Provider: %s version %s does not match the SDK version %s. Make sure all Airship dependencies are the same version."); return false; } @@ -314,14 +781,14 @@ private boolean isValid(CTPushProvider provider) { case XPS: case BPS: if (provider.getPlatform() != PushConstants.ANDROID_PLATFORM) { - config().log(PushConstants.LOG_TAG, "Invalid Provider: " + provider.getClass() + + config.log(PushConstants.LOG_TAG, "Invalid Provider: " + provider.getClass() + " delivery is only available for Android platforms." + provider.getPushType()); return false; } break; case ADM: if (provider.getPlatform() != PushConstants.AMAZON_PLATFORM) { - config().log(PushConstants.LOG_TAG, "Invalid Provider: " + + config.log(PushConstants.LOG_TAG, "Invalid Provider: " + provider.getClass() + " ADM delivery is only available for Amazon platforms." + provider.getPushType()); return false; @@ -332,13 +799,68 @@ private boolean isValid(CTPushProvider provider) { return true; } + private Date parseTimeToDate(String time) { + + final String inputFormat = "HH:mm"; + SimpleDateFormat inputParser = new SimpleDateFormat(inputFormat, Locale.US); + try { + return inputParser.parse(time); + } catch (java.text.ParseException e) { + return new Date(0); + } + } + + private void pushDeviceTokenEvent(String token, boolean register, PushType pushType) { + if (pushType == null) { + return; + } + token = !TextUtils.isEmpty(token) ? token : getCachedToken(pushType); + if (TextUtils.isEmpty(token)) { + return; + } + synchronized (tokenLock) { + JSONObject event = new JSONObject(); + JSONObject data = new JSONObject(); + String action = register ? "register" : "unregister"; + try { + data.put("action", action); + data.put("id", token); + data.put("type", pushType.getType()); + event.put("data", data); + config.getLogger().verbose(config.getAccountId(), pushType + action + " device token " + token); + analyticsManager.sendDataEvent(event); + } catch (Throwable t) { + // we won't get here + config.getLogger().verbose(config.getAccountId(), pushType + action + " device token failed", t); + } + } + } + + /** + * Fetches latest tokens from various providers and send to Clevertap's server + */ + private void refreshAllTokens() { + Task task = CTExecutorFactory.executors(config).ioTask(); + task.execute("PushProviders#refreshAllTokens", new Callable() { + @Override + public Void call() { + // refresh tokens of Push Providers + refreshCTProviderTokens(); + + // refresh tokens of custom Providers + refreshCustomProviderTokens(); + return null; + } + }); + } + private void refreshCTProviderTokens() { for (final CTPushProvider pushProvider : availableCTPushProviders) { try { pushProvider.requestToken(); } catch (Throwable t) { //no-op - config().log(PushConstants.LOG_TAG, "Token Refresh error " + pushProvider, t); + config.log(PushConstants.LOG_TAG, "Token Refresh error " + pushProvider, t); } } } @@ -346,15 +868,401 @@ private void refreshCTProviderTokens() { private void refreshCustomProviderTokens() { for (PushConstants.PushType pushType : customEnabledPushTypes) { try { - ctApiPushListener.pushDeviceTokenEvent(getCachedToken(pushType), true, pushType); + pushDeviceTokenEvent(getCachedToken(pushType), true, pushType); } catch (Throwable t) { - config().log(PushConstants.LOG_TAG, "Token Refresh error " + pushType, t); + config.log(PushConstants.LOG_TAG, "Token Refresh error " + pushType, t); } } } private void registerToken(String token, PushConstants.PushType pushType) { - ctApiPushListener.pushDeviceTokenEvent(token, true, pushType); + pushDeviceTokenEvent(token, true, pushType); cacheToken(token, pushType); } + + private void resetAlarmScheduler(Context context) { + if (getPingFrequency(context) <= 0) { + stopAlarmScheduler(context); + } else { + stopAlarmScheduler(context); + createAlarmScheduler(context); + } + } + + private void setPingFrequency(Context context, int pingFrequency) { + StorageHelper.putInt(context, Constants.PING_FREQUENCY, pingFrequency); + } + + private void stopAlarmScheduler(Context context) { + AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + Intent cancelIntent = new Intent(CTBackgroundIntentService.MAIN_ACTION); + cancelIntent.setPackage(context.getPackageName()); + PendingIntent alarmPendingIntent = PendingIntent + .getService(context, config.getAccountId().hashCode(), cancelIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + if (alarmManager != null && alarmPendingIntent != null) { + alarmManager.cancel(alarmPendingIntent); + } + } + + private void triggerNotification(Context context, Bundle extras, String notifMessage, String notifTitle, + int notificationId) { + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE); + + if (notificationManager == null) { + String notificationManagerError = "Unable to render notification, Notification Manager is null."; + config.getLogger().debug(config.getAccountId(), notificationManagerError); + return; + } + + String channelId = extras.getString(Constants.WZRK_CHANNEL_ID, ""); + boolean requiresChannelId = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + int messageCode = -1; + String value = ""; + + if (channelId.isEmpty()) { + messageCode = Constants.CHANNEL_ID_MISSING_IN_PAYLOAD; + value = extras.toString(); + } else if (notificationManager.getNotificationChannel(channelId) == null) { + messageCode = Constants.CHANNEL_ID_NOT_REGISTERED; + value = channelId; + } + if (messageCode != -1) { + ValidationResult channelIdError = ValidationResultFactory.create(512, messageCode, value); + config.getLogger().debug(config.getAccountId(), channelIdError.getErrorDesc()); + validationResultStack.pushValidationResult(channelIdError); + return; + } + } + + String icoPath = extras.getString(Constants.NOTIF_ICON); + Intent launchIntent = new Intent(context, CTPushNotificationReceiver.class); + + PendingIntent pIntent; + + // Take all the properties from the notif and add it to the intent + launchIntent.putExtras(extras); + launchIntent.removeExtra(Constants.WZRK_ACTIONS); + pIntent = PendingIntent.getBroadcast(context, (int) System.currentTimeMillis(), + launchIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + NotificationCompat.Style style; + String bigPictureUrl = extras.getString(Constants.WZRK_BIG_PICTURE); + if (bigPictureUrl != null && bigPictureUrl.startsWith("http")) { + try { + Bitmap bpMap = Utils.getNotificationBitmap(bigPictureUrl, false, context); + + if (bpMap == null) { + throw new Exception("Failed to fetch big picture!"); + } + + if (extras.containsKey(Constants.WZRK_MSG_SUMMARY)) { + String summaryText = extras.getString(Constants.WZRK_MSG_SUMMARY); + style = new NotificationCompat.BigPictureStyle() + .setSummaryText(summaryText) + .bigPicture(bpMap); + } else { + style = new NotificationCompat.BigPictureStyle() + .setSummaryText(notifMessage) + .bigPicture(bpMap); + } + } catch (Throwable t) { + style = new NotificationCompat.BigTextStyle() + .bigText(notifMessage); + config.getLogger() + .verbose(config.getAccountId(), + "Falling back to big text notification, couldn't fetch big picture", + t); + } + } else { + style = new NotificationCompat.BigTextStyle() + .bigText(notifMessage); + } + + int smallIcon; + try { + String x = ManifestInfo.getInstance(context).getNotificationIcon(); + if (x == null) { + throw new IllegalArgumentException(); + } + smallIcon = context.getResources().getIdentifier(x, "drawable", context.getPackageName()); + if (smallIcon == 0) { + throw new IllegalArgumentException(); + } + } catch (Throwable t) { + smallIcon = DeviceInfo.getAppIconAsIntId(context); + } + + int priorityInt = NotificationCompat.PRIORITY_DEFAULT; + String priority = extras.getString(Constants.NOTIF_PRIORITY); + if (priority != null) { + if (priority.equals(Constants.PRIORITY_HIGH)) { + priorityInt = NotificationCompat.PRIORITY_HIGH; + } + if (priority.equals(Constants.PRIORITY_MAX)) { + priorityInt = NotificationCompat.PRIORITY_MAX; + } + } + + // if we have no user set notificationID then try collapse key + if (notificationId == Constants.EMPTY_NOTIFICATION_ID) { + try { + Object collapse_key = extras.get(Constants.WZRK_COLLAPSE); + if (collapse_key != null) { + if (collapse_key instanceof Number) { + notificationId = ((Number) collapse_key).intValue(); + } else if (collapse_key instanceof String) { + try { + notificationId = Integer.parseInt(collapse_key.toString()); + config.getLogger().debug(config.getAccountId(), + "Converting collapse_key: " + collapse_key + " to notificationId int: " + + notificationId); + } catch (NumberFormatException e) { + notificationId = (collapse_key.toString().hashCode()); + config.getLogger().debug(config.getAccountId(), + "Converting collapse_key: " + collapse_key + " to notificationId int: " + + notificationId); + } + } + } + } catch (NumberFormatException e) { + // no-op + } + } else { + config.getLogger().debug(config.getAccountId(), "Have user provided notificationId: " + notificationId + + " won't use collapse_key (if any) as basis for notificationId"); + } + + // if after trying collapse_key notification is still empty set to random int + if (notificationId == Constants.EMPTY_NOTIFICATION_ID) { + notificationId = (int) (Math.random() * 100); + config.getLogger().debug(config.getAccountId(), "Setting random notificationId: " + notificationId); + } + + NotificationCompat.Builder nb; + if (requiresChannelId) { + nb = new NotificationCompat.Builder(context, channelId); + + // choices here are Notification.BADGE_ICON_NONE = 0, Notification.BADGE_ICON_SMALL = 1, Notification.BADGE_ICON_LARGE = 2. Default is Notification.BADGE_ICON_LARGE + String badgeIconParam = extras.getString(Constants.WZRK_BADGE_ICON, null); + if (badgeIconParam != null) { + try { + int badgeIconType = Integer.parseInt(badgeIconParam); + if (badgeIconType >= 0) { + nb.setBadgeIconType(badgeIconType); + } + } catch (Throwable t) { + // no-op + } + } + + String badgeCountParam = extras.getString(Constants.WZRK_BADGE_COUNT, null); + if (badgeCountParam != null) { + try { + int badgeCount = Integer.parseInt(badgeCountParam); + if (badgeCount >= 0) { + nb.setNumber(badgeCount); + } + } catch (Throwable t) { + // no-op + } + } + if (extras.containsKey(Constants.WZRK_SUBTITLE)) { + nb.setSubText(extras.getString(Constants.WZRK_SUBTITLE)); + } + } else { + // noinspection all + nb = new NotificationCompat.Builder(context); + } + + if (extras.containsKey(Constants.WZRK_COLOR)) { + int color = Color.parseColor(extras.getString(Constants.WZRK_COLOR)); + nb.setColor(color); + nb.setColorized(true); + } + + nb.setContentTitle(notifTitle) + .setContentText(notifMessage) + .setContentIntent(pIntent) + .setAutoCancel(true) + .setStyle(style) + .setPriority(priorityInt) + .setSmallIcon(smallIcon); + + nb.setLargeIcon(Utils.getNotificationBitmap(icoPath, true, context)); + + try { + if (extras.containsKey(Constants.WZRK_SOUND)) { + Uri soundUri = null; + + Object o = extras.get(Constants.WZRK_SOUND); + + if ((o instanceof Boolean && (Boolean) o)) { + soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); + } else if (o instanceof String) { + String s = (String) o; + if (s.equals("true")) { + soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); + } else if (!s.isEmpty()) { + if (s.contains(".mp3") || s.contains(".ogg") || s.contains(".wav")) { + s = s.substring(0, (s.length() - 4)); + } + soundUri = Uri + .parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.getPackageName() + + "/raw/" + s); + + } + } + + if (soundUri != null) { + nb.setSound(soundUri); + } + } + } catch (Throwable t) { + config.getLogger().debug(config.getAccountId(), "Could not process sound parameter", t); + } + + // add actions if any + JSONArray actions = null; + String actionsString = extras.getString(Constants.WZRK_ACTIONS); + if (actionsString != null) { + try { + actions = new JSONArray(actionsString); + } catch (Throwable t) { + config.getLogger() + .debug(config.getAccountId(), + "error parsing notification actions: " + t.getLocalizedMessage()); + } + } + + String intentServiceName = ManifestInfo.getInstance(context).getIntentServiceName(); + Class clazz = null; + if (intentServiceName != null) { + try { + clazz = Class.forName(intentServiceName); + } catch (ClassNotFoundException e) { + try { + clazz = Class.forName("com.clevertap.android.sdk.pushnotification.CTNotificationIntentService"); + } catch (ClassNotFoundException ex) { + Logger.d("No Intent Service found"); + } + } + } else { + try { + clazz = Class.forName("com.clevertap.android.sdk.pushnotification.CTNotificationIntentService"); + } catch (ClassNotFoundException ex) { + Logger.d("No Intent Service found"); + } + } + + boolean isCTIntentServiceAvailable = isServiceAvailable(context, clazz); + + if (actions != null && actions.length() > 0) { + for (int i = 0; i < actions.length(); i++) { + try { + JSONObject action = actions.getJSONObject(i); + String label = action.optString("l"); + String dl = action.optString("dl"); + String ico = action.optString(Constants.NOTIF_ICON); + String id = action.optString("id"); + boolean autoCancel = action.optBoolean("ac", true); + if (label.isEmpty() || id.isEmpty()) { + config.getLogger().debug(config.getAccountId(), + "not adding push notification action: action label or id missing"); + continue; + } + int icon = 0; + if (!ico.isEmpty()) { + try { + icon = context.getResources().getIdentifier(ico, "drawable", context.getPackageName()); + } catch (Throwable t) { + config.getLogger().debug(config.getAccountId(), + "unable to add notification action icon: " + t.getLocalizedMessage()); + } + } + + boolean sendToCTIntentService = (autoCancel && isCTIntentServiceAvailable); + + Intent actionLaunchIntent; + if (sendToCTIntentService) { + actionLaunchIntent = new Intent(CTNotificationIntentService.MAIN_ACTION); + actionLaunchIntent.setPackage(context.getPackageName()); + actionLaunchIntent.putExtra("ct_type", CTNotificationIntentService.TYPE_BUTTON_CLICK); + if (!dl.isEmpty()) { + actionLaunchIntent.putExtra("dl", dl); + } + } else { + if (!dl.isEmpty()) { + actionLaunchIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(dl)); + } else { + actionLaunchIntent = context.getPackageManager() + .getLaunchIntentForPackage(context.getPackageName()); + } + } + + if (actionLaunchIntent != null) { + actionLaunchIntent.putExtras(extras); + actionLaunchIntent.removeExtra(Constants.WZRK_ACTIONS); + actionLaunchIntent.putExtra("actionId", id); + actionLaunchIntent.putExtra("autoCancel", autoCancel); + actionLaunchIntent.putExtra("wzrk_c2a", id); + actionLaunchIntent.putExtra("notificationId", notificationId); + + actionLaunchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); + } + + PendingIntent actionIntent; + int requestCode = ((int) System.currentTimeMillis()) + i; + if (sendToCTIntentService) { + actionIntent = PendingIntent.getService(context, requestCode, + actionLaunchIntent, PendingIntent.FLAG_UPDATE_CURRENT); + } else { + actionIntent = PendingIntent.getActivity(context, requestCode, + actionLaunchIntent, PendingIntent.FLAG_UPDATE_CURRENT); + } + nb.addAction(icon, label, actionIntent); + + } catch (Throwable t) { + config.getLogger() + .debug(config.getAccountId(), + "error adding notification action : " + t.getLocalizedMessage()); + } + } + } + + Notification n = nb.build(); + notificationManager.notify(notificationId, n); + config.getLogger().debug(config.getAccountId(), "Rendered notification: " + n.toString()); + + String ttl = extras.getString(Constants.WZRK_TIME_TO_LIVE, + (System.currentTimeMillis() + Constants.DEFAULT_PUSH_TTL) / 1000 + ""); + long wzrk_ttl = Long.parseLong(ttl); + String wzrk_pid = extras.getString(Constants.WZRK_PUSH_ID); + DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); + config.getLogger().verbose("Storing Push Notification..." + wzrk_pid + " - with ttl - " + ttl); + dbAdapter.storePushNotificationId(wzrk_pid, wzrk_ttl); + + boolean notificationViewedEnabled = "true".equals(extras.getString(Constants.WZRK_RNV, "")); + if (!notificationViewedEnabled) { + ValidationResult notificationViewedError = ValidationResultFactory + .create(512, Constants.NOTIFICATION_VIEWED_DISABLED, extras.toString()); + config.getLogger().debug(notificationViewedError.getErrorDesc()); + validationResultStack.pushValidationResult(notificationViewedError); + return; + } + analyticsManager.pushNotificationViewedEvent(extras); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + private static JobInfo getJobInfo(int jobId, JobScheduler jobScheduler) { + for (JobInfo jobInfo : jobScheduler.getAllPendingJobs()) { + if (jobInfo.getId() == jobId) { + return jobInfo; + } + } + return null; + } } \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/fcm/FcmMessageHandlerImpl.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/fcm/FcmMessageHandlerImpl.java index 80e2fea4e..267ce9327 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/fcm/FcmMessageHandlerImpl.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/fcm/FcmMessageHandlerImpl.java @@ -32,7 +32,7 @@ public boolean onMessageReceived(final Context context, final RemoteMessage mess if (info.fromCleverTap) { if (cleverTapAPI != null) { - cleverTapAPI.config().log(LOG_TAG, + cleverTapAPI.getCoreState().getConfig().log(LOG_TAG, FCM_LOG_TAG + "received notification from CleverTap: " + extras.toString()); } else { Logger.d(LOG_TAG, FCM_LOG_TAG + "received notification from CleverTap: " + extras.toString()); diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/fcm/FcmPushProvider.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/fcm/FcmPushProvider.java index 666353b52..bfc801d18 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/fcm/FcmPushProvider.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/fcm/FcmPushProvider.java @@ -3,8 +3,10 @@ import static com.clevertap.android.sdk.pushnotification.PushConstants.ANDROID_PLATFORM; import android.annotation.SuppressLint; +import android.content.Context; import androidx.annotation.NonNull; import androidx.annotation.RestrictTo; +import com.clevertap.android.sdk.CleverTapInstanceConfig; import com.clevertap.android.sdk.pushnotification.CTPushProvider; import com.clevertap.android.sdk.pushnotification.CTPushProviderListener; import com.clevertap.android.sdk.pushnotification.PushConstants; @@ -16,11 +18,11 @@ @SuppressLint(value = "unused") public class FcmPushProvider implements CTPushProvider { - private IFcmSdkHandler mHandler; + private IFcmSdkHandler handler; @SuppressLint(value = "unused") - public FcmPushProvider(CTPushProviderListener ctPushListener) { - mHandler = new FcmSdkHandlerImpl(ctPushListener); + public FcmPushProvider(CTPushProviderListener ctPushListener, Context context, CleverTapInstanceConfig config) { + handler = new FcmSdkHandlerImpl(ctPushListener, context, config); } @Override @@ -31,7 +33,7 @@ public int getPlatform() { @NonNull @Override public PushConstants.PushType getPushType() { - return mHandler.getPushType(); + return handler.getPushType(); } /** @@ -41,7 +43,7 @@ public PushConstants.PushType getPushType() { */ @Override public boolean isAvailable() { - return mHandler.isAvailable(); + return handler.isAvailable(); } /** @@ -51,7 +53,7 @@ public boolean isAvailable() { */ @Override public boolean isSupported() { - return mHandler.isSupported(); + return handler.isSupported(); } @Override @@ -61,10 +63,10 @@ public int minSDKSupportVersionCode() { @Override public void requestToken() { - mHandler.requestToken(); + handler.requestToken(); } void setHandler(final IFcmSdkHandler handler) { - mHandler = handler; + this.handler = handler; } } \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/fcm/FcmSdkHandlerImpl.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/fcm/FcmSdkHandlerImpl.java index 79b77af04..8b02d3a76 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/fcm/FcmSdkHandlerImpl.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/fcm/FcmSdkHandlerImpl.java @@ -1,13 +1,15 @@ package com.clevertap.android.sdk.pushnotification.fcm; -import static com.clevertap.android.sdk.PackageUtils.isGooglePlayServicesAvailable; -import static com.clevertap.android.sdk.PackageUtils.isGooglePlayStoreAvailable; import static com.clevertap.android.sdk.pushnotification.PushConstants.FCM_LOG_TAG; import static com.clevertap.android.sdk.pushnotification.PushConstants.LOG_TAG; import static com.clevertap.android.sdk.pushnotification.PushConstants.PushType.FCM; +import static com.clevertap.android.sdk.utils.PackageUtils.isGooglePlayServicesAvailable; +import static com.clevertap.android.sdk.utils.PackageUtils.isGooglePlayStoreAvailable; +import android.content.Context; import android.text.TextUtils; import androidx.annotation.NonNull; +import com.clevertap.android.sdk.CleverTapInstanceConfig; import com.clevertap.android.sdk.ManifestInfo; import com.clevertap.android.sdk.Utils; import com.clevertap.android.sdk.pushnotification.CTPushProviderListener; @@ -25,11 +27,18 @@ public class FcmSdkHandlerImpl implements IFcmSdkHandler { private final CTPushProviderListener listener; - private ManifestInfo mManifestInfo; + private final CleverTapInstanceConfig config; - public FcmSdkHandlerImpl(final CTPushProviderListener listener) { + private final Context context; + + private ManifestInfo manifestInfo; + + public FcmSdkHandlerImpl(final CTPushProviderListener listener, final Context context, + final CleverTapInstanceConfig config) { + this.context = context; + this.config = config; this.listener = listener; - this.mManifestInfo = ManifestInfo.getInstance(listener.context()); + this.manifestInfo = ManifestInfo.getInstance(context); } public PushType getPushType() { @@ -39,19 +48,19 @@ public PushType getPushType() { @Override public boolean isAvailable() { try { - if (!isGooglePlayServicesAvailable(listener.context())) { - listener.config().log(LOG_TAG, FCM_LOG_TAG + "Google Play services is currently unavailable."); + if (!isGooglePlayServicesAvailable(context)) { + config.log(LOG_TAG, FCM_LOG_TAG + "Google Play services is currently unavailable."); return false; } String senderId = getSenderId(); if (TextUtils.isEmpty(senderId)) { - listener.config() + config .log(LOG_TAG, FCM_LOG_TAG + "The FCM sender ID is not set. Unable to register for FCM."); return false; } } catch (Throwable t) { - listener.config().log(LOG_TAG, FCM_LOG_TAG + "Unable to register with FCM.", t); + config.log(LOG_TAG, FCM_LOG_TAG + "Unable to register with FCM.", t); return false; } return true; @@ -59,46 +68,48 @@ public boolean isAvailable() { @Override public boolean isSupported() { - return isGooglePlayStoreAvailable(listener.context()); + return isGooglePlayStoreAvailable(context); } @Override public void requestToken() { try { String tokenUsingManifestMetaEntry = Utils - .getFcmTokenUsingManifestMetaEntry(listener.context(), listener.config()); + .getFcmTokenUsingManifestMetaEntry(context, config); if (!TextUtils.isEmpty(tokenUsingManifestMetaEntry)) { - listener.config().log(LOG_TAG, FCM_LOG_TAG + "FCM token - " + tokenUsingManifestMetaEntry); + config.log(LOG_TAG, FCM_LOG_TAG + "FCM token - " + tokenUsingManifestMetaEntry); listener.onNewToken(tokenUsingManifestMetaEntry, getPushType()); } else { - listener.config() + config .log(LOG_TAG, FCM_LOG_TAG + "Requesting FCM token using googleservices.json"); FirebaseInstanceId.getInstance().getInstanceId() .addOnCompleteListener(new OnCompleteListener() { @Override public void onComplete(@NonNull Task task) { if (!task.isSuccessful()) { - listener.config() - .log(LOG_TAG, FCM_LOG_TAG + "FCM token using googleservices.json failed", task.getException()); + config + .log(LOG_TAG, FCM_LOG_TAG + "FCM token using googleservices.json failed", + task.getException()); listener.onNewToken(null, getPushType()); return; } // Get new Instance ID token String token = task.getResult() != null ? task.getResult().getToken() : null; - listener.config().log(LOG_TAG, FCM_LOG_TAG + "FCM token using googleservices.json - " + token); + config + .log(LOG_TAG, FCM_LOG_TAG + "FCM token using googleservices.json - " + token); listener.onNewToken(token, getPushType()); } }); } } catch (Throwable t) { - listener.config().log(LOG_TAG, FCM_LOG_TAG + "Error requesting FCM token", t); + config.log(LOG_TAG, FCM_LOG_TAG + "Error requesting FCM token", t); listener.onNewToken(null, getPushType()); } } String getFCMSenderID() { - return mManifestInfo.getFCMSenderId(); + return manifestInfo.getFCMSenderId(); } String getSenderId() { @@ -111,6 +122,6 @@ String getSenderId() { } void setManifestInfo(final ManifestInfo manifestInfo) { - mManifestInfo = manifestInfo; + this.manifestInfo = manifestInfo; } } \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/response/ARPResponse.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/ARPResponse.java new file mode 100644 index 000000000..a873e18f5 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/ARPResponse.java @@ -0,0 +1,148 @@ +package com.clevertap.android.sdk.response; + +import android.content.Context; +import android.content.SharedPreferences; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.ControllerManager; +import com.clevertap.android.sdk.Logger; +import com.clevertap.android.sdk.StorageHelper; +import com.clevertap.android.sdk.network.NetworkManager; +import com.clevertap.android.sdk.product_config.CTProductConfigController; +import com.clevertap.android.sdk.validation.Validator; +import java.util.ArrayList; +import java.util.Iterator; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +public class ARPResponse extends CleverTapResponseDecorator { + + private final CTProductConfigController ctProductConfigController; + + private final CleverTapResponse cleverTapResponse; + + private final CleverTapInstanceConfig config; + + private final Logger logger; + + private final NetworkManager networkManager; + + private final Validator validator; + + public ARPResponse(CleverTapResponse cleverTapResponse, CleverTapInstanceConfig config, + NetworkManager networkManager, + Validator validator, ControllerManager controllerManager) { + this.cleverTapResponse = cleverTapResponse; + this.config = config; + ctProductConfigController = controllerManager.getCTProductConfigController(); + logger = this.config.getLogger(); + this.networkManager = networkManager; + this.validator = validator; + } + + @Override + public void processResponse(final JSONObject response, final String stringBody, final Context context) { + // Handle "arp" (additional request parameters) + try { + if (response.has("arp")) { + final JSONObject arp = (JSONObject) response.get("arp"); + if (arp.length() > 0) { + if (ctProductConfigController != null) { + ctProductConfigController.setArpValue(arp); + } + //Handle Discarded events in ARP + try { + processDiscardedEventsList(arp); + } catch (Throwable t) { + logger + .verbose("Error handling discarded events response: " + t.getLocalizedMessage()); + } + handleARPUpdate(context, arp); + } + } + } catch (Throwable t) { + logger.verbose(config.getAccountId(), "Failed to process ARP", t); + } + + // process Console response + cleverTapResponse.processResponse(response, stringBody, context); + } + + //Saves ARP directly to new namespace + private void handleARPUpdate(final Context context, final JSONObject arp) { + if (arp == null || arp.length() == 0) { + return; + } + + final String nameSpaceKey = networkManager.getNewNamespaceARPKey(); + if (nameSpaceKey == null) { + return; + } + + final SharedPreferences prefs = StorageHelper.getPreferences(context, nameSpaceKey); + final SharedPreferences.Editor editor = prefs.edit(); + + final Iterator keys = arp.keys(); + while (keys.hasNext()) { + final String key = keys.next(); + try { + final Object o = arp.get(key); + if (o instanceof Number) { + final int update = ((Number) o).intValue(); + editor.putInt(key, update); + } else if (o instanceof String) { + if (((String) o).length() < 100) { + editor.putString(key, (String) o); + } else { + logger.verbose(config.getAccountId(), + "ARP update for key " + key + " rejected (string value too long)"); + } + } else if (o instanceof Boolean) { + editor.putBoolean(key, (Boolean) o); + } else { + logger + .verbose(config.getAccountId(), + "ARP update for key " + key + " rejected (invalid data type)"); + } + } catch (JSONException e) { + // Ignore + } + } + logger.verbose(config.getAccountId(), + "Stored ARP for namespace key: " + nameSpaceKey + " values: " + arp.toString()); + StorageHelper.persist(editor); + } + + /** + * Dashboard has a feature where marketers can discard event. We get that list in the ARP response, + * SDK then checks if the event is in the discarded list before sending it to LC + * + * @param response response from server + */ + private void processDiscardedEventsList(JSONObject response) { + if (!response.has(Constants.DISCARDED_EVENT_JSON_KEY)) { + logger.verbose(config.getAccountId(), "ARP doesn't contain the Discarded Events key"); + return; + } + + try { + ArrayList discardedEventsList = new ArrayList<>(); + JSONArray discardedEventsArray = response.getJSONArray(Constants.DISCARDED_EVENT_JSON_KEY); + + if (discardedEventsArray != null) { + for (int i = 0; i < discardedEventsArray.length(); i++) { + discardedEventsList.add(discardedEventsArray.getString(i)); + } + } + if (validator != null) { + validator.setDiscardedEvents(discardedEventsList); + } else { + logger.verbose(config.getAccountId(), "Validator object is NULL"); + } + } catch (JSONException e) { + logger + .verbose(config.getAccountId(), "Error parsing discarded events list" + e.getLocalizedMessage()); + } + } +} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/response/BaseResponse.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/BaseResponse.java new file mode 100644 index 000000000..a0a6a837a --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/BaseResponse.java @@ -0,0 +1,58 @@ +package com.clevertap.android.sdk.response; + +import android.content.Context; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import com.clevertap.android.sdk.DeviceInfo; +import com.clevertap.android.sdk.LocalDataStore; +import com.clevertap.android.sdk.Logger; +import com.clevertap.android.sdk.network.NetworkManager; +import org.json.JSONObject; + +public class BaseResponse extends CleverTapResponseDecorator { + + private final CleverTapResponse cleverTapResponse; + + private final CleverTapInstanceConfig config; + + private final LocalDataStore localDataStore; + + private final Logger logger; + + private final NetworkManager networkManager; + + public BaseResponse(Context context, CleverTapInstanceConfig config, + DeviceInfo deviceInfo, NetworkManager networkManager, LocalDataStore localDataStore, + CleverTapResponse cleverTapResponse) { + this.cleverTapResponse = cleverTapResponse; + this.config = config; + logger = this.config.getLogger(); + this.networkManager = networkManager; + this.localDataStore = localDataStore; + } + + @Override + public void processResponse(final JSONObject jsonBody, final String responseStr, final Context context) { + + if (responseStr == null) { + logger.verbose(config.getAccountId(), "Problem processing queue response, response is null"); + return; + } + try { + logger.verbose(config.getAccountId(), "Trying to process response: " + responseStr); + + JSONObject response = new JSONObject(responseStr); + // in app + cleverTapResponse.processResponse(response, responseStr, context); + + try { + localDataStore.syncWithUpstream(context, response); + } catch (Throwable t) { + logger.verbose(config.getAccountId(), "Failed to sync local cache with upstream", t); + } + + } catch (Throwable t) { + networkManager.incrementResponseFailureCount(); + logger.verbose(config.getAccountId(), "Problem process send queue response", t); + } + } +} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/response/CleverTapResponse.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/CleverTapResponse.java new file mode 100644 index 000000000..fe9453467 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/CleverTapResponse.java @@ -0,0 +1,16 @@ +package com.clevertap.android.sdk.response; + +import android.content.Context; +import android.util.Log; +import org.json.JSONObject; + +/** + * Abstract Response that will be wrapped by {@link CleverTapResponseDecorator} objects + */ +public abstract class CleverTapResponse { + + public void processResponse(final JSONObject jsonBody, final String stringBody, + final Context context) { + Log.i("CleverTapResponse", "Done processing response!"); + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/response/CleverTapResponseDecorator.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/CleverTapResponseDecorator.java new file mode 100644 index 000000000..81d0bfb73 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/CleverTapResponseDecorator.java @@ -0,0 +1,14 @@ +package com.clevertap.android.sdk.response; + +import android.content.Context; +import org.json.JSONObject; + +/** + * Abstract Decorator that will be used to decorate {@link CleverTapResponseHelper} + *
    Extend this class to create different kind of response + */ +abstract class CleverTapResponseDecorator extends CleverTapResponse { + + public abstract void processResponse(JSONObject jsonBody, String stringBody, + Context context); +} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/response/CleverTapResponseHelper.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/CleverTapResponseHelper.java new file mode 100644 index 000000000..dc8ac0b0c --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/CleverTapResponseHelper.java @@ -0,0 +1,10 @@ +package com.clevertap.android.sdk.response; + +/** + * The Concrete Response class whose object we’re going to dynamically add new behavior to. + * for ex:

    {@code}CleverTapResponse cleverTapResponse = new CleverTapResponseHelper();
    + *                 cleverTapResponse = new GeofenceResponse(cleverTapResponse);
    + *                 cleverTapResponse = new InAppResponse(cleverTapResponse);
    + *         
    + */ +public class CleverTapResponseHelper extends CleverTapResponse {} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/response/ConsoleResponse.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/ConsoleResponse.java new file mode 100644 index 000000000..cea5c86d1 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/ConsoleResponse.java @@ -0,0 +1,62 @@ +package com.clevertap.android.sdk.response; + +import android.content.Context; +import com.clevertap.android.sdk.CleverTapAPI; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import com.clevertap.android.sdk.Logger; +import org.json.JSONArray; +import org.json.JSONObject; + +public class ConsoleResponse extends CleverTapResponseDecorator { + + private final CleverTapResponse cleverTapResponse; + + private final CleverTapInstanceConfig config; + + + private final Logger logger; + + public ConsoleResponse(CleverTapResponse cleverTapResponse, CleverTapInstanceConfig config) { + this.cleverTapResponse = cleverTapResponse; + this.config = config; + logger = this.config.getLogger(); + } + + @Override + public void processResponse(final JSONObject response, final String stringBody, final Context context) { + // Handle "console" - print them as info to the console + try { + /** + * Console info is no longer used + * But the feature was to enable logs from LCr + */ + if (response.has("console")) { + final JSONArray console = (JSONArray) response.get("console"); + if (console.length() > 0) { + for (int i = 0; i < console.length(); i++) { + logger.debug(config.getAccountId(), console.get(i).toString()); + } + } + } + } catch (Throwable t) { + // Ignore + } + + // Handle server set debug level + try { + if (response.has("dbg_lvl")) { + final int debugLevel = response.getInt("dbg_lvl"); + if (debugLevel >= 0) { + CleverTapAPI.setDebugLevel(debugLevel); + logger.verbose(config.getAccountId(), + "Set debug level to " + debugLevel + " for this session (set by upstream)"); + } + } + } catch (Throwable t) { + // Ignore + } + + // process InBox Response + cleverTapResponse.processResponse(response, stringBody, context); + } +} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/response/DisplayUnitResponse.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/DisplayUnitResponse.java new file mode 100644 index 000000000..97af01d4d --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/DisplayUnitResponse.java @@ -0,0 +1,103 @@ +package com.clevertap.android.sdk.response; + +import android.content.Context; +import com.clevertap.android.sdk.BaseCallbackManager; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.ControllerManager; +import com.clevertap.android.sdk.Logger; +import com.clevertap.android.sdk.displayunits.CTDisplayUnitController; +import com.clevertap.android.sdk.displayunits.model.CleverTapDisplayUnit; +import java.util.ArrayList; +import org.json.JSONArray; +import org.json.JSONObject; + +public class DisplayUnitResponse extends CleverTapResponseDecorator { + + private final Object displayUnitControllerLock = new Object(); + + private final BaseCallbackManager callbackManager; + + private final CleverTapResponse cleverTapResponse; + + private final CleverTapInstanceConfig config; + + private final ControllerManager controllerManager; + + private final Logger logger; + + public DisplayUnitResponse(CleverTapResponse cleverTapResponse, + CleverTapInstanceConfig config, + BaseCallbackManager callbackManager, ControllerManager controllerManager) { + this.cleverTapResponse = cleverTapResponse; + this.config = config; + logger = this.config.getLogger(); + this.callbackManager = callbackManager; + this.controllerManager = controllerManager; + } + + //Logic for the processing of Display Unit response + + @Override + public void processResponse(final JSONObject response, final String stringBody, final Context context) { + + logger.verbose(config.getAccountId(), "Processing Display Unit items..."); + + if (config.isAnalyticsOnly()) { + logger.verbose(config.getAccountId(), + "CleverTap instance is configured to analytics only, not processing Display Unit response"); + // process feature flag response + cleverTapResponse.processResponse(response, stringBody, context); + return; + } + + // Adding response null check because this will get processed first in case of analytics + if (response == null) { + logger.verbose(config.getAccountId(), Constants.FEATURE_DISPLAY_UNIT + + "Can't parse Display Unit Response, JSON response object is null"); + return; + } + + if (!response.has(Constants.DISPLAY_UNIT_JSON_RESPONSE_KEY)) { + logger.verbose(config.getAccountId(), + Constants.FEATURE_DISPLAY_UNIT + "JSON object doesn't contain the Display Units key"); + // process feature flag response + cleverTapResponse.processResponse(response, stringBody, context); + return; + } + try { + logger + .verbose(config.getAccountId(), + Constants.FEATURE_DISPLAY_UNIT + "Processing Display Unit response"); + parseDisplayUnits(response.getJSONArray(Constants.DISPLAY_UNIT_JSON_RESPONSE_KEY)); + } catch (Throwable t) { + logger.verbose(config.getAccountId(), Constants.FEATURE_DISPLAY_UNIT + "Failed to parse response", t); + } + + // process feature flag response + cleverTapResponse.processResponse(response, stringBody, context); + } + + /** + * Parses the Display Units using the JSON response + * + * @param messages - Json array of Display Unit items + */ + private void parseDisplayUnits(JSONArray messages) { + if (messages == null || messages.length() == 0) { + logger.verbose(config.getAccountId(), + Constants.FEATURE_DISPLAY_UNIT + "Can't parse Display Units, jsonArray is either empty or null"); + return; + } + + synchronized (displayUnitControllerLock) {// lock to avoid multiple instance creation for controller + if (controllerManager.getCTDisplayUnitController() == null) { + controllerManager.setCTDisplayUnitController(new CTDisplayUnitController()); + } + } + ArrayList displayUnits = controllerManager.getCTDisplayUnitController() + .updateDisplayUnits(messages); + + callbackManager.notifyDisplayUnitsLoaded(displayUnits); + } +} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/response/FeatureFlagResponse.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/FeatureFlagResponse.java new file mode 100644 index 000000000..f51fed784 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/FeatureFlagResponse.java @@ -0,0 +1,76 @@ +package com.clevertap.android.sdk.response; + +import android.content.Context; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.ControllerManager; +import com.clevertap.android.sdk.Logger; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +public class FeatureFlagResponse extends CleverTapResponseDecorator { + + private final CleverTapResponse cleverTapResponse; + + private final CleverTapInstanceConfig config; + + private final Logger logger; + + private final ControllerManager controllerManager; + + public FeatureFlagResponse(CleverTapResponse cleverTapResponse, + CleverTapInstanceConfig config, ControllerManager controllerManager) { + this.cleverTapResponse = cleverTapResponse; + this.config = config; + logger = this.config.getLogger(); + this.controllerManager = controllerManager; + } + + @Override + public void processResponse(final JSONObject response, final String stringBody, final Context context) { + logger.verbose(config.getAccountId(), "Processing Feature Flags response..."); + + if (config.isAnalyticsOnly()) { + logger.verbose(config.getAccountId(), + "CleverTap instance is configured to analytics only, not processing Feature Flags response"); + // process product config response + cleverTapResponse.processResponse(response, stringBody, context); + return; + } + + if (response == null) { + logger.verbose(config.getAccountId(), + Constants.FEATURE_FLAG_UNIT + "Can't parse Feature Flags Response, JSON response object is null"); + return; + } + + if (!response.has(Constants.FEATURE_FLAG_JSON_RESPONSE_KEY)) { + logger.verbose(config.getAccountId(), + Constants.FEATURE_FLAG_UNIT + "JSON object doesn't contain the Feature Flags key"); + // process product config response + cleverTapResponse.processResponse(response, stringBody, context); + return; + } + try { + logger + .verbose(config.getAccountId(), + Constants.FEATURE_FLAG_UNIT + "Processing Feature Flags response"); + parseFeatureFlags(response.getJSONObject(Constants.FEATURE_FLAG_JSON_RESPONSE_KEY)); + } catch (Throwable t) { + logger.verbose(config.getAccountId(), Constants.FEATURE_FLAG_UNIT + "Failed to parse response", t); + } + + // process product config response + cleverTapResponse.processResponse(response, stringBody, context); + + } + + private void parseFeatureFlags(JSONObject responseKV) throws JSONException { + JSONArray kvArray = responseKV.getJSONArray(Constants.KEY_KV); + + if (kvArray != null && controllerManager.getCTFeatureFlagsController() != null) { + controllerManager.getCTFeatureFlagsController().updateFeatureFlags(responseKV); + } + } +} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/response/GeofenceResponse.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/GeofenceResponse.java new file mode 100644 index 000000000..c534d81e6 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/GeofenceResponse.java @@ -0,0 +1,78 @@ +package com.clevertap.android.sdk.response; + +import android.content.Context; +import com.clevertap.android.sdk.BaseCallbackManager; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.Logger; +import org.json.JSONObject; + +public class GeofenceResponse extends CleverTapResponseDecorator { + + private final BaseCallbackManager callbackManager; + + private final CleverTapResponse cleverTapResponse; + + private final CleverTapInstanceConfig config; + + private final Logger logger; + + public GeofenceResponse(final CleverTapResponse cleverTapResponse, + CleverTapInstanceConfig config, + BaseCallbackManager callbackManager) { + this.cleverTapResponse = cleverTapResponse; + this.config = config; + logger = this.config.getLogger(); + this.callbackManager = callbackManager; + } + + @Override + public void processResponse(final JSONObject response, final String stringBody, final Context context) { + logger.verbose(config.getAccountId(), "Processing GeoFences response..."); + + if (config.isAnalyticsOnly()) { + logger.verbose(config.getAccountId(), + "CleverTap instance is configured to analytics only, not processing geofence response"); + + // process further response + cleverTapResponse.processResponse(response, stringBody, context); + return; + } + + if (response == null) { + logger.verbose(config.getAccountId(), + Constants.LOG_TAG_GEOFENCES + "Can't parse Geofences Response, JSON response object is null"); + return; + } + + if (!response.has(Constants.GEOFENCES_JSON_RESPONSE_KEY)) { + logger.verbose(config.getAccountId(), + Constants.LOG_TAG_GEOFENCES + "JSON object doesn't contain the Geofences key"); + // process further response + cleverTapResponse.processResponse(response, stringBody, context); + return; + } + try { + if (callbackManager.getGeofenceCallback() != null) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("geofences", response.getJSONArray(Constants.GEOFENCES_JSON_RESPONSE_KEY)); + + logger + .verbose(config.getAccountId(), + Constants.LOG_TAG_GEOFENCES + "Processing Geofences response"); + callbackManager.getGeofenceCallback().handleGeoFences(jsonObject); + } else { + logger.debug(config.getAccountId(), + Constants.LOG_TAG_GEOFENCES + "Geofence SDK has not been initialized to handle the response"); + } + } catch (Throwable t) { + logger + .verbose(config.getAccountId(), + Constants.LOG_TAG_GEOFENCES + "Failed to handle Geofences response", t); + } + + // process further response + cleverTapResponse.processResponse(response, stringBody, context); + + } +} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/response/InAppResponse.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/InAppResponse.java new file mode 100644 index 000000000..268f62f7f --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/InAppResponse.java @@ -0,0 +1,132 @@ +package com.clevertap.android.sdk.response; + +import android.content.Context; +import android.content.SharedPreferences; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.ControllerManager; +import com.clevertap.android.sdk.Logger; +import com.clevertap.android.sdk.StorageHelper; +import com.clevertap.android.sdk.task.CTExecutorFactory; +import com.clevertap.android.sdk.task.Task; +import java.util.concurrent.Callable; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +public class InAppResponse extends CleverTapResponseDecorator { + + private final CleverTapResponse cleverTapResponse; + + private final CleverTapInstanceConfig config; + + private final ControllerManager controllerManager; + + private final boolean isSendTest; + + private final Logger logger; + + public InAppResponse(CleverTapResponse cleverTapResponse, CleverTapInstanceConfig config, + ControllerManager controllerManager, final boolean isSendTest) { + this.cleverTapResponse = cleverTapResponse; + this.config = config; + logger = this.config.getLogger(); + this.controllerManager = controllerManager; + this.isSendTest = isSendTest; + } + + @Override + public void processResponse(final JSONObject response, final String stringBody, final Context context) { + try { + + if (config.isAnalyticsOnly()) { + logger.verbose(config.getAccountId(), + "CleverTap instance is configured to analytics only, not processing inapp messages"); + // process metadata response + cleverTapResponse.processResponse(response, stringBody, context); + return; + } + + logger.verbose(config.getAccountId(), "InApp: Processing response"); + + if (!response.has("inapp_notifs")) { + logger.verbose(config.getAccountId(), + "InApp: Response JSON object doesn't contain the inapp key, failing"); + // process metadata response + cleverTapResponse.processResponse(response, stringBody, context); + return; + } + + int perSession = 10; + int perDay = 10; + if (response.has(Constants.INAPP_MAX_PER_SESSION) && response + .get(Constants.INAPP_MAX_PER_SESSION) instanceof Integer) { + perSession = response.getInt(Constants.INAPP_MAX_PER_SESSION); + } + + if (response.has("imp") && response.get("imp") instanceof Integer) { + perDay = response.getInt("imp"); + } + + if (!isSendTest && controllerManager.getInAppFCManager() != null) { + Logger.v("Updating InAppFC Limits"); + controllerManager.getInAppFCManager().updateLimits(context, perDay, perSession); + controllerManager.getInAppFCManager().processResponse(context, response);// Handle stale_inapp + } + + JSONArray inappNotifs; + try { + inappNotifs = response.getJSONArray(Constants.INAPP_JSON_RESPONSE_KEY); + } catch (JSONException e) { + logger.debug(config.getAccountId(), "InApp: In-app key didn't contain a valid JSON array"); + // process metadata response + cleverTapResponse.processResponse(response, stringBody, context); + return; + } + + // Add all the new notifications to the queue + SharedPreferences prefs = StorageHelper.getPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + try { + JSONArray inappsFromPrefs = new JSONArray( + StorageHelper.getStringFromPrefs(context, config, Constants.INAPP_KEY, "[]")); + + // Now add the rest of them :) + if (inappNotifs != null && inappNotifs.length() > 0) { + for (int i = 0; i < inappNotifs.length(); i++) { + try { + JSONObject inappNotif = inappNotifs.getJSONObject(i); + inappsFromPrefs.put(inappNotif); + } catch (JSONException e) { + Logger.v("InAppManager: Malformed inapp notification"); + } + } + } + + // Commit all the changes + editor.putString(StorageHelper.storageKeyWithSuffix(config, Constants.INAPP_KEY), + inappsFromPrefs.toString()); + StorageHelper.persist(editor); + } catch (Throwable e) { + logger.verbose(config.getAccountId(), "InApp: Failed to parse the in-app notifications properly"); + logger.verbose(config.getAccountId(), "InAppManager: Reason: " + e.getMessage(), e); + } + // Fire the first notification, if any + Task task = CTExecutorFactory.executors(config) + .postAsyncSafelyTask(Constants.TAG_FEATURE_IN_APPS); + task.execute("InAppResponse#processResponse", new Callable() { + @Override + public Void call() { + controllerManager.getInAppController().showNotificationIfAvailable(context); + return null; + } + }); + } catch (Throwable t) { + Logger.v("InAppManager: Failed to parse response", t); + } + + // process metadata response + cleverTapResponse.processResponse(response, stringBody, context); + + } +} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/response/InboxResponse.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/InboxResponse.java new file mode 100644 index 000000000..a16b7878f --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/InboxResponse.java @@ -0,0 +1,86 @@ +package com.clevertap.android.sdk.response; + +import android.content.Context; +import com.clevertap.android.sdk.BaseCallbackManager; +import com.clevertap.android.sdk.CTLockManager; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.ControllerManager; +import com.clevertap.android.sdk.Logger; +import org.json.JSONArray; +import org.json.JSONObject; + +public class InboxResponse extends CleverTapResponseDecorator { + + private final Object inboxControllerLock; + + private final BaseCallbackManager callbackManager; + + private final CleverTapResponse cleverTapResponse; + + private final CleverTapInstanceConfig config; + + private final Logger logger; + + private final ControllerManager controllerManager; + + public InboxResponse(CleverTapResponse cleverTapResponse, CleverTapInstanceConfig config, + CTLockManager ctLockManager, + final BaseCallbackManager callbackManager, ControllerManager controllerManager) { + this.cleverTapResponse = cleverTapResponse; + this.config = config; + this.callbackManager = callbackManager; + logger = this.config.getLogger(); + inboxControllerLock = ctLockManager.getInboxControllerLock(); + this.controllerManager = controllerManager; + } + + //NotificationInbox + @Override + public void processResponse(final JSONObject response, final String stringBody, final Context context) { + + if (config.isAnalyticsOnly()) { + logger.verbose(config.getAccountId(), + "CleverTap instance is configured to analytics only, not processing inbox messages"); + + // process PushAmp response + cleverTapResponse.processResponse(response, stringBody, context); + + return; + } + + logger.verbose(config.getAccountId(), "Inbox: Processing response"); + + if (!response.has(Constants.INBOX_JSON_RESPONSE_KEY)) { + logger.verbose(config.getAccountId(), "Inbox: Response JSON object doesn't contain the inbox key"); + // process PushAmp response + cleverTapResponse.processResponse(response, stringBody, context); + return; + } + try { + _processInboxMessages(response.getJSONArray(Constants.INBOX_JSON_RESPONSE_KEY)); + } catch (Throwable t) { + logger.verbose(config.getAccountId(), "InboxResponse: Failed to parse response", t); + } + + // process PushAmp response + cleverTapResponse.processResponse(response, stringBody, context); + + } + + + // always call async + private void _processInboxMessages(JSONArray messages) { + synchronized (inboxControllerLock) { + if (controllerManager.getCTInboxController() == null) { + controllerManager.initializeInbox(); + } + if (controllerManager.getCTInboxController() != null) { + boolean update = controllerManager.getCTInboxController().updateMessages(messages); + if (update) { + callbackManager._notifyInboxMessagesDidUpdate(); + } + } + } + } +} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/response/MetadataResponse.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/MetadataResponse.java new file mode 100644 index 000000000..2d55829ef --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/MetadataResponse.java @@ -0,0 +1,70 @@ +package com.clevertap.android.sdk.response; + +import android.content.Context; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import com.clevertap.android.sdk.DeviceInfo; +import com.clevertap.android.sdk.Logger; +import com.clevertap.android.sdk.network.NetworkManager; +import org.json.JSONObject; + +public class MetadataResponse extends CleverTapResponseDecorator { + + + private final CleverTapResponse cleverTapResponse; + + private final CleverTapInstanceConfig config; + + private final DeviceInfo deviceInfo; + + private final Logger logger; + + private final NetworkManager networkManager; + + public MetadataResponse(CleverTapResponse cleverTapResponse, CleverTapInstanceConfig config, + DeviceInfo deviceInfo, + NetworkManager networkManager) { + this.cleverTapResponse = cleverTapResponse; + this.config = config; + logger = this.config.getLogger(); + this.deviceInfo = deviceInfo; + this.networkManager = networkManager; + } + + + @Override + public void processResponse(final JSONObject response, final String stringBody, final Context context) { + // Always look for a GUID in the response, and if present, then perform a force update + try { + if (response.has("g")) { + final String deviceID = response.getString("g"); + deviceInfo.forceUpdateDeviceId(deviceID); + logger.verbose(config.getAccountId(), "Got a new device ID: " + deviceID); + } + } catch (Throwable t) { + logger.verbose(config.getAccountId(), "Failed to update device ID!", t); + } + + // Handle i + try { + if (response.has("_i")) { + final long i = response.getLong("_i"); + networkManager.setI(context, i); + } + } catch (Throwable t) { + // Ignore + } + + // Handle j + try { + if (response.has("_j")) { + final long j = response.getLong("_j"); + networkManager.setJ(context, j); + } + } catch (Throwable t) { + // Ignore + } + + // process ARP response + cleverTapResponse.processResponse(response, stringBody, context); + } +} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/response/ProductConfigResponse.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/ProductConfigResponse.java new file mode 100644 index 000000000..659aa1478 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/ProductConfigResponse.java @@ -0,0 +1,96 @@ +package com.clevertap.android.sdk.response; + +import android.content.Context; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.ControllerManager; +import com.clevertap.android.sdk.CoreMetaData; +import com.clevertap.android.sdk.Logger; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +public class ProductConfigResponse extends CleverTapResponseDecorator { + + private final CleverTapResponse cleverTapResponse; + + private final CleverTapInstanceConfig config; + + private final CoreMetaData coreMetaData; + + private final Logger logger; + + private final ControllerManager controllerManager; + + public ProductConfigResponse(CleverTapResponse cleverTapResponse, + CleverTapInstanceConfig config, + CoreMetaData coreMetaData, ControllerManager controllerManager) { + this.cleverTapResponse = cleverTapResponse; + this.config = config; + logger = this.config.getLogger(); + this.coreMetaData = coreMetaData; + this.controllerManager = controllerManager; + } + + @Override + public void processResponse(final JSONObject response, final String stringBody, final Context context) { + logger.verbose(config.getAccountId(), "Processing Product Config response..."); + + if (config.isAnalyticsOnly()) { + logger.verbose(config.getAccountId(), + "CleverTap instance is configured to analytics only, not processing Product Config response"); + // process geofence response + cleverTapResponse.processResponse(response, stringBody, context); + return; + } + + if (response == null) { + logger.verbose(config.getAccountId(), Constants.LOG_TAG_PRODUCT_CONFIG + + "Can't parse Product Config Response, JSON response object is null"); + onProductConfigFailed(); + return; + } + + if (!response.has(Constants.REMOTE_CONFIG_FLAG_JSON_RESPONSE_KEY)) { + logger.verbose(config.getAccountId(), + Constants.LOG_TAG_PRODUCT_CONFIG + "JSON object doesn't contain the Product Config key"); + onProductConfigFailed(); + // process geofence response + cleverTapResponse.processResponse(response, stringBody, context); + return; + } + try { + logger + .verbose(config.getAccountId(), + Constants.LOG_TAG_PRODUCT_CONFIG + "Processing Product Config response"); + parseProductConfigs(response.getJSONObject(Constants.REMOTE_CONFIG_FLAG_JSON_RESPONSE_KEY)); + } catch (Throwable t) { + onProductConfigFailed(); + logger.verbose(config.getAccountId(), + Constants.LOG_TAG_PRODUCT_CONFIG + "Failed to parse Product Config response", t); + } + + // process geofence response + cleverTapResponse.processResponse(response, stringBody, context); + + } + + private void onProductConfigFailed() { + if (coreMetaData.isProductConfigRequested()) { + if (controllerManager.getCTProductConfigController() != null) { + controllerManager.getCTProductConfigController().onFetchFailed(); + } + coreMetaData.setProductConfigRequested(false); + } + } + + private void parseProductConfigs(JSONObject responseKV) throws JSONException { + JSONArray kvArray = responseKV.getJSONArray(Constants.KEY_KV); + + if (kvArray != null && controllerManager.getCTProductConfigController() != null) { + controllerManager.getCTProductConfigController().onFetchSuccess(responseKV); + } else { + onProductConfigFailed(); + } + } +} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/response/PushAmpResponse.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/PushAmpResponse.java new file mode 100644 index 000000000..38a7ffd66 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/response/PushAmpResponse.java @@ -0,0 +1,145 @@ +package com.clevertap.android.sdk.response; + +import static com.clevertap.android.sdk.utils.CTJsonConverter.getRenderedTargetList; + +import android.content.Context; +import android.os.Bundle; +import com.clevertap.android.sdk.BaseCallbackManager; +import com.clevertap.android.sdk.CTLockManager; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.ControllerManager; +import com.clevertap.android.sdk.Logger; +import com.clevertap.android.sdk.db.BaseDatabaseManager; +import com.clevertap.android.sdk.db.DBAdapter; +import com.clevertap.android.sdk.pushnotification.PushProviders; +import java.util.Iterator; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +public class PushAmpResponse extends CleverTapResponseDecorator { + + private final BaseCallbackManager callbackManager; + + private final CleverTapResponse cleverTapResponse; + + private final CleverTapInstanceConfig config; + + private final Context context; + + private final DBAdapter dbAdapter; + + private final Logger logger; + + private final ControllerManager controllerManager; + + public PushAmpResponse(CleverTapResponse cleverTapResponse, + Context context, + CleverTapInstanceConfig config, + BaseDatabaseManager dbManager, + BaseCallbackManager callbackManager, + ControllerManager controllerManager) { + this.cleverTapResponse = cleverTapResponse; + this.context = context; + this.config = config; + logger = this.config.getLogger(); + dbAdapter = dbManager.loadDBAdapter(context); + this.callbackManager = callbackManager; + this.controllerManager = controllerManager; + } + + @Override + public void processResponse(final JSONObject response, final String stringBody, final Context context) { + //Handle Push Amplification response + if (config.isAnalyticsOnly()) { + logger.verbose(config.getAccountId(), + "CleverTap instance is configured to analytics only, not processing push amp response"); + + // process Display Unit response + cleverTapResponse.processResponse(response, stringBody, context); + + return; + } + try { + if (response.has("pushamp_notifs")) { + logger.verbose(config.getAccountId(), "Processing pushamp messages..."); + JSONObject pushAmpObject = response.getJSONObject("pushamp_notifs"); + final JSONArray pushNotifications = pushAmpObject.getJSONArray("list"); + if (pushNotifications.length() > 0) { + logger.verbose(config.getAccountId(), "Handling Push payload locally"); + handlePushNotificationsInResponse(pushNotifications); + } + if (pushAmpObject.has("pf")) { + try { + int frequency = pushAmpObject.getInt("pf"); + controllerManager.getPushProviders().updatePingFrequencyIfNeeded(context, frequency); + } catch (Throwable t) { + logger + .verbose("Error handling ping frequency in response : " + t.getMessage()); + } + + } + if (pushAmpObject.has("ack")) { + boolean ack = pushAmpObject.getBoolean("ack"); + logger.verbose("Received ACK -" + ack); + if (ack) { + JSONArray rtlArray = getRenderedTargetList(dbAdapter); + String[] rtlStringArray = new String[0]; + if (rtlArray != null) { + rtlStringArray = new String[rtlArray.length()]; + } + for (int i = 0; i < rtlStringArray.length; i++) { + rtlStringArray[i] = rtlArray.getString(i); + } + logger.verbose("Updating RTL values..."); + dbAdapter.updatePushNotificationIds(rtlStringArray); + } + } + } + } catch (Throwable t) { + //Ignore + } + + // process Display Unit response + cleverTapResponse.processResponse(response, stringBody, context); + } + + //PN + @SuppressWarnings("rawtypes") + private void handlePushNotificationsInResponse(JSONArray pushNotifications) { + try { + for (int i = 0; i < pushNotifications.length(); i++) { + Bundle pushBundle = new Bundle(); + JSONObject pushObject = pushNotifications.getJSONObject(i); + if (pushObject.has("wzrk_ttl")) { + pushBundle.putLong("wzrk_ttl", pushObject.getLong("wzrk_ttl")); + } + + Iterator iterator = pushObject.keys(); + while (iterator.hasNext()) { + String key = iterator.next().toString(); + pushBundle.putString(key, pushObject.getString(key)); + } + if (!pushBundle.isEmpty() && !dbAdapter + .doesPushNotificationIdExist(pushObject.getString("wzrk_pid"))) { + logger.verbose("Creating Push Notification locally"); + if (callbackManager.getPushAmpListener() != null) { + callbackManager.getPushAmpListener().onPushAmpPayloadReceived(pushBundle); + } else { + controllerManager.getPushProviders() + ._createNotification(context, pushBundle, Constants.EMPTY_NOTIFICATION_ID); + } + } else { + logger.verbose(config.getAccountId(), + "Push Notification already shown, ignoring local notification :" + pushObject + .getString("wzrk_pid")); + } + } + } catch (JSONException e) { + logger.verbose(config.getAccountId(), "Error parsing push notification JSON"); + } + } + + +} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/task/CTExecutorFactory.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/task/CTExecutorFactory.java new file mode 100644 index 000000000..112b0c49d --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/task/CTExecutorFactory.java @@ -0,0 +1,34 @@ +package com.clevertap.android.sdk.task; + +import com.clevertap.android.sdk.CleverTapInstanceConfig; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Factory class to create & cache Executors{@link CTExecutors} + * Every account has it's dedicated Executor + */ +public class CTExecutorFactory { + + private static final Map executorMap = Collections + .synchronizedMap(new HashMap()); + + public static CTExecutors executors(CleverTapInstanceConfig config) { + if (config == null) { + throw new IllegalArgumentException("Can't create task for null config"); + } + CTExecutors executorForAccount = executorMap.get(config.getAccountId()); + if (executorForAccount == null) { + synchronized (CTExecutorFactory.class) { + executorForAccount = executorMap.get(config.getAccountId()); + if (executorForAccount == null) { + executorForAccount = new CTExecutors(config); + executorMap.put(config.getAccountId(), executorForAccount); + } + } + } + return executorForAccount; + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/task/CTExecutors.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/task/CTExecutors.java new file mode 100644 index 000000000..09e7b41bf --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/task/CTExecutors.java @@ -0,0 +1,106 @@ +package com.clevertap.android.sdk.task; + +import androidx.annotation.RestrictTo; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import java.util.HashMap; +import java.util.concurrent.Executor; + +/** + * Global executor pools per account. + *

    + * Grouping tasks like this avoids the effects of task starvation (e.g. disk reads don't wait behind + * webservice requests). + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public class CTExecutors { + + public final IOExecutor IO_EXECUTOR = new IOExecutor(); + + public final MainThreadExecutor MAIN_EXECUTOR = new MainThreadExecutor(); + + public final MainThreadExecutor DEFAULT_CALLBACK_EXECUTOR = MAIN_EXECUTOR; + + protected final CleverTapInstanceConfig config; + + private final HashMap postAsyncSafelyTasks = new HashMap<>(); + + CTExecutors(CleverTapInstanceConfig config) { + this.config = config; + } + + /** + * Use this task when you want to offload some background task + * @param + * @return + */ + public Task ioTask() { + return taskOnExecutorWithName(IO_EXECUTOR, DEFAULT_CALLBACK_EXECUTOR, "ioTask"); + } + + /** + * Use this task to execute a runnable to main thread + * @param + * @return + */ + + public Task mainTask() { + return taskOnExecutorWithName(MAIN_EXECUTOR, DEFAULT_CALLBACK_EXECUTOR, "Main"); + } + + /** + * Use this task to execute a job in a sequential fashion for a particular feature + * @param featureTag - name of the feature. e.g we have separate single pool executor for InApps & Geofences etc. + * @param + * @return + */ + public Task postAsyncSafelyTask(String featureTag) { + if (featureTag == null) { + throw new IllegalArgumentException("Tag can't be null"); + } + PostAsyncSafelyExecutor postAsyncSafelyExecutor = postAsyncSafelyTasks.get(featureTag); + + if (postAsyncSafelyExecutor == null) { + postAsyncSafelyExecutor = new PostAsyncSafelyExecutor(); + postAsyncSafelyTasks.put(featureTag, postAsyncSafelyExecutor); + } + return taskOnExecutorWithName(postAsyncSafelyExecutor, DEFAULT_CALLBACK_EXECUTOR, "PostAsyncSafely"); + } + + /** + * Common single thread pool for a particular account. + * Use this for general purpose single pipe-lining of jobs. + * @param + * @return + */ + public Task postAsyncSafelyTask() { + return postAsyncSafelyTask(config.getAccountId()); + } + + /** + * Use this task to use your own custom executor for executing jobs & getting callbacks on default executors + * @param taskExecutor - executor on which the provided task will be executed + * @param taskName - custom name for the task (e.g we have main, iotask , postasycnsafely) + * @param + * @return - task + */ + public Task taskOnExecutor(Executor taskExecutor, String taskName) { + return taskOnExecutorWithName(taskExecutor, DEFAULT_CALLBACK_EXECUTOR, taskName); + } + + /** + * Use this task to use your own custom executor for executing jobs & getting callbacks on the provided callback executors + * @param taskExecutor - executor on which the provided task will be executed + * @param callbackExecutor - executor on which the callbacks will be executed + * @param taskName - custom name for the task (e.g we have main, iotask , postasycnsafely) + * @param + * @return - task + */ + public Task taskOnExecutorWithName(Executor taskExecutor, + Executor callbackExecutor, String taskName) { + if (taskExecutor == null || callbackExecutor == null) { + throw new IllegalArgumentException("Can't create task " + + taskName + " with null executors"); + } + return new Task<>(config, taskExecutor, callbackExecutor, taskName); + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/task/Executable.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/task/Executable.java new file mode 100644 index 000000000..d7a55ea0c --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/task/Executable.java @@ -0,0 +1,17 @@ +package com.clevertap.android.sdk.task; + +import java.util.concurrent.Executor; + +/** + * Stuffs which can be executed on an executor with certain input parameter + * @param + */ +abstract class Executable { + protected final Executor executor; + + Executable(final Executor executor) { + this.executor = executor; + } + + abstract void execute(final TResult input); +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/task/FailureExecutable.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/task/FailureExecutable.java new file mode 100644 index 000000000..2b6a9a0be --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/task/FailureExecutable.java @@ -0,0 +1,33 @@ +package com.clevertap.android.sdk.task; + +import java.util.concurrent.Executor; + +/** + * Wrapper class to execute runnable after a task is failed. + * Ref{@link OnFailureListener} + * + * @param + */ +class FailureExecutable extends Executable { + + public OnFailureListener getFailureListener() { + return failureListener; + } + + private final OnFailureListener failureListener; + + public FailureExecutable(final Executor executor, final OnFailureListener listener) { + super(executor); + failureListener = listener; + } + + @Override + void execute(final TResult input) { + executor.execute(new Runnable() { + @Override + public void run() { + failureListener.onFailure(input); + } + }); + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/task/IOExecutor.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/task/IOExecutor.java new file mode 100644 index 000000000..783902f9e --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/task/IOExecutor.java @@ -0,0 +1,96 @@ +package com.clevertap.android.sdk.task; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Executor service pool containing more threads to offload the IO tasks. + */ +class IOExecutor implements ExecutorService { + + private final int numCores = Runtime.getRuntime().availableProcessors(); + + public void setExecutorService(final ExecutorService executorService) { + this.executorService = executorService; + } + + ExecutorService executorService = new ThreadPoolExecutor(numCores * 2, numCores * 2, + 60L, TimeUnit.SECONDS, new LinkedBlockingQueue()); + + @Override + public boolean awaitTermination(final long timeout, final TimeUnit unit) throws InterruptedException { + return executorService.awaitTermination(timeout, unit); + } + + @Override + public void execute(final Runnable command) { + executorService.execute(command); + } + + @Override + public List> invokeAll(final Collection> tasks) throws InterruptedException { + return executorService.invokeAll(tasks); + } + + @Override + public List> invokeAll(final Collection> tasks, final long timeout, + final TimeUnit unit) + throws InterruptedException { + return executorService.invokeAll(tasks, timeout, unit); + } + + @Override + public T invokeAny(final Collection> tasks) + throws ExecutionException, InterruptedException { + return executorService.invokeAny(tasks); + } + + @Override + public T invokeAny(final Collection> tasks, final long timeout, final TimeUnit unit) + throws ExecutionException, InterruptedException, TimeoutException { + return executorService.invokeAny(tasks, timeout, unit); + } + + @Override + public boolean isShutdown() { + return executorService.isShutdown(); + } + + @Override + public boolean isTerminated() { + return executorService.isTerminated(); + } + + @Override + public void shutdown() { + executorService.shutdown(); + } + + @Override + public List shutdownNow() { + return executorService.shutdownNow(); + } + + @Override + public Future submit(final Callable task) { + return executorService.submit(task); + } + + @Override + public Future submit(final Runnable task, final T result) { + return executorService.submit(task, result); + } + + @Override + public Future submit(final Runnable task) { + return executorService.submit(task); + } +} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/task/MainLooperHandler.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/task/MainLooperHandler.java new file mode 100644 index 000000000..ca12327c8 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/task/MainLooperHandler.java @@ -0,0 +1,23 @@ +package com.clevertap.android.sdk.task; + +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; + +@RestrictTo(Scope.LIBRARY) +public class MainLooperHandler extends Handler { + + public Runnable getPendingRunnable() { + return pendingRunnable; + } + + private Runnable pendingRunnable = null; + + public void setPendingRunnable(final Runnable pendingRunnable) { + this.pendingRunnable = pendingRunnable; + } + public MainLooperHandler() { + super(Looper.getMainLooper()); + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/task/MainThreadExecutor.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/task/MainThreadExecutor.java new file mode 100644 index 000000000..6c9beb10a --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/task/MainThreadExecutor.java @@ -0,0 +1,23 @@ +package com.clevertap.android.sdk.task; + +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.NonNull; +import java.util.concurrent.Executor; + +/** + * Executor service to delegate runnables to Main Thread. + */ +public class MainThreadExecutor implements Executor { + + void setMainThreadHandler(final Handler mainThreadHandler) { + this.mainThreadHandler = mainThreadHandler; + } + + Handler mainThreadHandler = new Handler(Looper.getMainLooper()); + + @Override + public void execute(@NonNull Runnable command) { + mainThreadHandler.post(command); + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/task/OnFailureListener.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/task/OnFailureListener.java new file mode 100644 index 000000000..46d89eb16 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/task/OnFailureListener.java @@ -0,0 +1,10 @@ +package com.clevertap.android.sdk.task; + +/** + * Interface to provide failure callbacks + * @param + */ +public interface OnFailureListener { + + void onFailure(TResult result); +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/task/OnSuccessListener.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/task/OnSuccessListener.java new file mode 100644 index 000000000..b81f3d6bd --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/task/OnSuccessListener.java @@ -0,0 +1,9 @@ +package com.clevertap.android.sdk.task; + +/** + * Interface to provide success callback + * @param + */ +public interface OnSuccessListener { + void onSuccess(TResult result); +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/task/PostAsyncSafelyExecutor.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/task/PostAsyncSafelyExecutor.java new file mode 100644 index 000000000..f1af91bc3 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/task/PostAsyncSafelyExecutor.java @@ -0,0 +1,139 @@ +package com.clevertap.android.sdk.task; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.RunnableFuture; +import java.util.concurrent.TimeUnit; + +/** + * Post async safely executor is nothing but a single thread pool executor + * It's a single pipeline to execute task sequentially + */ +class PostAsyncSafelyExecutor implements ExecutorService { + + private long EXECUTOR_THREAD_ID = 0; + + void setExecutor(final ExecutorService executor) { + this.executor = executor; + } + + ExecutorService executor = Executors.newSingleThreadExecutor(); + + @Override + public boolean awaitTermination(final long timeout, final TimeUnit unit) throws InterruptedException { + return executor.awaitTermination(timeout, unit); + } + + @Override + public void execute(final Runnable task) { + if (task == null) { + throw new NullPointerException("PostAsyncSafelyExecutor#execute: task can't ne null"); + } + final boolean executeSync = Thread.currentThread().getId() == EXECUTOR_THREAD_ID; + if (executeSync) { + task.run(); + } else { + executor.execute(new Runnable() { + @Override + public void run() { + EXECUTOR_THREAD_ID = Thread.currentThread().getId(); + task.run(); + } + }); + } + } + + @Override + public List> invokeAll(final Collection> tasks) throws UnsupportedOperationException { + throw new UnsupportedOperationException("PostAsyncSafelyExecutor#invokeAll: This method is not supported"); + } + + @Override + public List> invokeAll(final Collection> tasks, final long timeout, + final TimeUnit unit) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("PostAsyncSafelyExecutor#invokeAll: This method is not supported"); + } + + @Override + public T invokeAny(final Collection> tasks) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("PostAsyncSafelyExecutor#invokeAny: This method is not supported"); + } + + @Override + public T invokeAny(final Collection> tasks, final long timeout, final TimeUnit unit) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("PostAsyncSafelyExecutor#invokeAny: This method is not supported"); + } + + @Override + public boolean isShutdown() { + return executor.isShutdown(); + } + + @Override + public boolean isTerminated() { + return executor.isTerminated(); + } + + @Override + public void shutdown() { + executor.shutdown(); + } + + @Override + public List shutdownNow() { + return executor.shutdownNow(); + } + + @Override + public Future submit(final Callable task) { + if (task == null) { + throw new NullPointerException("PostAsyncSafelyExecutor#submit: task can't ne null"); + } + Future future = null; + final boolean executeSync = Thread.currentThread().getId() == EXECUTOR_THREAD_ID; + if (executeSync) { + try { + task.call(); + } catch (Exception e) { + e.printStackTrace(); + } + } else { + future = executor.submit(new Callable() { + @Override + public T call() throws Exception { + EXECUTOR_THREAD_ID = Thread.currentThread().getId(); + return task.call(); + } + }); + } + return future; + } + + @Override + public Future submit(final Runnable task, final T result) { + if (task == null) { + throw new NullPointerException("PostAsyncSafelyExecutor#submit: task can't ne null"); + } + RunnableFuture futureTask = new FutureTask<>(task, result); + execute(futureTask); + return futureTask; + } + + @Override + public Future submit(final Runnable task) { + if (task == null) { + throw new NullPointerException("PostAsyncSafelyExecutor#submit: task can't ne null"); + } + RunnableFuture futureTask = new FutureTask<>(task, null); + execute(futureTask); + return futureTask; + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/task/SuccessExecutable.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/task/SuccessExecutable.java new file mode 100644 index 000000000..4028fe8eb --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/task/SuccessExecutable.java @@ -0,0 +1,36 @@ +package com.clevertap.android.sdk.task; + +import com.clevertap.android.sdk.CleverTapInstanceConfig; + +import java.util.concurrent.Executor; + +/** + * Wrapper class to execute runnable after a task is successful. + * Ref: {@link OnSuccessListener} + * + * @param + */ +class SuccessExecutable extends Executable { + + private final OnSuccessListener successListener; + + protected SuccessExecutable(final Executor executor, OnSuccessListener listener, + final CleverTapInstanceConfig config) { + super(executor); + successListener = listener; + } + + public OnSuccessListener getSuccessListener() { + return successListener; + } + + @Override + void execute(final TResult input) { + executor.execute(new Runnable() { + @Override + public void run() { + successListener.onSuccess(input); + } + }); + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/task/Task.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/task/Task.java new file mode 100644 index 000000000..ab42c5385 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/task/Task.java @@ -0,0 +1,222 @@ +package com.clevertap.android.sdk.task; + +import androidx.annotation.NonNull; + +import com.clevertap.android.sdk.CleverTapInstanceConfig; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + +/** + * Definition of task is to execute some work & return success or failure callbacks + * + * @param + */ +public class Task { + + protected enum STATE {FAILED, SUCCESS, READY_TO_RUN, RUNNING} + + protected final CleverTapInstanceConfig config; + + protected final Executor defaultCallbackExecutor; + + protected final Executor executor; + + protected final List> failureExecutables = new ArrayList<>(); + + protected TResult result; + + protected final List> successExecutables = new ArrayList<>(); + + protected STATE taskState = STATE.READY_TO_RUN; + private final String taskName; + + Task(final CleverTapInstanceConfig config, Executor executor, + final Executor defaultCallbackExecutor, final String taskName) { + this.executor = executor; + this.defaultCallbackExecutor = defaultCallbackExecutor; + this.config = config; + this.taskName = taskName; + } + + /** + * Register listener to get failure callbacks on the provided executor + * + * @param executor - executor on which the failure callback will be called + * @param listener- failure listener + * @return task + */ + @NonNull + public synchronized Task addOnFailureListener(@NonNull final Executor executor, + final OnFailureListener listener) { + if (listener != null) { + failureExecutables.add(new FailureExecutable<>(executor, listener)); + } + return this; + } + + /** + * Register listener to get failure callbacks on main thread + * + * @param listener- failure listener + * @return task + */ + @NonNull + public Task addOnFailureListener(@NonNull OnFailureListener listener) { + return addOnFailureListener(defaultCallbackExecutor, listener); + } + + /** + * Register listener to get success callbacks on the provided executor + * + * @param executor - executor on which the success callback will be called + * @param listener- success listener + * @return task + */ + @NonNull + public Task addOnSuccessListener(@NonNull final Executor executor, + final OnSuccessListener listener) { + if (listener != null) { + successExecutables.add(new SuccessExecutable<>(executor, listener, config)); + } + return this; + } + + /** + * Register listener to get success callbacks on main thread + * + * @param listener- success listener + * @return task + */ + @NonNull + public Task addOnSuccessListener(@NonNull OnSuccessListener listener) { + return addOnSuccessListener(defaultCallbackExecutor, listener); + } + + /** + * Simple method to execute the task + * + * @param logTag - tag name to identify the task state in logs. + * @param callable - piece of code to run + */ + public void execute(final String logTag, final Callable callable) { + executor.execute(newRunnableForTask(logTag, callable)); + } + + /** + * Returns the state of task + * Ref{@link STATE} + * + * @return + */ + public boolean isSuccess() { + return taskState == STATE.SUCCESS; + } + + /*** + * Removes the failure listener from the task. + * @param listener - failure listener + * @return task + */ + @SuppressWarnings("unused") + @NonNull + public Task removeOnFailureListener(@NonNull OnFailureListener listener) { + Iterator> iterator = failureExecutables.iterator(); + while (iterator.hasNext()) { + FailureExecutable item = iterator.next(); + if (item.getFailureListener() == listener) { + iterator.remove(); + } + } + return this; + } + + /*** + * Removes the Success listener from the task. + * @param listener - success listener + * @return task + */ + + @SuppressWarnings("unused") + @NonNull + public Task removeOnSuccessListener(@NonNull OnSuccessListener listener) { + Iterator> iterator = successExecutables.iterator(); + while (iterator.hasNext()) { + SuccessExecutable item = iterator.next(); + if (item.getSuccessListener() == listener) { + iterator.remove(); + } + } + return this; + } + + /** + * Use this method in-case we need future task for the execution + * + * @param logTag - tag name to identify the task state in logs. + * @param callable - piece of code to run + */ + public Future submit(final String logTag, final Callable callable) { + if (!(executor instanceof ExecutorService)) { + throw new UnsupportedOperationException( + "Can't use this method without ExecutorService, Use Execute alternatively "); + } + return ((ExecutorService) executor).submit(newRunnableForTask(logTag, callable)); + } + + void onFailure(final Exception e) { + setState(STATE.FAILED); + for (Executable failureExecutable : failureExecutables) { + failureExecutable.execute(e); + } + } + + void onSuccess(final TResult result) { + setState(STATE.SUCCESS); + setResult(result); + for (Executable successExecutable : successExecutables) { + successExecutable.execute(this.result); + } + } + + void setResult(final TResult result) { + this.result = result; + } + + void setState(final STATE taskState) { + this.taskState = taskState; + } + + /** + * Wraps the provided piece of code in runnable to execute on executor. + * + * @param logTag - tag with which this task can be identified in the logs. + * @param callable - piece of code to run. + * @return Runnable + */ + private Runnable newRunnableForTask(final String logTag, final Callable callable) { + return new Runnable() { + @Override + public void run() { + try { + config.getLogger() + .verbose(taskName + " Task: " + logTag + " starting on..." + Thread.currentThread().getName()); + TResult result = callable.call(); + config.getLogger().verbose( + taskName + " Task: " + logTag + " executed successfully on..." + Thread.currentThread().getName()); + onSuccess(result); + } catch (Exception e) { + onFailure(e); + config.getLogger().verbose( + taskName + " Task: " + logTag + " failed to execute on..." + Thread.currentThread().getName(), e); + e.printStackTrace(); + } + } + }; + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTJsonConverter.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/CTJsonConverter.java similarity index 67% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/CTJsonConverter.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/utils/CTJsonConverter.java index e02a3bfb0..c79ef8d98 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTJsonConverter.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/CTJsonConverter.java @@ -1,10 +1,20 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.utils; import android.location.Location; import android.os.Bundle; +import androidx.annotation.NonNull; import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.DeviceInfo; +import com.clevertap.android.sdk.Logger; +import com.clevertap.android.sdk.db.DBAdapter; +import com.clevertap.android.sdk.inapp.CTInAppNotification; +import com.clevertap.android.sdk.inbox.CTInboxMessage; +import com.clevertap.android.sdk.validation.ValidationResult; +import java.util.ArrayList; import java.util.Iterator; +import java.util.List; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -12,7 +22,6 @@ @RestrictTo(Scope.LIBRARY) public class CTJsonConverter { - public static JSONObject toJsonObject(String json, Logger logger, String accountId) { JSONObject cache = null; if (json != null) { @@ -27,7 +36,20 @@ public static JSONObject toJsonObject(String json, Logger logger, String account return (cache != null) ? cache : new JSONObject(); } - static JSONObject from(DeviceInfo deviceInfo, Location locationFromUser, boolean enableNetworkInfoReporting + public static JSONObject displayUnitFromExtras(Bundle extras) throws JSONException { + JSONObject r = new JSONObject(); + + String pushJsonPayload = extras.getString(Constants.DISPLAY_UNIT_PREVIEW_PUSH_PAYLOAD_KEY); + Logger.v("Received Display Unit via push payload: " + pushJsonPayload); + JSONArray displayUnits = new JSONArray(); + r.put(Constants.DISPLAY_UNIT_JSON_RESPONSE_KEY, displayUnits); + JSONObject testPushObject = new JSONObject(pushJsonPayload); + displayUnits.put(testPushObject); + + return r; + } + + public static JSONObject from(DeviceInfo deviceInfo, Location locationFromUser, boolean enableNetworkInfoReporting , boolean deviceIsMultiUser) throws JSONException { final JSONObject evtData = new JSONObject(); @@ -99,7 +121,7 @@ static JSONObject from(DeviceInfo deviceInfo, Location locationFromUser, boolean } //Validation - static JSONObject getErrorObject(ValidationResult vr) { + public static JSONObject getErrorObject(ValidationResult vr) { JSONObject error = new JSONObject(); try { error.put("c", vr.getErrorCode()); @@ -110,7 +132,7 @@ static JSONObject getErrorObject(ValidationResult vr) { return error; } - static JSONArray getRenderedTargetList(DBAdapter dbAdapter) { + public static JSONArray getRenderedTargetList(DBAdapter dbAdapter) { String[] pushIds = dbAdapter.fetchPushNotificationIds(); JSONArray renderedTargets = new JSONArray(); for (String pushId : pushIds) { @@ -120,22 +142,7 @@ static JSONArray getRenderedTargetList(DBAdapter dbAdapter) { return renderedTargets; } - static JSONObject getWzrkFields(CTInAppNotification root) throws JSONException { - final JSONObject fields = new JSONObject(); - JSONObject jsonObject = root.getJsonDescription(); - Iterator iterator = jsonObject.keys(); - - while (iterator.hasNext()) { - String keyName = iterator.next(); - if (keyName.startsWith(Constants.WZRK_PREFIX)) { - fields.put(keyName, jsonObject.get(keyName)); - } - } - - return fields; - } - - static JSONObject getWzrkFields(Bundle root) throws JSONException { + public static JSONObject getWzrkFields(Bundle root) throws JSONException { final JSONObject fields = new JSONObject(); for (String s : root.keySet()) { final Object o = root.get(s); @@ -154,11 +161,48 @@ static JSONObject getWzrkFields(Bundle root) throws JSONException { return fields; } - static JSONObject getWzrkFields(CTInboxMessage root) { + public static JSONObject getWzrkFields(CTInAppNotification root) throws JSONException { + final JSONObject fields = new JSONObject(); + JSONObject jsonObject = root.getJsonDescription(); + Iterator iterator = jsonObject.keys(); + + while (iterator.hasNext()) { + String keyName = iterator.next(); + if (keyName.startsWith(Constants.WZRK_PREFIX)) { + fields.put(keyName, jsonObject.get(keyName)); + } + } + + return fields; + } + + public static JSONObject getWzrkFields(CTInboxMessage root) { return root.getWzrkParams(); } - static String toJsonString(Object value) { + public static Object[] toArray(@NonNull JSONArray jsonArray) { + Object[] array = new Object[jsonArray.length()]; + try { + for (int i = 0; i < jsonArray.length(); i++) { + array[i] = jsonArray.get(i); + } + } catch (JSONException e) { + e.printStackTrace(); + } + return array; + } + + public static JSONArray toJsonArray(@NonNull List list) { + JSONArray array = new JSONArray(); + for (Object item : list) { + if (item != null) { + array.put(item); + } + } + return array; + } + + public static String toJsonString(Object value) { String val = null; try { @@ -170,4 +214,16 @@ static String toJsonString(Object value) { return val; } + public static ArrayList toList(@NonNull JSONArray array) { + ArrayList list = new ArrayList<>(); + for (int i = 0; i < array.length(); i++) { + try { + list.add(array.get(i)); + } catch (JSONException e) { + e.printStackTrace(); + } + } + return list; + } + } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/FileUtils.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/FileUtils.java new file mode 100644 index 000000000..331698f11 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/FileUtils.java @@ -0,0 +1,151 @@ +package com.clevertap.android.sdk.utils; + +import android.content.Context; +import android.text.TextUtils; +import androidx.annotation.NonNull; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; +import com.clevertap.android.sdk.CleverTapInstanceConfig; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import org.json.JSONObject; + +@RestrictTo(Scope.LIBRARY_GROUP) +public class FileUtils { + + private final CleverTapInstanceConfig config; + + private final Context context; + + public FileUtils(@NonNull final Context context, @NonNull final CleverTapInstanceConfig config) { + this.context = context; + this.config = config; + } + + public void deleteDirectory(String dirName) { + if (TextUtils.isEmpty(dirName)) { + return; + } + try { + synchronized (FileUtils.class) { + File file = new File(context.getFilesDir(), dirName); + if (file.exists() && file.isDirectory()) { + String[] children = file.list(); + for (String child : children) { + boolean deleted = new File(file, child).delete(); + config.getLogger().verbose(config.getAccountId(), + "File" + child + " isDeleted:" + deleted); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + config.getLogger().verbose(config.getAccountId(), + "writeFileOnInternalStorage: failed" + dirName + " Error:" + e.getLocalizedMessage()); + } + } + + public void deleteFile(String fileName) { + if (TextUtils.isEmpty(fileName)) { + return; + } + try { + synchronized (FileUtils.class) { + File file = new File(context.getFilesDir(), fileName); + if (file.exists()) { + if (file.delete()) { + config.getLogger().verbose(config.getAccountId(), "File Deleted:" + fileName); + } else { + config.getLogger().verbose(config.getAccountId(), "Failed to delete file" + fileName); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + config.getLogger().verbose(config.getAccountId(), + "writeFileOnInternalStorage: failed" + fileName + " Error:" + e.getLocalizedMessage()); + } + } + + public String readFromFile(String fileNameWithPath) throws IOException { + + String content = ""; + InputStream inputStream = null; + InputStreamReader inputStreamReader = null; + BufferedReader bufferedReader = null; + //Make sure to use a try-catch statement to catch any errors + try { + //Make your FilePath and File + String yourFilePath = context.getFilesDir() + "/" + fileNameWithPath; + File yourFile = new File(yourFilePath); + //Make an InputStream with your File in the constructor + inputStream = new FileInputStream(yourFile); + StringBuilder stringBuilder = new StringBuilder(); + //Check to see if your inputStream is null + //If it isn't use the inputStream to make a InputStreamReader + //Use that to make a BufferedReader + //Also create an empty String + inputStreamReader = new InputStreamReader(inputStream); + bufferedReader = new BufferedReader(inputStreamReader); + String receiveString; + //Use a while loop to append the lines from the Buffered reader + while ((receiveString = bufferedReader.readLine()) != null) { + stringBuilder.append(receiveString); + } + //Close your InputStream and save stringBuilder as a String + inputStream.close(); + content = stringBuilder.toString(); + } catch (Exception e) { + config.getLogger() + .verbose(config.getAccountId(), "[Exception While Reading: " + e.getLocalizedMessage()); + //Log your error with Log.e + }finally { + if (inputStream != null) { + inputStream.close(); + } + if (inputStreamReader != null) { + inputStreamReader.close(); + } + if (bufferedReader != null) { + bufferedReader.close(); + } + } + return content; + } + + public void writeJsonToFile(String dirName, + String fileName, JSONObject jsonObject) throws IOException { + FileWriter writer = null; + try { + if (jsonObject == null || TextUtils.isEmpty(dirName) || TextUtils.isEmpty(fileName)) { + return; + } + synchronized (FileUtils.class) { + File file = new File(context.getFilesDir(), dirName); + if (!file.exists()) { + if (!file.mkdir()) { + return;// if directory is not created don't proceed and return + } + } + + File file1 = new File(file, fileName); + writer = new FileWriter(file1, false); + writer.append(jsonObject.toString()); + writer.flush(); + } + } catch (Exception e) { + e.printStackTrace(); + config.getLogger().verbose(config.getAccountId(), + "writeFileOnInternalStorage: failed" + e.getLocalizedMessage()); + }finally { + if(writer != null){ + writer.close(); + } + } + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/ImageCache.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/ImageCache.java similarity index 91% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/ImageCache.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/utils/ImageCache.java index 42e2b0f0f..5d2d59491 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/ImageCache.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/ImageCache.java @@ -1,10 +1,12 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.utils; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.util.Base64; import android.util.LruCache; +import com.clevertap.android.sdk.Logger; +import com.clevertap.android.sdk.Utils; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; @@ -27,7 +29,7 @@ public class ImageCache { private static final String FILE_PREFIX = "CT_IMAGE_"; - private static LruCache mMemoryCache; + private static LruCache memoryCache; private static File imageFileDirectory; @@ -36,7 +38,7 @@ public class ImageCache { @SuppressWarnings("WeakerAccess") // only adds to mem cache, use getForFetchBitmap for disk cache support public static boolean addBitmap(String key, Bitmap bitmap) { - if (mMemoryCache == null) { + if (memoryCache == null) { return false; } if (getBitmapFromMemCache(key) == null) { @@ -49,7 +51,7 @@ public static boolean addBitmap(String key, Bitmap bitmap) { Logger.v("CleverTap.ImageCache: insufficient memory to add image: " + key); return false; } - mMemoryCache.put(key, bitmap); + memoryCache.put(key, bitmap); Logger.v("CleverTap.ImageCache: added image for key: " + key); } } @@ -61,7 +63,7 @@ public static boolean addBitmap(String key, Bitmap bitmap) { public static Bitmap getBitmap(String key) { synchronized (ImageCache.class) { if (key != null) { - return mMemoryCache == null ? null : mMemoryCache.get(key); + return memoryCache == null ? null : memoryCache.get(key); } else { return null; } @@ -85,11 +87,11 @@ public static Bitmap getOrFetchBitmap(String url) { public static void init() { synchronized (ImageCache.class) { - if (mMemoryCache == null) { + if (memoryCache == null) { Logger.v("CleverTap.ImageCache: init with max device memory: " + maxMemory + "KB and allocated cache size: " + cacheSize + "KB"); try { - mMemoryCache = new LruCache(cacheSize) { + memoryCache = new LruCache(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { // The cache size will be measured in kilobytes rather than @@ -113,7 +115,7 @@ public static void initWithPersistence(Context context) { } if (messageDigest == null) { try { - messageDigest = MessageDigest.getInstance("SHA1"); + messageDigest = MessageDigest.getInstance("SHA256"); } catch (NoSuchAlgorithmException e) { Logger.d( "CleverTap.ImageCache: image file system caching unavailable as SHA1 hash function not available on platform"); @@ -128,10 +130,10 @@ public static void removeBitmap(String key, boolean isPersisted) { if (isPersisted) { removeFromFileSystem(key); } - if (mMemoryCache == null) { + if (memoryCache == null) { return; } - mMemoryCache.remove(key); + memoryCache.remove(key); Logger.v("CleverTap.ImageCache: removed image for key: " + key); cleanup(); } @@ -141,7 +143,7 @@ private static void cleanup() { synchronized (ImageCache.class) { if (isEmpty()) { Logger.v("CTInAppNotification.ImageCache: cache is empty, removing it"); - mMemoryCache = null; + memoryCache = null; } } } @@ -166,13 +168,13 @@ private static Bitmap decodeImageFromFile(File file) { private static int getAvailableMemory() { synchronized (ImageCache.class) { - return mMemoryCache == null ? 0 : cacheSize - mMemoryCache.size(); + return memoryCache == null ? 0 : cacheSize - memoryCache.size(); } } private static Bitmap getBitmapFromMemCache(String key) { if (key != null) { - return mMemoryCache == null ? null : mMemoryCache.get(key); + return memoryCache == null ? null : memoryCache.get(key); } return null; } @@ -225,7 +227,7 @@ private static File getOrFetchAndWriteImageFile(String url) { private static boolean isEmpty() { synchronized (ImageCache.class) { - return mMemoryCache.size() <= 0; + return memoryCache.size() <= 0; } } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/PackageUtils.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/PackageUtils.java similarity index 97% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/PackageUtils.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/utils/PackageUtils.java index c27b1d716..938f7bb37 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/PackageUtils.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/PackageUtils.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.utils; import android.content.Context; import android.content.pm.PackageManager; diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/UriHelper.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/UriHelper.java similarity index 90% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/UriHelper.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/utils/UriHelper.java index c85de1384..e198e2b53 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/UriHelper.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/UriHelper.java @@ -1,15 +1,20 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.utils; import android.net.Uri; import android.net.UrlQuerySanitizer; import android.os.Bundle; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.Logger; import java.net.URLDecoder; import java.util.Set; import org.json.JSONObject; -final class UriHelper { +@RestrictTo(Scope.LIBRARY) +public final class UriHelper { - static Bundle getAllKeyValuePairs(String url, boolean encodeValues) { + public static Bundle getAllKeyValuePairs(String url, boolean encodeValues) { if (url == null) { return new Bundle(); } @@ -39,7 +44,7 @@ static Bundle getAllKeyValuePairs(String url, boolean encodeValues) { } /*package*/ - static JSONObject getUrchinFromUri(Uri uri) { + public static JSONObject getUrchinFromUri(Uri uri) { JSONObject referrer = new JSONObject(); try { diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/ManifestValidator.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/validation/ManifestValidator.java similarity index 75% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/ManifestValidator.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/validation/ManifestValidator.java index 4f4910b41..008c78a0c 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/ManifestValidator.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/validation/ManifestValidator.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.validation; import android.app.Application; import android.content.Context; @@ -6,6 +6,13 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; +import com.clevertap.android.sdk.ActivityLifecycleCallback; +import com.clevertap.android.sdk.CleverTapAPI; +import com.clevertap.android.sdk.DeviceInfo; +import com.clevertap.android.sdk.InAppNotificationActivity; +import com.clevertap.android.sdk.Logger; +import com.clevertap.android.sdk.Utils; +import com.clevertap.android.sdk.inbox.CTInboxActivity; import com.clevertap.android.sdk.pushnotification.CTNotificationIntentService; import com.clevertap.android.sdk.pushnotification.CTPushNotificationReceiver; import com.clevertap.android.sdk.pushnotification.PushConstants.PushType; @@ -15,11 +22,11 @@ import java.util.ArrayList; -final class ManifestValidator { +public final class ManifestValidator { private final static String ourApplicationClassName = "com.clevertap.android.sdk.Application"; - static void validate(final Context context, DeviceInfo deviceInfo, PushProviders pushProviders) { + public static void validate(final Context context, DeviceInfo deviceInfo, PushProviders pushProviders) { if (!Utils.hasPermission(context, "android.permission.INTERNET")) { Logger.d("Missing Permission: android.permission.INTERNET"); } @@ -55,29 +62,55 @@ private static void checkReceiversServices(final Context context, PushProviders CTBackgroundIntentService.class.getName()); validateActivityInManifest((Application) context.getApplicationContext(), InAppNotificationActivity.class); + validateActivityInManifest((Application) context.getApplicationContext(), + CTInboxActivity.class); + validateReceiverInManifest((Application) context.getApplicationContext(), + "com.clevertap.android.geofence.CTGeofenceReceiver"); + validateReceiverInManifest((Application) context.getApplicationContext(), + "com.clevertap.android.geofence.CTLocationUpdateReceiver"); + validateReceiverInManifest((Application) context.getApplicationContext(), + "com.clevertap.android.geofence.CTGeofenceBootReceiver"); } catch (Exception e) { Logger.v("Receiver/Service issue : " + e.toString()); - } ArrayList enabledPushTypes = pushProviders.getAvailablePushTypes(); if (enabledPushTypes == null) { return; } + for (PushType pushType : enabledPushTypes) { - //no-op if (pushType == PushType.FCM) { try { - // use class name string directly here to avoid class not found issues on class import, because we only use FCM + // use class name string directly here to avoid class not found issues on class import validateServiceInManifest((Application) context.getApplicationContext(), "com.clevertap.android.sdk.pushnotification.fcm.FcmMessageListenerService"); + } catch (Exception e) { + Logger.v("Receiver/Service issue : " + e.toString()); + + } catch (Error error) { + Logger.v("FATAL : " + error.getMessage()); + } + }else if(pushType == PushType.HPS){ + try { + // use class name string directly here to avoid class not found issues on class import validateServiceInManifest((Application) context.getApplicationContext(), - "com.clevertap.android.sdk.FcmTokenListenerService"); + "com.clevertap.android.hms.CTHmsMessageService"); } catch (Exception e) { Logger.v("Receiver/Service issue : " + e.toString()); } catch (Error error) { Logger.v("FATAL : " + error.getMessage()); + } + }else if(pushType == PushType.XPS){ + try { + // use class name string directly here to avoid class not found issues on class import + validateReceiverInManifest((Application) context.getApplicationContext(), + "com.clevertap.android.xps.XiaomiMessageReceiver"); + } catch (Exception e) { + Logger.v("Receiver/Service issue : " + e.toString()); + } catch (Error error) { + Logger.v("FATAL : " + error.getMessage()); } } } @@ -116,11 +149,11 @@ private static void validateReceiverInManifest(Application application, String r for (ActivityInfo activityInfo : receivers) { if (activityInfo.name.equals(receiverClassName)) { - Logger.i(receiverClassName.replaceFirst("com.clevertap.android.sdk.", "") + " is present"); + Logger.i(receiverClassName.replaceFirst("com.clevertap.android.", "") + " is present"); return; } } - Logger.i(receiverClassName.replaceFirst("com.clevertap.android.sdk.", "") + " not present"); + Logger.i(receiverClassName.replaceFirst("com.clevertap.android.", "") + " not present"); } private static void validateServiceInManifest(Application application, String serviceClassName) diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/ValidationResult.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/validation/ValidationResult.java similarity index 66% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/ValidationResult.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/validation/ValidationResult.java index d00fb44f2..ae0ef82a6 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/ValidationResult.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/validation/ValidationResult.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.validation; import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; @@ -15,33 +15,33 @@ public final class ValidationResult { private Object object; - ValidationResult(int errorCode, String errorDesc) { + public ValidationResult(int errorCode, String errorDesc) { this.errorCode = errorCode; this.errorDesc = errorDesc; } - ValidationResult() { + public ValidationResult() { this.errorCode = 0; } - int getErrorCode() { + public int getErrorCode() { return errorCode; } - void setErrorCode(int errorCode) { + public void setErrorCode(int errorCode) { this.errorCode = errorCode; } - String getErrorDesc() { + public String getErrorDesc() { return errorDesc; } - void setErrorDesc(String errorDesc) { - this.errorDesc = errorDesc; + public Object getObject() { + return object; } - Object getObject() { - return object; + public void setErrorDesc(String errorDesc) { + this.errorDesc = errorDesc; } void setObject(Object object) { diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/ValidationResultFactory.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/validation/ValidationResultFactory.java similarity index 98% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/ValidationResultFactory.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/validation/ValidationResultFactory.java index 80afa32bf..00524d7ca 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/ValidationResultFactory.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/validation/ValidationResultFactory.java @@ -1,7 +1,8 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.validation; import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; +import com.clevertap.android.sdk.Constants; /** * Groups all error messages in one place diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/ValidationResultStack.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/validation/ValidationResultStack.java similarity index 89% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/ValidationResultStack.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/validation/ValidationResultStack.java index a08f65faa..1236ddfd0 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/ValidationResultStack.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/validation/ValidationResultStack.java @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.validation; import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; @@ -7,7 +7,7 @@ @RestrictTo(Scope.LIBRARY) public class ValidationResultStack { - private static final Boolean pendingValidationResultsLock = true; + private static final Object pendingValidationResultsLock = new Object(); private ArrayList pendingValidationResults = new ArrayList<>(); @@ -34,7 +34,7 @@ public void pushValidationResult(ValidationResult vr) { } } - ValidationResult popValidationResult() { + public ValidationResult popValidationResult() { // really a shift ValidationResult vr = null; diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/Validator.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/validation/Validator.java similarity index 94% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/Validator.java rename to clevertap-core/src/main/java/com/clevertap/android/sdk/validation/Validator.java index 309ca7005..de73b6eed 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/Validator.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/validation/Validator.java @@ -1,5 +1,7 @@ -package com.clevertap.android.sdk; +package com.clevertap.android.sdk.validation; +import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.Logger; import java.util.ArrayList; import java.util.BitSet; import java.util.Date; @@ -12,9 +14,9 @@ /** * Provides methods to validate various entities. */ -final class Validator { +public final class Validator { - enum ValidationContext { + public enum ValidationContext { Profile(), Event() } @@ -24,9 +26,9 @@ private enum RestrictedMultiValueFields { Phone(), Age(), FBID(), GPID(), Birthday() } - static final String ADD_VALUES_OPERATION = "multiValuePropertyAddValues"; + public static final String ADD_VALUES_OPERATION = "multiValuePropertyAddValues"; - static final String REMOVE_VALUES_OPERATION = "multiValuePropertyRemoveValues"; + public static final String REMOVE_VALUES_OPERATION = "multiValuePropertyRemoveValues"; private static final String[] eventNameCharsNotAllowed = {".", ":", "$", "'", "\"", "\\"}; @@ -53,7 +55,7 @@ private enum RestrictedMultiValueFields { * @return The {@link ValidationResult} object containing the object, * and the error code(if any) */ - ValidationResult cleanEventName(String name) { + public ValidationResult cleanEventName(String name) { ValidationResult vr = new ValidationResult(); name = name.trim(); @@ -83,7 +85,7 @@ ValidationResult cleanEventName(String name) { * First calls cleanObjectKey * Known property keys are reserved for multi-value properties, subsequent validation is done for those */ - ValidationResult cleanMultiValuePropertyKey(String name) { + public ValidationResult cleanMultiValuePropertyKey(String name) { ValidationResult vr = cleanObjectKey(name); name = (String) vr.getObject(); @@ -118,7 +120,7 @@ ValidationResult cleanMultiValuePropertyKey(String name) { * @return The {@link ValidationResult} object containing the value, * and the error code(if any) */ - ValidationResult cleanMultiValuePropertyValue(String value) { + public ValidationResult cleanMultiValuePropertyValue(String value) { ValidationResult vr = new ValidationResult(); // trim whitespace and force lowercase @@ -156,7 +158,7 @@ ValidationResult cleanMultiValuePropertyValue(String value) { * @return The {@link ValidationResult} object containing the object, * and the error code(if any) */ - ValidationResult cleanObjectKey(String name) { + public ValidationResult cleanObjectKey(String name) { ValidationResult vr = new ValidationResult(); name = name.trim(); for (String x : objectKeyCharsNotAllowed) { @@ -187,7 +189,7 @@ ValidationResult cleanObjectKey(String name) { * @return The cleaned object */ @SuppressWarnings("unchecked") - ValidationResult cleanObjectValue(Object o, ValidationContext validationContext) + public ValidationResult cleanObjectValue(Object o, ValidationContext validationContext) throws IllegalArgumentException { ValidationResult vr = new ValidationResult(); // If it's any type of number, send it back @@ -292,7 +294,7 @@ ValidationResult cleanObjectValue(Object o, ValidationContext validationContext) * @param name The event name * @return Boolean indication whether the event name has been discarded from Dashboard */ - ValidationResult isEventDiscarded(String name) { + public ValidationResult isEventDiscarded(String name) { ValidationResult error = new ValidationResult(); if (name == null) { ValidationResult vr = ValidationResultFactory.create(510, Constants.EVENT_NAME_NULL); @@ -323,7 +325,7 @@ ValidationResult isEventDiscarded(String name) { * @param name The event name * @return Boolean indication whether the event name is restricted */ - ValidationResult isRestrictedEventName(String name) { + public ValidationResult isRestrictedEventName(String name) { ValidationResult error = new ValidationResult(); if (name == null) { ValidationResult vr = ValidationResultFactory.create(510, Constants.EVENT_NAME_NULL); @@ -358,7 +360,7 @@ ValidationResult isRestrictedEventName(String name) { * @return The {@link ValidationResult} object containing the merged value, * and the error code(if any) */ - ValidationResult mergeMultiValuePropertyForKey(JSONArray currentValues, JSONArray newValues, String action, + public ValidationResult mergeMultiValuePropertyForKey(JSONArray currentValues, JSONArray newValues, String action, String key) { ValidationResult vr = new ValidationResult(); boolean remove = REMOVE_VALUES_OPERATION.equals(action); diff --git a/clevertap-core/src/main/res/layout-land/inapp_cover.xml b/clevertap-core/src/main/res/layout-land/inapp_cover.xml index a6f5d48dd..04e34644b 100644 --- a/clevertap-core/src/main/res/layout-land/inapp_cover.xml +++ b/clevertap-core/src/main/res/layout-land/inapp_cover.xml @@ -77,7 +77,7 @@ - - - - - - - - - - diff --git a/clevertap-core/src/main/res/layout-land/inbox_carousel_text_layout.xml b/clevertap-core/src/main/res/layout-land/inbox_carousel_text_layout.xml index c2ff6a049..558d96766 100644 --- a/clevertap-core/src/main/res/layout-land/inbox_carousel_text_layout.xml +++ b/clevertap-core/src/main/res/layout-land/inbox_carousel_text_layout.xml @@ -16,7 +16,7 @@ android:baselineAligned="false" android:weightSum="2"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/clevertap-core/src/main/res/layout/inbox_carousel_text_layout.xml b/clevertap-core/src/main/res/layout/inbox_carousel_text_layout.xml index dd818d138..2806b3da7 100644 --- a/clevertap-core/src/main/res/layout/inbox_carousel_text_layout.xml +++ b/clevertap-core/src/main/res/layout/inbox_carousel_text_layout.xml @@ -20,7 +20,7 @@ android:orientation="vertical" android:visibility="visible"> - diff --git a/clevertap-core/src/main/res/layout/inbox_icon_message_layout.xml b/clevertap-core/src/main/res/layout/inbox_icon_message_layout.xml index afc0d3649..717efb9fa 100644 --- a/clevertap-core/src/main/res/layout/inbox_icon_message_layout.xml +++ b/clevertap-core/src/main/res/layout/inbox_icon_message_layout.xml @@ -69,13 +69,13 @@ android:layout_height="wrap_content" android:layout_below="@id/body_linear_layout"> - - - - - - - - - - - - - - - - - diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/CleverTapAPITest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/CleverTapAPITest.kt index 867c6cfed..314661dea 100644 --- a/clevertap-core/src/test/java/com/clevertap/android/sdk/CleverTapAPITest.kt +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/CleverTapAPITest.kt @@ -1,28 +1,525 @@ package com.clevertap.android.sdk -import android.app.Activity +import android.location.Location import android.os.Bundle +import com.clevertap.android.sdk.task.CTExecutorFactory +import com.clevertap.android.sdk.task.MockCTExecutors import com.clevertap.android.shared.test.BaseTestCase +import com.clevertap.android.shared.test.Constant +import org.json.JSONObject import org.junit.* +import org.junit.Assert.* import org.junit.runner.* import org.mockito.* +import org.mockito.Mockito.* import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class CleverTapAPITest : BaseTestCase() { + private lateinit var corestate: MockCoreState + @Before @Throws(Exception::class) override fun setUp() { super.setUp() + corestate = MockCoreState(application, cleverTapInstanceConfig) + } + + /* @Test + fun testActivity() { + val activity = mock(Activity::class.java) + val bundle = Bundle() + //create + activity.onCreate(bundle, null) + CleverTapAPI.onActivityCreated(activity, null) + }*/ + + @Test + fun testCleverTapAPI_constructor_when_InitialAppEnteredForegroundTime_greater_than_5_secs() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(any())).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + mockStatic(CleverTapFactory::class.java).use { + mockStatic(Utils::class.java).use { + + // Arrange + `when`(CleverTapFactory.getCoreState(application, cleverTapInstanceConfig, null)) + .thenReturn(corestate) + `when`(Utils.getNow()).thenReturn(Int.MAX_VALUE) + + CoreMetaData.setInitialAppEnteredForegroundTime(0) + + // Act + CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig) + + // Assert + assertTrue("isCreatedPostAppLaunch must be true", cleverTapInstanceConfig.isCreatedPostAppLaunch) + verify(corestate.sessionManager).setLastVisitTime() + verify(corestate.deviceInfo).setDeviceNetworkInfoReportingFromStorage() + verify(corestate.deviceInfo).setCurrentUserOptOutStateFromStorage() + val actualConfig = + StorageHelper.getString(application, "instance:" + cleverTapInstanceConfig.accountId, "") + assertEquals(cleverTapInstanceConfig.toJSONString(), actualConfig) + } + } + } + } + + @Test + fun testCleverTapAPI_constructor_when_InitialAppEnteredForegroundTime_less_than_5_secs() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(any())).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + mockStatic(Utils::class.java).use { + mockStatic(CleverTapFactory::class.java).use { + // Arrange + `when`(CleverTapFactory.getCoreState(application, cleverTapInstanceConfig, null)) + .thenReturn(corestate) + `when`(Utils.getNow()).thenReturn(0) + + CoreMetaData.setInitialAppEnteredForegroundTime(Int.MAX_VALUE) + + // Act + CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig) + + // Assert + assertFalse( + "isCreatedPostAppLaunch must be false", + cleverTapInstanceConfig.isCreatedPostAppLaunch + ) + verify(corestate.sessionManager).setLastVisitTime() + verify(corestate.deviceInfo).setDeviceNetworkInfoReportingFromStorage() + verify(corestate.deviceInfo).setCurrentUserOptOutStateFromStorage() + + val string = + StorageHelper.getString(application, "instance:" + cleverTapInstanceConfig.accountId, "") + assertEquals(cleverTapInstanceConfig.toJSONString(), string) + } + } + } + } + + @Test + fun test_setLocationForGeofences() { + val location = Location("") + location.apply { + latitude = 17.4444 + longitude = 4.444 + } + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(any())).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + + mockStatic(CleverTapFactory::class.java).use { + // Arrange + `when`(CleverTapFactory.getCoreState(application, cleverTapInstanceConfig, null)) + .thenReturn(corestate) + cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig) + cleverTapAPI.setLocationForGeofences(location, 45) + assertTrue(corestate.coreMetaData.isLocationForGeofence) + assertEquals(corestate.coreMetaData.geofenceSDKVersion, 45) + verify(corestate.locationManager)._setLocation(location) + } + } + } + + @Test + fun test_setGeofenceCallback() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(any())).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + mockStatic(CleverTapFactory::class.java).use { + // Arrange + `when`(CleverTapFactory.getCoreState(application, cleverTapInstanceConfig, null)) + .thenReturn(corestate) + cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig) + + val geofenceCallback = object : GeofenceCallback { + override fun handleGeoFences(jsonObject: JSONObject?) { + TODO("Not yet implemented") + } + + override fun triggerLocation() { + TODO("Not yet implemented") + } + } + + cleverTapAPI.geofenceCallback = geofenceCallback + + assertEquals(geofenceCallback, cleverTapAPI.geofenceCallback) + } + } + } + + @Test + fun test_pushGeoFenceError() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(any())).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + mockStatic(CleverTapFactory::class.java).use { + `when`(CleverTapFactory.getCoreState(application, cleverTapInstanceConfig, null)) + .thenReturn(corestate) + cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig) + + val expectedErrorCode = 999 + val expectedErrorMessage = "Fire in the hall" + + cleverTapAPI.pushGeoFenceError(expectedErrorCode, expectedErrorMessage) + + val actualValidationResult = corestate.validationResultStack.popValidationResult() + assertEquals(999, actualValidationResult.errorCode) + assertEquals("Fire in the hall", actualValidationResult.errorDesc) + } + } + } + + @Test + fun test_pushGeoFenceExitedEvent() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(any())).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + mockStatic(CleverTapFactory::class.java).use { + val argumentCaptor = + ArgumentCaptor.forClass( + JSONObject::class.java + ) + + `when`(CleverTapFactory.getCoreState(application, cleverTapInstanceConfig, null)) + .thenReturn(corestate) + cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig) + + val expectedJson = JSONObject("{\"key\":\"value\"}") + + cleverTapAPI.pushGeoFenceExitedEvent(expectedJson) + + verify(corestate.analyticsManager).raiseEventForGeofences( + ArgumentMatchers.anyString(), argumentCaptor.capture() + ) + + assertEquals(expectedJson, argumentCaptor.value) + } + } + } + + @Test + fun test_pushGeoFenceEnteredEvent() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(any())).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + mockStatic(CleverTapFactory::class.java).use { + `when`(CleverTapFactory.getCoreState(application, cleverTapInstanceConfig, null)) + .thenReturn(corestate) + cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig) + + val expectedJson = JSONObject("{\"key\":\"value\"}") + + cleverTapAPI.pushGeofenceEnteredEvent(expectedJson) + val argumentCaptor = + ArgumentCaptor.forClass( + JSONObject::class.java + ) + + verify(corestate.analyticsManager).raiseEventForGeofences( + ArgumentMatchers.anyString(), argumentCaptor.capture() + ) + + assertEquals(expectedJson, argumentCaptor.value) + } + } } @Test - fun testActivity() { - val activity = Mockito.mock(Activity::class.java) - val bundle = Bundle() - //create - activity.onCreate(bundle, null) - CleverTapAPI.onActivityCreated(activity, null) + fun test_changeCredentials_whenDefaultConfigNotNull_credentialsMustNotChange() { + mockStatic(CleverTapFactory::class.java).use { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(any())).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + `when`( + CleverTapFactory.getCoreState( + ArgumentMatchers.any(), + ArgumentMatchers.any(), + ArgumentMatchers.any() + ) + ) + .thenReturn(corestate) + CleverTapAPI.getDefaultInstance(application) + CleverTapAPI.changeCredentials("acct123", "token123", "eu") + val instance = ManifestInfo.getInstance(application) + assertNotEquals("acct123", instance.accountId) + assertNotEquals("token123", instance.acountToken) + assertNotEquals("eu", instance.accountRegion) + } + } + } + + @Test + fun test_changeCredentials_whenDefaultConfigNull_credentialsMustChange() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(any())).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + CleverTapAPI.defaultConfig = null + CleverTapAPI.changeCredentials("acct123", "token123", "eu") + val instance = ManifestInfo.getInstance(application) + assertEquals("acct123", instance.accountId) + assertEquals("token123", instance.acountToken) + assertEquals("eu", instance.accountRegion) + } + } + + @Test + fun test_createNotification_whenInstancesNull__createNotificationMustBeCalled() { + + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(any())).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + mockStatic(CleverTapFactory::class.java).use { + `when`(CTExecutorFactory.executors(any())).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + `when`( + CleverTapFactory.getCoreState( + ArgumentMatchers.any(), + ArgumentMatchers.any(), + ArgumentMatchers.any() + ) + ) + .thenReturn(corestate) + val bundle = Bundle() + //CleverTapAPI.getDefaultInstance(application) + //CleverTapAPI.setInstances(null) + CleverTapAPI.createNotification(application, bundle) + verify(corestate.pushProviders)._createNotification( + application, + bundle, + Constants.EMPTY_NOTIFICATION_ID + ) + } + } + } + + @Test + fun test_createNotification_whenInstanceNotNullAndAcctIDMatches__createNotificationMustBeCalled() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(any())).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + mockStatic(CleverTapFactory::class.java).use { + `when`( + CleverTapFactory.getCoreState( + ArgumentMatchers.any(), + ArgumentMatchers.any(), + ArgumentMatchers.any() + ) + ) + .thenReturn(corestate) + val bundle = Bundle() + bundle.putString(Constants.WZRK_ACCT_ID_KEY, Constant.ACC_ID) + CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig) + CleverTapAPI.createNotification(application, bundle) + verify(corestate.pushProviders)._createNotification( + application, + bundle, + Constants.EMPTY_NOTIFICATION_ID + ) + } + } + } + + @Test + fun test_createNotification_whenInstanceNotNullAndAcctIdDontMatch_createNotificationMustNotBeCalled() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(any())).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + mockStatic(CleverTapFactory::class.java).use { + `when`( + CleverTapFactory.getCoreState( + ArgumentMatchers.any(), + ArgumentMatchers.any(), + ArgumentMatchers.any() + ) + ) + .thenReturn(corestate) + val bundle = Bundle() + bundle.putString(Constants.WZRK_ACCT_ID_KEY, "acct123") + CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig) + CleverTapAPI.createNotification(application, bundle) + verify(corestate.pushProviders, never())._createNotification( + application, + bundle, + Constants.EMPTY_NOTIFICATION_ID + ) + } + } + } + + @Test + fun test_getNotificationInfoWhenExtrasNull_fromCleverTapMustBeFalse() { + + val notificationInfo = CleverTapAPI.getNotificationInfo(null) + assertFalse(notificationInfo.fromCleverTap) + } + + @Test + fun test_getNotificationInfoWhenNotificationTagMissing_fromCleverTapAndShouldRenderMustBeFalse() { + + val extras = Bundle() + //extras.putString(Constants.NOTIFICATION_TAG,"tag") + val notificationInfo = CleverTapAPI.getNotificationInfo(extras) + assertFalse(notificationInfo.fromCleverTap) + assertFalse(notificationInfo.shouldRender) + } + + @Test + fun test_getNotificationInfoWhenNMTagMissing_fromCleverTapMustBeTrueAndShouldRenderMustBeFalse() { + + val extras = Bundle() + extras.putString(Constants.NOTIFICATION_TAG, "tag") + val notificationInfo = CleverTapAPI.getNotificationInfo(extras) + assertTrue(notificationInfo.fromCleverTap) + assertFalse(notificationInfo.shouldRender) + } + + @Test + fun test_getNotificationInfoWhenNotificationTagAndNMTagPresent_fromCleverTapAndshouldRenderMustBeTrue() { + + val extras = Bundle() + extras.putString(Constants.NOTIFICATION_TAG, "tag") + extras.putString("nm", "nmTag") + val notificationInfo = CleverTapAPI.getNotificationInfo(extras) + assertTrue(notificationInfo.fromCleverTap) + assertTrue(notificationInfo.shouldRender) + } + + @Test + fun test_processPushNotification_whenInstancesNull__processCustomPushNotificationMustBeCalled() { + + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(any())).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + mockStatic(CleverTapFactory::class.java).use { + `when`( + CleverTapFactory.getCoreState( + ArgumentMatchers.any(), + ArgumentMatchers.any(), + ArgumentMatchers.any() + ) + ) + .thenReturn(corestate) + val bundle = Bundle() + //CleverTapAPI.getDefaultInstance(application) + //CleverTapAPI.setInstances(null) + CleverTapAPI.processPushNotification(application, bundle) + verify(corestate.pushProviders).processCustomPushNotification(bundle) + } + } + } + + @Test + fun test_processPushNotification_whenInstancesNotNull__processCustomPushNotificationMustBeCalled() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(any())).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + mockStatic(CleverTapFactory::class.java).use { + `when`( + CleverTapFactory.getCoreState( + ArgumentMatchers.any(), + ArgumentMatchers.any(), + ArgumentMatchers.any() + ) + ) + .thenReturn(corestate) + val bundle = Bundle() + bundle.putString(Constants.WZRK_ACCT_ID_KEY, Constant.ACC_ID) + CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig) + CleverTapAPI.processPushNotification(application, bundle) + verify(corestate.pushProviders).processCustomPushNotification(bundle) + } + } + } + +/* @Test + fun testPushDeepLink(){ + // Arrange + var cleverTapAPISpy : CleverTapAPI = Mockito.spy(cleverTapAPI) + val uri = Uri.parse("https://www.google.com/") + + //Act + cleverTapAPISpy.pushDeepLink(uri) + + //Assert + verify(cleverTapAPISpy).pushDeepLink(uri,false) + } + + @Test + fun testPushDeviceTokenEvent(){ + // Arrange + val ctAPI = CleverTapAPI.instanceWithConfig(application,cleverTapInstanceConfig) + var cleverTapAPISpy = Mockito.spy(ctAPI) + + cleverTapAPISpy.pushDeviceTokenEvent("12345",true,FCM) + verify(cleverTapAPISpy).queueEvent(ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.anyInt()) + } + + @Test + fun testPushLink(){ + var cleverTapAPISpy : CleverTapAPI = Mockito.spy(cleverTapAPI) + val uri = Uri.parse("https://www.google.com/") + + val mockStatic = Mockito.mockStatic(StorageHelper::class.java) + `when`(StorageHelper.getInt(ArgumentMatchers.any(), ArgumentMatchers.anyString(), ArgumentMatchers.anyInt())).thenReturn(0) + + //Act + cleverTapAPISpy.pushInstallReferrer("abc","def","ghi") + + verify(cleverTapAPISpy).pushDeepLink(ArgumentMatchers.any(), ArgumentMatchers.anyBoolean()) + }*/ + + @After + fun tearDown() { + CleverTapAPI.setInstances(null) // clear existing CleverTapAPI instances } } \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/EventQueueManagerTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/EventQueueManagerTest.kt new file mode 100644 index 000000000..97f868cf1 --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/EventQueueManagerTest.kt @@ -0,0 +1,865 @@ +package com.clevertap.android.sdk + +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkInfo.DetailedState +import com.clevertap.android.sdk.events.EventGroup.PUSH_NOTIFICATION_VIEWED +import com.clevertap.android.sdk.events.EventGroup.REGULAR +import com.clevertap.android.sdk.events.EventQueueManager +import com.clevertap.android.sdk.login.IdentityRepo +import com.clevertap.android.sdk.login.IdentityRepoFactory +import com.clevertap.android.sdk.login.LoginInfoProvider +import com.clevertap.android.sdk.network.NetworkManager +import com.clevertap.android.sdk.task.CTExecutorFactory +import com.clevertap.android.sdk.task.MockCTExecutors +import com.clevertap.android.sdk.validation.ValidationResult +import com.clevertap.android.shared.test.BaseTestCase +import org.json.JSONObject +import org.junit.* +import org.junit.runner.* +import org.mockito.* +import org.mockito.Mockito.* +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows.shadowOf +import org.robolectric.shadows.ShadowNetworkInfo +import java.util.TimeZone +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +@RunWith(RobolectricTestRunner::class) +class EventQueueManagerTest : BaseTestCase() { + + private lateinit var corestate: MockCoreState + private lateinit var eventQueueManager: EventQueueManager + private lateinit var json: JSONObject + + @Before + override fun setUp() { + super.setUp() + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + corestate = MockCoreState(application, cleverTapInstanceConfig) + eventQueueManager = + spy( + EventQueueManager( + corestate.databaseManager, + application, + cleverTapInstanceConfig, + corestate.eventMediator, + corestate.sessionManager, + corestate.callbackManager, + corestate.mainLooperHandler, + corestate.deviceInfo, + corestate.validationResultStack, + corestate.networkManager as NetworkManager, + corestate.coreMetaData, + corestate.ctLockManager, + corestate.localDataStore + ) + ) + json = JSONObject() + } + } + + @Test + fun test_queueEvent_will_not_add_to_queue_when_event_should_be_dropped() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + `when`(corestate.eventMediator.shouldDropEvent(json, Constants.PING_EVENT)) + .thenReturn(true) + eventQueueManager.queueEvent(application, json, Constants.PING_EVENT) + verify(eventQueueManager, never()).addToQueue(application, json, Constants.PING_EVENT) + } + } + + @Test + fun test_queueEvent_will_add_to_queue_when_event_should_not_be_dropped() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + `when`(corestate.eventMediator.shouldDropEvent(json, Constants.FETCH_EVENT)) + .thenReturn(false) + doNothing().`when`(eventQueueManager).addToQueue(application, json, Constants.FETCH_EVENT) + eventQueueManager.queueEvent(application, json, Constants.FETCH_EVENT) + verify(eventQueueManager).addToQueue(application, json, Constants.FETCH_EVENT) + } + } + + @Test + fun test_queueEvent_will_process_further_and_add_to_queue_when_event_should_not_be_dropped() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + `when`(corestate.eventMediator.shouldDropEvent(json, Constants.PROFILE_EVENT)) + .thenReturn(false) + doNothing().`when`(eventQueueManager).addToQueue(application, json, Constants.PROFILE_EVENT) + doNothing().`when`(eventQueueManager).pushInitialEventsAsync() + doNothing().`when`(corestate.sessionManager).lazyCreateSession(application) + + eventQueueManager.queueEvent(application, json, Constants.PROFILE_EVENT) + + verify(corestate.sessionManager).lazyCreateSession(application) + verify(eventQueueManager).pushInitialEventsAsync() + verify(eventQueueManager).addToQueue(application, json, Constants.PROFILE_EVENT) + } + } + + @Test + fun test_queueEvent_will_delay_add_to_queue_when_event_processing_should_be_delayed() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + val captor = ArgumentCaptor.forClass(Runnable::class.java) + `when`(corestate.eventMediator.shouldDropEvent(json, Constants.PROFILE_EVENT)) + .thenReturn(false) + + `when`(corestate.eventMediator.shouldDeferProcessingEvent(json, Constants.PROFILE_EVENT)) + .thenReturn(true) + doNothing().`when`(eventQueueManager).addToQueue(application, json, Constants.PROFILE_EVENT) + doNothing().`when`(eventQueueManager).pushInitialEventsAsync() + doNothing().`when`(corestate.sessionManager).lazyCreateSession(application) + + eventQueueManager.queueEvent(application, json, Constants.PROFILE_EVENT) + + verify(corestate.mainLooperHandler).postDelayed(captor.capture(), ArgumentMatchers.anyLong()) + + captor.value.run() + + verify(corestate.sessionManager).lazyCreateSession(application) + verify(eventQueueManager).pushInitialEventsAsync() + verify(eventQueueManager).addToQueue(application, json, Constants.PROFILE_EVENT) + } + } + + @Test + fun test_addToQueue_when_event_is_notification_viewed() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + doNothing().`when`(eventQueueManager).processPushNotificationViewedEvent(application, json) + + eventQueueManager.addToQueue(application, json, Constants.NV_EVENT) + + verify(eventQueueManager).processPushNotificationViewedEvent(application, json) + verify(eventQueueManager, never()).processEvent(application, json, Constants.NV_EVENT) + } + } + + @Test + fun test_addToQueue_when_event_is_not_notification_viewed() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + doNothing().`when`(eventQueueManager).processEvent(application, json, Constants.PROFILE_EVENT) + + eventQueueManager.addToQueue(application, json, Constants.PROFILE_EVENT) + + verify(eventQueueManager, never()).processPushNotificationViewedEvent(application, json) + verify(eventQueueManager).processEvent(application, json, Constants.PROFILE_EVENT) + } + } + + @Test + fun test_processPushNotificationViewedEvent_when_there_is_no_validation_error() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + val captor = ArgumentCaptor.forClass(Runnable::class.java) + corestate.coreMetaData.currentSessionId = 1000 + `when`(eventQueueManager.now).thenReturn(7000) + doNothing().`when`(eventQueueManager).flushQueueAsync(application, PUSH_NOTIFICATION_VIEWED) + + eventQueueManager.processPushNotificationViewedEvent(application, json) + + assertNull(json.optJSONObject(Constants.ERROR_KEY)) + assertEquals("event", json.getString("type")) + assertEquals(1000, json.getInt("s")) + assertEquals(7000, json.getInt("ep")) + + verify(corestate.databaseManager).queuePushNotificationViewedEventToDB(application, json) + verify(corestate.mainLooperHandler).removeCallbacks(captor.capture()) + verify(corestate.mainLooperHandler).post(captor.capture()) + + captor.value.run() + + verify(eventQueueManager).flushQueueAsync(application, PUSH_NOTIFICATION_VIEWED) + } + } + + @Test + fun test_processPushNotificationViewedEvent_when_there_is_validation_error() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + // Arrange + val validationResult = ValidationResult() + validationResult.errorDesc = "Fire in the hall" + validationResult.errorCode = 999 + + corestate.validationResultStack.pushValidationResult(validationResult) + + val captor = ArgumentCaptor.forClass(Runnable::class.java) + corestate.coreMetaData.currentSessionId = 1000 + `when`(eventQueueManager.now).thenReturn(7000) + doNothing().`when`(eventQueueManager).flushQueueAsync(application, PUSH_NOTIFICATION_VIEWED) + + // Act + eventQueueManager.processPushNotificationViewedEvent(application, json) + + // Assert + assertEquals(validationResult.errorCode, json.getJSONObject(Constants.ERROR_KEY)["c"]) + assertEquals(validationResult.errorDesc, json.getJSONObject(Constants.ERROR_KEY)["d"]) + assertEquals("event", json.getString("type")) + assertEquals(1000, json.getInt("s")) + assertEquals(7000, json.getInt("ep")) + + verify(corestate.databaseManager).queuePushNotificationViewedEventToDB(application, json) + verify(corestate.mainLooperHandler).removeCallbacks(captor.capture()) + verify(corestate.mainLooperHandler).post(captor.capture()) + + captor.value.run() + + verify(eventQueueManager).flushQueueAsync(application, PUSH_NOTIFICATION_VIEWED) + } + } + + @Test + fun test_pushInitialEventsAsync_does_not_pushBasicProfile_when_inCurrentSession() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + corestate.coreMetaData.currentSessionId = 10000 + doNothing().`when`(eventQueueManager).pushBasicProfile(null) + + eventQueueManager.pushInitialEventsAsync() + + verify(eventQueueManager, never()).pushBasicProfile(null) + } + } + + @Test + fun test_pushInitialEventsAsync_pushesBasicProfile_when_not_inCurrentSession() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + corestate.coreMetaData.currentSessionId = -1 + doNothing().`when`(eventQueueManager).pushBasicProfile(null) + + eventQueueManager.pushInitialEventsAsync() + + verify(eventQueueManager).pushBasicProfile(null) + } + } + + @Test + fun test_scheduleQueueFlush() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + // Arrange + + val captor = ArgumentCaptor.forClass(Runnable::class.java) + doNothing().`when`(eventQueueManager).flushQueueSync(ArgumentMatchers.any(), ArgumentMatchers.any()) + + // Act + eventQueueManager.scheduleQueueFlush(application) + + // Assert + verify(corestate.mainLooperHandler).removeCallbacks(captor.capture()) + verify(corestate.mainLooperHandler).postDelayed(captor.capture(), ArgumentMatchers.anyLong()) + + captor.value.run() + + verify(eventQueueManager).flushQueueSync(application, REGULAR) + verify(eventQueueManager).flushQueueSync(application, PUSH_NOTIFICATION_VIEWED) + } + } + + @Test + fun test_flush() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + doNothing().`when`(eventQueueManager).flushQueueAsync(application, REGULAR) + + eventQueueManager.flush() + + verify(eventQueueManager).flushQueueAsync(application, REGULAR) + } + } + + @Test + fun test_flushQueueSync_when_net_is_offline() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + val cm = application.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val shadowOfCM = shadowOf(cm) + shadowOfCM.setActiveNetworkInfo(null) // make offline + + eventQueueManager.flushQueueSync(application, PUSH_NOTIFICATION_VIEWED) + + verify(corestate.networkManager, never()).needsHandshakeForDomain(PUSH_NOTIFICATION_VIEWED) + } + } + + @Test + fun test_flushQueueSync_when_net_is_online_and_metadata_is_offline() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + corestate.coreMetaData.isOffline = true + val cm = application.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val shadowOfCM = shadowOf(cm) + val netInfo = + ShadowNetworkInfo.newInstance(DetailedState.CONNECTED, ConnectivityManager.TYPE_WIFI, 1, true, true) + shadowOfCM.setActiveNetworkInfo(netInfo) // make offline + + eventQueueManager.flushQueueSync(application, PUSH_NOTIFICATION_VIEWED) + + verify(corestate.networkManager, never()).needsHandshakeForDomain(PUSH_NOTIFICATION_VIEWED) + } + } + + @Test + fun test_flushQueueSync_when_HandshakeForDomain_not_needed() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + corestate.coreMetaData.isOffline = false + val cm = application.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val shadowOfCM = shadowOf(cm) + val netInfo = + ShadowNetworkInfo.newInstance(DetailedState.CONNECTED, ConnectivityManager.TYPE_WIFI, 1, true, true) + shadowOfCM.setActiveNetworkInfo(netInfo) // make offline + + eventQueueManager.flushQueueSync(application, PUSH_NOTIFICATION_VIEWED) + + verify(corestate.networkManager, never()).initHandshake(ArgumentMatchers.any(), ArgumentMatchers.any()) + verify(corestate.networkManager).flushDBQueue(application, PUSH_NOTIFICATION_VIEWED) + } + } + + @Test + fun test_flushQueueSync_when_HandshakeForDomain_is_needed() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + val captor = ArgumentCaptor.forClass(Runnable::class.java) + corestate.coreMetaData.isOffline = false + val cm = application.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val shadowOfCM = shadowOf(cm) + val netInfo = + ShadowNetworkInfo.newInstance(DetailedState.CONNECTED, ConnectivityManager.TYPE_WIFI, 1, true, true) + shadowOfCM.setActiveNetworkInfo(netInfo) // make offline + `when`(corestate.networkManager.needsHandshakeForDomain(PUSH_NOTIFICATION_VIEWED)).thenReturn(true) + + eventQueueManager.flushQueueSync(application, PUSH_NOTIFICATION_VIEWED) + + verify(corestate.networkManager).initHandshake(ArgumentMatchers.any(), captor.capture()) + + captor.value.run() + + verify(corestate.networkManager).flushDBQueue(application, PUSH_NOTIFICATION_VIEWED) + } + } + + @Test + fun test_pushBasicProfile_when_input_is_null() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + // Arrange + doReturn(null).`when`(eventQueueManager).queueEvent( + ArgumentMatchers.any(), + ArgumentMatchers.any(), ArgumentMatchers.anyInt() + ) + + val expectedDeviceId = "device_12345" + val expectedDeviceCarrier = "carrier_12345" + val expectedDeviceCC = "CC_12345" + val expectedDeviceTZ = "tz_12345" + + val expectedTZ = TimeZone.getDefault() + expectedTZ.id = expectedDeviceTZ + TimeZone.setDefault(expectedTZ) + + `when`(corestate.deviceInfo.deviceID).thenReturn(expectedDeviceId) + `when`(corestate.deviceInfo.carrier).thenReturn(expectedDeviceCarrier) + `when`(corestate.deviceInfo.countryCode).thenReturn(expectedDeviceCC) + + val captor = ArgumentCaptor.forClass(JSONObject::class.java) + val captorEventType = ArgumentCaptor.forClass(Int::class.java) + + // Act + eventQueueManager.pushBasicProfile(null) + + // Assert + verify(eventQueueManager).queueEvent(ArgumentMatchers.any(), captor.capture(), captorEventType.capture()) + val actualProfile = captor.value["profile"] as JSONObject + assertEquals(expectedDeviceCarrier, actualProfile["Carrier"]) + assertEquals(expectedDeviceCC, actualProfile["cc"]) + assertEquals(expectedDeviceTZ, actualProfile["tz"]) + } + } + + @Test + fun test_pushBasicProfile_when_carrier_or_cc_is_null() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + // Arrange + doReturn(null).`when`(eventQueueManager).queueEvent( + ArgumentMatchers.any(), + ArgumentMatchers.any(), ArgumentMatchers.anyInt() + ) + + val expectedDeviceId = "device_12345" + + `when`(corestate.deviceInfo.deviceID).thenReturn(expectedDeviceId) + `when`(corestate.deviceInfo.carrier).thenReturn(null) + `when`(corestate.deviceInfo.countryCode).thenReturn(null) + + val captor = ArgumentCaptor.forClass(JSONObject::class.java) + val captorEventType = ArgumentCaptor.forClass(Int::class.java) + + // Act + eventQueueManager.pushBasicProfile(null) + + // Assert + verify(eventQueueManager).queueEvent(ArgumentMatchers.any(), captor.capture(), captorEventType.capture()) + val actualProfile = captor.value["profile"] as JSONObject + assertNull(actualProfile.optString("Carrier", null)) + assertNull(actualProfile.optString("cc", null)) + } + } + + @Test + fun test_pushBasicProfile_when_carrier_or_cc_is_blank() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + // Arrange + doReturn(null).`when`(eventQueueManager).queueEvent( + ArgumentMatchers.any(), + ArgumentMatchers.any(), ArgumentMatchers.anyInt() + ) + + val expectedDeviceId = "device_12345" + + `when`(corestate.deviceInfo.deviceID).thenReturn(expectedDeviceId) + `when`(corestate.deviceInfo.carrier).thenReturn("") + `when`(corestate.deviceInfo.countryCode).thenReturn("") + + val captor = ArgumentCaptor.forClass(JSONObject::class.java) + val captorEventType = ArgumentCaptor.forClass(Int::class.java) + + // Act + eventQueueManager.pushBasicProfile(null) + + // Assert + verify(eventQueueManager).queueEvent(ArgumentMatchers.any(), captor.capture(), captorEventType.capture()) + val actualProfile = captor.value["profile"] as JSONObject + assertNull(actualProfile.optString("Carrier", null)) + assertNull(actualProfile.optString("cc", null)) + } + } + + @Test + fun test_pushBasicProfile_when_input_is_not_null() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + mockStatic(IdentityRepoFactory::class.java).use { + val mockIdentityRepo = mock(IdentityRepo::class.java) + `when`( + IdentityRepoFactory.getRepo( + application, + corestate.config, + corestate.deviceInfo, + corestate.validationResultStack + ) + ).thenReturn(mockIdentityRepo) + + // Arrange + doReturn(null).`when`(eventQueueManager).queueEvent( + ArgumentMatchers.any(), + ArgumentMatchers.any(), ArgumentMatchers.anyInt() + ) + + val expectedDeviceId = "device_12345" + + `when`(corestate.deviceInfo.deviceID).thenReturn(expectedDeviceId) + + val captor = ArgumentCaptor.forClass(JSONObject::class.java) + val captorEventType = ArgumentCaptor.forClass(Int::class.java) + + val inputJson = JSONObject() + val subInputJson = JSONObject() + subInputJson.put("age", 70) + inputJson.put("name", "abc") + inputJson.put("details", subInputJson) + + // Act + eventQueueManager.pushBasicProfile(inputJson) + + // Assert + verify(eventQueueManager).queueEvent( + ArgumentMatchers.any(), + captor.capture(), + captorEventType.capture() + ) + val actualProfile = captor.value["profile"] as JSONObject + val actualDetails = actualProfile["details"] as JSONObject + + assertEquals("abc", actualProfile["name"]) + assertEquals(70, actualDetails["age"]) + } + } + } + + @Test + fun test_pushBasicProfile_when_key_is_profile_identifier() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + mockStatic(IdentityRepoFactory::class.java).use { + val mockIdentityRepo = mock(IdentityRepo::class.java) + val mockLoginInfoProvider = mock(LoginInfoProvider::class.java) + `when`( + IdentityRepoFactory.getRepo( + application, + corestate.config, + corestate.deviceInfo, + corestate.validationResultStack + ) + ).thenReturn(mockIdentityRepo) + + // Arrange + doReturn(null).`when`(eventQueueManager).queueEvent( + ArgumentMatchers.any(), + ArgumentMatchers.any(), ArgumentMatchers.anyInt() + ) + + val expectedDeviceId = "device_12345" + + `when`(corestate.deviceInfo.deviceID).thenReturn(expectedDeviceId) + + val captor = ArgumentCaptor.forClass(JSONObject::class.java) + val captorEventType = ArgumentCaptor.forClass(Int::class.java) + + val inputJson = JSONObject() + val subInputJson = JSONObject() + subInputJson.put("age", 70) + inputJson.put("name", "abc") + inputJson.put("details", subInputJson) + + `when`(eventQueueManager.loginInfoProvider).thenReturn(mockLoginInfoProvider) + `when`(mockIdentityRepo.hasIdentity("name")).thenReturn(true) + + // Act + eventQueueManager.pushBasicProfile(inputJson) + + // Assert + verify(mockLoginInfoProvider).cacheGUIDForIdentifier(expectedDeviceId, "name", "abc") + verify(eventQueueManager).queueEvent( + ArgumentMatchers.any(), + captor.capture(), + captorEventType.capture() + ) + val actualProfile = captor.value["profile"] as JSONObject + val actualDetails = actualProfile["details"] as JSONObject + + assertEquals("abc", actualProfile["name"]) + assertEquals(70, actualDetails["age"]) + } + } + } + + @Test + fun test_processEvent_when_type_is_page_event() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + // Arrange + val expectedScreenName = "Home Page" + val expectedSessionId = 9898 + val expectedEpoch = 5000 + val expectedIsFirstSession = true + val expectedLastSessionLength = 3600 + val expectedGeofenceSDKVersion = 10 + + corestate.coreMetaData.setCurrentScreenName(expectedScreenName) + corestate.coreMetaData.currentSessionId = expectedSessionId + corestate.coreMetaData.isFirstSession = expectedIsFirstSession + corestate.coreMetaData.lastSessionLength = expectedLastSessionLength + corestate.coreMetaData.geofenceSDKVersion = expectedGeofenceSDKVersion + corestate.coreMetaData.isLocationForGeofence = true + CoreMetaData.setActivityCount(0) + val actualEvent = JSONObject() + + `when`(eventQueueManager.now).thenReturn(expectedEpoch) + doNothing().`when`(eventQueueManager).scheduleQueueFlush(application) + + // Act + eventQueueManager.processEvent(application, actualEvent, Constants.PAGE_EVENT) + + // Assert + + // assert following mappings are present in json + assertEquals(1, CoreMetaData.getActivityCount()) + assertEquals(expectedScreenName, actualEvent["n"]) + assertEquals(expectedSessionId, actualEvent["s"]) + assertEquals(1, actualEvent["pg"]) + assertEquals("page", actualEvent["type"]) + assertEquals(expectedEpoch, actualEvent["ep"]) + assertEquals(expectedIsFirstSession, actualEvent["f"]) + assertEquals(expectedLastSessionLength, actualEvent["lsl"]) + + // assert following values are not modified + assertEquals(expectedGeofenceSDKVersion, corestate.coreMetaData.geofenceSDKVersion) + assertFalse(corestate.coreMetaData.isBgPing) + assertTrue(corestate.coreMetaData.isLocationForGeofence) + + // assert following keys are absent in json + assertNull(actualEvent.opt(Constants.ERROR_KEY)) + assertNull(actualEvent.opt("pai")) + assertNull(actualEvent.opt("mc")) + assertNull(actualEvent.opt("nt")) + assertNull(actualEvent.opt("gf")) + assertNull(actualEvent.opt("gfSDKVersion")) + + // assert following methods called + verify(corestate.localDataStore).setDataSyncFlag(actualEvent) + verify(corestate.databaseManager).queueEventToDB(application, actualEvent, Constants.PAGE_EVENT) + verify(corestate.localDataStore, never()).persistEvent(application, actualEvent, Constants.PAGE_EVENT) + verify(eventQueueManager).scheduleQueueFlush(application) + } + } + + @Test + fun test_processEvent_when_type_is_ping_event() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + mockStatic(Utils::class.java).use { + + // Arrange + val validationResult = ValidationResult() + validationResult.errorDesc = "Fire in the hall" + validationResult.errorCode = 999 + + corestate.validationResultStack.pushValidationResult(validationResult) + + val expectedSessionId = 9898 + val expectedEpoch = 5000 + val expectedIsFirstSession = true + val expectedLastSessionLength = 3600 + val expectedGeofenceSDKVersion = 0 + val expectedMemoryConsumption = 100000L + val expectedNetworkType = "4G" + val expectedActivityCount = 10 + + corestate.coreMetaData.currentSessionId = expectedSessionId + corestate.coreMetaData.isFirstSession = expectedIsFirstSession + corestate.coreMetaData.lastSessionLength = expectedLastSessionLength + corestate.coreMetaData.geofenceSDKVersion = expectedGeofenceSDKVersion + corestate.coreMetaData.isLocationForGeofence = true + CoreMetaData.setActivityCount(expectedActivityCount) + + `when`(Utils.getMemoryConsumption()).thenReturn(expectedMemoryConsumption) + `when`(Utils.getCurrentNetworkType(application)).thenReturn(expectedNetworkType) + `when`(eventQueueManager.now).thenReturn(expectedEpoch) + doNothing().`when`(eventQueueManager).scheduleQueueFlush(application) + + val actualEvent = JSONObject() + actualEvent.put("bk", 1) + + `when`(eventQueueManager.now).thenReturn(expectedEpoch) + doNothing().`when`(eventQueueManager).scheduleQueueFlush(application) + + // Act + eventQueueManager.processEvent(application, actualEvent, Constants.PING_EVENT) + + // Assert + + // assert mapping are as expected + assertEquals(expectedMemoryConsumption, actualEvent["mc"]) + assertEquals(expectedNetworkType, actualEvent["nt"]) + assertTrue(corestate.coreMetaData.isBgPing) + + // assert below mapping is removed + assertNull(actualEvent.opt("bk")) + + // assert following keys are absent in json + assertNull(actualEvent.opt("n")) + + // assert validation error present + assertEquals(validationResult.errorCode, actualEvent.getJSONObject(Constants.ERROR_KEY)["c"]) + assertEquals(validationResult.errorDesc, actualEvent.getJSONObject(Constants.ERROR_KEY)["d"]) + + // --------- + + // assert following mappings are present in json + assertEquals(expectedActivityCount, CoreMetaData.getActivityCount()) + assertEquals(expectedSessionId, actualEvent["s"]) + assertEquals(expectedActivityCount, actualEvent["pg"]) + assertEquals("ping", actualEvent["type"]) + assertEquals(expectedEpoch, actualEvent["ep"]) + assertEquals(expectedIsFirstSession, actualEvent["f"]) + assertEquals(expectedLastSessionLength, actualEvent["lsl"]) + assertEquals(true, actualEvent["gf"]) + assertEquals(expectedGeofenceSDKVersion, actualEvent["gfSDKVersion"]) + + // assert following values are modified + assertEquals(expectedGeofenceSDKVersion, corestate.coreMetaData.geofenceSDKVersion) + + assertFalse(corestate.coreMetaData.isLocationForGeofence) + + // assert following keys are absent in json + assertNull(actualEvent.opt("pai")) + + // assert following methods called + verify(corestate.localDataStore).setDataSyncFlag(actualEvent) + verify(corestate.databaseManager).queueEventToDB(application, actualEvent, Constants.PING_EVENT) + verify(corestate.localDataStore, never()).persistEvent(application, actualEvent, Constants.PING_EVENT) + verify(eventQueueManager).scheduleQueueFlush(application) + } + } + } + + @Test + fun test_processEvent_when_type_is_profile_event() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + + val actualEvent = JSONObject() + + eventQueueManager.processEvent(application, actualEvent, Constants.PROFILE_EVENT) + + assertEquals("profile", actualEvent["type"]) + // assert following keys are absent in json + assertNull(actualEvent.opt("pai")) + } + } + + @Test + fun test_processEvent_when_type_is_data_event() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + + val actualEvent = JSONObject() + + eventQueueManager.processEvent(application, actualEvent, Constants.DATA_EVENT) + + assertEquals("data", actualEvent["type"]) + // assert following keys are absent in json + assertNull(actualEvent.opt("pai")) + } + } + + @Test + fun test_processEvent_when_type_is_normal_event() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn( + MockCTExecutors( + cleverTapInstanceConfig + ) + ) + + val actualEvent = JSONObject() + actualEvent.put("evtName", Constants.APP_LAUNCHED_EVENT) + + eventQueueManager.processEvent(application, actualEvent, Constants.RAISED_EVENT) + + // assert following keys are present in json + assertEquals("event", actualEvent["type"]) + assertNotNull(actualEvent.opt("pai")) + } + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/LocationManagerTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/LocationManagerTest.kt new file mode 100644 index 000000000..1a72eea7d --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/LocationManagerTest.kt @@ -0,0 +1,170 @@ +package com.clevertap.android.sdk + +import android.content.Context +import android.location.Location +import com.clevertap.android.sdk.events.BaseEventQueueManager +import com.clevertap.android.sdk.events.EventQueueManager +import com.clevertap.android.shared.test.BaseTestCase +import org.junit.* +import org.junit.runner.* +import org.mockito.Mockito.* +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows.shadowOf +import java.util.concurrent.Future +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +@RunWith(RobolectricTestRunner::class) +class LocationManagerTest : BaseTestCase() { + + private lateinit var coreMetaData: CoreMetaData + private lateinit var locationManager: LocationManager + private lateinit var eventQueueManager: BaseEventQueueManager + private lateinit var location: Location + + @Before + @Throws(Exception::class) + override fun setUp() { + super.setUp() + coreMetaData = CoreMetaData() + eventQueueManager = mock(EventQueueManager::class.java) + locationManager = spy(LocationManager(application, cleverTapInstanceConfig, coreMetaData, eventQueueManager)) + location = Location("").apply { + latitude = 17.355 + longitude = 7.355 + accuracy = 50f + } + } + + @Test + fun test_setLocation_returns_null_when_Location_is_null() { + val future = locationManager._setLocation(null) + + assertNull(future) + } + + @Test + fun test_setLocation_returns_null_when_app_in_background_and_Location_is_not_for_geofence() { + CoreMetaData.setAppForeground(false) + coreMetaData.isLocationForGeofence = false + + val future = locationManager._setLocation(location) + + assertNull(future) + } + + @Test + fun test_setLocation_returns_null_when_Location_is_for_geofence_and_last_location_geofence_ping_is_less_than_10_secs() { + `when`(locationManager.now).thenReturn(100) + CoreMetaData.setAppForeground(true) + coreMetaData.isLocationForGeofence = true + locationManager.lastLocationPingTimeForGeofence = 100 + + val future = locationManager._setLocation(location) + + assertNull(future) + assertEquals(locationManager.lastLocationPingTimeForGeofence, 100) + } + + @Test + fun test_setLocation_returns_future_when_Location_is_for_geofence_and_last_location_geofence_ping_is_greater_than_10_secs() { + val future = mock(Future::class.java) + + `when`(locationManager.now).thenReturn(150) + `when`(eventQueueManager.queueEvent(any(), any(), anyInt())) + .thenReturn(future) + + CoreMetaData.setAppForeground(true) + coreMetaData.isLocationForGeofence = true + locationManager.lastLocationPingTimeForGeofence = 100 + + val futureActual = locationManager._setLocation(location) + + verify(eventQueueManager).queueEvent(any(), any(), anyInt()) + assertNotNull(futureActual) + assertEquals(locationManager.lastLocationPingTimeForGeofence, 150) + } + + @Test + fun test_setLocation_returns_null_when_Location_is_not_for_geofence_and_last_location_ping_is_less_than_10_secs() { + `when`(locationManager.now).thenReturn(100) + CoreMetaData.setAppForeground(true) + coreMetaData.isLocationForGeofence = false + locationManager.lastLocationPingTime = 100 + + val future = locationManager._setLocation(location) + + assertNull(future) + assertEquals(locationManager.lastLocationPingTime, 100) + } + + @Test + fun test_setLocation_returns_future_when_Location_is_not_for_geofence_and_last_location_ping_is_greater_than_10_secs() { + val future = mock(Future::class.java) + + `when`(locationManager.now).thenReturn(150) + `when`(eventQueueManager.queueEvent(any(), any(), anyInt())) + .thenReturn(future) + + CoreMetaData.setAppForeground(true) + coreMetaData.isLocationForGeofence = false + locationManager.lastLocationPingTime = 100 + + val futureActual = locationManager._setLocation(location) + + verify(eventQueueManager).queueEvent(any(), any(), anyInt()) + assertNotNull(futureActual) + assertEquals(locationManager.lastLocationPingTime, 150) + } + + @Test + fun test_getLocation_returns_null_when_location_manager_dont_have_providers_enabled() { + val actualLocation = locationManager._getLocation() + assertNull(actualLocation) + } + + @Test + fun test_getLocation_returns_location_when_location_manager_has_any_providers_enabled() { + val systemService = application.getSystemService(Context.LOCATION_SERVICE) as android.location.LocationManager + val shadowOf = shadowOf(systemService) + shadowOf.setProviderEnabled(android.location.LocationManager.NETWORK_PROVIDER, true) + shadowOf.setLastKnownLocation(android.location.LocationManager.NETWORK_PROVIDER, location) + + val actualLocation = locationManager._getLocation() + + assertEquals(location, actualLocation) + } + + @Test + fun test_getLocation_returns_null_when_location_provider_does_not_have_location() { + val systemService = application.getSystemService(Context.LOCATION_SERVICE) as android.location.LocationManager + val shadowOf = shadowOf(systemService) + shadowOf.setProviderEnabled(android.location.LocationManager.NETWORK_PROVIDER, true) + shadowOf.setLastKnownLocation(android.location.LocationManager.NETWORK_PROVIDER, null) + + val actualLocation = locationManager._getLocation() + + assertNull(actualLocation) + } + + @Test + fun test_getLocation_returns_location_with_best_accuracy() { + val systemService = application.getSystemService(Context.LOCATION_SERVICE) as android.location.LocationManager + val shadowOf = shadowOf(systemService) + shadowOf.setProviderEnabled(android.location.LocationManager.NETWORK_PROVIDER, true) + shadowOf.setProviderEnabled(android.location.LocationManager.GPS_PROVIDER, true) + shadowOf.setLastKnownLocation(android.location.LocationManager.NETWORK_PROVIDER, location) + + val gpsLocation = Location("").apply { + latitude = 17.355 + longitude = 7.355 + accuracy = 10f + } + shadowOf.setLastKnownLocation(android.location.LocationManager.GPS_PROVIDER, gpsLocation) + + val actualLocation = locationManager._getLocation() + + assertEquals(gpsLocation, actualLocation) + } +} diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/MockAnalyticsManager.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/MockAnalyticsManager.kt new file mode 100644 index 000000000..6149ab592 --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/MockAnalyticsManager.kt @@ -0,0 +1,35 @@ +package com.clevertap.android.sdk + +import android.os.Bundle +import com.clevertap.android.sdk.inapp.CTInAppNotification +import org.json.JSONObject +import java.util.ArrayList + +class MockAnalyticsManager : BaseAnalyticsManager() { + + override fun addMultiValuesForKey(key: String, values: ArrayList) {} + override fun fetchFeatureFlags() {} + override fun forcePushAppLaunchedEvent() {} + override fun pushAppLaunchedEvent() {} + override fun pushDisplayUnitClickedEventForID(unitID: String) {} + override fun pushDisplayUnitViewedEventForID(unitID: String) {} + override fun pushError(errorMessage: String, errorCode: Int) {} + override fun pushEvent(eventName: String, eventActions: Map) {} + override fun pushInAppNotificationStateEvent( + clicked: Boolean, data: CTInAppNotification, + customData: Bundle + ) { + } + + override fun pushInstallReferrer(url: String) {} + override fun pushInstallReferrer(source: String, medium: String, campaign: String) {} + override fun pushNotificationClickedEvent(extras: Bundle) {} + override fun pushNotificationViewedEvent(extras: Bundle) {} + override fun pushProfile(profile: Map) {} + override fun removeMultiValuesForKey(key: String, values: ArrayList) {} + override fun removeValueForKey(key: String) {} + override fun sendDataEvent(event: JSONObject) {} + override fun sendFetchEvent(eventObject: JSONObject?) { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/MockCoreState.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/MockCoreState.kt new file mode 100644 index 000000000..ecc8b00d5 --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/MockCoreState.kt @@ -0,0 +1,32 @@ +package com.clevertap.android.sdk + +import android.content.Context +import com.clevertap.android.sdk.db.DBManager +import com.clevertap.android.sdk.events.EventMediator +import com.clevertap.android.sdk.network.NetworkManager +import com.clevertap.android.sdk.pushnotification.PushProviders +import com.clevertap.android.sdk.task.MainLooperHandler +import com.clevertap.android.sdk.validation.ValidationResultStack +import org.mockito.* + +class MockCoreState(context: Context, cleverTapInstanceConfig: CleverTapInstanceConfig) : CoreState(context) { + + init { + config = cleverTapInstanceConfig + deviceInfo = Mockito.mock(DeviceInfo::class.java) + pushProviders = Mockito.mock(PushProviders::class.java) + sessionManager = Mockito.mock(SessionManager::class.java) + locationManager = Mockito.mock(LocationManager::class.java) + coreMetaData = CoreMetaData() + callbackManager = CallbackManager(cleverTapInstanceConfig, deviceInfo) + validationResultStack = Mockito.mock(ValidationResultStack::class.java) + analyticsManager = Mockito.mock(AnalyticsManager::class.java) + eventMediator = Mockito.mock(EventMediator::class.java) + databaseManager = Mockito.mock(DBManager::class.java) + validationResultStack = ValidationResultStack() + mainLooperHandler = Mockito.mock(MainLooperHandler::class.java) + networkManager = Mockito.mock(NetworkManager::class.java) + ctLockManager = CTLockManager() + localDataStore = Mockito.mock(LocalDataStore::class.java) + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/MockDeviceInfo.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/MockDeviceInfo.kt new file mode 100644 index 000000000..084c268a5 --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/MockDeviceInfo.kt @@ -0,0 +1,145 @@ +package com.clevertap.android.sdk + +import android.content.Context +import org.json.JSONObject + +class MockDeviceInfo( + context: Context?, config: CleverTapInstanceConfig?, cleverTapID: String?, + coreMetaData: CoreMetaData? +) : DeviceInfo( + context, config, + cleverTapID, coreMetaData +) { + + val ctId: String? + + init { + ctId = cleverTapID + } + + override fun getDeviceID(): String? { + return ctId + } + + override fun onInitDeviceInfo(cleverTapID: String?) { + } + + override fun getAttributionID(): String { + return "some-att-id" + } + + override fun getBluetoothVersion(): String { + return "2.0" + } + + override fun getBuild(): Int { + return 1 + } + + override fun getCarrier(): String { + return "Android" + } + + override fun getContext(): Context { + return super.getContext() + } + + override fun getCountryCode(): String { + return "us" + } + + override fun getDPI(): Int { + return 420 + } + + override fun getGoogleAdID(): String { + return "__48703e3cc2ff468ab3c641edf2770d74" + } + + override fun getHeight(): Double { + return 4.27 + } + + override fun getHeightPixels(): Int { + return 1920 + } + + override fun getLibrary(): String { + return "Android" + } + + override fun getManufacturer(): String { + return super.getManufacturer() + } + + override fun getModel(): String { + return "sdk_gphone_x86" + } + + override fun getNetworkType(): String { + return "4G" + } + + override fun getNotificationsEnabledForUser(): Boolean { + return true + } + + override fun getOsName(): String { + return "Android" + } + + override fun getOsVersion(): String { + return "11" + } + + override fun getSdkVersion(): Int { + return 40100 + } + + override fun getVersionName(): String { + return "1.0" + } + + override fun getWidth(): Double { + return 2.57 + } + + override fun getWidthPixels(): Int { + return 1024 + } + + override fun isBluetoothEnabled(): Boolean { + return true + } + + override fun isLimitAdTrackingEnabled(): Boolean { + return false + } + + override fun isWifiConnected(): Boolean { + return true + } + + override fun getAppLaunchedFields(): JSONObject { + val obj = JSONObject() + obj.put("Build", "1") + obj.put("Version", "1.0") + obj.put("OS Version", "11") + obj.put("SDK Version", 40100) + obj.put("Make", "Google") + obj.put("Model", "sdk_gphone_x86") + obj.put("Carrier", "Android") + obj.put("useIP", false) + obj.put("OS", "Android") + obj.put("wdt", 2.57) + obj.put("hgt", 4.27) + obj.put("dpi", 420) + obj.put("dt", 1) + obj.put("cc", "us") + return obj + } + + override fun optOutKey(): String { + return "false" + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/displayunits/CTDisplayUnitControllerTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/displayunits/CTDisplayUnitControllerTest.kt new file mode 100644 index 000000000..690917b73 --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/displayunits/CTDisplayUnitControllerTest.kt @@ -0,0 +1,75 @@ +package com.clevertap.android.sdk.displayunits + +import com.clevertap.android.sdk.displayunits.model.CleverTapDisplayUnit +import com.clevertap.android.sdk.displayunits.model.MockCleverTapDisplayUnit +import com.clevertap.android.shared.test.BaseTestCase +import org.json.JSONArray +import org.json.JSONObject +import org.junit.* +import org.junit.runner.* +import org.mockito.Mockito.* +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class CTDisplayUnitControllerTest : BaseTestCase() { + + private lateinit var ctDisplayUnitController: CTDisplayUnitController + + @Before + override fun setUp() { + super.setUp() + ctDisplayUnitController = CTDisplayUnitController() + } + + @Test + fun test_updateDisplayUnits_whenResponseArrayIsNull_returnNullDisplayUnit() { + val list = ctDisplayUnitController.updateDisplayUnits(null) + Assert.assertNull(list) + Assert.assertNull(ctDisplayUnitController.allDisplayUnits) + Assert.assertNull(ctDisplayUnitController.getDisplayUnitForID("12121212")) + } + + @Test + fun test_getDisplayUnitForID_whenEmptyOrNullUnitID_returnNullDisplayUnit() { + Assert.assertNull(ctDisplayUnitController.getDisplayUnitForID(null)) + Assert.assertNull(ctDisplayUnitController.getDisplayUnitForID("")) + } + + @Test + fun test_reset() { + // first we put non empty response to ensure that after reset the values are cleared or not + + val list = ctDisplayUnitController.updateDisplayUnits(MockCleverTapDisplayUnit().getMockResponse(1)) + Assert.assertNotNull(list) + Assert.assertTrue(list?.size == 1) + + //after reset the units should get cleared + ctDisplayUnitController.reset() + Assert.assertNull(ctDisplayUnitController.allDisplayUnits) + Assert.assertNull(ctDisplayUnitController.getDisplayUnitForID("12121212")) + } + + @Test + fun test_updateDisplayUnits_whenAnyException_returnNullDisplayUnit() { + mockStatic(CleverTapDisplayUnit::class.java).use { + `when`(CleverTapDisplayUnit.toDisplayUnit(any(JSONObject::class.java))).thenThrow( + java.lang.RuntimeException( + "Something went wrong" + ) + ) + ctDisplayUnitController.updateDisplayUnits(MockCleverTapDisplayUnit().getMockResponse(1)) + Assert.assertNull(ctDisplayUnitController.getDisplayUnitForID(null)) + Assert.assertNull(ctDisplayUnitController.getDisplayUnitForID("")) + } + } + + @Test + fun test_updateDisplayUnits_validDisplayUnitResponse_shouldReturnValidDisplayUnit() { + ctDisplayUnitController.updateDisplayUnits(MockCleverTapDisplayUnit().getMockResponse(1)) + Assert.assertNotNull(ctDisplayUnitController.allDisplayUnits) + Assert.assertTrue(ctDisplayUnitController.allDisplayUnits!!.size > 0) + val displayUnit = ctDisplayUnitController.getDisplayUnitForID("mock-notification-id") + Assert.assertNotNull(displayUnit) + Assert.assertTrue(displayUnit is CleverTapDisplayUnit) + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/displayunits/CTDisplayUnitTypeTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/displayunits/CTDisplayUnitTypeTest.kt new file mode 100644 index 000000000..ae5e1591d --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/displayunits/CTDisplayUnitTypeTest.kt @@ -0,0 +1,43 @@ +package com.clevertap.android.sdk.displayunits + +import org.junit.* + +internal class CTDisplayUnitTypeTest { + + @Test + fun test_type_valid() { + Assert.assertEquals(CTDisplayUnitType.SIMPLE, CTDisplayUnitType.type("simple")) + Assert.assertEquals(CTDisplayUnitType.SIMPLE_WITH_IMAGE, CTDisplayUnitType.type("simple-image")) + Assert.assertEquals(CTDisplayUnitType.CAROUSEL, CTDisplayUnitType.type("carousel")) + Assert.assertEquals(CTDisplayUnitType.CAROUSEL_WITH_IMAGE, CTDisplayUnitType.type("carousel-image")) + Assert.assertEquals(CTDisplayUnitType.MESSAGE_WITH_ICON, CTDisplayUnitType.type("message-icon")) + Assert.assertEquals(CTDisplayUnitType.CUSTOM_KEY_VALUE, CTDisplayUnitType.type("custom-key-value")) + } + + @Test + fun test_type_inValid() { + Assert.assertNull(CTDisplayUnitType.type("random")) + Assert.assertNull(CTDisplayUnitType.type("")) + } + + @Test + fun test_toString() { + var type = "simple" + Assert.assertEquals(type, CTDisplayUnitType.type(type).toString()) + + type = "simple-image" + Assert.assertEquals(type, CTDisplayUnitType.type(type).toString()) + + type = "carousel" + Assert.assertEquals(type, CTDisplayUnitType.type(type).toString()) + + type = "carousel-image" + Assert.assertEquals(type, CTDisplayUnitType.type(type).toString()) + + type = "message-icon" + Assert.assertEquals(type, CTDisplayUnitType.type(type).toString()) + + type = "custom-key-value" + Assert.assertEquals(type, CTDisplayUnitType.type(type).toString()) + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/displayunits/model/CleverTapDisplayUnitContentTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/displayunits/model/CleverTapDisplayUnitContentTest.kt new file mode 100644 index 000000000..d66fe0f3e --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/displayunits/model/CleverTapDisplayUnitContentTest.kt @@ -0,0 +1,128 @@ +package com.clevertap.android.sdk.displayunits.model + +import android.os.Parcel +import android.text.TextUtils +import com.clevertap.android.shared.test.BaseTestCase +import org.junit.* +import org.junit.runner.* +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class CleverTapDisplayUnitContentTest : BaseTestCase() { + + @Test + fun test_toContent_nullObject_ReturnInvalidObject() { + val displayUnitContent = CleverTapDisplayUnitContent.toContent(null) + Assert.assertTrue(TextUtils.isEmpty(displayUnitContent.message)) + Assert.assertTrue(TextUtils.isEmpty(displayUnitContent.messageColor)) + Assert.assertTrue(TextUtils.isEmpty(displayUnitContent.media)) + Assert.assertTrue(TextUtils.isEmpty(displayUnitContent.actionUrl)) + Assert.assertTrue(TextUtils.isEmpty(displayUnitContent.contentType)) + Assert.assertTrue(TextUtils.isEmpty(displayUnitContent.icon)) + Assert.assertTrue(TextUtils.isEmpty(displayUnitContent.posterUrl)) + Assert.assertTrue(TextUtils.isEmpty(displayUnitContent.title)) + Assert.assertTrue(TextUtils.isEmpty(displayUnitContent.titleColor)) + Assert.assertTrue(!TextUtils.isEmpty(displayUnitContent.error)) + } + + @Test + fun test_toDisplayUnit_validArray_ReturnValidObject() { + val displayUnitContent = CleverTapDisplayUnitContent.toContent(MockDisplayUnitContent().getContent()) + Assert.assertFalse(TextUtils.isEmpty(displayUnitContent.message)) + Assert.assertFalse(TextUtils.isEmpty(displayUnitContent.messageColor)) + Assert.assertFalse(TextUtils.isEmpty(displayUnitContent.media)) + Assert.assertFalse(TextUtils.isEmpty(displayUnitContent.actionUrl)) + Assert.assertFalse(TextUtils.isEmpty(displayUnitContent.contentType)) + Assert.assertFalse(TextUtils.isEmpty(displayUnitContent.icon)) + Assert.assertFalse(TextUtils.isEmpty(displayUnitContent.posterUrl)) + Assert.assertFalse(TextUtils.isEmpty(displayUnitContent.title)) + Assert.assertFalse(TextUtils.isEmpty(displayUnitContent.titleColor)) + Assert.assertFalse(!TextUtils.isEmpty(displayUnitContent.error)) + } + + @Test + fun test_toString() { + val displayUnitContentString = + CleverTapDisplayUnitContent.toContent(MockDisplayUnitContent().getContent()).toString() + Assert.assertTrue(displayUnitContentString.contains("title:")) + Assert.assertTrue(displayUnitContentString.contains("titleColor:")) + Assert.assertTrue(displayUnitContentString.contains("message:")) + Assert.assertTrue(displayUnitContentString.contains("messageColor:")) + Assert.assertTrue(displayUnitContentString.contains("media:")) + Assert.assertTrue(displayUnitContentString.contains("posterUrl:")) + Assert.assertTrue(displayUnitContentString.contains("actionUrl:")) + Assert.assertTrue(displayUnitContentString.contains("error:")) + } + + @Test + fun test_createFromParcel_verify() { + val displayUnitContent = CleverTapDisplayUnitContent.toContent(MockDisplayUnitContent().getContent()) + val parcel = Parcel.obtain() + displayUnitContent.writeToParcel(parcel, displayUnitContent.describeContents()) + parcel.setDataPosition(0) + + val createdFromParcel = CleverTapDisplayUnitContent.CREATOR.createFromParcel(parcel) + Assert.assertEquals(displayUnitContent.message, createdFromParcel.message) + Assert.assertEquals(displayUnitContent.messageColor, createdFromParcel.messageColor) + Assert.assertEquals(displayUnitContent.media, createdFromParcel.media) + Assert.assertEquals(displayUnitContent.actionUrl, createdFromParcel.actionUrl) + Assert.assertEquals(displayUnitContent.contentType, createdFromParcel.contentType) + Assert.assertEquals(displayUnitContent.icon, createdFromParcel.icon) + Assert.assertEquals(displayUnitContent.posterUrl, createdFromParcel.posterUrl) + Assert.assertEquals(displayUnitContent.title, createdFromParcel.title) + Assert.assertEquals(displayUnitContent.titleColor, createdFromParcel.titleColor) + Assert.assertEquals(displayUnitContent.error, createdFromParcel.error) + } + + @Test + fun test_mediaIsVideo() { + val displayUnitContent = CleverTapDisplayUnitContent.toContent(MockDisplayUnitContent().getContent()) + displayUnitContent.contentType = null + Assert.assertFalse(displayUnitContent.mediaIsVideo()) + + displayUnitContent.contentType = "audio" + Assert.assertFalse(displayUnitContent.mediaIsVideo()) + + displayUnitContent.contentType = "videoabc" + Assert.assertTrue(displayUnitContent.mediaIsVideo()) + } + + @Test + fun test_mediaIsAudio() { + val displayUnitContent = CleverTapDisplayUnitContent.toContent(MockDisplayUnitContent().getContent()) + displayUnitContent.contentType = null + Assert.assertFalse(displayUnitContent.mediaIsAudio()) + + displayUnitContent.contentType = "video" + Assert.assertFalse(displayUnitContent.mediaIsAudio()) + + displayUnitContent.contentType = "audioabc" + Assert.assertTrue(displayUnitContent.mediaIsAudio()) + } + + @Test + fun test_mediaIsGIF() { + val displayUnitContent = CleverTapDisplayUnitContent.toContent(MockDisplayUnitContent().getContent()) + displayUnitContent.contentType = null + Assert.assertFalse(displayUnitContent.mediaIsGIF()) + + displayUnitContent.contentType = "image" + Assert.assertFalse(displayUnitContent.mediaIsGIF()) + + displayUnitContent.contentType = "image/gif" + Assert.assertTrue(displayUnitContent.mediaIsGIF()) + } + + @Test + fun test_mediaIsImage() { + val displayUnitContent = CleverTapDisplayUnitContent.toContent(MockDisplayUnitContent().getContent()) + displayUnitContent.contentType = null + Assert.assertFalse(displayUnitContent.mediaIsImage()) + + displayUnitContent.contentType = "image/gif" + Assert.assertFalse(displayUnitContent.mediaIsImage()) + + displayUnitContent.contentType = "image" + Assert.assertTrue(displayUnitContent.mediaIsImage()) + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/displayunits/model/CleverTapDisplayUnitTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/displayunits/model/CleverTapDisplayUnitTest.kt new file mode 100644 index 000000000..c7e2b50dc --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/displayunits/model/CleverTapDisplayUnitTest.kt @@ -0,0 +1,72 @@ +package com.clevertap.android.sdk.displayunits.model + +import android.os.Parcel +import android.text.TextUtils +import com.clevertap.android.shared.test.BaseTestCase +import org.junit.* +import org.junit.runner.* +import org.robolectric.RobolectricTestRunner +import org.skyscreamer.jsonassert.JSONAssert +import java.util.TreeMap + +@RunWith(RobolectricTestRunner::class) +class CleverTapDisplayUnitTest : BaseTestCase() { + + @Test + fun test_toDisplayUnit_nullArray_ReturnInvalidObject() { + val displayUnit = CleverTapDisplayUnit.toDisplayUnit(null) + Assert.assertNull(displayUnit.bgColor) + Assert.assertNull(displayUnit.jsonObject) + Assert.assertNull(displayUnit.contents) + Assert.assertNull(displayUnit.customExtras) + Assert.assertNull(displayUnit.type) + Assert.assertNull(displayUnit.wzrkFields) + Assert.assertNotNull(displayUnit.error) + Assert.assertEquals(displayUnit.unitID, "") + } + + @Test + fun test_toDisplayUnit_validArray_ReturnValidObject() { + val displayUnit = CleverTapDisplayUnit.toDisplayUnit(MockCleverTapDisplayUnit().getAUnit()) + Assert.assertNotNull(displayUnit.bgColor) + Assert.assertNotNull(displayUnit.jsonObject) + Assert.assertNotNull(displayUnit.contents) + Assert.assertTrue(displayUnit.customExtras!!.size > 0) + Assert.assertNotNull(displayUnit.type) + Assert.assertNotNull(displayUnit.wzrkFields) + Assert.assertNull(displayUnit.error) + Assert.assertNotNull(displayUnit.unitID) + } + + @Test + fun test_toString() { + val displayUnitString = CleverTapDisplayUnit.toDisplayUnit(MockCleverTapDisplayUnit().getAUnit()).toString() + Assert.assertTrue(displayUnitString.contains("Unit id-")) + Assert.assertTrue(displayUnitString.contains("Type-")) + Assert.assertTrue(displayUnitString.contains("bgColor-")) + Assert.assertTrue(displayUnitString.contains("Content Item:")) + Assert.assertTrue(displayUnitString.contains("Custom KV:")) + Assert.assertTrue(displayUnitString.contains("JSON -")) + Assert.assertTrue(displayUnitString.contains("Error-")) + } + + @Test + fun test_createFromParcel_verify() { + val displayUnit = CleverTapDisplayUnit.toDisplayUnit(MockCleverTapDisplayUnit().getAUnit()) + val parcel = Parcel.obtain() + displayUnit.writeToParcel(parcel, displayUnit.describeContents()) + parcel.setDataPosition(0) + + val createdFromParcel = CleverTapDisplayUnit.CREATOR.createFromParcel(parcel) + Assert.assertEquals(displayUnit.unitID, createdFromParcel.unitID) + Assert.assertEquals(displayUnit.wzrkFields.toString(), createdFromParcel.wzrkFields.toString()) + Assert.assertEquals(displayUnit.error, createdFromParcel.error) + Assert.assertEquals(displayUnit.type, createdFromParcel.type) + Assert.assertEquals( + TreeMap(displayUnit.customExtras).toString(), + TreeMap(createdFromParcel.customExtras).toString() + ) + Assert.assertEquals(displayUnit.bgColor, createdFromParcel.bgColor) + JSONAssert.assertEquals(displayUnit.jsonObject, createdFromParcel.jsonObject, false) + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/displayunits/model/MockCleverTapDisplayUnit.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/displayunits/model/MockCleverTapDisplayUnit.kt new file mode 100644 index 000000000..daed83b0a --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/displayunits/model/MockCleverTapDisplayUnit.kt @@ -0,0 +1,34 @@ +package com.clevertap.android.sdk.displayunits.model + +import com.clevertap.android.sdk.Constants +import com.clevertap.android.sdk.displayunits.CTDisplayUnitType +import org.json.JSONArray +import org.json.JSONObject + +class MockCleverTapDisplayUnit { + + fun getMockResponse(noItems: Int): JSONArray { + val jsonArray = JSONArray() + for (i in 1..noItems) + jsonArray.put(MockCleverTapDisplayUnit().getAUnit()) + return jsonArray + } + + fun getAUnit(): JSONObject { + val jsonObject = JSONObject() + jsonObject.put(Constants.NOTIFICATION_ID_TAG, "mock-notification-id") + jsonObject.put(Constants.KEY_TYPE, CTDisplayUnitType.SIMPLE.type) + jsonObject.put(Constants.KEY_BG, "mock-unit-bg") + val contentArray = JSONArray() + for (i in 1..3) { + contentArray.put(MockDisplayUnitContent().getContent()) + } + jsonObject.put(Constants.KEY_CONTENT, contentArray) + + val customKV = JSONObject() + customKV.put("k1","v1") + customKV.put("k2","v2") + jsonObject.put(Constants.KEY_CUSTOM_KV, customKV) + return jsonObject + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/displayunits/model/MockDisplayUnitContent.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/displayunits/model/MockDisplayUnitContent.kt new file mode 100644 index 000000000..cff7dd69d --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/displayunits/model/MockDisplayUnitContent.kt @@ -0,0 +1,42 @@ +package com.clevertap.android.sdk.displayunits.model + +import com.clevertap.android.sdk.Constants +import com.clevertap.android.sdk.displayunits.CTDisplayUnitType +import org.json.JSONObject + +class MockDisplayUnitContent { + + fun getContent(): JSONObject { + val contentObj = JSONObject() + contentObj.put(Constants.KEY_TYPE, CTDisplayUnitType.SIMPLE.name) + val titleObject = JSONObject() + contentObj.put(Constants.KEY_TITLE, titleObject) + titleObject.put(Constants.KEY_TEXT, "mock-content-title") + titleObject.put(Constants.KEY_COLOR, "mock-content-title-color") + + val msgObject = JSONObject() + contentObj.put(Constants.KEY_MESSAGE, msgObject) + msgObject.put(Constants.KEY_TEXT, "mock-msg-text") + msgObject.put(Constants.KEY_COLOR, "mock-content-msg-color") + + val mediaObject = JSONObject() + contentObj.put(Constants.KEY_MEDIA, mediaObject) + mediaObject.put(Constants.KEY_URL, "mock-content-icon-url") + mediaObject.put(Constants.KEY_CONTENT_TYPE, "mock-content-media-type") + mediaObject.put(Constants.KEY_POSTER_URL, "mock-content-poster-url") + + val actionObject = JSONObject() + contentObj.put(Constants.KEY_ACTION, actionObject) + + val iconObject = JSONObject() + contentObj.put(Constants.KEY_ICON, iconObject) + iconObject.put(Constants.KEY_URL, "mock-content-icon-url") + + val urlObject = JSONObject() + actionObject.put(Constants.KEY_URL, urlObject) + val androidObject = JSONObject() + urlObject.put(Constants.KEY_ANDROID, androidObject) + androidObject.put(Constants.KEY_TEXT, "mock-content-android-action-url") + return contentObj + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/featureFlags/CTFeatureFlagFactoryTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/featureFlags/CTFeatureFlagFactoryTest.kt new file mode 100644 index 000000000..06c0b627d --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/featureFlags/CTFeatureFlagFactoryTest.kt @@ -0,0 +1,59 @@ +package com.clevertap.android.sdk.featureFlags + +import com.clevertap.android.sdk.BaseAnalyticsManager +import com.clevertap.android.sdk.BaseCallbackManager +import com.clevertap.android.sdk.CallbackManager +import com.clevertap.android.sdk.CoreMetaData +import com.clevertap.android.sdk.DeviceInfo +import com.clevertap.android.sdk.MockAnalyticsManager +import com.clevertap.android.sdk.MockDeviceInfo +import com.clevertap.android.shared.test.BaseTestCase +import org.junit.* +import org.junit.runner.* +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +internal class CTFeatureFlagFactoryTest : BaseTestCase() { + + val guid = "1212212" + lateinit var coreMetaData: CoreMetaData + lateinit var deviceInfo: DeviceInfo + lateinit var analyticsManager: BaseAnalyticsManager + lateinit var callbackManager: BaseCallbackManager + + override fun setUp() { + super.setUp() + coreMetaData = CoreMetaData() + deviceInfo = MockDeviceInfo(application, cleverTapInstanceConfig, guid, coreMetaData) + analyticsManager = MockAnalyticsManager() + callbackManager = CallbackManager(cleverTapInstanceConfig, deviceInfo) + } + + @Test + fun test_getInstance_shouldNotReturnNull() { + val controller = CTFeatureFlagsFactory.getInstance( + application, + guid, + cleverTapInstanceConfig, + callbackManager, + analyticsManager + ) + Assert.assertNotNull(controller) + Assert.assertTrue(controller is CTFeatureFlagsController) + } + + @Test + fun test_getInstance_instanceInitializedProperly() { + val controller = CTFeatureFlagsFactory.getInstance( + application, + guid, + cleverTapInstanceConfig, + callbackManager, + analyticsManager + ) + Assert.assertNotNull(controller.mFileUtils) + Assert.assertEquals(controller.config, cleverTapInstanceConfig) + Assert.assertEquals(controller.mCallbackManager, callbackManager) + Assert.assertEquals(controller.mAnalyticsManager, analyticsManager) + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/featureFlags/CTFeatureFlagsControllerTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/featureFlags/CTFeatureFlagsControllerTest.kt new file mode 100644 index 000000000..0b1dc3cde --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/featureFlags/CTFeatureFlagsControllerTest.kt @@ -0,0 +1,176 @@ +package com.clevertap.android.sdk.featureFlags + +import com.clevertap.android.sdk.BaseAnalyticsManager +import com.clevertap.android.sdk.BaseCallbackManager +import com.clevertap.android.sdk.CTFeatureFlagsListener +import com.clevertap.android.sdk.CallbackManager +import com.clevertap.android.sdk.CoreMetaData +import com.clevertap.android.sdk.DeviceInfo +import com.clevertap.android.sdk.MockDeviceInfo +import com.clevertap.android.sdk.task.CTExecutorFactory +import com.clevertap.android.sdk.task.MockCTExecutors +import com.clevertap.android.sdk.utils.FileUtils +import com.clevertap.android.shared.test.BaseTestCase +import org.junit.* +import org.junit.runner.* +import org.mockito.Mockito.* +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class CTFeatureFlagsControllerTest : BaseTestCase() { + + private lateinit var mCTFeatureFlagsController: CTFeatureFlagsController + private lateinit var coreMetaData: CoreMetaData + private lateinit var analyticsManager: BaseAnalyticsManager + private lateinit var callbackManager: BaseCallbackManager + private lateinit var deviceInfo: DeviceInfo + private lateinit var fileUtils: FileUtils + private lateinit var featureFlagsListener: CTFeatureFlagsListener + private val guid = "1212121212" + + @Before + @Throws(Exception::class) + override fun setUp() { + super.setUp() + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn(MockCTExecutors(cleverTapInstanceConfig)) + coreMetaData = CoreMetaData() + analyticsManager = mock(BaseAnalyticsManager::class.java) + deviceInfo = MockDeviceInfo(application, cleverTapInstanceConfig, guid, coreMetaData) + callbackManager = CallbackManager(cleverTapInstanceConfig, deviceInfo) + fileUtils = spy(FileUtils(application, cleverTapInstanceConfig)) + featureFlagsListener = mock(CTFeatureFlagsListener::class.java) + callbackManager.featureFlagListener = featureFlagsListener + mCTFeatureFlagsController = CTFeatureFlagsController( + guid, + cleverTapInstanceConfig, + callbackManager, + analyticsManager, fileUtils + ) + } + } + + @Test + fun test_constructor_whenFeatureFlagIsNotSave_InitShouldReturnTrue() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn(MockCTExecutors(cleverTapInstanceConfig)) + val controller = spy(mCTFeatureFlagsController) + Assert.assertTrue(controller.isInitialized) + } + } + + @Test + fun when_Non_Empty_Feature_Flag_Config_Then_Init_Return_True() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn(MockCTExecutors(cleverTapInstanceConfig)) + `when`(fileUtils.readFromFile(mCTFeatureFlagsController.getCachedFullPath())) + .thenReturn(MockFFResponse().getResponseJSON().toString()) + val controller = CTFeatureFlagsController( + guid, + cleverTapInstanceConfig, + callbackManager, + analyticsManager, fileUtils + ) + Assert.assertTrue(controller.isInitialized) + } + } + + @Test + fun when_Non_Empty_Feature_Flag_Config_Read_Exception_Then_Init_Return_True() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn(MockCTExecutors(cleverTapInstanceConfig)) + `when`(fileUtils.readFromFile(mCTFeatureFlagsController.getCachedFullPath())) + .thenThrow(RuntimeException("Something Went Wrong")) + val controller = CTFeatureFlagsController( + guid, + cleverTapInstanceConfig, + callbackManager, + analyticsManager, fileUtils + ) + Assert.assertFalse(controller.isInitialized) + } + } + + @Test + fun when_Feature_Flag_Response_Success() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn(MockCTExecutors(cleverTapInstanceConfig)) + `when`(fileUtils.readFromFile(mCTFeatureFlagsController.getCachedFullPath())) + .thenThrow(RuntimeException("Something Went Wrong")) + val jsonObject = MockFFResponse().getResponseJSON() + mCTFeatureFlagsController.updateFeatureFlags(jsonObject) + verify(featureFlagsListener).featureFlagsUpdated() + verify( + fileUtils + ).writeJsonToFile( + mCTFeatureFlagsController.cachedDirName, + mCTFeatureFlagsController.cachedFileName, + jsonObject + ) + + Assert.assertTrue(mCTFeatureFlagsController.get("feature_A", false)) + Assert.assertFalse(mCTFeatureFlagsController.get("feature_B", true)) + Assert.assertFalse(mCTFeatureFlagsController.get("feature_C", true)) + Assert.assertTrue(mCTFeatureFlagsController.get("feature_D", false)) + Assert.assertTrue(mCTFeatureFlagsController.get("feature_E", true)) + Assert.assertFalse(mCTFeatureFlagsController.get("feature_F", false)) + } + } + + @Test + fun test_get_whenNotInitialized_shouldReturnDefaultValue() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn(MockCTExecutors(cleverTapInstanceConfig)) + val jsonObject = MockFFResponse().getResponseJSON() + mCTFeatureFlagsController.updateFeatureFlags(jsonObject) + mCTFeatureFlagsController.isInitialized = false + Assert.assertFalse(mCTFeatureFlagsController.get("feature_A", false)) + } + } + + @Test + fun test_Fetch_FF_Success() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn(MockCTExecutors(cleverTapInstanceConfig)) + mCTFeatureFlagsController.fetchFeatureFlags() + verify(analyticsManager).fetchFeatureFlags() + } + } + + @Test + fun test_resetWithGuid() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn(MockCTExecutors(cleverTapInstanceConfig)) + val guid = "121u3203u129" + val controller = spy(mCTFeatureFlagsController) + controller.resetWithGuid(guid) + Assert.assertEquals(guid, controller.guid) + verify(controller).init() + } + } + + @Test + fun test_setGuidAndInit_whenInitialised_initMethodShouldNotGetCalled() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn(MockCTExecutors(cleverTapInstanceConfig)) + val controller = spy(mCTFeatureFlagsController) + val newGuid = "131" + controller.setGuidAndInit(newGuid) + Assert.assertNotEquals(controller.guid, newGuid) + verify(controller, never()).init() + } + } + + @Test + fun test_setGuidAndInit_whenNotInitialised_initMethodShouldGetCalled() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn(MockCTExecutors(cleverTapInstanceConfig)) + val controller = spy(mCTFeatureFlagsController) + val newGuid = "131" + controller.isInitialized = false + controller.setGuidAndInit(newGuid) + Assert.assertEquals(controller.guid, newGuid) + verify(controller).init() + } + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/featureFlags/FeatureFlagTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/featureFlags/FeatureFlagTest.kt deleted file mode 100644 index 746ba27f5..000000000 --- a/clevertap-core/src/test/java/com/clevertap/android/sdk/featureFlags/FeatureFlagTest.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.clevertap.android.sdk.featureFlags - -import com.clevertap.android.shared.test.BaseTestCase -import org.junit.* -import org.junit.runner.* -import org.mockito.* -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class FeatureFlagTest : BaseTestCase() { - - @Before - @Throws(Exception::class) - override fun setUp() { - super.setUp() - } - - @Test - fun testFetch() { - Mockito.`when`(cleverTapAPI!!.featureFlag()) - .thenReturn(CTFeatureFlagsController(application, "12121", cleverTapInstanceConfig, cleverTapAPI)) - cleverTapAPI!!.featureFlag().fetchFeatureFlags() - Mockito.verify(cleverTapAPI)!!.fetchFeatureFlags() - } - - @Test - fun testGet() { - val ctFeatureFlagsController = Mockito.mock(CTFeatureFlagsController::class.java) - Mockito.`when`(cleverTapAPI!!.featureFlag()).thenReturn(ctFeatureFlagsController) - Mockito.`when`(ctFeatureFlagsController["isFeatureA", true]).thenReturn(false) - Assert.assertFalse(cleverTapAPI!!.featureFlag()["isFeatureA", true]) - } -} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/featureFlags/MockFFResponse.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/featureFlags/MockFFResponse.kt new file mode 100644 index 000000000..550265d39 --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/featureFlags/MockFFResponse.kt @@ -0,0 +1,32 @@ +package com.clevertap.android.sdk.featureFlags + +import com.clevertap.android.sdk.Constants +import com.clevertap.android.sdk.product_config.CTProductConfigConstants.PRODUCT_CONFIG_JSON_KEY_FOR_KEY +import com.clevertap.android.sdk.product_config.CTProductConfigConstants.PRODUCT_CONFIG_JSON_KEY_FOR_VALUE +import org.json.JSONArray +import org.json.JSONObject + +class MockFFResponse { + + fun getFetchedFFConfig(): HashMap { + val fetchedConfig: HashMap = HashMap() + fetchedConfig.put("feature_A", true) + fetchedConfig.put("feature_B", false) + fetchedConfig.put("feature_C", false) + fetchedConfig.put("feature_D", true) + return fetchedConfig + } + + fun getResponseJSON(): JSONObject { + val response = JSONObject() + val array = JSONArray() + for (entry in getFetchedFFConfig()) { + val jsonObject = JSONObject() + jsonObject.put(PRODUCT_CONFIG_JSON_KEY_FOR_KEY, entry.key) + jsonObject.put(PRODUCT_CONFIG_JSON_KEY_FOR_VALUE, entry.value) + array.put(jsonObject) + } + response.put(Constants.KEY_KV, array) + return response + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/product_config/CTProductConfigControllerTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/product_config/CTProductConfigControllerTest.kt new file mode 100644 index 000000000..f29cd45c5 --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/product_config/CTProductConfigControllerTest.kt @@ -0,0 +1,325 @@ +package com.clevertap.android.sdk.product_config + +import android.os.Looper.getMainLooper +import com.clevertap.android.sdk.BaseAnalyticsManager +import com.clevertap.android.sdk.BaseCallbackManager +import com.clevertap.android.sdk.CallbackManager +import com.clevertap.android.sdk.CoreMetaData +import com.clevertap.android.sdk.DeviceInfo +import com.clevertap.android.sdk.MockDeviceInfo +import com.clevertap.android.sdk.task.CTExecutorFactory +import com.clevertap.android.sdk.task.MockCTExecutors +import com.clevertap.android.sdk.utils.FileUtils +import com.clevertap.android.shared.test.BaseTestCase +import org.json.JSONObject +import org.junit.* +import org.junit.Test +import org.junit.jupiter.api.* +import org.junit.jupiter.api.MethodOrderer.* +import org.junit.runner.* +import org.mockito.* +import org.mockito.Mockito.* +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows.shadowOf +import java.util.HashMap +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +@RunWith(RobolectricTestRunner::class) +@TestMethodOrder(OrderAnnotation::class) +class CTProductConfigControllerTest : BaseTestCase() { + + private lateinit var mProductConfigController: CTProductConfigController + private lateinit var coreMetaData: CoreMetaData + private lateinit var analyticsManager: BaseAnalyticsManager + private lateinit var callbackManager: BaseCallbackManager + private lateinit var productConfigSettings: ProductConfigSettings + private lateinit var deviceInfo: DeviceInfo + private lateinit var fileUtils: FileUtils + private lateinit var listener: CTProductConfigListener + private lateinit var guid: String + + @Throws(Exception::class) + @BeforeEach + override fun setUp() { + super.setUp() + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn(MockCTExecutors(cleverTapInstanceConfig)) + guid = "1212121221" + coreMetaData = CoreMetaData() + analyticsManager = mock(BaseAnalyticsManager::class.java) + deviceInfo = MockDeviceInfo(application, cleverTapInstanceConfig, guid, coreMetaData) + callbackManager = CallbackManager(cleverTapInstanceConfig, deviceInfo) + listener = mock(CTProductConfigListener::class.java) + callbackManager.productConfigListener = listener + productConfigSettings = mock(ProductConfigSettings::class.java) + `when`(productConfigSettings.guid).thenReturn(guid) + fileUtils = spy(FileUtils(application, cleverTapInstanceConfig)) + mProductConfigController = CTProductConfigController( + application, + cleverTapInstanceConfig, + analyticsManager, + coreMetaData, + callbackManager, + productConfigSettings, fileUtils + ) + } + + mProductConfigController.setDefaults(MockPCResponse().getDefaultConfig()) + } + + @Test + fun testInit() { + Assert.assertTrue(mProductConfigController.isInitialized.get()) + } + + @Test + fun testFetch_Valid_Guid_Window_Expired() { + val windowInSeconds = TimeUnit.MINUTES.toSeconds(12) + `when`(productConfigSettings.nextFetchIntervalInSeconds).thenReturn(windowInSeconds) + val lastResponseTime = System.currentTimeMillis() - 2 * windowInSeconds * TimeUnit.SECONDS.toMillis(1) + `when`(productConfigSettings.lastFetchTimeStampInMillis).thenReturn(lastResponseTime) + + mProductConfigController.fetch() + verify(analyticsManager).sendFetchEvent(any()) + Assert.assertTrue(coreMetaData.isProductConfigRequested) + } + + @Test + fun testFetch_Valid_Guid_Window_Not_Expired_Request_Not_Sent() { + shadowOf(getMainLooper()).idle() + val windowInSeconds = TimeUnit.MINUTES.toSeconds(12) + `when`(productConfigSettings.nextFetchIntervalInSeconds).thenReturn(windowInSeconds) + val lastResponseTime = System.currentTimeMillis() - windowInSeconds / 2 * TimeUnit.SECONDS.toMillis(1) + `when`(productConfigSettings.lastFetchTimeStampInMillis).thenReturn(lastResponseTime) + + mProductConfigController.fetch() + verify(analyticsManager, never()).sendFetchEvent(any()) + Assert.assertFalse(coreMetaData.isProductConfigRequested) + } + + @Test + fun testFetch_InValid_Guid_Request_Not_Sent() { + val windowInSeconds = TimeUnit.MINUTES.toSeconds(12) + `when`(productConfigSettings.nextFetchIntervalInSeconds).thenReturn(windowInSeconds) + val lastResponseTime = System.currentTimeMillis() - (windowInSeconds * 1000 * 2) + `when`(productConfigSettings.lastFetchTimeStampInMillis).thenReturn(lastResponseTime) + `when`(productConfigSettings.guid).thenReturn("") + mProductConfigController.fetch() + verify(analyticsManager, never()).sendFetchEvent(any()) + Assert.assertFalse(coreMetaData.isProductConfigRequested) + } + + @Test + fun testReset_Settings() { + mProductConfigController.resetSettings() + verify(productConfigSettings).reset(any(FileUtils::class.java)) + } + + @Test + fun test_Reset() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn(MockCTExecutors(cleverTapInstanceConfig)) + mProductConfigController.reset() + Assert.assertEquals(mProductConfigController.defaultConfigs.size, 0) + Assert.assertEquals(mProductConfigController.activatedConfigs.size, 0) + verify(productConfigSettings).initDefaults() + verify(fileUtils).deleteDirectory(mProductConfigController.productConfigDirName) + } + } + + @Test + fun test_setArpValue() { + val jsonObject = JSONObject() + mProductConfigController.setArpValue(jsonObject) + verify(productConfigSettings).setARPValue(jsonObject) + } + + @Test + fun test_setMinimumFetchIntervalInSeconds() { + val timeInSec = TimeUnit.MINUTES.toSeconds(5) + mProductConfigController.setMinimumFetchIntervalInSeconds(timeInSec) + verify(productConfigSettings).setMinimumFetchIntervalInSeconds(timeInSec) + } + + @Test + fun test_Getters() { + Assert.assertEquals(mProductConfigController.analyticsManager, analyticsManager) + Assert.assertEquals(mProductConfigController.callbackManager, callbackManager) + Assert.assertEquals(mProductConfigController.config, cleverTapInstanceConfig) + Assert.assertEquals(mProductConfigController.coreMetaData, coreMetaData) + Assert.assertEquals(mProductConfigController.settings, productConfigSettings) + } + + @Test + fun test_activate() { + + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn(MockCTExecutors(cleverTapInstanceConfig)) + `when`(fileUtils.readFromFile(mProductConfigController.activatedFullPath)).thenReturn( + JSONObject( + MockPCResponse().getFetchedConfig() as Map<*, *> + ).toString() + ) + mProductConfigController.activate() + verify(listener).onActivated() + Assert.assertEquals(333333L, mProductConfigController.getLong("fetched_long")) + Assert.assertEquals("This is fetched string", mProductConfigController.getString("fetched_str")) + Assert.assertEquals(44444.4444, mProductConfigController.getDouble("fetched_double"), 0.1212) + Assert.assertEquals(true, mProductConfigController.getBoolean("fetched_bool")) + Assert.assertEquals("This is def_string", mProductConfigController.getString("def_str")) + Assert.assertEquals(11111L, mProductConfigController.getLong("def_long")) + Assert.assertEquals(2222.2222, mProductConfigController.getDouble("def_double"), 0.1212) + Assert.assertEquals(false, mProductConfigController.getBoolean("def_bool")) + } + } + + @Test + fun test_activate_Fail() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(productConfigSettings.guid).thenReturn("") + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn(MockCTExecutors(cleverTapInstanceConfig)) + mProductConfigController.activate() + verify(listener, never()).onActivated() + } + } + + @Test + fun test_fetch_activate() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn(MockCTExecutors(cleverTapInstanceConfig)) + val windowInSeconds = TimeUnit.MINUTES.toSeconds(12) + `when`(productConfigSettings.nextFetchIntervalInSeconds).thenReturn(windowInSeconds) + val lastResponseTime = System.currentTimeMillis() - 2 * windowInSeconds * TimeUnit.SECONDS.toMillis(1) + `when`(productConfigSettings.lastFetchTimeStampInMillis).thenReturn(lastResponseTime) + + mProductConfigController.fetchAndActivate() + verify(analyticsManager).sendFetchEvent(any()) + Assert.assertTrue(coreMetaData.isProductConfigRequested) + + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn(MockCTExecutors(cleverTapInstanceConfig)) + + `when`(fileUtils.readFromFile(mProductConfigController.activatedFullPath)).thenReturn( + JSONObject( + MockPCResponse().getFetchedConfig() as Map<*, *> + ).toString() + ) + mProductConfigController.activate() + verify(listener).onActivated() + Assert.assertEquals(mProductConfigController.getString("fetched_str"), "This is fetched string") + Assert.assertEquals(mProductConfigController.getLong("fetched_long"), 333333L) + Assert.assertEquals(mProductConfigController.getDouble("fetched_double"), 44444.4444, 0.1212) + Assert.assertEquals(mProductConfigController.getBoolean("fetched_bool"), true) + Assert.assertEquals(mProductConfigController.getString("def_str"), "This is def_string") + Assert.assertEquals(mProductConfigController.getLong("def_long"), 11111L) + Assert.assertEquals(mProductConfigController.getDouble("def_double"), 2222.2222, 0.1212) + Assert.assertEquals(mProductConfigController.getBoolean("def_bool"), false) + } + } + + @Test + fun test_getLastFetchTimeStampInMillis() { + mProductConfigController.lastFetchTimeStampInMillis + verify(productConfigSettings).lastFetchTimeStampInMillis + } + + @Test + fun test_onFetchSuccess() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn(MockCTExecutors(cleverTapInstanceConfig)) + mProductConfigController.onFetchSuccess(MockPCResponse().getMockPCResponse()) + verify(fileUtils).writeJsonToFile( + ArgumentMatchers.eq(mProductConfigController.productConfigDirName), + ArgumentMatchers.eq(CTProductConfigConstants.FILE_NAME_ACTIVATED), + any(JSONObject::class.java) + ) + verify(listener).onFetched() + } + } + + @Test + fun test_onFetchFailed() { + mProductConfigController.onFetchFailed() + Assert.assertEquals(mProductConfigController.isFetchAndActivating, false) + } + + @Test + fun test_setGuidAndInit_whenCleverTapIDNull_GuidNotSet() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn(MockCTExecutors(cleverTapInstanceConfig)) + val controller = spy(mProductConfigController) + val newGuid = "" + controller.setGuidAndInit(newGuid) + Assert.assertEquals(controller.settings.guid, guid) + verify(productConfigSettings, never()).setGuid(newGuid) + verify(controller, never()).initAsync() + } + } + + @Test + fun test_setGuidAndInit_tryingToSetGuidWhenAlreadyInitialised_ShouldNotUpdateGuid() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn(MockCTExecutors(cleverTapInstanceConfig)) + val controller = spy(mProductConfigController) + controller.isInitialized = AtomicBoolean(true) + val newGuid = "333333333" + controller.setGuidAndInit(newGuid) + Assert.assertEquals(controller.settings.guid, guid) + verify(productConfigSettings, never()).setGuid(newGuid) + verify(controller, never()).initAsync() + } + } + + @Test + fun test_setGuidAndInit_tryingToSetGuidWhenNotInitialised_GuidIsUpdatedWithInitialisation() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn(MockCTExecutors(cleverTapInstanceConfig)) + val settings = ProductConfigSettings(guid, cleverTapInstanceConfig, fileUtils) + val controller = spy( + CTProductConfigController( + application, + cleverTapInstanceConfig, + analyticsManager, + coreMetaData, + callbackManager, + settings, fileUtils + ) + ) + controller.isInitialized.set(false) + val newGuid = "333333333" + controller.setGuidAndInit(newGuid) + Assert.assertEquals(controller.settings.guid, newGuid) + verify(controller).initAsync() + Assert.assertTrue(controller.isInitialized.get()) + } + } + + @Test + fun test_setDefaultsWithXmlParser() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn(MockCTExecutors(cleverTapInstanceConfig)) + val resId = 12212 + val xmlParser = mock(DefaultXmlParser::class.java) + val mockPCResponse = MockPCResponse() + `when`(xmlParser.getDefaultsFromXml(application, resId)).thenReturn(mockPCResponse.getDefaultConfig() as HashMap) + val controller = spy(mProductConfigController) + controller.setDefaultsWithXmlParser(resId, xmlParser) + verify(controller).initAsync() + Assert.assertEquals(controller.getString("def_str"),"This is def_string" ) + Assert.assertEquals(controller.getString("def_long"),"11111" ) + Assert.assertEquals(controller.getString("def_double"),"2222.2222" ) + Assert.assertEquals(controller.getString("def_bool"),"false" ) + } + } + + @Test + fun test_setDefaultUsingXML() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn(MockCTExecutors(cleverTapInstanceConfig)) + val controller = spy(mProductConfigController) + val resId = 12212 + controller.setDefaults(resId) + verify(controller).setDefaultsWithXmlParser(eq(resId), any(DefaultXmlParser::class.java)) + } + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/product_config/CTProductConfigFactoryTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/product_config/CTProductConfigFactoryTest.kt new file mode 100644 index 000000000..e19a6d363 --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/product_config/CTProductConfigFactoryTest.kt @@ -0,0 +1,63 @@ +package com.clevertap.android.sdk.product_config + +import com.clevertap.android.sdk.BaseAnalyticsManager +import com.clevertap.android.sdk.BaseCallbackManager +import com.clevertap.android.sdk.CallbackManager +import com.clevertap.android.sdk.CoreMetaData +import com.clevertap.android.sdk.DeviceInfo +import com.clevertap.android.sdk.MockAnalyticsManager +import com.clevertap.android.sdk.MockDeviceInfo +import com.clevertap.android.shared.test.BaseTestCase +import org.junit.* +import org.junit.runner.* +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +internal class CTProductConfigFactoryTest : BaseTestCase() { + + val guid = "1212212" + lateinit var coreMetaData: CoreMetaData + lateinit var deviceInfo: DeviceInfo + lateinit var analyticsManager: BaseAnalyticsManager + lateinit var callbackManager: BaseCallbackManager + + override fun setUp() { + super.setUp() + coreMetaData = CoreMetaData() + deviceInfo = MockDeviceInfo(application, cleverTapInstanceConfig, guid, coreMetaData) + analyticsManager = MockAnalyticsManager() + callbackManager = CallbackManager(cleverTapInstanceConfig, deviceInfo) + } + + @Test + fun test_getInstance_not_null() { + val controller = CTProductConfigFactory.getInstance( + application, + deviceInfo, + cleverTapInstanceConfig, + analyticsManager, + coreMetaData, + callbackManager + ) + Assert.assertNotNull(controller) + Assert.assertTrue(controller is CTProductConfigController) + } + + @Test + fun test_getInstance_config_correct() { + val controller = CTProductConfigFactory.getInstance( + application, + deviceInfo, + cleverTapInstanceConfig, + analyticsManager, + coreMetaData, + callbackManager + ) + Assert.assertNotNull(controller.fileUtils) + Assert.assertEquals(controller.settings.guid, guid) + Assert.assertEquals(controller.config, cleverTapInstanceConfig) + Assert.assertEquals(controller.coreMetaData, coreMetaData) + Assert.assertEquals(controller.callbackManager, callbackManager) + Assert.assertEquals(controller.analyticsManager, analyticsManager) + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/product_config/DefaultXmlParserTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/product_config/DefaultXmlParserTest.kt new file mode 100644 index 000000000..f86d867f6 --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/product_config/DefaultXmlParserTest.kt @@ -0,0 +1,45 @@ +package com.clevertap.android.sdk.product_config + +import android.content.res.Resources +import android.content.res.XmlResourceParser +import com.clevertap.android.shared.test.BaseTestCase +import org.junit.* +import org.mockito.Mockito.* +import java.util.HashMap + +internal class DefaultXmlParserTest : BaseTestCase() { + + private lateinit var defaultXmlParser: DefaultXmlParser + private lateinit var resources: Resources + private lateinit var xmlResourceParser: XmlResourceParser + + + override fun setUp() { + super.setUp() + defaultXmlParser = DefaultXmlParser() + resources = mock(Resources::class.java) + xmlResourceParser = mock(XmlResourceParser::class.java) + } + + @Test + fun test_getDefaultsFromXml() { + val defaultXmlParser = spy(defaultXmlParser) + val context = spy(application) + `when`(context.resources).thenReturn(resources) + val resourceID = 1212121 + defaultXmlParser.getDefaultsFromXml(context, resourceID) + val map = HashMap() + verify(defaultXmlParser).getDefaultsFromXml(resources, resourceID, map) + } + + @Test + fun test_getDefaultsFromXml_whenContextResourcesAreNull_ParserMethodShouldNotGetCalled() { + val defaultXmlParser = spy(defaultXmlParser) + val context = spy(application) + `when`(context.resources).thenReturn(null) + val resourceID = 1212121 + defaultXmlParser.getDefaultsFromXml(context, resourceID) + val map = HashMap() + verify(defaultXmlParser,never()).getDefaultsFromXml(resources, resourceID, map) + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/product_config/MockPCResponse.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/product_config/MockPCResponse.kt new file mode 100644 index 000000000..b55f368ad --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/product_config/MockPCResponse.kt @@ -0,0 +1,40 @@ +package com.clevertap.android.sdk.product_config + +import com.clevertap.android.sdk.Constants +import org.json.JSONArray +import org.json.JSONObject + +class MockPCResponse { + + fun getFetchedConfig(): HashMap { + val fetchedConfig: HashMap = HashMap() + fetchedConfig.put("fetched_str", "This is fetched string") + fetchedConfig.put("fetched_long", 333333L) + fetchedConfig.put("fetched_double", 44444.4444) + fetchedConfig.put("fetched_bool", true) + return fetchedConfig + } + + fun getDefaultConfig(): HashMap { + val defaultConfig: HashMap = HashMap() + defaultConfig.put("def_str", "This is def_string") + defaultConfig.put("def_long", "11111") + defaultConfig.put("def_double", "2222.2222") + defaultConfig.put("def_bool", "false") + return defaultConfig + } + + fun getMockPCResponse(): JSONObject { + val response = JSONObject() + val array = JSONArray() + for (entry in getFetchedConfig()) { + val jsonObject = JSONObject() + jsonObject.put(CTProductConfigConstants.PRODUCT_CONFIG_JSON_KEY_FOR_KEY, entry.key) + jsonObject.put(CTProductConfigConstants.PRODUCT_CONFIG_JSON_KEY_FOR_VALUE, entry.value) + array.put(jsonObject) + } + response.put(Constants.KEY_KV, array) + response.put(CTProductConfigConstants.KEY_LAST_FETCHED_TIMESTAMP, (System.currentTimeMillis() / 1000).toInt()) + return response + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/product_config/ProductConfigSettingsTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/product_config/ProductConfigSettingsTest.kt new file mode 100644 index 000000000..08bf2893e --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/product_config/ProductConfigSettingsTest.kt @@ -0,0 +1,155 @@ +package com.clevertap.android.sdk.product_config + +import com.clevertap.android.sdk.product_config.CTProductConfigConstants.DEFAULT_MIN_FETCH_INTERVAL_SECONDS +import com.clevertap.android.sdk.task.CTExecutorFactory +import com.clevertap.android.sdk.task.MockCTExecutors +import com.clevertap.android.sdk.utils.FileUtils +import com.clevertap.android.shared.test.BaseTestCase +import org.json.JSONObject +import org.junit.* +import org.junit.runner.* +import org.mockito.Mockito.* +import org.robolectric.RobolectricTestRunner +import java.lang.RuntimeException +import java.util.concurrent.TimeUnit + +@RunWith(RobolectricTestRunner::class) +internal class ProductConfigSettingsTest : BaseTestCase() { + + lateinit var settings: ProductConfigSettings + val guid = "1212121" + lateinit var fileUtils: FileUtils + + @Before + override fun setUp() { + super.setUp() + fileUtils = mock(FileUtils::class.java) + settings = ProductConfigSettings(guid, cleverTapInstanceConfig, fileUtils) + } + + @Test + fun aInitTest() { + Assert.assertEquals(settings.lastFetchTimeStampInMillis, 0) + Assert.assertEquals(settings.nextFetchIntervalInSeconds, DEFAULT_MIN_FETCH_INTERVAL_SECONDS) + } + + @Test + fun testTimestamp() { + val timeMillis = System.currentTimeMillis() + settings.lastFetchTimeStampInMillis = timeMillis + Assert.assertEquals(timeMillis, settings.lastFetchTimeStampInMillis) + } + + @Test + fun test_set_guid() { + val guid = "121212" + settings.guid = guid + Assert.assertEquals(guid, settings.guid) + } + + @Test + fun test_Set_ARP_SDK_Time_Not_Set() { + //*************** Client has not set the fetch interval from SDK side + // case 1: Server Settings is greater than default values + var jsonObject = JSONObject() + jsonObject.put(CTProductConfigConstants.PRODUCT_CONFIG_NO_OF_CALLS, 2) + jsonObject.put(CTProductConfigConstants.PRODUCT_CONFIG_WINDOW_LENGTH_MINS, 60) + settings.setARPValue(jsonObject) + + Assert.assertEquals(settings.nextFetchIntervalInSeconds, TimeUnit.MINUTES.toSeconds(60 / 2)) + + // case 2: Server Settings is smaller than default values + jsonObject = JSONObject() + jsonObject.put(CTProductConfigConstants.PRODUCT_CONFIG_NO_OF_CALLS, 10) + jsonObject.put(CTProductConfigConstants.PRODUCT_CONFIG_WINDOW_LENGTH_MINS, 60) + settings.setARPValue(jsonObject) + + Assert.assertEquals(settings.nextFetchIntervalInSeconds, DEFAULT_MIN_FETCH_INTERVAL_SECONDS) + } + + @Test + fun test_Set_ARP_SDK_Time_Set() { + //*************** Client has set the fetch interval from SDK side + // case 1: Server Settings is greater than SDK set value + settings.setMinimumFetchIntervalInSeconds(TimeUnit.MINUTES.toSeconds(17)) + var jsonObject = JSONObject() + jsonObject.put(CTProductConfigConstants.PRODUCT_CONFIG_NO_OF_CALLS, 2) + jsonObject.put(CTProductConfigConstants.PRODUCT_CONFIG_WINDOW_LENGTH_MINS, 60) + settings.setARPValue(jsonObject) + + Assert.assertEquals(settings.nextFetchIntervalInSeconds, TimeUnit.MINUTES.toSeconds(60 / 2)) + + + settings.setMinimumFetchIntervalInSeconds(TimeUnit.MINUTES.toSeconds(45)) + // case 2: Server Settings is smaller than default values + jsonObject = JSONObject() + jsonObject.put(CTProductConfigConstants.PRODUCT_CONFIG_NO_OF_CALLS, 10) + jsonObject.put(CTProductConfigConstants.PRODUCT_CONFIG_WINDOW_LENGTH_MINS, 50) + settings.setARPValue(jsonObject) + + Assert.assertEquals(settings.nextFetchIntervalInSeconds, TimeUnit.MINUTES.toSeconds(45)) + } + + @Test + fun testReset() { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(cleverTapInstanceConfig)).thenReturn(MockCTExecutors(cleverTapInstanceConfig)) + settings.reset(fileUtils) + verify(fileUtils).deleteFile(settings.fullPath) + + } + aInitTest() + } + + @Test + fun testLoadSettings_Empty_String() { + settings.loadSettings(fileUtils) + `when`(fileUtils.readFromFile(anyString())).thenReturn("") + verify(fileUtils).readFromFile(settings.fullPath) + aInitTest() + } + + @Test(expected = IllegalArgumentException::class) + fun when_Null_fileUtils_then_loadSetting_throws_Exception() { + settings.loadSettings(null) + } + + @Test + fun when_fileUtils_Throws_Exception_loadSetting_throws_Exception() { + val spySettings = spy(settings) + `when`(fileUtils.readFromFile(settings.fullPath)).thenThrow(RuntimeException("Something went wrong")) + spySettings.loadSettings(fileUtils) + verify(spySettings, never()).getJsonObject(anyString()) + verify(spySettings, never()).populateMapWithJson(any(JSONObject::class.java)) + } + + @Test + fun testLoadSettings_Non_Empty_Data() { + + val map = HashMap() + map.put(CTProductConfigConstants.PRODUCT_CONFIG_NO_OF_CALLS, "10") + map.put(CTProductConfigConstants.PRODUCT_CONFIG_WINDOW_LENGTH_MINS, "60") + val timeStamp = System.currentTimeMillis() + map.put(CTProductConfigConstants.KEY_LAST_FETCHED_TIMESTAMP, timeStamp.toString()) + map.put( + CTProductConfigConstants.PRODUCT_CONFIG_MIN_INTERVAL_IN_SECONDS, + TimeUnit.MINUTES.toSeconds(16).toString() + ) + val jsonString = JSONObject(map as Map<*, *>).toString() + `when`(fileUtils.readFromFile(anyString())).thenReturn(jsonString) + + settings.loadSettings(fileUtils) + settings.setMinimumFetchIntervalInSeconds(TimeUnit.MINUTES.toSeconds(45)) + + verify(fileUtils).readFromFile(settings.fullPath) + Assert.assertEquals(settings.lastFetchTimeStampInMillis, timeStamp) + Assert.assertEquals(settings.nextFetchIntervalInSeconds, TimeUnit.MINUTES.toSeconds(45)) + + + settings.loadSettings(fileUtils) + settings.setMinimumFetchIntervalInSeconds(TimeUnit.MINUTES.toSeconds(5)) + + Assert.assertEquals(settings.lastFetchTimeStampInMillis, timeStamp) + Assert.assertEquals(settings.nextFetchIntervalInSeconds, TimeUnit.MINUTES.toSeconds(6)) + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/product_config/ProductConfigTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/product_config/ProductConfigTest.kt deleted file mode 100644 index 4d8974b87..000000000 --- a/clevertap-core/src/test/java/com/clevertap/android/sdk/product_config/ProductConfigTest.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.clevertap.android.sdk.product_config - -import com.clevertap.android.shared.test.BaseTestCase -import org.junit.* -import org.mockito.* - -class ProductConfigTest : BaseTestCase() { - - @Before - @Throws(Exception::class) - override fun setUp() { - super.setUp() - } - - @Test - fun testFetch() { - Mockito.`when`(cleverTapAPI!!.productConfig()) - .thenReturn(CTProductConfigController(application, "12121", cleverTapInstanceConfig, cleverTapAPI)) - cleverTapAPI!!.productConfig().fetch() - Mockito.verify(cleverTapAPI)!!.fetchProductConfig() - } - - @Test - fun testGetBoolean() { - val ctProductConfigController = Mockito.mock(CTProductConfigController::class.java) - Mockito.`when`(cleverTapAPI!!.productConfig()).thenReturn(ctProductConfigController) - Mockito.`when`(ctProductConfigController.getBoolean("testBool")).thenReturn(false) - Assert.assertFalse(cleverTapAPI!!.productConfig().getBoolean("testBool")) - } - - @Test - fun testGetLong() { - val ctProductConfigController = Mockito.mock(CTProductConfigController::class.java) - Mockito.`when`(cleverTapAPI!!.productConfig()).thenReturn(ctProductConfigController) - Mockito.`when`(ctProductConfigController.getLong("testLong")).thenReturn(122212121L) - Assert.assertNotEquals(12, cleverTapAPI!!.productConfig().getLong("testLong")) - Assert.assertEquals(122212121, cleverTapAPI!!.productConfig().getLong("testLong")) - } - - @Test - fun testGetDouble() { - val ctProductConfigController = Mockito.mock(CTProductConfigController::class.java) - Mockito.`when`(cleverTapAPI!!.productConfig()).thenReturn(ctProductConfigController) - Mockito.`when`(ctProductConfigController.getDouble("testDouble")).thenReturn(122.21) - Assert.assertNotEquals(12.0, cleverTapAPI!!.productConfig().getDouble("testDouble")) - Assert.assertEquals(122.21, cleverTapAPI!!.productConfig().getDouble("testDouble"), 0.0) - } - - @Test - fun testGetString() { - val ctProductConfigController = Mockito.mock(CTProductConfigController::class.java) - Mockito.`when`(cleverTapAPI!!.productConfig()).thenReturn(ctProductConfigController) - Mockito.`when`(ctProductConfigController.getString("testString")).thenReturn("Testing String") - Assert.assertNotEquals("Wrong Value", cleverTapAPI!!.productConfig().getString("testString")) - Assert.assertEquals("Testing String", cleverTapAPI!!.productConfig().getString("testString")) - } - - @Test - fun testActivate() { -// CTProductConfigController ctProductConfigController = mock(CTProductConfigController.class); -// when(cleverTapAPI.productConfig()).thenReturn(ctProductConfigController); -// Assert.assertEquals("Testing String", cleverTapAPI.productConfig().getString("testString")); - } -} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/product_config/ProductConfigUtilTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/product_config/ProductConfigUtilTest.kt new file mode 100644 index 000000000..55937cc40 --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/product_config/ProductConfigUtilTest.kt @@ -0,0 +1,22 @@ +package com.clevertap.android.sdk.product_config + +import org.junit.* +import org.junit.jupiter.api.Test + +class ProductConfigUtilTest { + @Test + fun isSupportedDataType(){ + val obj = "1212" + Assert.assertTrue(ProductConfigUtil.isSupportedDataType(obj)) + + val obj1 = 1212 + Assert.assertTrue(ProductConfigUtil.isSupportedDataType(obj1)) + + val obj2 = false + Assert.assertTrue(ProductConfigUtil.isSupportedDataType(obj2)) + + val obj3 = Object() + Assert.assertFalse(ProductConfigUtil.isSupportedDataType(obj3)) + + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/pushnotification/amp/CTBackgroundIntentServiceTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/pushnotification/amp/CTBackgroundIntentServiceTest.kt new file mode 100644 index 000000000..3186091e1 --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/pushnotification/amp/CTBackgroundIntentServiceTest.kt @@ -0,0 +1,37 @@ +package com.clevertap.android.sdk.pushnotification.amp + +import android.content.Intent +import com.clevertap.android.sdk.CleverTapAPI +import com.clevertap.android.shared.test.BaseTestCase +import com.clevertap.android.shared.test.TestApplication +import org.junit.* +import org.junit.Assert.* +import org.junit.runner.* +import org.mockito.Mockito.* +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [28], application = TestApplication::class) +class CTBackgroundIntentServiceTest : BaseTestCase() { + + override fun setUp() { + super.setUp() + } + + @Test + fun test_handleIntent() { + mockStatic(CleverTapAPI::class.java).use { + val exceptionMessage = "CleverTapAPI#runBackgroundIntentService called" + `when`(CleverTapAPI.runBackgroundIntentService(any())).thenThrow(RuntimeException(exceptionMessage)) + + val exception = assertThrows(RuntimeException::class.java) { + val serviceController = Robolectric.buildIntentService(CTBackgroundIntentService::class.java, Intent()) + serviceController.create().handleIntent() + } + + assertTrue(exceptionMessage.equals(exception.message)) + } + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/pushnotification/amp/CTBackgroundJobServiceTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/pushnotification/amp/CTBackgroundJobServiceTest.kt new file mode 100644 index 000000000..7733ccc83 --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/pushnotification/amp/CTBackgroundJobServiceTest.kt @@ -0,0 +1,29 @@ +package com.clevertap.android.sdk.pushnotification.amp + +import android.app.job.JobParameters +import com.clevertap.android.shared.test.BaseTestCase +import com.clevertap.android.shared.test.TestApplication +import org.junit.* +import org.junit.runner.* +import org.mockito.Mockito.* +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [28], application = TestApplication::class) +class CTBackgroundJobServiceTest : BaseTestCase() { + + private lateinit var service: CTBackgroundJobService + private lateinit var mockParams: JobParameters + override fun setUp() { + super.setUp() + service = spy(Robolectric.setupService(CTBackgroundJobService::class.java)) + mockParams = mock(JobParameters::class.java) + } + + @Test + fun test_onStartJob() { + + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/pushnotification/fcm/FcmMessageListenerServiceTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/pushnotification/fcm/FcmMessageListenerServiceTest.kt index ab948e949..0a65b4bc9 100644 --- a/clevertap-core/src/test/java/com/clevertap/android/sdk/pushnotification/fcm/FcmMessageListenerServiceTest.kt +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/pushnotification/fcm/FcmMessageListenerServiceTest.kt @@ -1,6 +1,7 @@ package com.clevertap.android.sdk.pushnotification.fcm import android.content.Context +import android.content.Intent import android.os.Bundle import com.clevertap.android.sdk.pushnotification.fcm.TestFcmConstants.Companion.FCM_TOKEN import com.clevertap.android.shared.test.BaseTestCase @@ -9,6 +10,7 @@ import com.google.firebase.messaging.RemoteMessage import org.junit.* import org.junit.runner.* import org.mockito.* +import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config @@ -22,12 +24,10 @@ class FcmMessageListenerServiceTest : BaseTestCase() { @Before override fun setUp() { super.setUp() - service = Mockito.spy( - FcmMessageListenerService - ::class.java - ) + val serviceController = Robolectric.buildService(FcmMessageListenerService::class.java, Intent()) + serviceController.create().startCommand(0, 1) + service = serviceController.get() mockedMessageHandler = Mockito.mock(FcmMessageHandlerImpl::class.java) - Mockito.doReturn(application).`when`(service).applicationContext } @Test diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/pushnotification/fcm/FcmPushProviderTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/pushnotification/fcm/FcmPushProviderTest.kt index 144f5f69e..56a5a9430 100644 --- a/clevertap-core/src/test/java/com/clevertap/android/sdk/pushnotification/fcm/FcmPushProviderTest.kt +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/pushnotification/fcm/FcmPushProviderTest.kt @@ -1,5 +1,8 @@ package com.clevertap.android.sdk.pushnotification.fcm +import com.clevertap.android.sdk.CoreMetaData +import com.clevertap.android.sdk.DeviceInfo +import com.clevertap.android.sdk.MockDeviceInfo import com.clevertap.android.sdk.pushnotification.CTPushProviderListener import com.clevertap.android.sdk.pushnotification.PushConstants.ANDROID_PLATFORM import com.clevertap.android.shared.test.BaseTestCase @@ -22,7 +25,7 @@ class FcmPushProviderTest : BaseTestCase() { override fun setUp() { super.setUp() ctPushProviderListener = mock(CTPushProviderListener::class.java) - provider = FcmPushProvider(ctPushProviderListener) + provider = FcmPushProvider(ctPushProviderListener, application, cleverTapInstanceConfig) sdkHandler = mock(FcmSdkHandlerImpl::class.java) provider.setHandler(sdkHandler) } diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/pushnotification/fcm/FcmSdkHandlerImplTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/pushnotification/fcm/FcmSdkHandlerImplTest.kt index 070e292cf..79cf310bb 100644 --- a/clevertap-core/src/test/java/com/clevertap/android/sdk/pushnotification/fcm/FcmSdkHandlerImplTest.kt +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/pushnotification/fcm/FcmSdkHandlerImplTest.kt @@ -1,10 +1,10 @@ package com.clevertap.android.sdk.pushnotification.fcm import com.clevertap.android.sdk.ManifestInfo -import com.clevertap.android.sdk.PackageUtils import com.clevertap.android.sdk.pushnotification.CTPushProviderListener import com.clevertap.android.sdk.pushnotification.PushConstants.PushType.FCM import com.clevertap.android.sdk.pushnotification.fcm.TestFcmConstants.Companion.FCM_SENDER_ID +import com.clevertap.android.sdk.utils.PackageUtils import com.clevertap.android.shared.test.BaseTestCase import com.clevertap.android.shared.test.TestApplication import com.google.firebase.FirebaseApp @@ -28,9 +28,7 @@ class FcmSdkHandlerImplTest : BaseTestCase() { override fun setUp() { super.setUp() listener = mock(CTPushProviderListener::class.java) - handler = FcmSdkHandlerImpl(listener) - `when`(listener.context()).thenReturn(application) - `when`(listener.config()).thenReturn(cleverTapInstanceConfig) + handler = FcmSdkHandlerImpl(listener, application, cleverTapInstanceConfig) manifestInfo = mock(ManifestInfo::class.java) handler.setManifestInfo(manifestInfo) } @@ -38,7 +36,7 @@ class FcmSdkHandlerImplTest : BaseTestCase() { @Test fun isAvailable_Unavailable_PlayServices_Returns_False() { mockStatic(PackageUtils::class.java).use { - `when`(PackageUtils.isGooglePlayServicesAvailable(listener.context())).thenReturn(false) + `when`(PackageUtils.isGooglePlayServicesAvailable(application)).thenReturn(false) Assert.assertFalse(handler.isAvailable) } } @@ -46,7 +44,7 @@ class FcmSdkHandlerImplTest : BaseTestCase() { @Test fun isAvailable_Valid_Manifest_Returns_True() { mockStatic(PackageUtils::class.java).use { - `when`(PackageUtils.isGooglePlayServicesAvailable(listener.context())).thenReturn(true) + `when`(PackageUtils.isGooglePlayServicesAvailable(application)).thenReturn(true) `when`(manifestInfo.fcmSenderId).thenReturn(FCM_SENDER_ID) Assert.assertTrue(handler.isAvailable) } @@ -55,7 +53,7 @@ class FcmSdkHandlerImplTest : BaseTestCase() { @Test fun isAvailable_InValid_Manifest_Valid_Config_Json_Returns_True() { mockStatic(PackageUtils::class.java).use { - `when`(PackageUtils.isGooglePlayServicesAvailable(listener.context())).thenReturn(true) + `when`(PackageUtils.isGooglePlayServicesAvailable(application)).thenReturn(true) `when`(manifestInfo.fcmSenderId).thenReturn(null) val app = mock(FirebaseApp::class.java) @@ -72,7 +70,7 @@ class FcmSdkHandlerImplTest : BaseTestCase() { @Test fun isAvailable_InValid_Manifest_InValid_Config_Json_Returns_False() { mockStatic(PackageUtils::class.java).use { - `when`(PackageUtils.isGooglePlayServicesAvailable(listener.context())).thenReturn(true) + `when`(PackageUtils.isGooglePlayServicesAvailable(application)).thenReturn(true) `when`(manifestInfo.fcmSenderId).thenReturn(null) val app = mock(FirebaseApp::class.java) @@ -89,7 +87,7 @@ class FcmSdkHandlerImplTest : BaseTestCase() { @Test fun isAvailable_Exception_Returns_False() { mockStatic(PackageUtils::class.java).use { - `when`(PackageUtils.isGooglePlayServicesAvailable(listener.context())).thenThrow(RuntimeException("Something Went Wrong")) + `when`(PackageUtils.isGooglePlayServicesAvailable(application)).thenThrow(RuntimeException("Something Went Wrong")) Assert.assertFalse(handler.isAvailable) } } @@ -97,7 +95,7 @@ class FcmSdkHandlerImplTest : BaseTestCase() { @Test fun isSupported_Returns_True() { mockStatic(PackageUtils::class.java).use { - `when`(PackageUtils.isGooglePlayStoreAvailable(listener.context())).thenReturn(true) + `when`(PackageUtils.isGooglePlayStoreAvailable(application)).thenReturn(true) Assert.assertTrue(handler.isSupported) } } @@ -105,7 +103,7 @@ class FcmSdkHandlerImplTest : BaseTestCase() { @Test fun isSupported_Returns_False() { mockStatic(PackageUtils::class.java).use { - `when`(PackageUtils.isGooglePlayStoreAvailable(listener.context())).thenReturn(false) + `when`(PackageUtils.isGooglePlayStoreAvailable(application)).thenReturn(false) Assert.assertFalse(handler.isSupported) } } diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/task/CTExecutorFactoryTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/task/CTExecutorFactoryTest.kt new file mode 100644 index 000000000..c042bbdbf --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/task/CTExecutorFactoryTest.kt @@ -0,0 +1,35 @@ +package com.clevertap.android.sdk.task + +import com.clevertap.android.shared.test.BaseTestCase +import org.junit.* +import org.junit.runner.* +import org.mockito.* +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class CTExecutorFactoryTest : BaseTestCase() { + + override fun setUp() { + super.setUp() + } + + @Test(expected = IllegalArgumentException::class) + fun test_executors_whenConfigNull_ShouldThrowException() { + CTExecutorFactory.executors(null) + } + + @Test + fun test_executors_whenConfigNotNull_ShouldReturnValidObject() { + val executor = CTExecutorFactory.executors(cleverTapInstanceConfig) + Assert.assertTrue(executor is CTExecutors) + } + + @Test + fun test_executors_whenTwoDiffrentConfigs_ShouldReturnDifferentObjects() { + val executor = CTExecutorFactory.executors(cleverTapInstanceConfig) + val newConfig = Mockito.spy(cleverTapInstanceConfig) + Mockito.`when`(newConfig.accountId).thenReturn("312312312312") + val executorNew = CTExecutorFactory.executors(newConfig) + Assert.assertNotEquals(executor, executorNew) + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/task/IOExecutorTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/task/IOExecutorTest.kt new file mode 100644 index 000000000..d1debb04b --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/task/IOExecutorTest.kt @@ -0,0 +1,131 @@ +package com.clevertap.android.sdk.task + +import com.clevertap.android.shared.test.BaseTestCase +import org.junit.* +import org.junit.runner.* +import org.mockito.* +import org.robolectric.RobolectricTestRunner +import java.util.concurrent.Callable +import java.util.concurrent.ExecutorService +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeUnit.SECONDS + +@RunWith(RobolectricTestRunner::class) +class IOExecutorTest : BaseTestCase() { + + private lateinit var mExecutorService: ExecutorService + private lateinit var executor: IOExecutor + + @Before + override fun setUp() { + super.setUp() + mExecutorService = Mockito.mock(ExecutorService::class.java) + executor = IOExecutor() + executor.setExecutorService(mExecutorService) + } + + @Test + fun test_executorServiceConfigs() { + val executor = IOExecutor() + Assert.assertTrue(executor.executorService is ThreadPoolExecutor) + val executorService = executor.executorService as ThreadPoolExecutor + val noProcessors = Runtime.getRuntime().availableProcessors() + Assert.assertEquals(executorService.corePoolSize, 2 * noProcessors) + Assert.assertEquals(executorService.maximumPoolSize, 2 * noProcessors) + Assert.assertEquals(executorService.getKeepAliveTime(SECONDS), 60L) + } + + @Test + fun test_awaitTermination() { + val timeout = 10L + val unit = TimeUnit.MINUTES + executor.awaitTermination(timeout, unit) + Mockito.verify(mExecutorService).awaitTermination(timeout, unit) + } + + @Test + fun test_execute() { + val runnable = Runnable { } + executor.execute(runnable) + Mockito.verify(mExecutorService).execute(runnable) + } + + @Test + fun test_invokeAll() { + val taskCollection = ArrayList>() + executor.invokeAll(taskCollection) + Mockito.verify(mExecutorService).invokeAll(taskCollection) + } + + @Test + fun test_invokeAllWithTimeOut() { + val taskCollection = ArrayList>() + val timeout = 10L + val unit = TimeUnit.MINUTES + executor.invokeAll(taskCollection, timeout, unit) + Mockito.verify(mExecutorService).invokeAll(taskCollection, timeout, unit) + } + + @Test + fun test_invokeAny() { + val taskCollection = ArrayList>() + executor.invokeAny(taskCollection) + Mockito.verify(mExecutorService).invokeAny(taskCollection) + } + + @Test + fun test_invokeAnyWithTimeOut() { + val taskCollection = ArrayList>() + val timeout = 10L + val unit = TimeUnit.MINUTES + executor.invokeAny(taskCollection, timeout, unit) + Mockito.verify(mExecutorService).invokeAny(taskCollection, timeout, unit) + } + + @Test + fun test_isShutdown() { + executor.isShutdown() + Mockito.verify(mExecutorService).isShutdown() + } + + @Test + fun test_isTerminated() { + executor.isTerminated() + Mockito.verify(mExecutorService).isTerminated() + } + + @Test + fun test_shutdown() { + executor.shutdown() + Mockito.verify(mExecutorService).shutdown() + } + + @Test + fun test_shutdownNow() { + executor.shutdownNow() + Mockito.verify(mExecutorService).shutdownNow() + } + + @Test + fun test_submit_Callable() { + val callable = Callable { } + executor.submit(callable) + Mockito.verify(mExecutorService).submit(callable) + } + + @Test + fun test_submit_RunnableWithResult() { + val callable = Runnable { } + val result = "121" + executor.submit(callable, result) + Mockito.verify(mExecutorService).submit(callable, result) + } + + @Test + fun test_submit_RunnableOnly() { + val callable = Runnable { } + executor.submit(callable) + Mockito.verify(mExecutorService).submit(callable) + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/task/MainThreadExecutorTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/task/MainThreadExecutorTest.kt new file mode 100644 index 000000000..d3c1ed426 --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/task/MainThreadExecutorTest.kt @@ -0,0 +1,48 @@ +package com.clevertap.android.sdk.task + +import android.os.Handler +import android.os.Looper +import com.clevertap.android.shared.test.BaseTestCase +import org.junit.* +import org.junit.runner.* +import org.mockito.* +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class MainThreadExecutorTest : BaseTestCase() { + + @Before + override fun setUp() { + super.setUp() + } + + @Test + fun test_execute() { + val handler = Mockito.mock(Handler::class.java) + val executor = MainThreadExecutor() + executor.mainThreadHandler = handler + val runnable = Runnable { } + executor.execute(runnable) + Mockito.verify(handler).post(runnable) + } + + @Test + fun test_setMainThreadHandler() { + val handler = Handler() + val executor = MainThreadExecutor() + executor.setMainThreadHandler(handler) + Assert.assertEquals(executor.mainThreadHandler, handler) + } + + @Test + fun test_execute_whenRunnableRuns_RunsOnMainThread() { + val executor = MainThreadExecutor() + val runnable = Runnable { + Assert.assertTrue(Looper.myLooper() == Looper.getMainLooper()) + } + executor.execute(runnable) + + //or + Assert.assertTrue(executor.mainThreadHandler.looper == Looper.getMainLooper()) + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/task/MockCTExecutors.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/task/MockCTExecutors.kt new file mode 100644 index 000000000..73434e758 --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/task/MockCTExecutors.kt @@ -0,0 +1,26 @@ +package com.clevertap.android.sdk.task + +import com.clevertap.android.sdk.CleverTapInstanceConfig + +class MockCTExecutors(config: CleverTapInstanceConfig) : CTExecutors(config) { + + override fun ioTask(): Task { + val executor = MockExecutorService() + return Task(config, executor, executor, "ioTask") + } + + override fun mainTask(): Task { + val executor = MockExecutor() + return Task(config, executor, executor, "mainTask") + } + + override fun postAsyncSafelyTask(): Task { + val executor = MockExecutorService() + return Task(config, executor, executor, "postAsyncSafelyTask") + } + + override fun postAsyncSafelyTask(featureTask: String): Task { + val executor = MockExecutorService() + return Task(config, executor, executor, "postAsyncSafelyTask") + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/task/MockExecutor.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/task/MockExecutor.kt new file mode 100644 index 000000000..61e692438 --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/task/MockExecutor.kt @@ -0,0 +1,10 @@ +package com.clevertap.android.sdk.task + +import java.util.concurrent.Executor + +class MockExecutor:Executor { + + override fun execute(command: Runnable?) { + command?.run() + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/task/MockExecutorService.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/task/MockExecutorService.kt new file mode 100644 index 000000000..c51b17ccf --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/task/MockExecutorService.kt @@ -0,0 +1,72 @@ +package com.clevertap.android.sdk.task + +import java.util.concurrent.Callable +import java.util.concurrent.ExecutorService +import java.util.concurrent.Future +import java.util.concurrent.FutureTask +import java.util.concurrent.TimeUnit + +class MockExecutorService:ExecutorService { + + override fun execute(command: Runnable?) { + command?.run() + } + + override fun shutdown() { + throw UnsupportedOperationException("Not Supported") + } + + override fun shutdownNow(): MutableList { + throw UnsupportedOperationException("Not Supported") + } + + override fun isShutdown(): Boolean { + throw UnsupportedOperationException("Not Supported") + } + + override fun isTerminated(): Boolean { + throw UnsupportedOperationException("Not Supported") + } + + override fun awaitTermination(timeout: Long, unit: TimeUnit?): Boolean { + throw UnsupportedOperationException("Not Supported") + } + + override fun submit(task: Callable?): Future { + val futureTask = FutureTask(task) + futureTask.run() + return futureTask + } + + override fun submit(task: Runnable?, result: T): Future { + val futureTask = FutureTask(task, result) + futureTask.run() + return futureTask + } + + override fun submit(task: Runnable?): Future<*> { + val futureTask = FutureTask(task, null) + futureTask.run() + return futureTask + } + + override fun invokeAll(tasks: MutableCollection>?): MutableList> { + throw UnsupportedOperationException("Not Supported") + } + + override fun invokeAll( + tasks: MutableCollection>?, + timeout: Long, + unit: TimeUnit? + ): MutableList> { + throw java.lang.UnsupportedOperationException("Not Supported") + } + + override fun invokeAny(tasks: MutableCollection>?): T { + throw java.lang.UnsupportedOperationException("Not Supported") + } + + override fun invokeAny(tasks: MutableCollection>?, timeout: Long, unit: TimeUnit?): T { + throw java.lang.UnsupportedOperationException("Not Supported") + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/task/PostAsyncSafelyExecutorTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/task/PostAsyncSafelyExecutorTest.kt new file mode 100644 index 000000000..e81cce714 --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/task/PostAsyncSafelyExecutorTest.kt @@ -0,0 +1,159 @@ +package com.clevertap.android.sdk.task + +import com.clevertap.android.shared.test.BaseTestCase +import org.junit.* +import org.junit.runner.* +import org.mockito.* +import org.robolectric.RobolectricTestRunner +import java.util.concurrent.Callable +import java.util.concurrent.ExecutorService +import java.util.concurrent.TimeUnit + +@RunWith(RobolectricTestRunner::class) +class PostAsyncSafelyExecutorTest : BaseTestCase() { + + private lateinit var mExecutorService: ExecutorService + private lateinit var executor: PostAsyncSafelyExecutor + + @Before + override fun setUp() { + super.setUp() + mExecutorService = Mockito.mock(ExecutorService::class.java) + executor = PostAsyncSafelyExecutor() + executor.setExecutor(mExecutorService) + } + + @Test + fun test_executorServiceConfigs() { + val executor = PostAsyncSafelyExecutor() + Assert.assertTrue(executor.executor is ExecutorService) + } + + @Test + fun test_awaitTermination() { + val timeout = 10L + val unit = TimeUnit.MINUTES + executor.awaitTermination(timeout, unit) + Mockito.verify(mExecutorService).awaitTermination(timeout, unit) + } + + @Test + fun test_execute() { + executor.execute({ + println("Do something") + }) + Mockito.verify(mExecutorService).execute(Mockito.any(Runnable::class.java)) + } + + @Test + @Ignore + fun test_execute_whenExecuteCalledFromAsyncThread() { + //TODO + var threadID1 = -1L + var threadID2 = -2L + executor.execute({ + threadID1 = Thread.currentThread().id + executor.execute({ + threadID2 = Thread.currentThread().id + }) + }) + Mockito.verify(mExecutorService).execute(Mockito.any(Runnable::class.java)) + Thread.sleep(1000) + Assert.assertEquals(threadID1, threadID2) + } + + @Test(expected = NullPointerException::class) + fun test_execute_WhenNullRunnable_ThrowsNPE() { + executor.execute(null) + } + + @Test(expected = UnsupportedOperationException::class) + fun test_invokeAll() { + val taskCollection = ArrayList>() + executor.invokeAll(taskCollection) + } + + @Test(expected = UnsupportedOperationException::class) + fun test_invokeAllWithTimeOut() { + val taskCollection = ArrayList>() + val timeout = 10L + val unit = TimeUnit.MINUTES + executor.invokeAll(taskCollection, timeout, unit) + } + + @Test(expected = UnsupportedOperationException::class) + fun test_invokeAny() { + val taskCollection = ArrayList>() + executor.invokeAny(taskCollection) + } + + @Test(expected = UnsupportedOperationException::class) + fun test_invokeAnyWithTimeOut() { + val taskCollection = ArrayList>() + val timeout = 10L + val unit = TimeUnit.MINUTES + executor.invokeAny(taskCollection, timeout, unit) + } + + @Test + fun test_isShutdown() { + executor.isShutdown() + Mockito.verify(mExecutorService).isShutdown() + } + + @Test + fun test_isTerminated() { + executor.isTerminated() + Mockito.verify(mExecutorService).isTerminated() + } + + @Test + fun test_shutdown() { + executor.shutdown() + Mockito.verify(mExecutorService).shutdown() + } + + @Test + fun test_shutdownNow() { + executor.shutdownNow() + Mockito.verify(mExecutorService).shutdownNow() + } + + @Test(expected = NullPointerException::class) + fun test_submit_WhenNullCallable_ThrowsNPE() { + executor.submit(null) + } + + @Test + fun test_submit_Callable() { + val callable = Callable { } + executor.submit(callable) + Mockito.verify(mExecutorService).submit(Mockito.any(Callable::class.java)) + } + + @Test + fun test_submit_RunnableWithResult() { + val callable = Runnable { } + val result = "121" + executor.submit(callable, result) + Mockito.verify(mExecutorService).execute(Mockito.any(Runnable::class.java)) + } + + @Test(expected = NullPointerException::class) + fun test_submitWithResult_WhenNullRunnable_ThrowNPE() { + val result = "121" + executor.submit(null, result) + } + + @Test + fun test_submit_RunnableOnly() { + val callable = Runnable { } + executor.submit(callable) + Mockito.verify(mExecutorService).execute(Mockito.any(Runnable::class.java)) + } + + @Test(expected = NullPointerException::class) + fun test_submitRunnableOnly_WhenNullRunnable_ThrowsNPE() { + executor.submit(null) + } +} \ No newline at end of file diff --git a/clevertap-core/src/test/res/values/strings.xml b/clevertap-core/src/test/res/values/strings.xml deleted file mode 100644 index 966bb8c73..000000000 --- a/clevertap-core/src/test/res/values/strings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - R65-RR9-9R5Z - c22-562 - \ No newline at end of file diff --git a/clevertap-geofence/build.gradle b/clevertap-geofence/build.gradle index 20ae17d86..370559aab 100644 --- a/clevertap-geofence/build.gradle +++ b/clevertap-geofence/build.gradle @@ -8,8 +8,6 @@ ext { licenseName = 'MIT License' licenseUrl = 'https://opensource.org/licenses/MIT' allLicenses = ["MIT"] - - minSdkVersionVal = 16 } apply from: "../gradle-scripts/commons.gradle" @@ -21,7 +19,10 @@ dependencies { compileOnly deps.androidXConcurrentFutures // Unit testing dependencies - testImplementation deps.junit + testImplementation deps.junitPlatform + testImplementation deps.junitApi + testRuntimeOnly deps.junitEngine + testImplementation deps.mockitoCore testImplementation deps.robolectric @@ -44,7 +45,7 @@ dependencies { testImplementation deps.installreferrer testImplementation deps.playServicesLocation - testImplementation 'org.skyscreamer:jsonassert:1.5.0' + testImplementation deps.jsonAssert testImplementation deps.workManagerTesting testImplementation 'org.awaitility:awaitility:4.0.3' testImplementation deps.androidXConcurrentFutures diff --git a/clevertap-geofence/src/main/AndroidManifest.xml b/clevertap-geofence/src/main/AndroidManifest.xml index 85b501d30..5d734d5cf 100644 --- a/clevertap-geofence/src/main/AndroidManifest.xml +++ b/clevertap-geofence/src/main/AndroidManifest.xml @@ -8,9 +8,9 @@ - + - + diff --git a/clevertap-geofence/src/main/java/com/clevertap/android/geofence/CTGeofenceAPI.java b/clevertap-geofence/src/main/java/com/clevertap/android/geofence/CTGeofenceAPI.java index cf2756b2d..427862408 100644 --- a/clevertap-geofence/src/main/java/com/clevertap/android/geofence/CTGeofenceAPI.java +++ b/clevertap-geofence/src/main/java/com/clevertap/android/geofence/CTGeofenceAPI.java @@ -41,7 +41,7 @@ public interface OnGeofenceApiInitializedListener { private static CTGeofenceAPI ctGeofenceAPI; - private static Logger logger; + private static final Logger logger; private String accountId; @@ -78,13 +78,9 @@ public interface OnGeofenceApiInitializedListener { */ @SuppressWarnings("WeakerAccess") @NonNull - public static CTGeofenceAPI getInstance(@NonNull Context context) { + public static synchronized CTGeofenceAPI getInstance(@NonNull Context context) { if (ctGeofenceAPI == null) { - synchronized (CTGeofenceAPI.class) { - if (ctGeofenceAPI == null) { - ctGeofenceAPI = new CTGeofenceAPI(context); - } - } + ctGeofenceAPI = new CTGeofenceAPI(context); } return ctGeofenceAPI; } diff --git a/clevertap-geofence/src/main/java/com/clevertap/android/geofence/CTGeofenceConstants.java b/clevertap-geofence/src/main/java/com/clevertap/android/geofence/CTGeofenceConstants.java index 3b2042875..8794bd4bb 100644 --- a/clevertap-geofence/src/main/java/com/clevertap/android/geofence/CTGeofenceConstants.java +++ b/clevertap-geofence/src/main/java/com/clevertap/android/geofence/CTGeofenceConstants.java @@ -40,6 +40,8 @@ public class CTGeofenceConstants { static final String KEY_LAST_DISPLACEMENT = "last_displacement"; + static final String KEY_LAST_GEO_NOTIFICATION_RESPONSIVENESS = "last_geo_notification_responsiveness"; + static final String TAG_WORK_LOCATION_UPDATES = "com.clevertap.android.geofence.work.location"; static final int ERROR_CODE = 515; diff --git a/clevertap-geofence/src/main/java/com/clevertap/android/geofence/CTGeofenceSettings.java b/clevertap-geofence/src/main/java/com/clevertap/android/geofence/CTGeofenceSettings.java index 500eb52b4..b8cd088c4 100644 --- a/clevertap-geofence/src/main/java/com/clevertap/android/geofence/CTGeofenceSettings.java +++ b/clevertap-geofence/src/main/java/com/clevertap/android/geofence/CTGeofenceSettings.java @@ -39,6 +39,9 @@ public static final class Builder { private float smallestDisplacement = GoogleLocationAdapter.SMALLEST_DISPLACEMENT_IN_METERS; + private int geofenceNotificationResponsiveness = + GoogleGeofenceAdapter.GEOFENCE_NOTIFICATION_RESPONSIVENESS_IN_MILLISECONDS; + public Builder() { } @@ -200,6 +203,29 @@ public CTGeofenceSettings.Builder setSmallestDisplacement(float smallestDisplace this.smallestDisplacement = smallestDisplacement; return this; } + + /** + * Sets the best-effort notification responsiveness of the geofence. Defaults to 0.
    + * Setting a big responsiveness value, for example 5 minutes, can save power significantly.
    + * For example, if you set a responsiveness value of five minutes your app only checks for an entrance or + * exit + * alert once every five minutes.Setting lower values doesn't necessarily mean that users are notified within + * that time period (for example, if you set a value of 5 seconds it may take a bit longer than that to + * receive the alert). + * + * @param geofenceNotificationResponsiveness in (milliseconds) defines the best-effort description of how + * soon should the callback be called when the transition associated + * with the Geofence is triggered. + * For instance, if set to 300000 milliseconds the callback will be + * called 5 minutes within entering or + * exiting the geofence. + * @return {@link CTGeofenceSettings.Builder} + */ + public CTGeofenceSettings.Builder setGeofenceNotificationResponsiveness( + int geofenceNotificationResponsiveness) { + this.geofenceNotificationResponsiveness = geofenceNotificationResponsiveness; + return this; + } } /** @@ -255,6 +281,8 @@ public CTGeofenceSettings.Builder setSmallestDisplacement(float smallestDisplace private final byte locationFetchMode; // WorkManager or BroadcastReceiver + private final int geofenceNotificationResponsiveness; + private final @LogLevel int logLevel; @@ -270,6 +298,7 @@ private CTGeofenceSettings(Builder builder) { interval = builder.interval; fastestInterval = builder.fastestInterval; smallestDisplacement = builder.smallestDisplacement; + geofenceNotificationResponsiveness = builder.geofenceNotificationResponsiveness; } @Override @@ -286,7 +315,13 @@ public boolean equals(Object o) { locationFetchMode == that.locationFetchMode && logLevel == that.logLevel && geofenceMonitoringCount == that.geofenceMonitoringCount && id.equals(that.id) && interval == that.interval && fastestInterval == that.fastestInterval - && smallestDisplacement == that.smallestDisplacement; + && smallestDisplacement == that.smallestDisplacement && geofenceNotificationResponsiveness + == that.geofenceNotificationResponsiveness; + } + + @Override + public int hashCode() { + return super.hashCode(); } public long getFastestInterval() { @@ -325,4 +360,8 @@ public float getSmallestDisplacement() { public boolean isBackgroundLocationUpdatesEnabled() { return backgroundLocationUpdates; } + + public int getGeofenceNotificationResponsiveness() { + return geofenceNotificationResponsiveness; + } } diff --git a/clevertap-geofence/src/main/java/com/clevertap/android/geofence/CTGeofenceTaskManager.java b/clevertap-geofence/src/main/java/com/clevertap/android/geofence/CTGeofenceTaskManager.java index 8402acb8f..0598760de 100644 --- a/clevertap-geofence/src/main/java/com/clevertap/android/geofence/CTGeofenceTaskManager.java +++ b/clevertap-geofence/src/main/java/com/clevertap/android/geofence/CTGeofenceTaskManager.java @@ -109,13 +109,9 @@ void setExecutorService(ExecutorService es) { this.es = es; } - static CTGeofenceTaskManager getInstance() { + static synchronized CTGeofenceTaskManager getInstance() { if (taskManager == null) { - synchronized (CTGeofenceTaskManager.class) { - if (taskManager == null) { - taskManager = new CTGeofenceTaskManager(); - } - } + taskManager = new CTGeofenceTaskManager(); } return taskManager; } diff --git a/clevertap-geofence/src/main/java/com/clevertap/android/geofence/GoogleGeofenceAdapter.java b/clevertap-geofence/src/main/java/com/clevertap/android/geofence/GoogleGeofenceAdapter.java index eae362d16..12bfbf8d5 100644 --- a/clevertap-geofence/src/main/java/com/clevertap/android/geofence/GoogleGeofenceAdapter.java +++ b/clevertap-geofence/src/main/java/com/clevertap/android/geofence/GoogleGeofenceAdapter.java @@ -28,6 +28,11 @@ class GoogleGeofenceAdapter implements CTGeofenceAdapter { private static final long GEOFENCE_EXPIRATION_IN_MILLISECONDS = Geofence.NEVER_EXPIRE; + /** + * How soon should the callback be called when the transition associated with the Geofence is triggered. + */ + static final int GEOFENCE_NOTIFICATION_RESPONSIVENESS_IN_MILLISECONDS = 0; + private final Context context; private final GeofencingClient geofencingClient; @@ -174,11 +179,21 @@ private GeofencingRequest getGeofencingRequest(ArrayList googleFenceLi private ArrayList getGoogleGeofences(@NonNull List fenceList) { ArrayList googleFenceList = new ArrayList<>(); + CTGeofenceSettings geofenceSettings = CTGeofenceAPI.getInstance(context).getGeofenceSettings(); + int geofenceNotificationResponsiveness = GEOFENCE_NOTIFICATION_RESPONSIVENESS_IN_MILLISECONDS; + if (geofenceSettings != null) { + geofenceNotificationResponsiveness = geofenceSettings.getGeofenceNotificationResponsiveness(); + } + + CTGeofenceAPI.getLogger().debug(CTGeofenceAPI.GEOFENCE_LOG_TAG, + "Setting geofenceNotificationResponsiveness to " + geofenceNotificationResponsiveness); + for (CTGeofence ctGeofence : fenceList) { googleFenceList.add(new Geofence.Builder() // Set the request ID of the geofence. This is a string to identify this // geofence. .setRequestId(ctGeofence.getId()) + .setNotificationResponsiveness(geofenceNotificationResponsiveness) .setCircularRegion(ctGeofence.getLatitude(), ctGeofence.getLongitude(), ctGeofence.getRadius()) diff --git a/clevertap-geofence/src/main/java/com/clevertap/android/geofence/GoogleLocationAdapter.java b/clevertap-geofence/src/main/java/com/clevertap/android/geofence/GoogleLocationAdapter.java index 2604aebb0..9b0f9ee39 100644 --- a/clevertap-geofence/src/main/java/com/clevertap/android/geofence/GoogleLocationAdapter.java +++ b/clevertap-geofence/src/main/java/com/clevertap/android/geofence/GoogleLocationAdapter.java @@ -36,12 +36,12 @@ class GoogleLocationAdapter implements CTLocationAdapter { * Applicable for both fetch modes ({@link CTGeofenceSettings#FETCH_CURRENT_LOCATION_PERIODIC}) and * ({@link CTGeofenceSettings#FETCH_LAST_LOCATION_PERIODIC}) */ - static final long INTERVAL_IN_MILLIS = 30 * 60 * 1000; + static final long INTERVAL_IN_MILLIS = 30 * 60 * 1000L; /** * Applicable only for ({@link CTGeofenceSettings#FETCH_CURRENT_LOCATION_PERIODIC}) */ - static final long INTERVAL_FASTEST_IN_MILLIS = 30 * 60 * 1000; + static final long INTERVAL_FASTEST_IN_MILLIS = 30 * 60 * 1000L; /** * Applicable only for ({@link CTGeofenceSettings#FETCH_CURRENT_LOCATION_PERIODIC}) @@ -51,7 +51,7 @@ class GoogleLocationAdapter implements CTLocationAdapter { /** * Applicable only for ({@link CTGeofenceSettings#FETCH_LAST_LOCATION_PERIODIC}) */ - private static final long FLEX_INTERVAL_IN_MILLIS = 10 * 60 * 1000; + private static final long FLEX_INTERVAL_IN_MILLIS = 10 * 60 * 1000L; private boolean backgroundLocationUpdatesEnabled; diff --git a/clevertap-geofence/src/main/java/com/clevertap/android/geofence/Utils.java b/clevertap-geofence/src/main/java/com/clevertap/android/geofence/Utils.java index e1a1c2c0e..d7250598e 100644 --- a/clevertap-geofence/src/main/java/com/clevertap/android/geofence/Utils.java +++ b/clevertap-geofence/src/main/java/com/clevertap/android/geofence/Utils.java @@ -220,6 +220,8 @@ static CTGeofenceSettings readSettingsFromFile(@NonNull Context context) { .setFastestInterval(jsonObject.getLong(CTGeofenceConstants.KEY_LAST_FASTEST_INTERVAL)) .setSmallestDisplacement( (float) jsonObject.getDouble(CTGeofenceConstants.KEY_LAST_DISPLACEMENT)) + .setGeofenceNotificationResponsiveness( + jsonObject.getInt(CTGeofenceConstants.KEY_LAST_GEO_NOTIFICATION_RESPONSIVENESS)) .build(); CTGeofenceAPI.getLogger().debug(CTGeofenceAPI.GEOFENCE_LOG_TAG, @@ -293,6 +295,8 @@ static void writeSettingsToFile(Context context, @NonNull CTGeofenceSettings ctG settings.put(CTGeofenceConstants.KEY_LAST_INTERVAL, ctGeofenceSettings.getInterval()); settings.put(CTGeofenceConstants.KEY_LAST_FASTEST_INTERVAL, ctGeofenceSettings.getFastestInterval()); settings.put(CTGeofenceConstants.KEY_LAST_DISPLACEMENT, ctGeofenceSettings.getSmallestDisplacement()); + settings.put(CTGeofenceConstants.KEY_LAST_GEO_NOTIFICATION_RESPONSIVENESS, + ctGeofenceSettings.getGeofenceNotificationResponsiveness()); settings.put(CTGeofenceConstants.KEY_ID, CTGeofenceAPI.getInstance(context).getAccountId()); boolean writeJsonToFile = FileUtils.writeJsonToFile(context, FileUtils.getCachedDirName(context), diff --git a/clevertap-geofence/src/test/java/com/clevertap/android/geofence/UtilsTest.java b/clevertap-geofence/src/test/java/com/clevertap/android/geofence/UtilsTest.java index 96f45ee66..828571fe4 100644 --- a/clevertap-geofence/src/test/java/com/clevertap/android/geofence/UtilsTest.java +++ b/clevertap-geofence/src/test/java/com/clevertap/android/geofence/UtilsTest.java @@ -44,7 +44,8 @@ application = TestApplication.class ) @PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*", "androidx.*", "org.json.*"}) -@PrepareForTest({FileUtils.class, CTGeofenceAPI.class, CleverTapAPI.class, com.clevertap.android.sdk.Utils.class}) +@PrepareForTest({FileUtils.class, CTGeofenceAPI.class, CleverTapAPI.class, + com.clevertap.android.sdk.Utils.class}) public class UtilsTest extends BaseTestCase { @Rule diff --git a/clevertap-geofence/src/test/java/com/clevertap/android/geofence/fakes/CTGeofenceSettingsFake.java b/clevertap-geofence/src/test/java/com/clevertap/android/geofence/fakes/CTGeofenceSettingsFake.java index 00781da63..77c23120a 100644 --- a/clevertap-geofence/src/test/java/com/clevertap/android/geofence/fakes/CTGeofenceSettingsFake.java +++ b/clevertap-geofence/src/test/java/com/clevertap/android/geofence/fakes/CTGeofenceSettingsFake.java @@ -49,7 +49,7 @@ public static String getSettingsJsonString() { jsonObject = new JSONObject("{\"last_accuracy\":1,\"last_fetch_mode\":1," + "\"last_bg_location_updates\":true,\"last_log_level\":3,\"last_geo_count\":47," + "\"last_interval\":1800000,\"last_fastest_interval\":1800000,\"last_displacement\":200," + - "\"id\":\"4RW-Z6Z-485Z\"}").toString(); + "\"id\":\"4RW-Z6Z-485Z\",\"last_geo_notification_responsiveness\":0}").toString(); } catch (JSONException e) { e.printStackTrace(); diff --git a/clevertap-hms/build.gradle b/clevertap-hms/build.gradle index 4053a78b0..17bd84eab 100644 --- a/clevertap-hms/build.gradle +++ b/clevertap-hms/build.gradle @@ -23,7 +23,4 @@ dependencies { testImplementation project(':test_shared') testImplementation deps.huaweiPush - testImplementation deps.kotlinStdlib - testImplementation deps.json - testImplementation deps.gson } \ No newline at end of file diff --git a/clevertap-hms/gradle/wrapper/gradle-wrapper.jar b/clevertap-hms/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index f6b961fd5..000000000 Binary files a/clevertap-hms/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/clevertap-hms/gradle/wrapper/gradle-wrapper.properties b/clevertap-hms/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index c80aa5eda..000000000 --- a/clevertap-hms/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Wed Jul 22 15:44:59 IST 2020 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip diff --git a/clevertap-hms/gradlew b/clevertap-hms/gradlew deleted file mode 100755 index 9a27a6ce1..000000000 --- a/clevertap-hms/gradlew +++ /dev/null @@ -1,172 +0,0 @@ -#!/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/clevertap-hms/gradlew.bat b/clevertap-hms/gradlew.bat deleted file mode 100644 index f9553162f..000000000 --- a/clevertap-hms/gradlew.bat +++ /dev/null @@ -1,84 +0,0 @@ -@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/clevertap-hms/src/main/java/com/clevertap/android/hms/HmsMessageHandlerImpl.java b/clevertap-hms/src/main/java/com/clevertap/android/hms/HmsMessageHandlerImpl.java index 1da688d51..3c9549c24 100644 --- a/clevertap-hms/src/main/java/com/clevertap/android/hms/HmsMessageHandlerImpl.java +++ b/clevertap-hms/src/main/java/com/clevertap/android/hms/HmsMessageHandlerImpl.java @@ -44,7 +44,7 @@ void createNotificationWithMessageBundle(final Context context, final Bundle mes .getGlobalInstance(context, getAccountIdFromNotificationBundle(messageBundle)); CleverTapAPI.createNotification(context, messageBundle); if (cleverTapAPI != null) { - cleverTapAPI.config().log(LOG_TAG, HMS_LOG_TAG + "Creating Notification"); + cleverTapAPI.getCoreState().getConfig().log(LOG_TAG, HMS_LOG_TAG + "Creating Notification"); } else { Logger.d(LOG_TAG, HMS_LOG_TAG + "Creating Notification"); } diff --git a/clevertap-hms/src/main/java/com/clevertap/android/hms/HmsPushProvider.java b/clevertap-hms/src/main/java/com/clevertap/android/hms/HmsPushProvider.java index 52c1176a3..01ec71a96 100644 --- a/clevertap-hms/src/main/java/com/clevertap/android/hms/HmsPushProvider.java +++ b/clevertap-hms/src/main/java/com/clevertap/android/hms/HmsPushProvider.java @@ -5,8 +5,10 @@ import static com.clevertap.android.sdk.pushnotification.PushConstants.PushType.HPS; import android.annotation.SuppressLint; +import android.content.Context; import androidx.annotation.NonNull; import androidx.annotation.RestrictTo; +import com.clevertap.android.sdk.CleverTapInstanceConfig; import com.clevertap.android.sdk.pushnotification.CTPushProvider; import com.clevertap.android.sdk.pushnotification.CTPushProviderListener; import com.clevertap.android.sdk.pushnotification.PushConstants; @@ -23,16 +25,11 @@ public class HmsPushProvider implements CTPushProvider { private @NonNull IHmsSdkHandler hmsSdkHandler; - @NonNull - @Override - public PushConstants.PushType getPushType() { - return HPS; - } - @SuppressLint(value = "unused") - public HmsPushProvider(@NonNull CTPushProviderListener ctPushListener) { + public HmsPushProvider(@NonNull CTPushProviderListener ctPushListener, Context context, + CleverTapInstanceConfig config) { this.ctPushListener = ctPushListener; - this.hmsSdkHandler = new HmsSdkHandler(ctPushListener); + this.hmsSdkHandler = new HmsSdkHandler(context, config); } @Override @@ -40,6 +37,12 @@ public int getPlatform() { return ANDROID_PLATFORM; } + @NonNull + @Override + public PushConstants.PushType getPushType() { + return HPS; + } + @Override public boolean isAvailable() { return hmsSdkHandler.isAvailable(); diff --git a/clevertap-hms/src/main/java/com/clevertap/android/hms/HmsSdkHandler.java b/clevertap-hms/src/main/java/com/clevertap/android/hms/HmsSdkHandler.java index 25b1dd63b..d8e88a431 100644 --- a/clevertap-hms/src/main/java/com/clevertap/android/hms/HmsSdkHandler.java +++ b/clevertap-hms/src/main/java/com/clevertap/android/hms/HmsSdkHandler.java @@ -5,8 +5,9 @@ import static com.clevertap.android.hms.HmsConstants.HMS_LOG_TAG; import static com.clevertap.android.sdk.pushnotification.PushConstants.LOG_TAG; +import android.content.Context; import android.text.TextUtils; -import com.clevertap.android.sdk.BaseCTApiListener; +import com.clevertap.android.sdk.CleverTapInstanceConfig; import com.huawei.agconnect.config.AGConnectServicesConfig; import com.huawei.hms.aaid.HmsInstanceId; import com.huawei.hms.api.HuaweiApiAvailability; @@ -16,19 +17,22 @@ */ class HmsSdkHandler implements IHmsSdkHandler { - private final BaseCTApiListener mListener; + private final Context context; - HmsSdkHandler(BaseCTApiListener ctPushListener) { - this.mListener = ctPushListener; + private final CleverTapInstanceConfig mConfig; + + HmsSdkHandler(final Context context, final CleverTapInstanceConfig config) { + this.context = context.getApplicationContext(); + mConfig = config; } @Override public String appId() { String appId = null; try { - appId = AGConnectServicesConfig.fromContext(mListener.context()).getString(APP_ID_KEY); + appId = AGConnectServicesConfig.fromContext(context).getString(APP_ID_KEY); } catch (Throwable t) { - mListener.config().log(LOG_TAG, HMS_LOG_TAG + "HMS availability check failed."); + mConfig.log(LOG_TAG, HMS_LOG_TAG + "HMS availability check failed."); } return appId; } @@ -41,9 +45,9 @@ public boolean isAvailable() { @Override public boolean isSupported() { try { - return HuaweiApiAvailability.getInstance().isHuaweiMobileNoticeAvailable(mListener.context()) == 0; + return HuaweiApiAvailability.getInstance().isHuaweiMobileNoticeAvailable(context) == 0; } catch (Throwable e) { - mListener.config().log(LOG_TAG, HMS_LOG_TAG + "HMS is supported check failed."); + mConfig.log(LOG_TAG, HMS_LOG_TAG + "HMS is supported check failed."); return false; } } @@ -54,10 +58,10 @@ public String onNewToken() { try { String appId = appId(); if (!TextUtils.isEmpty(appId)) { - token = HmsInstanceId.getInstance(mListener.context()).getToken(appId, HCM_SCOPE); + token = HmsInstanceId.getInstance(context).getToken(appId, HCM_SCOPE); } } catch (Throwable t) { - mListener.config().log(LOG_TAG, HMS_LOG_TAG + "Error requesting HMS token", t); + mConfig.log(LOG_TAG, HMS_LOG_TAG + "Error requesting HMS token", t); } return token; } diff --git a/clevertap-hms/src/test/java/AndroidManifest.xml b/clevertap-hms/src/test/java/AndroidManifest.xml deleted file mode 100644 index 5205769ed..000000000 --- a/clevertap-hms/src/test/java/AndroidManifest.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/clevertap-hms/src/test/java/com/clevertap/android/hms/HmsHandlerTest.java b/clevertap-hms/src/test/java/com/clevertap/android/hms/HmsHandlerTest.java deleted file mode 100644 index af691ac06..000000000 --- a/clevertap-hms/src/test/java/com/clevertap/android/hms/HmsHandlerTest.java +++ /dev/null @@ -1,171 +0,0 @@ -package com.clevertap.android.hms; - -import static com.clevertap.android.hms.HmsConstants.APP_ID_KEY; -import static com.clevertap.android.hms.HmsConstants.HCM_SCOPE; -import static com.clevertap.android.hms.HmsTestConstants.HMS_APP_ID; -import static com.clevertap.android.hms.HmsTestConstants.HMS_TOKEN; -import static org.mockito.Mockito.*; - -import com.clevertap.android.sdk.Application; -import com.clevertap.android.sdk.CleverTapInstanceConfig; -import com.clevertap.android.sdk.pushnotification.CTPushProviderListener; -import com.clevertap.android.shared.test.BaseTestCase; -import com.clevertap.android.shared.test.Constant; -import com.clevertap.android.shared.test.TestApplication; -import com.huawei.agconnect.config.AGConnectServicesConfig; -import com.huawei.hms.aaid.HmsInstanceId; -import com.huawei.hms.api.HuaweiApiAvailability; -import org.junit.*; -import org.junit.runner.*; -import org.mockito.*; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -@RunWith(RobolectricTestRunner.class) -@Config(sdk = 28, application = TestApplication.class) -public class HmsHandlerTest extends BaseTestCase { - - private AGConnectServicesConfig config; - - private HuaweiApiAvailability huaweiApi; - - private HmsInstanceId instance; - - private Application mApplication; - - private HmsSdkHandler sdkHandler; - - @Before - public void setUp() { - mApplication = TestApplication.Companion.getApplication(); - CleverTapInstanceConfig mCleverTapInstanceConfig = CleverTapInstanceConfig - .createInstance(TestApplication.Companion.getApplication(), Constant.ACC_ID, Constant.ACC_TOKEN); - - final CTPushProviderListener ctPushProviderListener = Mockito.mock(CTPushProviderListener.class); - sdkHandler = new HmsSdkHandler(ctPushProviderListener); - Mockito.when(ctPushProviderListener.context()).thenReturn(TestApplication.Companion.getApplication()); - Mockito.when(ctPushProviderListener.config()).thenReturn(mCleverTapInstanceConfig); - - instance = Mockito.mock(HmsInstanceId.class); - config = Mockito.mock(AGConnectServicesConfig.class); - huaweiApi = Mockito.mock(HuaweiApiAvailability.class); - } - - @Test - public void testAppId_Invalid() { - Mockito.when(config.getString(APP_ID_KEY)).thenThrow(new RuntimeException("Something went wrong")); - try (MockedStatic mocked = mockStatic(AGConnectServicesConfig.class)) { - Mockito.when(AGConnectServicesConfig.fromContext(mApplication)).thenReturn(config); - String appId = sdkHandler.appId(); - Assert.assertNull(appId); - } - } - - @Test - public void testAppId_Valid() { - Mockito.when(config.getString(APP_ID_KEY)).thenReturn(HMS_APP_ID); - try (MockedStatic mocked = mockStatic(AGConnectServicesConfig.class)) { - Mockito.when(AGConnectServicesConfig.fromContext(mApplication)).thenReturn(config); - String appId = sdkHandler.appId(); - Assert.assertNotNull(appId); - } - } - - @Test - public void testIsAvailable_Returns_False() { - Mockito.when(config.getString(APP_ID_KEY)).thenThrow(new RuntimeException("Something Went Wrong")); - try (MockedStatic mocked = mockStatic(AGConnectServicesConfig.class)) { - Mockito.when(AGConnectServicesConfig.fromContext(mApplication)).thenReturn(config); - Assert.assertFalse(sdkHandler.isAvailable()); - } - } - - @Test - public void testIsAvailable_Returns_True() { - AGConnectServicesConfig config = Mockito.mock(AGConnectServicesConfig.class); - Mockito.when(config.getString(APP_ID_KEY)).thenReturn(HMS_APP_ID); - try (MockedStatic mocked = mockStatic(AGConnectServicesConfig.class)) { - Mockito.when(AGConnectServicesConfig.fromContext(mApplication)).thenReturn(config); - Assert.assertTrue(sdkHandler.isAvailable()); - } - } - - @Test - public void testIsSupported_Returns_False() { - huaweiApi = Mockito.mock(HuaweiApiAvailability.class); - Mockito.when(huaweiApi.isHuaweiMobileNoticeAvailable(mApplication)).thenReturn(1); - try (MockedStatic mocked = mockStatic(HuaweiApiAvailability.class)) { - Mockito.when(HuaweiApiAvailability.getInstance()).thenReturn(huaweiApi); - Assert.assertFalse(sdkHandler.isSupported()); - } - } - - @Test - public void testIsSupported_Returns_False_Exception() { - huaweiApi = Mockito.mock(HuaweiApiAvailability.class); - Mockito.when(huaweiApi.isHuaweiMobileNoticeAvailable(mApplication)) - .thenThrow(new RuntimeException("Something went wrong")); - try (MockedStatic mocked = mockStatic(HuaweiApiAvailability.class)) { - Mockito.when(HuaweiApiAvailability.getInstance()).thenReturn(huaweiApi); - Assert.assertFalse(sdkHandler.isSupported()); - } - } - - @Test - public void testIsSupported_Returns_True() { - huaweiApi = Mockito.mock(HuaweiApiAvailability.class); - Mockito.when(huaweiApi.isHuaweiMobileNoticeAvailable(mApplication)).thenReturn(0); - try (MockedStatic mocked = mockStatic(HuaweiApiAvailability.class)) { - Mockito.when(HuaweiApiAvailability.getInstance()).thenReturn(huaweiApi); - Assert.assertTrue(sdkHandler.isSupported()); - } - } - - @Test - public void testNewToken_Exception() { - try { - Mockito.when(config.getString(APP_ID_KEY)).thenReturn(HMS_APP_ID); - Mockito.when(instance.getToken(HMS_APP_ID, HCM_SCOPE)) - .thenThrow(new RuntimeException("Something went wrong")); - try (MockedStatic mocked = mockStatic(HmsInstanceId.class)) { - Mockito.when(HmsInstanceId.getInstance(mApplication)).thenReturn(instance); - try (MockedStatic mocked1 = mockStatic(AGConnectServicesConfig.class)) { - Mockito.when(AGConnectServicesConfig.fromContext(mApplication)).thenReturn(config); - String token = sdkHandler.onNewToken(); - Assert.assertNull(token); - } - } - } catch (Exception e) { - - } - } - - @Test - public void testNewToken_Invalid_AppId() { - Mockito.when(config.getString(APP_ID_KEY)).thenReturn(null); - try (MockedStatic mocked1 = mockStatic(AGConnectServicesConfig.class)) { - Mockito.when(AGConnectServicesConfig.fromContext(mApplication)).thenReturn(config); - String token = sdkHandler.onNewToken(); - Assert.assertNull(token); - } - } - - @Test - public void testNewToken_Success() { - try { - Mockito.when(config.getString(APP_ID_KEY)).thenReturn(HMS_APP_ID); - Mockito.when(instance.getToken(HMS_APP_ID, HCM_SCOPE)).thenReturn(HMS_TOKEN); - - try (MockedStatic mocked = mockStatic(HmsInstanceId.class)) { - Mockito.when(HmsInstanceId.getInstance(mApplication)).thenReturn(instance); - try (MockedStatic mocked1 = mockStatic(AGConnectServicesConfig.class)) { - Mockito.when(AGConnectServicesConfig.fromContext(mApplication)).thenReturn(config); - String token = sdkHandler.onNewToken(); - Assert.assertEquals(token, HMS_TOKEN); - } - } - } catch (Exception e) { - //do nothing - } - } -} \ No newline at end of file diff --git a/clevertap-hms/src/test/java/com/clevertap/android/hms/HmsHandlerTest.kt b/clevertap-hms/src/test/java/com/clevertap/android/hms/HmsHandlerTest.kt new file mode 100644 index 000000000..0b2c5d70c --- /dev/null +++ b/clevertap-hms/src/test/java/com/clevertap/android/hms/HmsHandlerTest.kt @@ -0,0 +1,147 @@ +package com.clevertap.android.hms + +import com.clevertap.android.sdk.Application +import com.clevertap.android.shared.test.BaseTestCase +import com.clevertap.android.shared.test.TestApplication +import com.huawei.agconnect.config.AGConnectServicesConfig +import com.huawei.hms.aaid.HmsInstanceId +import com.huawei.hms.api.HuaweiApiAvailability +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [28], application = TestApplication::class) +class HmsHandlerTest : BaseTestCase() { + private var config: AGConnectServicesConfig? = null + private lateinit var huaweiApi: HuaweiApiAvailability + private var instance: HmsInstanceId? = null + private var sdkHandler: HmsSdkHandler? = null + @Before + override fun setUp() { + super.setUp() + sdkHandler = HmsSdkHandler(application, cleverTapInstanceConfig) + instance = Mockito.mock(HmsInstanceId::class.java) + config = Mockito.mock(AGConnectServicesConfig::class.java) + huaweiApi = Mockito.mock(HuaweiApiAvailability::class.java) + } + + @Test + fun testAppId_Invalid() { + Mockito.`when`(config!!.getString(HmsConstants.APP_ID_KEY)).thenThrow(RuntimeException("Something went wrong")) + Mockito.mockStatic(AGConnectServicesConfig::class.java).use { mocked -> + Mockito.`when`(AGConnectServicesConfig.fromContext(application)).thenReturn(config) + val appId = sdkHandler!!.appId() + Assert.assertNull(appId) + } + } + + @Test + fun testAppId_Valid() { + Mockito.`when`(config!!.getString(HmsConstants.APP_ID_KEY)).thenReturn(HmsTestConstants.HMS_APP_ID) + Mockito.mockStatic(AGConnectServicesConfig::class.java).use { mocked -> + Mockito.`when`(AGConnectServicesConfig.fromContext(application)).thenReturn(config) + val appId = sdkHandler!!.appId() + Assert.assertNotNull(appId) + } + } + + @Test + fun testIsAvailable_Returns_False() { + Mockito.`when`(config!!.getString(HmsConstants.APP_ID_KEY)).thenThrow(RuntimeException("Something Went Wrong")) + Mockito.mockStatic(AGConnectServicesConfig::class.java).use { mocked -> + Mockito.`when`(AGConnectServicesConfig.fromContext(application)).thenReturn(config) + Assert.assertFalse(sdkHandler!!.isAvailable) + } + } + + @Test + fun testIsAvailable_Returns_True() { + val config = Mockito.mock(AGConnectServicesConfig::class.java) + Mockito.`when`(config.getString(HmsConstants.APP_ID_KEY)).thenReturn(HmsTestConstants.HMS_APP_ID) + Mockito.mockStatic(AGConnectServicesConfig::class.java).use { mocked -> + Mockito.`when`(AGConnectServicesConfig.fromContext(application)).thenReturn(config) + Assert.assertTrue(sdkHandler!!.isAvailable) + } + } + + @Test + fun testIsSupported_Returns_False() { + Mockito.`when`(huaweiApi.isHuaweiMobileNoticeAvailable(application)).thenReturn(1) + Mockito.mockStatic(HuaweiApiAvailability::class.java).use { mocked -> + Mockito.`when`(HuaweiApiAvailability.getInstance()).thenReturn(huaweiApi) + Assert.assertFalse(sdkHandler!!.isSupported) + } + } + + @Test + fun testIsSupported_Returns_False_Exception() { + huaweiApi = Mockito.mock(HuaweiApiAvailability::class.java) + Mockito.`when`(huaweiApi.isHuaweiMobileNoticeAvailable(application)) + .thenThrow(RuntimeException("Something went wrong")) + Mockito.mockStatic(HuaweiApiAvailability::class.java).use { mocked -> + Mockito.`when`(HuaweiApiAvailability.getInstance()).thenReturn(huaweiApi) + Assert.assertFalse(sdkHandler!!.isSupported) + } + } + + @Test + fun testIsSupported_Returns_True() { + huaweiApi = Mockito.mock(HuaweiApiAvailability::class.java) + Mockito.`when`(huaweiApi.isHuaweiMobileNoticeAvailable(application)).thenReturn(0) + Mockito.mockStatic(HuaweiApiAvailability::class.java).use { mocked -> + Mockito.`when`(HuaweiApiAvailability.getInstance()).thenReturn(huaweiApi) + Assert.assertTrue(sdkHandler!!.isSupported) + } + } + + @Test + fun testNewToken_Exception() { + try { + Mockito.`when`(config!!.getString(HmsConstants.APP_ID_KEY)).thenReturn(HmsTestConstants.HMS_APP_ID) + Mockito.`when`(instance!!.getToken(HmsTestConstants.HMS_APP_ID, HmsConstants.HCM_SCOPE)) + .thenThrow(RuntimeException("Something went wrong")) + Mockito.mockStatic(HmsInstanceId::class.java).use { mocked -> + Mockito.`when`(HmsInstanceId.getInstance(application)).thenReturn(instance) + Mockito.mockStatic(AGConnectServicesConfig::class.java).use { mocked1 -> + Mockito.`when`(AGConnectServicesConfig.fromContext(application)).thenReturn(config) + val token = sdkHandler!!.onNewToken() + Assert.assertNull(token) + } + } + } catch (e: Exception) { + } + } + + @Test + fun testNewToken_Invalid_AppId() { + Mockito.`when`(config!!.getString(HmsConstants.APP_ID_KEY)).thenReturn(null) + Mockito.mockStatic(AGConnectServicesConfig::class.java).use { mocked1 -> + Mockito.`when`(AGConnectServicesConfig.fromContext(application)).thenReturn(config) + val token = sdkHandler!!.onNewToken() + Assert.assertNull(token) + } + } + + @Test + fun testNewToken_Success() { + try { + Mockito.`when`(config!!.getString(HmsConstants.APP_ID_KEY)).thenReturn(HmsTestConstants.HMS_APP_ID) + Mockito.`when`(instance!!.getToken(HmsTestConstants.HMS_APP_ID, HmsConstants.HCM_SCOPE)).thenReturn(HmsTestConstants.HMS_TOKEN) + Mockito.mockStatic(HmsInstanceId::class.java).use { mocked -> + Mockito.`when`(HmsInstanceId.getInstance(application)).thenReturn(instance) + Mockito.mockStatic(AGConnectServicesConfig::class.java).use { mocked1 -> + Mockito.`when`(AGConnectServicesConfig.fromContext(application)).thenReturn(config) + val token = sdkHandler!!.onNewToken() + Assert.assertEquals(token, HmsTestConstants.HMS_TOKEN) + } + } + } catch (e: Exception) { + //do nothing + } + } +} \ No newline at end of file diff --git a/clevertap-hms/src/test/java/com/clevertap/android/hms/HmsPushProviderTest.kt b/clevertap-hms/src/test/java/com/clevertap/android/hms/HmsPushProviderTest.kt index a828a2e67..15ea4302b 100644 --- a/clevertap-hms/src/test/java/com/clevertap/android/hms/HmsPushProviderTest.kt +++ b/clevertap-hms/src/test/java/com/clevertap/android/hms/HmsPushProviderTest.kt @@ -26,11 +26,9 @@ class HmsPushProviderTest : BaseTestCase() { override fun setUp() { super.setUp() ctPushProviderListener = mock(CTPushProviderListener::class.java) - pushProvider = HmsPushProvider(ctPushProviderListener) + pushProvider = HmsPushProvider(ctPushProviderListener, application, cleverTapInstanceConfig) sdkHandler = mock(IHmsSdkHandler::class.java) pushProvider.setHmsSdkHandler(sdkHandler) - `when`(ctPushProviderListener.context()).thenReturn(application) - `when`(ctPushProviderListener.config()).thenReturn(cleverTapInstanceConfig) } @Test diff --git a/clevertap-xps/build.gradle b/clevertap-xps/build.gradle index 7620b51d3..1f67a729a 100644 --- a/clevertap-xps/build.gradle +++ b/clevertap-xps/build.gradle @@ -9,8 +9,6 @@ ext { licenseName = 'MIT License' licenseUrl = 'https://opensource.org/licenses/MIT' allLicenses = ["MIT"] - - minSdkVersionVal = 16 } apply from: "../gradle-scripts/commons.gradle" @@ -22,7 +20,4 @@ dependencies { compileOnly deps.androidXAnnotation testImplementation project(':test_shared') testImplementation fileTree('libs') - testImplementation deps.kotlinStdlib - testImplementation deps.json - testImplementation deps.gson } \ No newline at end of file diff --git a/clevertap-xps/libs/MiPush_SDK_Client_3_7_9.jar b/clevertap-xps/libs/MiPush_SDK_Client_3_7_9.jar deleted file mode 100644 index 10fd9bb9d..000000000 Binary files a/clevertap-xps/libs/MiPush_SDK_Client_3_7_9.jar and /dev/null differ diff --git a/clevertap-xps/libs/MiPush_SDK_Client_3_8_9.jar b/clevertap-xps/libs/MiPush_SDK_Client_3_8_9.jar new file mode 100644 index 000000000..3094b0fe1 Binary files /dev/null and b/clevertap-xps/libs/MiPush_SDK_Client_3_8_9.jar differ diff --git a/clevertap-xps/src/main/AndroidManifest.xml b/clevertap-xps/src/main/AndroidManifest.xml index 8b9136875..b39d39f22 100644 --- a/clevertap-xps/src/main/AndroidManifest.xml +++ b/clevertap-xps/src/main/AndroidManifest.xml @@ -6,7 +6,6 @@ - processInfos = am.getRunningAppProcesses(); @@ -103,7 +106,7 @@ boolean shouldInit(String mainProcessName) { } private void init() { - String packageName = ctApiListener.context().getPackageName(); + String packageName = context.getPackageName(); if (shouldInit(packageName)) { String appId = appId(); String appKey = appKey(); diff --git a/clevertap-xps/src/test/java/AndroidManifest.xml b/clevertap-xps/src/test/java/AndroidManifest.xml deleted file mode 100644 index b84a8719c..000000000 --- a/clevertap-xps/src/test/java/AndroidManifest.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/clevertap-xps/src/test/java/com/clevertap/android/xps/XiaomiPushProviderTest.kt b/clevertap-xps/src/test/java/com/clevertap/android/xps/XiaomiPushProviderTest.kt index 017a6d594..5767f4745 100644 --- a/clevertap-xps/src/test/java/com/clevertap/android/xps/XiaomiPushProviderTest.kt +++ b/clevertap-xps/src/test/java/com/clevertap/android/xps/XiaomiPushProviderTest.kt @@ -37,12 +37,10 @@ class XiaomiPushProviderTest : BaseTestCase() { //init provider listener ctPushProviderListener = Mockito.mock(CTPushProviderListener::class.java) - Mockito.`when`(ctPushProviderListener.context()).thenReturn(application) - Mockito.`when`(ctPushProviderListener.config()).thenReturn(cleverTapInstanceConfig) - sdkHandler = XiaomiSdkHandler(ctPushProviderListener) + sdkHandler = XiaomiSdkHandler(application, cleverTapInstanceConfig) sdkHandler.setManifestInfo(manifestInfo) //init push provider - xiaomiPushProvider = XiaomiPushProvider(ctPushProviderListener) + xiaomiPushProvider = XiaomiPushProvider(ctPushProviderListener, application, cleverTapInstanceConfig) xiaomiPushProvider.setMiSdkHandler(sdkHandler) } diff --git a/clevertap-xps/src/test/java/com/clevertap/android/xps/XiaomiSdkHandlerTest.kt b/clevertap-xps/src/test/java/com/clevertap/android/xps/XiaomiSdkHandlerTest.kt index 52bce89b8..c7cb9b8ec 100644 --- a/clevertap-xps/src/test/java/com/clevertap/android/xps/XiaomiSdkHandlerTest.kt +++ b/clevertap-xps/src/test/java/com/clevertap/android/xps/XiaomiSdkHandlerTest.kt @@ -21,7 +21,7 @@ class XiaomiSdkHandlerTest : BaseTestCase() { override fun setUp() { super.setUp() manifestInfo = Mockito.mock(ManifestInfo::class.java) - handler = XiaomiSdkHandler(baseCTApiListener) + handler = XiaomiSdkHandler(application, cleverTapInstanceConfig) handler.setManifestInfo(manifestInfo) } diff --git a/docs/CTCORECHANGELOG.md b/docs/CTCORECHANGELOG.md index 826b97648..a982b26c8 100644 --- a/docs/CTCORECHANGELOG.md +++ b/docs/CTCORECHANGELOG.md @@ -1,5 +1,12 @@ ## CleverTap Android SDK CHANGE LOG +### Version 4.1.0 (April 13, 2021) +* Adds support for Android 11 +* Reduces the SDK size and added performance improvements +* Removes the deprecated Product Experiences (Screen AB/Dynamic Variables) related code +* Removes support for JCenter +* Fixes a bug where Xiaomi, Huawei, Baidu and other push service tokens were not switched to new profiles when using `onUserLogin` + ### Version 4.0.4 (Mar 2, 2021) * Fixed FCM token refresh issue when multiple Firebase Projects are integrated in the application. If you're using multiple Firebase projects in your app, use this version instead of v4.0.0 ~ v4.0.3 diff --git a/docs/CTGEOFENCE.md b/docs/CTGEOFENCE.md index 1d869312f..f9e23d58e 100644 --- a/docs/CTGEOFENCE.md +++ b/docs/CTGEOFENCE.md @@ -1,22 +1,7 @@

    - +

    -## ⍗ Table of contents - -* [Introduction](#-introduction) -* [Installation](#-installation) -* [Permissions](#-permissions) -* [Initialization](#-initialization) -* [Settings parameters](#-settings-parameters) -* [Trigger Location](#-trigger-location) -* [Callbacks/Listeners](#-callbackslisteners) -* [Deactivation](#%EF%B8%8F-deactivation) -* [ProGuard](#-proguard) -* [Example Usage](#-example-usage) -* [FAQ](#-faq) -* [Questions](#-questions) - ## 👋 Introduction [(Back to top)](#-table-of-contents) @@ -31,8 +16,8 @@ CleverTap Android Geofence SDK provides **Geofencing capabilities** to CleverTap Add the following dependencies to the `build.gradle` ```Groovy -implementation "com.clevertap.android:clevertap-geofence-sdk:1.0.1" -implementation "com.clevertap.android:clevertap-android-sdk:4.0.4" // 3.9.0 and above +implementation "com.clevertap.android:clevertap-geofence-sdk:1.0.2" +implementation "com.clevertap.android:clevertap-android-sdk:4.1.0" // 3.9.0 and above implementation "com.google.android.gms:play-services-location:17.0.0" implementation "androidx.work:work-runtime:2.3.4" // required for FETCH_LAST_LOCATION_PERIODIC implementation "androidx.concurrent:concurrent-futures:1.0.0" // required for FETCH_LAST_LOCATION_PERIODIC @@ -76,6 +61,7 @@ CTGeofenceSettings ctGeofenceSettings = new CTGeofenceSettings.Builder() .setInterval(interval)//long value for interval in milliseconds .setFastestInterval(fastestInterval)//long value for fastest interval in milliseconds .setSmallestDisplacement(displacement)//float value for smallest Displacement in meters + .setGeofenceNotificationResponsiveness(geofenceNotificationResponsiveness)// int value for geofence notification responsiveness in milliseconds .build(); ``` **Note** - diff --git a/docs/CTGEOFENCECHANGELOG.md b/docs/CTGEOFENCECHANGELOG.md index 0574471da..5ffbe7664 100644 --- a/docs/CTGEOFENCECHANGELOG.md +++ b/docs/CTGEOFENCECHANGELOG.md @@ -1,7 +1,10 @@ ## CleverTap Geofence SDK CHANGE LOG -### Version 1.0.1 (October 1, 2020) +### Version 1.0.2 (April 13, 2021) +* Added new method in `CTGeofenceSettings` - `setGeofenceNotificationResponsiveness()` +* Add support for CleverTap Android SDK v4.1.0 +### Version 1.0.1 (October 1, 2020) * Add support for CleverTap Android SDK v4.0.0 ### Version 1.0.0 (September 2, 2020) diff --git a/docs/CTHUAWEIPUSH.md b/docs/CTHUAWEIPUSH.md index 678e4f6c6..fc56b615f 100644 --- a/docs/CTHUAWEIPUSH.md +++ b/docs/CTHUAWEIPUSH.md @@ -1,15 +1,7 @@

    - +

    -## ⍗ Table of contents - -* [Introduction](#-introduction) -* [Register](#-register) -* [Enable Push Kit](#-enable-push-kit) -* [Integration](#-integration) - - ## 👋 Introduction [(Back to top)](#-table-of-contents) @@ -40,7 +32,7 @@ Download the `agconnect-services.json` file from the Huawei Console. Move the do dependencies { // FOR HUAWEI ADD THIS - classpath "com.huawei.agconnect:agcp:1.3.1.300" + classpath "com.huawei.agconnect:agcp:1.4.1.300" } allprojects { @@ -54,8 +46,8 @@ allprojects { * Add the following to your app’s `build.gradle` file ```groovy -implementation "com.clevertap.android:clevertap-hms-sdk:1.0.0" -implementation "com.huawei.hms:push:5.0.0.300" +implementation "com.clevertap.android:clevertap-hms-sdk:1.0.1" +implementation "com.huawei.hms:push:5.1.1.301" //At the bottom of the file add this apply plugin: 'com.huawei.agconnect' diff --git a/docs/CTHUAWEIPUSHCHANGELOG.md b/docs/CTHUAWEIPUSHCHANGELOG.md index a18643ec5..1ef69dfc6 100644 --- a/docs/CTHUAWEIPUSHCHANGELOG.md +++ b/docs/CTHUAWEIPUSHCHANGELOG.md @@ -1,6 +1,9 @@ ## CleverTap Huawei Push SDK CHANGE LOG -### Version 1.0.0 (October 1, 2020) +### Version 1.0.1 (April 13, 2021) +* Updated Huawei Push SDK to v5.1.1.301 +* Supports CleverTap Android SDK v4.1.0 +### Version 1.0.0 (October 1, 2020) * Initial release! 🎉 * Supports CleverTap Android SDK v4.0.0 \ No newline at end of file diff --git a/docs/CTXIAOMIPUSH.md b/docs/CTXIAOMIPUSH.md index 9c80fa2fb..6a9d0499a 100644 --- a/docs/CTXIAOMIPUSH.md +++ b/docs/CTXIAOMIPUSH.md @@ -1,15 +1,7 @@

    - +

    -## ⍗ Table of contents - -* [Introduction](#-introduction) -* [Register](#%EF%B8%8F-register) -* [Create an Application](#-create-an-application) -* [App Details](#-app-details) -* [Integration](#-integration) - ## 👋 Introduction [(Back to top)](#-table-of-contents) @@ -50,7 +42,7 @@ Click on [Mi Push Console](http://admin.xmpush.global.xiaomi.com/) and click on * Add the CleverTap Xiaomi Push dependency in app’s `build.gradle` ```groovy - implementation "com.clevertap.android:clevertap-xiaomi-sdk:1.0.0" + implementation "com.clevertap.android:clevertap-xiaomi-sdk:1.0.1" ``` * Add the following to your app’s `AndroidManifest.xml` file diff --git a/docs/CTXIAOMIPUSHCHANGELOG.md b/docs/CTXIAOMIPUSHCHANGELOG.md index 4a4117f26..cde97b070 100644 --- a/docs/CTXIAOMIPUSHCHANGELOG.md +++ b/docs/CTXIAOMIPUSHCHANGELOG.md @@ -1,6 +1,9 @@ ## CleverTap Xiaomi Push SDK CHANGE LOG -### Version 1.0.0 (October 1, 2020) +### Version 1.0.1 (April 13, 2021) +* Updated Xiaomi Push SDK to v3.8.9 +* Supports CleverTap Android SDK v4.1.0 +### Version 1.0.0 (October 1, 2020) * Initial release! 🎉 * Supports CleverTap Android SDK v4.0.0 \ No newline at end of file diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md index 57281aa9a..61111ecd7 100644 --- a/docs/EXAMPLES.md +++ b/docs/EXAMPLES.md @@ -166,7 +166,7 @@ implementation "com.google.android.exoplayer:exoplayer-ui:2.11.5" Initializing the Inbox will provide a callback to two methods `inboxDidInitialize()` AND `inboxMessagesDidUpdate()` ```java -import com.clevertap.android.sdk.CTInboxActivity; +import com.clevertap.android.sdk.inbox.CTInboxActivity; import com.clevertap.android.sdk.CTInboxListener; import com.clevertap.android.sdk.CTInboxStyleConfig; import com.clevertap.android.sdk.CleverTapAPI; diff --git a/docs/Settings.md b/docs/Settings.md index 56140e73c..77880ea8e 100644 --- a/docs/Settings.md +++ b/docs/Settings.md @@ -47,4 +47,9 @@ Default is `50`. Default is `true`. * When **true**, this will allow SDK to register background location updates through any of the above mentioned fetch modes. -* When **false**, this will inform SDK to fetch location only in foreground when the app is launched or through `triggerLocation()` and not to register background location updates through any of the above mentioned fetch modes. \ No newline at end of file +* When **false**, this will inform SDK to fetch location only in foreground when the app is launched or through `triggerLocation()` and not to register background location updates through any of the above mentioned fetch modes. + +### Geofence Notification Responsiveness in milliseconds: +Default is `0`. + +* This can be used to set the notification responsiveness to a higher value. Doing so improves power consumption by increasing the latency of geofence alerts. For example, if you set a responsiveness value of five minutes your app only checks for an entrance or exit alert once every five minutes. Setting lower values doesn't necessarily mean that users are notified within that time period (for example, if you set a value of 5 seconds it may take a bit longer than that to receive the alert). \ No newline at end of file diff --git a/gradle-scripts/commons.gradle b/gradle-scripts/commons.gradle index c649e1fdd..92152dce9 100644 --- a/gradle-scripts/commons.gradle +++ b/gradle-scripts/commons.gradle @@ -1,14 +1,12 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'com.github.dcendents.android-maven' -apply plugin: 'com.jfrog.bintray' apply plugin: 'jacoco' +apply plugin: 'maven-publish' +apply plugin: 'signing' ext { - bintrayRepo = 'Maven' - bintrayName = libraryName - org = 'clevertap' - + Repo = 'Maven' publishedGroupId = 'com.clevertap.android' siteUrl = 'https://github.com/CleverTap/clevertap-android-sdk' @@ -19,6 +17,16 @@ ext { developerEmail = 'support@clevertap.com' } +ext["signing.keyId"] = '' +ext["signing.password"] = '' +ext["signing.secretKeyRingFile"] = '' +ext["ossrhUsername"] = '' +ext["ossrhPassword"] = '' +ext["sonatypeStagingProfileId"] = '' +ext["developerId"] = '' +ext["developerName"] = '' +ext["developerEmail"] = '' + version = libraryVersion group = publishedGroupId @@ -34,6 +42,10 @@ android { versionCode "${major}0${minor}0${patch}".toInteger() versionName libraryVersion + //AGP 4.1.0 change https://developer.android.com/studio/releases/gradle-plugin#version_properties_removed_from_buildconfig_class_in_library_projects +// buildConfigField ("int", "VERSION_CODE", "$versionCode") +// buildConfigField ("String", "VERSION_NAME", "\"$versionName\"") + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles 'consumer-rules.pro' } @@ -71,9 +83,9 @@ android { unitTests { includeAndroidResources = true returnDefaultValues = true - } - unitTests.all { - jvmArgs '-noverify' + all { + jvmArgs '-noverify' + } } } } @@ -97,84 +109,110 @@ task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest', 'crea def debugTree = fileTree(dir: "$project.buildDir/intermediates/javac/debug", excludes: fileFilter) def mainSrc = "$project.projectDir/src/main/java" - sourceDirectories.from = files([mainSrc]) - classDirectories.from = files([debugTree]) - executionData.from = fileTree(dir: project.buildDir, includes: [ + sourceDirectories.setFrom(files([mainSrc])) + classDirectories.setFrom(files([debugTree])) + executionData.setFrom(fileTree(dir: project.buildDir, includes: [ 'jacoco/testDebugUnitTest.exec', 'outputs/code_coverage/debugAndroidTest/connected/**/*.ec' - ]) + ])) } Properties properties = new Properties() if (project.rootProject.file('local.properties').exists()) { properties.load(project.rootProject.file('local.properties').newDataInputStream()) + properties.each { name, value -> + ext[name] = value + } +}else{ + ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID') + ext["signing.password"] = System.getenv('SIGNING_PASSWORD') + ext["signing.secretKeyRingFile"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE') + ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME') + ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD') + ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID') } -bintray { - user = properties.getProperty("bintray.user") - key = properties.getProperty("bintray.apikey") - - configurations = ['archives'] - pkg { - repo = bintrayRepo - name = bintrayName - userOrg = org - desc = libraryDescription - websiteUrl = siteUrl - vcsUrl = gitUrl - licenses = allLicenses - publish = true - publicDownloadNumbers = true - version { - desc = libraryDescription - } - } +task sourcesJar(type: Jar) { + baseName "$artifact" + from android.sourceSets.main.java.srcDirs + classifier = 'sources' +} + +artifacts { + archives sourcesJar } -install { - repositories.mavenInstaller { - // This generates POM.xml with proper parameters - pom { - project { - packaging 'aar' - groupId publishedGroupId - artifactId artifact - - // Add your description here - name libraryName - description libraryDescription - url siteUrl - - // Set your license +publishing { + publications { + release(MavenPublication) { + // The coordinates of the library, being set from variables that + // we'll set up later + groupId publishedGroupId + artifactId artifact + version version + + // Two artifacts, the `aar` (or `jar`) and the sources + if (project.plugins.findPlugin("com.android.library")) { + artifact("$buildDir/outputs/aar/${artifact}-${libraryVersion}.aar") + } else { + artifact("$buildDir/libs/${project.getName()}-${version}.jar") + } + artifact sourcesJar + + // Mostly self-explanatory metadata + pom { + name = artifact + description = libraryDescription + url = siteUrl licenses { license { - name licenseName - url licenseUrl + name = licenseName + url = licenseUrl } } developers { developer { - id developerId - name developerName - email developerEmail + id = developerId + name = developerName + email = developerEmail } + // Add all other devs here... } + // Version control info - if you're using GitHub, follow the format as seen here scm { - connection gitUrl - developerConnection gitUrl - url siteUrl - + connection = 'scm:git:github.com/CleverTap/clevertap-android-sdk.git' + developerConnection = 'scm:git:ssh:github.com/CleverTap/clevertap-android-sdk.git' + url = 'https://github.com/CleverTap/clevertap-android-sdk/tree/master' + } + // A slightly hacky fix so that your POM will include any transitive dependencies + // that your library builds upon + withXml { + def dependenciesNode = asNode().appendNode('dependencies') + + project.configurations.implementation.allDependencies.each { + def dependencyNode = dependenciesNode.appendNode('dependency') + dependencyNode.appendNode('groupId', it.group) + dependencyNode.appendNode('artifactId', it.name) + dependencyNode.appendNode('version', it.version) + } } } } } + // The repository to publish to, Sonatype/MavenCentral + repositories { + maven { + // This is an arbitrary name, you may also use "mavencentral" or + // any other name that's descriptive for you + name = Repo + url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" + credentials { + username ossrhUsername + password ossrhPassword + } + } + } } -task sourcesJar(type: Jar) { - baseName "$artifact" - from android.sourceSets.main.java.srcDirs - classifier = 'sources' -} - -artifacts { - archives sourcesJar +signing { + sign publishing.publications } \ No newline at end of file diff --git a/gradle-scripts/dependencies.gradle b/gradle-scripts/dependencies.gradle index 143a491ba..db57b4d80 100644 --- a/gradle-scripts/dependencies.gradle +++ b/gradle-scripts/dependencies.gradle @@ -26,14 +26,17 @@ ext.deps = [ 'androidXConcurrentFutures' : "androidx.concurrent:concurrent-futures:$concurrentFuturesVersion", 'workManagerTesting' : "androidx.work:work-testing:$workManagerVersion", 'robolectric' : "org.robolectric:robolectric:$robolectricVersion", - 'junit' : "junit:junit:$junitVersion", + + 'junitPlatform':"org.junit.platform:junit-platform-runner:$junitPlatformVersion", + 'junitApi':"org.junit.jupiter:junit-jupiter-api:$junitApiVersion", + 'junitEngine':"org.junit.jupiter:junit-jupiter-engine:$junitEngineVersion", + 'androidXTestCore' : "androidx.test:core:$androidxCoreVersion", 'mockitoCore' : "org.mockito:mockito-core:$mockitoVersion", 'androidXJunitExt' : "androidx.test.ext:junit:$junitExtVersion", 'clevertapCore' : "com.clevertap.android:clevertap-android-sdk:$coreVersion", 'clevertapGeofence' : "com.clevertap.android:clevertap-geofence-sdk:$geofenceVersion", 'clevertapXPS' : "com.clevertap.android:clevertap-xiaomi-sdk:$xpsVersion", - 'clevertapHMS' : "com.clevertap.android:clevertap-hms-sdk:$hmsVersion" - - + 'clevertapHMS' : "com.clevertap.android:clevertap-hms-sdk:$hmsVersion", + 'jsonAssert':"org.skyscreamer:jsonassert:$jsonAssertVersion" ] \ No newline at end of file diff --git a/gradle-scripts/versions.gradle b/gradle-scripts/versions.gradle index a3b9e89d7..8c343ba2e 100644 --- a/gradle-scripts/versions.gradle +++ b/gradle-scripts/versions.gradle @@ -1,23 +1,26 @@ ext { // Android SDK - compileSdkVersionVal = 29 - targetSdkVersionVal = 29 - buildToolsVersionVal = "29.0.3" + compileSdkVersionVal = 30 + targetSdkVersionVal = 30 + buildToolsVersionVal = "30.0.3" + + minSdkVersionVal = 16 // CleverTap modules - coreVersion = "4.0.4" - geofenceVersion = "1.0.1" - hmsVersion = "1.0.0" - xpsVersion = "1.0.0" + coreVersion = "4.1.0" + geofenceVersion = "1.0.2" + hmsVersion = "1.0.1" + xpsVersion = "1.0.1" //gradle plugins gradlePluginVersion = '4.0.1' googleServicesPluginVersion = '4.3.3' bintrayPluginVersion = '1.8.4' mavenPluginVersion = '2.1' - huaweiPluginVersion = '1.3.1.300' + huaweiPluginVersion = '1.4.1.300' kotlin_version = '1.3.72' jacocoVersion = '0.8.4' + jsonAssertVersion = '1.5.0' // Firebase firebaseFcmVersion = '20.2.4' @@ -50,12 +53,16 @@ ext { concurrentFuturesVersion = '1.0.0' // Huawei push - huaweiPushVersion = '5.0.0.300' + huaweiPushVersion = '5.1.1.301' // unit tests jsonVersion = '20200518' gsonVersion = '2.8.6' - junitVersion = '4.13' + + junitPlatformVersion = '1.7.0' + junitApiVersion = '5.7.0' + junitEngineVersion = '5.7.0' + junitExtVersion = '1.1.2' robolectricVersion = '4.3.1' testRunnerVersion = '1.3.0' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 40e918ae0..8be762d75 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,4 @@ -#Thu Sep 17 17:23:50 IST 2020 +#Wed Feb 17 20:00:46 IST 2021 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/sample/google-services.json b/sample/google-services.json index d47803034..bc049b5d5 100644 --- a/sample/google-services.json +++ b/sample/google-services.json @@ -1,35 +1,86 @@ { "project_info": { - "project_number": "your-project_number", - "firebase_url": "your-firebase_url", - "project_id": "your-project_id", - "storage_bucket": "abc.appspot.com" + "project_number": "112273261716", + "firebase_url": "https://bearded-robot-b21a8.firebaseio.com", + "project_id": "bearded-robot-b21a8", + "storage_bucket": "bearded-robot-b21a8.appspot.com" }, "client": [ { "client_info": { - "mobilesdk_app_id": "your-mobilesdk_app_id", + "mobilesdk_app_id": "1:112273261716:android:1da2103af05ede3a", + "android_client_info": { + "package_name": "com.clevertap.beardedrobot" + } + }, + "oauth_client": [ + { + "client_id": "112273261716-12m0pn39nkb4juu01p9jihrujuja6l2c.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.clevertap.beardedrobot", + "certificate_hash": "3db5b63c5c9c8efce20d2a8c74c6ec754e7aea9c" + } + }, + { + "client_id": "112273261716-9vd03s0ivfqgh4q3lklb2i1ksroga18k.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyBbQ8Qu2N_xktuRxIBxzNNoOWIvVB3OXoY" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "112273261716-9vd03s0ivfqgh4q3lklb2i1ksroga18k.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "112273261716-kv2gfoeh8rjmqtbfrv437ppjan06tdll.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.clevertap.BeardedRobot" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:112273261716:android:2c6b3cc8d6c84037066081", "android_client_info": { "package_name": "com.clevertap.demo" } }, "oauth_client": [ { - "client_id": "your-client_id", + "client_id": "112273261716-9vd03s0ivfqgh4q3lklb2i1ksroga18k.apps.googleusercontent.com", "client_type": 3 } ], "api_key": [ { - "current_key": "your-api-key" + "current_key": "AIzaSyBbQ8Qu2N_xktuRxIBxzNNoOWIvVB3OXoY" } ], "services": { "appinvite_service": { "other_platform_oauth_client": [ { - "client_id": "abc.apps.googleusercontent.com", + "client_id": "112273261716-9vd03s0ivfqgh4q3lklb2i1ksroga18k.apps.googleusercontent.com", "client_type": 3 + }, + { + "client_id": "112273261716-kv2gfoeh8rjmqtbfrv437ppjan06tdll.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.clevertap.BeardedRobot" + } } ] } diff --git a/sample/src/main/java/com/clevertap/demo/ui/main/HomeScreenFragment.kt b/sample/src/main/java/com/clevertap/demo/ui/main/HomeScreenFragment.kt index c6e56e6e9..a909f905d 100644 --- a/sample/src/main/java/com/clevertap/demo/ui/main/HomeScreenFragment.kt +++ b/sample/src/main/java/com/clevertap/demo/ui/main/HomeScreenFragment.kt @@ -119,6 +119,7 @@ class HomeScreenFragment : Fragment() { .setInterval(3600000) // 1 hour .setFastestInterval(1800000) // 30 minutes .setSmallestDisplacement(1000f) // 1 km + .setGeofenceNotificationResponsiveness(300000) // 5 minute .build(), cleverTapInstance ) setOnGeofenceApiInitializedListener { diff --git a/sample/src/main/res/values/styles.xml b/sample/src/main/res/values/styles.xml index 11727db65..c01c3a987 100644 --- a/sample/src/main/res/values/styles.xml +++ b/sample/src/main/res/values/styles.xml @@ -17,4 +17,10 @@ diff --git a/static/clevertap-logo.png b/static/clevertap-logo.png deleted file mode 100644 index e2d320f23..000000000 Binary files a/static/clevertap-logo.png and /dev/null differ diff --git a/templates/CTCORECHANGELOG.md b/templates/CTCORECHANGELOG.md index 826b97648..a982b26c8 100644 --- a/templates/CTCORECHANGELOG.md +++ b/templates/CTCORECHANGELOG.md @@ -1,5 +1,12 @@ ## CleverTap Android SDK CHANGE LOG +### Version 4.1.0 (April 13, 2021) +* Adds support for Android 11 +* Reduces the SDK size and added performance improvements +* Removes the deprecated Product Experiences (Screen AB/Dynamic Variables) related code +* Removes support for JCenter +* Fixes a bug where Xiaomi, Huawei, Baidu and other push service tokens were not switched to new profiles when using `onUserLogin` + ### Version 4.0.4 (Mar 2, 2021) * Fixed FCM token refresh issue when multiple Firebase Projects are integrated in the application. If you're using multiple Firebase projects in your app, use this version instead of v4.0.0 ~ v4.0.3 diff --git a/templates/CTGEOFENCE.md b/templates/CTGEOFENCE.md index f4e803bf4..3658e8381 100644 --- a/templates/CTGEOFENCE.md +++ b/templates/CTGEOFENCE.md @@ -1,22 +1,7 @@

    - +

    -## ⍗ Table of contents - -* [Introduction](#-introduction) -* [Installation](#-installation) -* [Permissions](#-permissions) -* [Initialization](#-initialization) -* [Settings parameters](#-settings-parameters) -* [Trigger Location](#-trigger-location) -* [Callbacks/Listeners](#-callbackslisteners) -* [Deactivation](#%EF%B8%8F-deactivation) -* [ProGuard](#-proguard) -* [Example Usage](#-example-usage) -* [FAQ](#-faq) -* [Questions](#-questions) - ## 👋 Introduction [(Back to top)](#-table-of-contents) @@ -76,6 +61,7 @@ CTGeofenceSettings ctGeofenceSettings = new CTGeofenceSettings.Builder() .setInterval(interval)//long value for interval in milliseconds .setFastestInterval(fastestInterval)//long value for fastest interval in milliseconds .setSmallestDisplacement(displacement)//float value for smallest Displacement in meters + .setGeofenceNotificationResponsiveness(geofenceNotificationResponsiveness)// int value for geofence notification responsiveness in milliseconds .build(); ``` **Note** - diff --git a/templates/CTGEOFENCECHANGELOG.md b/templates/CTGEOFENCECHANGELOG.md index 0574471da..5ffbe7664 100644 --- a/templates/CTGEOFENCECHANGELOG.md +++ b/templates/CTGEOFENCECHANGELOG.md @@ -1,7 +1,10 @@ ## CleverTap Geofence SDK CHANGE LOG -### Version 1.0.1 (October 1, 2020) +### Version 1.0.2 (April 13, 2021) +* Added new method in `CTGeofenceSettings` - `setGeofenceNotificationResponsiveness()` +* Add support for CleverTap Android SDK v4.1.0 +### Version 1.0.1 (October 1, 2020) * Add support for CleverTap Android SDK v4.0.0 ### Version 1.0.0 (September 2, 2020) diff --git a/templates/CTHUAWEIPUSH.md b/templates/CTHUAWEIPUSH.md index a08956386..79a718530 100644 --- a/templates/CTHUAWEIPUSH.md +++ b/templates/CTHUAWEIPUSH.md @@ -1,15 +1,7 @@

    - +

    -## ⍗ Table of contents - -* [Introduction](#-introduction) -* [Register](#-register) -* [Enable Push Kit](#-enable-push-kit) -* [Integration](#-integration) - - ## 👋 Introduction [(Back to top)](#-table-of-contents) diff --git a/templates/CTHUAWEIPUSHCHANGELOG.md b/templates/CTHUAWEIPUSHCHANGELOG.md index a18643ec5..1ef69dfc6 100644 --- a/templates/CTHUAWEIPUSHCHANGELOG.md +++ b/templates/CTHUAWEIPUSHCHANGELOG.md @@ -1,6 +1,9 @@ ## CleverTap Huawei Push SDK CHANGE LOG -### Version 1.0.0 (October 1, 2020) +### Version 1.0.1 (April 13, 2021) +* Updated Huawei Push SDK to v5.1.1.301 +* Supports CleverTap Android SDK v4.1.0 +### Version 1.0.0 (October 1, 2020) * Initial release! 🎉 * Supports CleverTap Android SDK v4.0.0 \ No newline at end of file diff --git a/templates/CTXIAOMIPUSH.md b/templates/CTXIAOMIPUSH.md index b31c63ded..f11ba0c14 100644 --- a/templates/CTXIAOMIPUSH.md +++ b/templates/CTXIAOMIPUSH.md @@ -1,15 +1,7 @@

    - +

    -## ⍗ Table of contents - -* [Introduction](#-introduction) -* [Register](#%EF%B8%8F-register) -* [Create an Application](#-create-an-application) -* [App Details](#-app-details) -* [Integration](#-integration) - ## 👋 Introduction [(Back to top)](#-table-of-contents) diff --git a/templates/CTXIAOMIPUSHCHANGELOG.md b/templates/CTXIAOMIPUSHCHANGELOG.md index 4a4117f26..cde97b070 100644 --- a/templates/CTXIAOMIPUSHCHANGELOG.md +++ b/templates/CTXIAOMIPUSHCHANGELOG.md @@ -1,6 +1,9 @@ ## CleverTap Xiaomi Push SDK CHANGE LOG -### Version 1.0.0 (October 1, 2020) +### Version 1.0.1 (April 13, 2021) +* Updated Xiaomi Push SDK to v3.8.9 +* Supports CleverTap Android SDK v4.1.0 +### Version 1.0.0 (October 1, 2020) * Initial release! 🎉 * Supports CleverTap Android SDK v4.0.0 \ No newline at end of file diff --git a/templates/EXAMPLES.md b/templates/EXAMPLES.md index eec2be42b..b89b09414 100644 --- a/templates/EXAMPLES.md +++ b/templates/EXAMPLES.md @@ -166,7 +166,7 @@ implementation "${ext.deps.exoPlayerUi}" Initializing the Inbox will provide a callback to two methods `inboxDidInitialize()` AND `inboxMessagesDidUpdate()` ```java -import com.clevertap.android.sdk.CTInboxActivity; +import com.clevertap.android.sdk.inbox.CTInboxActivity; import com.clevertap.android.sdk.CTInboxListener; import com.clevertap.android.sdk.CTInboxStyleConfig; import com.clevertap.android.sdk.CleverTapAPI; diff --git a/templates/README.md b/templates/README.md index b7292c894..23ab44842 100644 --- a/templates/README.md +++ b/templates/README.md @@ -1,23 +1,10 @@

    - +

    # CleverTap Android SDKs [![Build Status](https://app.bitrise.io/app/09efc6b9404a6341/status.svg?token=TejL3E1NHyTiR5ajHKGJ6Q)](https://app.bitrise.io/app/09efc6b9404a6341) -[![codebeat badge](https://codebeat.co/badges/49b05fa0-4228-443c-9f1c-d11efa6d2ef8)](https://codebeat.co/projects/github-com-clevertap-clevertap-android-sdk-master) -[ ![Download](https://api.bintray.com/packages/clevertap/Maven/CleverTapAndroidSDK/images/download.svg) ](https://bintray.com/clevertap/Maven/CleverTapAndroidSDK/_latestVersion) - -## ⍗ Table of contents -* [Introduction](#-introduction) -* [Installation](#-installation) - * [Dependencies](#-dependencies) -* [Integration](#-integration) -* [Initialization](#-initialization) -* [Example Usage](#-example-usage) -* [CleverTap Geofence SDK](#-clevertap-geofence-sdk) -* [CleverTap Xiaomi Push SDK](#-clevertap-xiaomi-push-sdk) -* [CleverTap Huawei Push SDK](#-clevertap-huawei-push-sdk) -* [License](#-license) +[![Download](https://api.bintray.com/packages/clevertap/Maven/CleverTapAndroidSDK/images/download.svg) ](https://bintray.com/clevertap/Maven/CleverTapAndroidSDK/_latestVersion) ## 👋 Introduction [(Back to top)](#-table-of-contents) @@ -33,7 +20,7 @@ To get started, sign up [here](https://clevertap.com/live-product-demo/) ## 🎉 Installation [(Back to top)](#-table-of-contents) -We publish the SDK to `jcenter` and `mavenCentral` as an `AAR` file. Just declare it as dependency in your `build.gradle` file. +We publish the SDK to `mavenCentral` as an `AAR` file. Just declare it as dependency in your `build.gradle` file. ```groovy dependencies { @@ -73,7 +60,7 @@ Also be sure to include the `google-services.json` classpath in your Project lev buildscript { repositories { google() - jcenter() + mavenCentral() // if you are including the aar file manually in your Module libs directory add this: flatDir { @@ -104,7 +91,7 @@ Interstitial InApp Notification templates support Audio and Video with the help implementation "${ext.deps.exoPlayerUi}" ``` -Once you've updated your module `build.gradle` file, make sure you have specified `jcenter()` and `google()` as a repositories in your project `build.gradle` and then sync your project in File -> Sync Project with Gradle Files. +Once you've updated your module `build.gradle` file, make sure you have specified `mavenCentral()` and `google()` as a repositories in your project `build.gradle` and then sync your project in File -> Sync Project with Gradle Files. ## 🎉 Integration [(Back to top)](#-table-of-contents) @@ -195,7 +182,6 @@ CleverTap Xiaomi Push SDK provides an out of the box service to use the Xiaomi P CleverTap Huawei Push SDK provides an out of the box service to use the Huawei Messaging Service. Find the integration steps for the CleverTap Huawei Push SDK [here](https://github.com/CleverTap/clevertap-android-sdk/blob/master/docs/CTHUAWEIPUSH.md) - ## 📄 License [(Back to top)](#-table-of-contents) CleverTap Android SDK is MIT licensed, as found in the [LICENSE](https://github.com/CleverTap/clevertap-android-sdk/blob/master/LICENSE) file. \ No newline at end of file diff --git a/templates/Settings.md b/templates/Settings.md index 56140e73c..77880ea8e 100644 --- a/templates/Settings.md +++ b/templates/Settings.md @@ -47,4 +47,9 @@ Default is `50`. Default is `true`. * When **true**, this will allow SDK to register background location updates through any of the above mentioned fetch modes. -* When **false**, this will inform SDK to fetch location only in foreground when the app is launched or through `triggerLocation()` and not to register background location updates through any of the above mentioned fetch modes. \ No newline at end of file +* When **false**, this will inform SDK to fetch location only in foreground when the app is launched or through `triggerLocation()` and not to register background location updates through any of the above mentioned fetch modes. + +### Geofence Notification Responsiveness in milliseconds: +Default is `0`. + +* This can be used to set the notification responsiveness to a higher value. Doing so improves power consumption by increasing the latency of geofence alerts. For example, if you set a responsiveness value of five minutes your app only checks for an entrance or exit alert once every five minutes. Setting lower values doesn't necessarily mean that users are notified within that time period (for example, if you set a value of 5 seconds it may take a bit longer than that to receive the alert). \ No newline at end of file diff --git a/test_shared/build.gradle b/test_shared/build.gradle index e0edff4ad..58deac582 100644 --- a/test_shared/build.gradle +++ b/test_shared/build.gradle @@ -2,17 +2,16 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 29 - buildToolsVersion "30.0.2" - testOptions.unitTests.includeAndroidResources = true + compileSdkVersion compileSdkVersionVal + buildToolsVersion buildToolsVersionVal defaultConfig { - minSdkVersion 16 - targetSdkVersion 29 + minSdkVersion minSdkVersionVal + targetSdkVersion targetSdkVersionVal versionCode 1 versionName "1.0" -// instrumentationRunner "androidx.test.runner.AndroidJUnitRunner" +// testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" } @@ -22,13 +21,17 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } - } dependencies { api project(':clevertap-core') // Unit ing dependencies - api deps.junit + api deps.junitPlatform + api deps.junitApi + api deps.junitEngine + api deps.kotlinStdlib + api deps.jsonAssert + api deps.gson def mockito_version = '3.5.11' api deps.mockitoCore api "org.mockito:mockito-inline:$mockito_version" @@ -42,15 +45,19 @@ dependencies { // AndroidJUnitRunner and JUnit Rules api deps.androidXTestRunner api deps.androidXTestRules + api deps.androidXCoreKTX api deps.espressoCore api 'eu.codearte.catch-exception:catch-exception:2.0' - api "androidx.core:core-ktx:1.3.1" + api "androidx.core:core-ktx:1.3.2" api deps.kotlinStdlib api 'org.jetbrains.kotlin:kotlin-test:1.1.51' def truth_version = '1.1' api "com.google.truth:truth:${truth_version}" + + api 'xmlpull:xmlpull:1.1.3.4d_b4_min' + } \ No newline at end of file diff --git a/test_shared/src/main/java/com/clevertap/android/shared/test/BaseTestCase.kt b/test_shared/src/main/java/com/clevertap/android/shared/test/BaseTestCase.kt index c7423e854..5db7bccc8 100644 --- a/test_shared/src/main/java/com/clevertap/android/shared/test/BaseTestCase.kt +++ b/test_shared/src/main/java/com/clevertap/android/shared/test/BaseTestCase.kt @@ -2,7 +2,6 @@ package com.clevertap.android.shared.test import android.os.Build.VERSION_CODES import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.clevertap.android.sdk.BaseCTApiListener import com.clevertap.android.sdk.CleverTapAPI import com.clevertap.android.sdk.CleverTapInstanceConfig import org.junit.* @@ -14,10 +13,17 @@ import org.robolectric.annotation.Config @RunWith( AndroidJUnit4::class ) +/** + * Naming Convention for Testing + * 1. Classes : + Test.kt + * e.g CTProductConfigControllerTest.kt for CTProductConfigController.java + * + * 2. Methods : test___ + * e.g test_constructor_whenFeatureFlagIsNotSave_InitShouldReturnTrue + */ abstract class BaseTestCase { protected lateinit var application: TestApplication - protected lateinit var baseCTApiListener: BaseCTApiListener protected lateinit var cleverTapAPI: CleverTapAPI protected lateinit var cleverTapInstanceConfig: CleverTapInstanceConfig @@ -27,8 +33,5 @@ abstract class BaseTestCase { cleverTapAPI = Mockito.mock(CleverTapAPI::class.java) cleverTapInstanceConfig = CleverTapInstanceConfig.createInstance(application, Constant.ACC_ID, Constant.ACC_TOKEN) - baseCTApiListener = Mockito.mock(BaseCTApiListener::class.java) - Mockito.`when`(baseCTApiListener.context()).thenReturn(application) - Mockito.`when`(baseCTApiListener.config()).thenReturn(cleverTapInstanceConfig) } } \ No newline at end of file diff --git a/test_shared/src/main/java/com/clevertap/android/shared/test/TestApplication.kt b/test_shared/src/main/java/com/clevertap/android/shared/test/TestApplication.kt index 8e792e259..bc500bd7d 100644 --- a/test_shared/src/main/java/com/clevertap/android/shared/test/TestApplication.kt +++ b/test_shared/src/main/java/com/clevertap/android/shared/test/TestApplication.kt @@ -9,7 +9,6 @@ import com.clevertap.android.sdk.CleverTapAPI class TestApplication : Application() { override fun onCreate() { - CleverTapAPI.setUIEditorConnectionEnabled(true) CleverTapAPI.setDebugLevel(3) ActivityLifecycleCallback.register(this) super.onCreate()