From ebaaa32fac372f68e5e3b2d666f7be73a2e76158 Mon Sep 17 00:00:00 2001 From: PoornimaApptentive <85186738+PoornimaApptentive@users.noreply.github.com> Date: Thu, 6 Jan 2022 11:34:20 -0800 Subject: [PATCH] Apptentive Android SDK 5.8.0 (#234) --- .github/CODEOWNERS | 2 +- .idea/runConfigurations.xml | 12 -- CHANGELOG.md | 19 ++- CONTRIBUTING.md | 2 +- License.txt | 2 +- README.md | 4 +- apptentive/build.gradle | 44 +++---- .../android/sdk/ApptentiveBaseActivity.java | 2 +- .../android/sdk/ApptentiveConfiguration.java | 21 ++++ .../android/sdk/ApptentiveInstance.java | 3 + .../android/sdk/ApptentiveInternal.java | 33 ++++-- .../android/sdk/ApptentiveNullInstance.java | 7 ++ .../sdk/conversation/Conversation.java | 12 +- .../module/engagement/EngagementModule.java | 7 +- .../fragment/AppStoreRatingFragment.java | 2 +- .../fragment/ApptentiveBaseFragment.java | 5 +- .../fragment/MessageCenterFragment.java | 4 +- .../fragment/NavigateToLinkFragment.java | 2 +- .../sdk/storage/EncryptedFileSerializer.java | 8 +- .../android/sdk/util/Constants.java | 2 +- .../android/sdk/util/ThrottleUtils.java | 47 ++++++++ build.gradle | 52 +++++++- samples/apptentive-example/build.gradle | 27 ++--- settings.gradle | 4 +- tests/test-app/build.gradle | 8 +- .../sdk/tests/misc/ThrottleUtilsTest.java | 111 ++++++++++++++++++ .../module/engagement/InteractionTest.java | 23 ++-- 27 files changed, 357 insertions(+), 108 deletions(-) delete mode 100644 .idea/runConfigurations.xml create mode 100644 apptentive/src/main/java/com/apptentive/android/sdk/util/ThrottleUtils.java create mode 100644 tests/test-app/src/androidTest/java/com/apptentive/android/sdk/tests/misc/ThrottleUtilsTest.java diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index fb970f2bd..bb234bca5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @frankus @twinklesharma1311 +* @twinklesharma1311 @ChaseApptentive @PoornimaApptentive diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index 7f68460d8..000000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cedd12f4..56de09da2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,24 @@ +# 2022-01-06 - v5.8.0 + +#### Major changes + +* Add safeguard parameter for interaction frequency of Rating Dialog. + +#### Fixes + +* Fix a couple of possible ANR issues. + +#### Improvements + +* Update License & README files. + # 2021-08-26 - v5.7.1 #### Fixes -* Fix a couple of issues related to Android 12 -* Fix Navigate to Link interaction for API 30+ +* Fix a couple of issues related to Android 12. +* Fix Navigate to Link interaction for API 30+. +* Remove all references to AdvertiserId. # 2021-08-04 - v5.7.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dbff38ff8..9da4425c8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ We love contributions! -Any contributions to the master apptentive-ios project must sign the [Individual Contributor License Agreement (CLA)](https://docs.google.com/a/apptentive.com/spreadsheet/viewform?formkey=dDhMaXJKQnRoX0dRMzZNYnp5bk1Sbmc6MQ#gid=0). It's a doc that makes our lawyers happy and ensures we can provide a solid open source project. +Any contributions to the master apptentive-android project must sign the [Individual Contributor License Agreement (CLA)](https://docs.google.com/a/apptentive.com/spreadsheet/viewform?formkey=dDhMaXJKQnRoX0dRMzZNYnp5bk1Sbmc6MQ#gid=0). It's a doc that makes our lawyers happy and ensures we can provide a solid open source project. When you want to submit a change, send us a [pull request](https://github.com/apptentive/apptentive-android/pulls). Before we merge, we'll check to make sure you're on the list of people who've signed our CLA. diff --git a/License.txt b/License.txt index 52e5e1300..91bcd62d7 100644 --- a/License.txt +++ b/License.txt @@ -1,4 +1,4 @@ -Copyright (c) 2011-2019, Apptentive, Inc. +Copyright (c) 2011-2021, Apptentive, Inc. All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md index a1bc73a4a..d30395749 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,13 @@ use your app, to talk to them at the right time, and in the right way. #### [Android Interface Customization](https://learn.apptentive.com/knowledge-base/android-interface-customization/) -#### [Apptentive SDK API Javadoc](http://www.apptentive.com/docs/android/api) +#### [Apptentive SDK API Javadoc](https://learn.apptentive.com/docs/android/api/index.html) ##### [API Changes here](docs/APIChanges.md) ##### [Release Notes](https://learn.apptentive.com/knowledge-base/android-sdk-release-notes/) -##### Binary releases are hosted for Maven [here](http://search.maven.org/#artifactdetails|com.apptentive|apptentive-android|5.7.1|aar) +##### Binary releases are hosted for Maven [here](http://search.maven.org/#artifactdetails|com.apptentive|apptentive-android|5.8.0|aar) #### Reporting Bugs diff --git a/apptentive/build.gradle b/apptentive/build.gradle index 57a88d61a..a87b860f8 100644 --- a/apptentive/build.gradle +++ b/apptentive/build.gradle @@ -3,36 +3,30 @@ init() apply plugin: 'com.android.library' dependencies { - implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'androidx.cardview:cardview:1.0.0' - implementation 'com.google.android.material:material:1.2.0' + implementation "androidx.appcompat:appcompat:$appcompat_library_version" + implementation "androidx.legacy:legacy-support-v4:$legacy_support_v4_version" + implementation "com.google.android.material:material:$material_design_version" + implementation "com.google.android.play:core:$play_core_version" + implementation "com.google.android.gms:play-services-base:$play_services_base_version" - // Play Core library required for in-app review flow - implementation 'com.google.android.play:core:1.8.0' + testImplementation "junit:junit:$junit_version" + testImplementation "org.powermock:powermock-module-junit4:$powermock_version" + testImplementation "org.powermock:powermock-module-junit4-rule:$powermock_version" + testImplementation "org.powermock:powermock-api-mockito2:$powermock_version" + testImplementation "org.powermock:powermock-classloading-xstream:$powermock_version" - // Play Services library required to check for GPS availability before showing in-app review - implementation 'com.google.android.gms:play-services-base:17.4.0' - - testImplementation 'junit:junit:4.13' - testImplementation 'org.powermock:powermock-module-junit4:1.6.6' - testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.6' - testImplementation 'org.powermock:powermock-api-mockito:1.6.6' - testImplementation 'org.powermock:powermock-classloading-xstream:1.6.6' - - // Required for instrumented tests - androidTestImplementation 'androidx.annotation:annotation:1.1.0' - androidTestImplementation 'androidx.test:runner:1.3.0' - androidTestImplementation 'androidx.test:rules:1.3.0' + androidTestImplementation "androidx.annotation:annotation:$annotation_version" + androidTestImplementation "androidx.test:runner:$androidx_test_version" + androidTestImplementation "androidx.test:rules:$androidx_test_version" } android { - compileSdkVersion 30 - buildToolsVersion '30.0.2' + compileSdkVersion rootProject.compileSdkVersion + buildToolsVersion rootProject.buildToolsVersion defaultConfig { - minSdkVersion 14 - targetSdkVersion 30 + minSdkVersion rootProject.minSdkVersion + targetSdkVersion rootProject.targetSdkVersion // BUILD_NUMBER is provided by Jenkins. Default to 1 in dev builds. versionCode System.getenv("BUILD_NUMBER") as Integer ?: System.getenv("TRAVIS_BUILD_NUMBER") as Integer ?: 1 versionName project.version @@ -56,8 +50,8 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_7 - targetCompatibility JavaVersion.VERSION_1_7 + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } testOptions { diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveBaseActivity.java b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveBaseActivity.java index 3c3248892..84edd6d14 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveBaseActivity.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveBaseActivity.java @@ -29,8 +29,8 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { @Override protected void onDestroy() { - super.onDestroy(); unregisterNotification(); + super.onDestroy(); } //endregion diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveConfiguration.java b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveConfiguration.java index f88fe59ff..1b18c7d5c 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveConfiguration.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveConfiguration.java @@ -12,6 +12,8 @@ import com.apptentive.android.sdk.module.engagement.interaction.model.TermsAndConditions; import com.apptentive.android.sdk.util.StringUtils; +import java.util.concurrent.TimeUnit; + public class ApptentiveConfiguration { private final String apptentiveKey; private final String apptentiveSignature; @@ -23,6 +25,7 @@ public class ApptentiveConfiguration { private Encryption encryption; private boolean shouldCollectAndroidIdOnPreOreoTargets; private TermsAndConditions surveyTermsAndConditions; + private Long interactionThrottle; public ApptentiveConfiguration(@NonNull String apptentiveKey, @NonNull String apptentiveSignature) { if (StringUtils.isNullOrEmpty(apptentiveKey)) { @@ -150,4 +153,22 @@ public TermsAndConditions getSurveyTermsAndConditions() { public void setSurveyTermsAndConditions(TermsAndConditions surveyTermsAndConditions) { this.surveyTermsAndConditions = surveyTermsAndConditions; } + + public Long getInteractionThrottle() { + return interactionThrottle != null ? interactionThrottle : TimeUnit.DAYS.toMillis(7); + } + + /** + * Sets a time limit throttle which determines when a rating interaction can be shown again. + * Default is 7 days. + * + * @see TimeUnit for conversion utils + * e.g. TimeUnit.MINUTES.toMillis(10); or TimeUnit.DAYS.toMillis(30); + * + * @param interactionThrottle The length of time (in milliseconds) to wait before showing + * the same interaction again. + */ + public void setRatingInteractionThrottle(Long interactionThrottle) { + this.interactionThrottle = interactionThrottle; + } } diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveInstance.java b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveInstance.java index 7c4869133..1e63cd32a 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveInstance.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveInstance.java @@ -19,12 +19,14 @@ import com.apptentive.android.sdk.conversation.Conversation; import com.apptentive.android.sdk.conversation.ConversationProxy; import com.apptentive.android.sdk.module.engagement.interaction.InteractionManager; +import com.apptentive.android.sdk.module.engagement.interaction.model.Interaction; import com.apptentive.android.sdk.module.engagement.interaction.model.TermsAndConditions; import com.apptentive.android.sdk.module.rating.IRatingProvider; import com.apptentive.android.sdk.module.survey.OnSurveyFinishedListener; import com.apptentive.android.sdk.storage.AppRelease; import com.apptentive.android.sdk.storage.ApptentiveTaskManager; import com.apptentive.android.sdk.util.Nullsafe; +import com.apptentive.android.sdk.util.ThrottleUtils; import java.util.Map; @@ -73,4 +75,5 @@ public interface ApptentiveInstance extends Nullsafe { String getDefaultAppDisplayName(); TermsAndConditions getSurveyTermsAndConditions(); + boolean shouldThrottleInteraction(Interaction.Type interactionType); } diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveInternal.java b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveInternal.java index d2717f9dc..95b56936d 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveInternal.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveInternal.java @@ -40,6 +40,7 @@ import com.apptentive.android.sdk.model.LogoutPayload; import com.apptentive.android.sdk.module.engagement.EngagementModule; import com.apptentive.android.sdk.module.engagement.interaction.InteractionManager; +import com.apptentive.android.sdk.module.engagement.interaction.model.Interaction; import com.apptentive.android.sdk.module.engagement.interaction.model.MessageCenterInteraction; import com.apptentive.android.sdk.module.engagement.interaction.model.TermsAndConditions; import com.apptentive.android.sdk.module.messagecenter.MessageManager; @@ -118,6 +119,8 @@ public class ApptentiveInternal implements ApptentiveInstance, ApptentiveNotific // Used for temporarily holding customData that needs to be sent on the next message the consumer sends. private Map customData; + private ThrottleUtils throttleUtils; + private static final String PUSH_ACTION = "action"; private static final String PUSH_CONVERSATION_ID = "conversation_id"; private static final int LOG_HISTORY_SIZE = 2; @@ -176,6 +179,7 @@ private ApptentiveInternal(Application application, ApptentiveConfiguration conf globalSharedPrefs = application.getSharedPreferences(Constants.PREF_NAME, Context.MODE_PRIVATE); apptentiveHttpClient = new ApptentiveHttpClient(apptentiveKey, apptentiveSignature, getEndpointBase(globalSharedPrefs)); + this.throttleUtils = new ThrottleUtils(configuration.getInteractionThrottle(), getGlobalSharedPrefs()); DeviceManager deviceManager = new DeviceManager(androidID); conversationManager = new ConversationManager(appContext, Util.getInternalDir(appContext, CONVERSATIONS_DIR, true), encryption, deviceManager); @@ -222,17 +226,6 @@ static void createInstance(@NonNull Application application, @NonNull Apptentive // set log level before we initialize log monitor since log monitor can override it as well ApptentiveLog.overrideLogLevel(configuration.getLogLevel()); - // troubleshooting mode - if (configuration.isTroubleshootingModeEnabled()) { - // initialize log writer - ApptentiveLog.initializeLogWriter(application.getApplicationContext(), LOG_HISTORY_SIZE); - - // try initializing log monitor - LogMonitor.startSession(application.getApplicationContext(), apptentiveKey, apptentiveSignature); - } else { - ApptentiveLog.i(TROUBLESHOOT, "Troubleshooting is disabled in the app configuration"); - } - synchronized (ApptentiveInternal.class) { if (sApptentiveInternal == null) { ApptentiveLog.i("Registering Apptentive Android SDK %s", Constants.getApptentiveSdkVersion()); @@ -240,10 +233,21 @@ static void createInstance(@NonNull Application application, @NonNull Apptentive // resolve Android ID boolean shouldGenerateRandomAndroidID = Build.VERSION.SDK_INT < Build.VERSION_CODES.O && !configuration.shouldCollectAndroidIdOnPreOreoTargets(); String androidID = resolveAndroidID(application.getApplicationContext(), shouldGenerateRandomAndroidID); - sApptentiveInternal = new ApptentiveInternal(application, configuration, androidID); dispatchOnConversationQueue(new DispatchTask() { @Override protected void execute() { + // troubleshooting mode + if (configuration.isTroubleshootingModeEnabled()) { + // initialize log writer + ApptentiveLog.initializeLogWriter(application.getApplicationContext(), LOG_HISTORY_SIZE); + + // try initializing log monitor + LogMonitor.startSession(application.getApplicationContext(), apptentiveKey, apptentiveSignature); + } else { + ApptentiveLog.i(TROUBLESHOOT, "Troubleshooting is disabled in the app configuration"); + } + + sApptentiveInternal = new ApptentiveInternal(application, configuration, androidID); sApptentiveInternal.start(); } }); @@ -398,6 +402,11 @@ public TermsAndConditions getSurveyTermsAndConditions() { return surveyTermsAndConditions; } + @Override + public boolean shouldThrottleInteraction(Interaction.Type interactionType) { + return throttleUtils.shouldThrottleInteraction(interactionType); + } + public boolean isApptentiveDebuggable() { return appRelease.isDebug(); } diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveNullInstance.java b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveNullInstance.java index ecbe7927d..9bc79b118 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveNullInstance.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveNullInstance.java @@ -18,11 +18,13 @@ import com.apptentive.android.sdk.conversation.ConversationProxy; import com.apptentive.android.sdk.debug.Assert; import com.apptentive.android.sdk.module.engagement.interaction.InteractionManager; +import com.apptentive.android.sdk.module.engagement.interaction.model.Interaction; import com.apptentive.android.sdk.module.engagement.interaction.model.TermsAndConditions; import com.apptentive.android.sdk.module.rating.IRatingProvider; import com.apptentive.android.sdk.module.survey.OnSurveyFinishedListener; import com.apptentive.android.sdk.storage.AppRelease; import com.apptentive.android.sdk.storage.ApptentiveTaskManager; +import com.apptentive.android.sdk.util.ThrottleUtils; import java.util.Map; @@ -216,6 +218,11 @@ public TermsAndConditions getSurveyTermsAndConditions() { return null; } + @Override + public boolean shouldThrottleInteraction(Interaction.Type interactionType) { + return false; + } + private void failMethodCall(String method) { Assert.assertFail("Unable to invoke '%s': Apptentive SDK is not properly initialized", method); } diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/conversation/Conversation.java b/apptentive/src/main/java/com/apptentive/android/sdk/conversation/Conversation.java index 1d8e7dddf..56858d31c 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/conversation/Conversation.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/conversation/Conversation.java @@ -134,6 +134,11 @@ public class Conversation implements DataChangedListener, Destroyable, DeviceDat @Override protected void execute() { try { + if (ApptentiveLog.canLog(ApptentiveLog.Level.VERBOSE)) { + ApptentiveLog.v(CONVERSATION, "Saving conversation data..."); + ApptentiveLog.v(CONVERSATION, "EventData: %s", getEventData().toString()); + ApptentiveLog.v(CONVERSATION, "Messages: %s", messageManager.getMessageStore().toString()); + } saveConversationData(); } catch (Exception e) { ApptentiveLog.e(CONVERSATION, e, "Exception while saving conversation data"); @@ -401,11 +406,6 @@ public void scheduleSaveConversationData() { * if succeed. */ private synchronized void saveConversationData() throws SerializerException { - if (ApptentiveLog.canLog(ApptentiveLog.Level.VERBOSE)) { - ApptentiveLog.v(CONVERSATION, "Saving conversation data..."); - ApptentiveLog.v(CONVERSATION, "EventData: %s", getEventData().toString()); - ApptentiveLog.v(CONVERSATION, "Messages: %s", messageManager.getMessageStore().toString()); - } long start = System.currentTimeMillis(); FileSerializer serializer = new EncryptedFileSerializer(conversationDataFile, encryption); @@ -438,7 +438,7 @@ boolean migrateConversationData() throws SerializerException { return false; } - void loadConversationData() throws SerializerException { + synchronized void loadConversationData() throws SerializerException { long start = System.currentTimeMillis(); FileSerializer serializer = new EncryptedFileSerializer(conversationDataFile, encryption); diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/EngagementModule.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/EngagementModule.java index e02f76a9a..593cfab7b 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/EngagementModule.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/EngagementModule.java @@ -19,6 +19,7 @@ import com.apptentive.android.sdk.model.ExtendedData; import com.apptentive.android.sdk.module.engagement.interaction.model.Interaction; import com.apptentive.android.sdk.module.engagement.interaction.model.MessageCenterInteraction; +import com.apptentive.android.sdk.util.ThrottleUtils; import com.apptentive.android.sdk.util.Util; import com.apptentive.android.sdk.util.threading.DispatchTask; @@ -90,7 +91,11 @@ private static boolean doEngage(Conversation conversation, Context context, Stri Interaction interaction = conversation.getApplicableInteraction(eventLabel, true); if (interaction != null) { - return launchInteraction(context, conversation, interaction); + if (!ApptentiveInternal.getInstance().shouldThrottleInteraction(interaction.getType())) { + return launchInteraction(context, conversation, interaction); + } else { + return false; + } } ApptentiveLog.d(INTERACTIONS, "No interaction to show for event: '%s'", eventLabel); return false; diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/AppStoreRatingFragment.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/AppStoreRatingFragment.java index e42a97753..37b29ac34 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/AppStoreRatingFragment.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/AppStoreRatingFragment.java @@ -86,8 +86,8 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { @Override public void onPause() { - super.onPause(); transit(); + super.onPause(); } @Override diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/ApptentiveBaseFragment.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/ApptentiveBaseFragment.java index a7f46ba01..524c9c0d2 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/ApptentiveBaseFragment.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/ApptentiveBaseFragment.java @@ -242,7 +242,6 @@ public void onInteractionUpdated(boolean successful) { @Override public void onStop() { - super.onStop(); try { if (Build.VERSION.SDK_INT >= 11 && getActivity() != null) { isChangingConfigurations = getActivity().isChangingConfigurations(); @@ -251,12 +250,11 @@ public void onStop() { ApptentiveLog.e(e, "Exception in %s.onStop()", ApptentiveBaseFragment.class.getSimpleName()); logException(e); } + super.onStop(); } @Override public void onDestroyView() { - super.onDestroyView(); - try { if (toolbar != null && fragmentMenuItems != null) { Menu toolbarMenu = toolbar.getMenu(); @@ -273,6 +271,7 @@ public void onDestroyView() { logException(e); } + super.onDestroyView(); } @Override diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/MessageCenterFragment.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/MessageCenterFragment.java index b576983ac..7a19012c6 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/MessageCenterFragment.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/MessageCenterFragment.java @@ -320,7 +320,6 @@ public void onStart() { } public void onStop() { - super.onStop(); try { ConversationProxy conversation = getConversation(); if (conversation != null) { @@ -330,6 +329,7 @@ public void onStop() { ApptentiveLog.e("Exception in %s.onStop()", MessageCenterFragment.class.getSimpleName()); logException(e); } + super.onStop(); } @Override @@ -390,7 +390,6 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { @Override public void onPause() { - super.onPause(); dispatchConversationTask(new ConversationDispatchTask() { @Override protected boolean execute(Conversation conversation) { @@ -398,6 +397,7 @@ protected boolean execute(Conversation conversation) { return true; } }, "pause message center fragment"); + super.onPause(); } @Override diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/NavigateToLinkFragment.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/NavigateToLinkFragment.java index 9c661b2a3..849db3262 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/NavigateToLinkFragment.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/NavigateToLinkFragment.java @@ -83,8 +83,8 @@ public void onCreate(Bundle savedInstanceState) { @Override public void onPause() { - super.onPause(); transit(); + super.onPause(); } diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/storage/EncryptedFileSerializer.java b/apptentive/src/main/java/com/apptentive/android/sdk/storage/EncryptedFileSerializer.java index edc860404..2d2e3cec3 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/storage/EncryptedFileSerializer.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/storage/EncryptedFileSerializer.java @@ -6,6 +6,8 @@ package com.apptentive.android.sdk.storage; +import static com.apptentive.android.sdk.util.ObjectUtils.isNullOrEmpty; + import com.apptentive.android.sdk.Encryption; import com.apptentive.android.sdk.util.Util; @@ -39,7 +41,11 @@ protected void serialize(FileOutputStream stream, Object object) throws Exceptio oos.writeObject(object); final byte[] unencryptedBytes = bos.toByteArray(); final byte[] encryptedBytes = encryption.encrypt(unencryptedBytes); - stream.write(encryptedBytes); // TODO: should we write using a buffer? + if (!isNullOrEmpty(encryptedBytes)) { + stream.write(encryptedBytes); // TODO: should we write using a buffer? + } else { + throw new SerializerException(new IllegalStateException("Encrypted bytes should not be null or 0")); + } } finally { Util.ensureClosed(bos); Util.ensureClosed(oos); diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/util/Constants.java b/apptentive/src/main/java/com/apptentive/android/sdk/util/Constants.java index a11998acc..2ec24ac0d 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/util/Constants.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/util/Constants.java @@ -9,7 +9,7 @@ public class Constants { public static final int API_VERSION = 10; - private static final String APPTENTIVE_SDK_VERSION = "5.7.1"; + private static final String APPTENTIVE_SDK_VERSION = "5.8.0"; public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 45000; public static final int DEFAULT_READ_TIMEOUT_MILLIS = 45000; diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/util/ThrottleUtils.java b/apptentive/src/main/java/com/apptentive/android/sdk/util/ThrottleUtils.java new file mode 100644 index 000000000..b1d8304ea --- /dev/null +++ b/apptentive/src/main/java/com/apptentive/android/sdk/util/ThrottleUtils.java @@ -0,0 +1,47 @@ +package com.apptentive.android.sdk.util; + +import android.annotation.SuppressLint; +import android.content.SharedPreferences; + +import com.apptentive.android.sdk.ApptentiveLog; +import com.apptentive.android.sdk.module.engagement.interaction.model.Interaction; + +import java.util.concurrent.TimeUnit; + +public class ThrottleUtils { + + public ThrottleUtils(Long ratingThrottle, SharedPreferences globalSharedPrefs) { + ratingThrottleLength = ratingThrottle; + sharedPrefs = globalSharedPrefs; + } + + private final Long ratingThrottleLength; + private final SharedPreferences sharedPrefs; + + private final long defaultThrottleLength = TimeUnit.SECONDS.toMillis(1); + + @SuppressLint("ApplySharedPref") + public boolean shouldThrottleInteraction(Interaction.Type interactionType) { + String interactionName = interactionType.name(); + long currentTime = System.currentTimeMillis(); + long interactionLastThrottled = sharedPrefs.getLong(interactionName, 0); + boolean interactionIsRating = (interactionType == Interaction.Type.InAppRatingDialog + || interactionType == Interaction.Type.RatingDialog); + + if ((interactionIsRating && (currentTime - interactionLastThrottled) < ratingThrottleLength)) { + logThrottle(interactionName, ratingThrottleLength, currentTime, interactionLastThrottled); + return true; + } else if (!interactionIsRating && (currentTime - interactionLastThrottled) < defaultThrottleLength) { + logThrottle(interactionName, defaultThrottleLength, currentTime, interactionLastThrottled); + return true; + } else { + sharedPrefs.edit().putLong(interactionName, currentTime).commit(); + return false; + } + } + + private void logThrottle(String interactionName, Long throttleLength, Long currentTime, Long lastThrottledTime) { + ApptentiveLog.w(interactionName + " throttled. Throttle length is " + throttleLength + + "ms. Can be shown again in " + (currentTime - lastThrottledTime) + "ms."); + } +} diff --git a/build.gradle b/build.gradle index 1b1772890..1bf27f5b3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,14 +1,62 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext { + // ----- SDK Compile and Build Versions ----- // + + compileSdkVersion = 30 + buildToolsVersion = '30.0.3' + minSdkVersion = 14 + targetSdkVersion = 30 + + // ----- App dependencies ----- // + + // AndroidX + // https://developer.android.com/jetpack/androidx/explorer + appcompat_library_version = '1.3.1' + legacy_support_v4_version = '1.0.0' + + // Material Design + // https://github.com/material-components/material-components-android/releases + material_design_version='1.4.0' + + // Google Play (For Google In-App Review) + // https://developer.android.com/guide/playcore + // https://developers.google.com/android/guides/setup + play_core_version = '1.10.2' + play_services_base_version = '17.6.0' + + + // ------- Testing ------- // + + // Junit + // https://mvnrepository.com/artifact/junit/junit + junit_version = '4.13.2' + + // PowerMock + // https://github.com/powermock/powermock + powermock_version = '2.0.9' + + // AndroidX + // https://developer.android.com/jetpack/androidx/explorer + // https://developer.android.com/jetpack/androidx/releases/test + annotation_version = '1.2.0' + androidx_test_version = '1.4.0' + } repositories { google() // "https://maven.google.com" mavenCentral() } dependencies { + // Android Gradle + // https://developer.android.com/studio/releases/gradle-plugin classpath 'com.android.tools.build:gradle:4.2.2' + + // Kotlin + // https://kotlinlang.org/docs/gradle.html + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.30" + + // HockeyApp classpath 'com.github.3mph4515:gradle-hockeyapp-plugin:3.7.6' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/samples/apptentive-example/build.gradle b/samples/apptentive-example/build.gradle index d672914a8..7c18243b6 100644 --- a/samples/apptentive-example/build.gradle +++ b/samples/apptentive-example/build.gradle @@ -3,14 +3,13 @@ apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android' android { - compileSdkVersion 29 - buildToolsVersion "29.0.2" - + compileSdkVersion 30 + buildToolsVersion "30.0.3" defaultConfig { applicationId "apptentive.com.example" minSdkVersion 14 - targetSdkVersion 29 + targetSdkVersion 30 versionCode 1 versionName "1.0" @@ -23,22 +22,18 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } - } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation project(':apptentive') + implementation 'com.apptentive:apptentive-android:5.7.1' + implementation 'androidx.core:core-ktx:1.6.0' + implementation "androidx.appcompat:appcompat:1.3.1" + implementation 'androidx.constraintlayout:constraintlayout:2.1.0' - implementation 'androidx.core:core-ktx:1.1.0' - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test.ext:junit:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' -} -repositories { - mavenCentral() + testImplementation "junit:junit:4.13.2" + + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' } diff --git a/settings.gradle b/settings.gradle index 9d6aca58d..892a83907 100644 --- a/settings.gradle +++ b/settings.gradle @@ -12,4 +12,6 @@ project(':apptentive-example').projectDir = new File('samples/apptentive-example // Internal App if (file('apptentive-internal-app/build.gradle').exists()) { include ':apptentive-internal-app' -} \ No newline at end of file +} + +rootProject.name = 'apptentive-sdk' \ No newline at end of file diff --git a/tests/test-app/build.gradle b/tests/test-app/build.gradle index 47badd24f..3133dd000 100644 --- a/tests/test-app/build.gradle +++ b/tests/test-app/build.gradle @@ -6,7 +6,7 @@ android { defaultConfig { minSdkVersion 14 - targetSdkVersion 29 + targetSdkVersion 30 versionCode 4 versionName "2.0" } @@ -25,7 +25,7 @@ android { dependencies { implementation project(':apptentive') - androidTestImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test.ext:junit:1.1.1' - androidTestImplementation 'androidx.test:rules:1.1.1' + androidTestImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test:rules:1.4.0' } \ No newline at end of file diff --git a/tests/test-app/src/androidTest/java/com/apptentive/android/sdk/tests/misc/ThrottleUtilsTest.java b/tests/test-app/src/androidTest/java/com/apptentive/android/sdk/tests/misc/ThrottleUtilsTest.java new file mode 100644 index 000000000..39502e1bd --- /dev/null +++ b/tests/test-app/src/androidTest/java/com/apptentive/android/sdk/tests/misc/ThrottleUtilsTest.java @@ -0,0 +1,111 @@ +package com.apptentive.android.sdk.tests.misc; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.content.Context; +import android.content.SharedPreferences; + +import com.apptentive.android.sdk.module.engagement.interaction.model.Interaction; +import com.apptentive.android.sdk.tests.ApptentiveTestCaseBase; +import com.apptentive.android.sdk.util.ThrottleUtils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.concurrent.TimeUnit; + +public class ThrottleUtilsTest extends ApptentiveTestCaseBase { + private SharedPreferences sharedPreferences; + private ThrottleUtils throttleUtils; + Interaction.Type ratingInteraction = Interaction.Type.InAppRatingDialog; + Interaction.Type noteInteractionOne = Interaction.Type.TextModal; + Interaction.Type noteInteractionTwo = Interaction.Type.TextModal; + Interaction.Type surveyInteraction = Interaction.Type.Survey; + + @Before + public void setUp() { + String APPTENTIVE_TEST_SHARED_PREF = "APPTENTIVE TEST SHARED PREF"; + sharedPreferences = targetContext.getSharedPreferences(APPTENTIVE_TEST_SHARED_PREF, Context.MODE_PRIVATE); + throttleUtils = new ThrottleUtils(100L, sharedPreferences); + } + + @After + public void tearDown() { + sharedPreferences = null; + } + + @Test + public final void shouldThrottleRatingInteractionTest() { + try { + // First call + assertFalse(throttleUtils.shouldThrottleInteraction(ratingInteraction)); + + // Call right after + assertTrue(throttleUtils.shouldThrottleInteraction(ratingInteraction)); + TimeUnit.MILLISECONDS.sleep(10L); + + // 10ms since first call + assertTrue(throttleUtils.shouldThrottleInteraction(ratingInteraction)); + TimeUnit.MILLISECONDS.sleep(50L); + + // 60ms since first call + assertTrue(throttleUtils.shouldThrottleInteraction(ratingInteraction)); + TimeUnit.MILLISECONDS.sleep(60L); + + // 120ms since first call (should be able to call again) + assertFalse(throttleUtils.shouldThrottleInteraction(ratingInteraction)); + TimeUnit.MILLISECONDS.sleep(50L); + + // 50ms since second call + assertTrue(throttleUtils.shouldThrottleInteraction(ratingInteraction)); + } catch (Exception e) { + + } + } + + @Test + public final void shouldThrottleInteractionWithOtherInteractionsTest() { + try { + // Default throttle length is 1 second aka 1000 ms + + // First call interactionTwo + assertFalse(throttleUtils.shouldThrottleInteraction(noteInteractionOne)); + TimeUnit.MILLISECONDS.sleep(300L); + + // 300ms since first call interactionTwo + assertTrue(throttleUtils.shouldThrottleInteraction(noteInteractionOne)); + + // Same Type as interactionTwo (so 300ms since last called this type) + assertTrue(throttleUtils.shouldThrottleInteraction(noteInteractionTwo)); + + // first call interactionFour + assertFalse(throttleUtils.shouldThrottleInteraction(surveyInteraction)); + TimeUnit.MILLISECONDS.sleep(500L); + + // 800ms since second call interactionTwo + assertTrue(throttleUtils.shouldThrottleInteraction(noteInteractionOne)); + assertTrue(throttleUtils.shouldThrottleInteraction(noteInteractionTwo)); + TimeUnit.MILLISECONDS.sleep(300L); + + // 1100ms since first call of interactionTwo (should be able to call) + assertFalse(throttleUtils.shouldThrottleInteraction(noteInteractionTwo)); + + // Same type as interactionThree that was just called (shouldn't be able to call) + assertTrue(throttleUtils.shouldThrottleInteraction(noteInteractionOne)); + + // 800ms since first call of interactionFour + assertTrue(throttleUtils.shouldThrottleInteraction(surveyInteraction)); + TimeUnit.MILLISECONDS.sleep(300L); + + // 1100ms since first call of interactionFour + assertFalse(throttleUtils.shouldThrottleInteraction(surveyInteraction)); + + // Call right after same interaction should not call + assertTrue(throttleUtils.shouldThrottleInteraction(surveyInteraction)); + } catch (Exception e) { + + } + } +} diff --git a/tests/test-app/src/androidTest/java/com/apptentive/android/sdk/tests/module/engagement/InteractionTest.java b/tests/test-app/src/androidTest/java/com/apptentive/android/sdk/tests/module/engagement/InteractionTest.java index ed33a6e43..989cc6c4e 100644 --- a/tests/test-app/src/androidTest/java/com/apptentive/android/sdk/tests/module/engagement/InteractionTest.java +++ b/tests/test-app/src/androidTest/java/com/apptentive/android/sdk/tests/module/engagement/InteractionTest.java @@ -6,13 +6,16 @@ package com.apptentive.android.sdk.tests.module.engagement; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + import androidx.test.runner.AndroidJUnit4; import com.apptentive.android.sdk.ApptentiveLog; import com.apptentive.android.sdk.module.engagement.interaction.model.InteractionCriteria; import com.apptentive.android.sdk.module.engagement.logic.DefaultRandomPercentProvider; import com.apptentive.android.sdk.module.engagement.logic.FieldManager; -import com.apptentive.android.sdk.module.engagement.logic.RandomPercentProvider; import com.apptentive.android.sdk.storage.AppRelease; import com.apptentive.android.sdk.storage.AppReleaseManager; import com.apptentive.android.sdk.storage.Device; @@ -28,10 +31,6 @@ import java.io.File; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - /** * Note: Right now, these tests need versionName and versionCode in the manifest to be "2.0" and 4", respectively. */ @@ -160,7 +159,7 @@ public void criteriaApplicationVersionCode() throws JSONException { String json = loadTextAssetAsString(TEST_DATA_DIR + "criteria/testCriteriaApplicationVersionCode.json"); json = json.replace("\"APPLICATION_VERSION_CODE\"", String.valueOf(appRelease.getVersionCode())); InteractionCriteria criteria = new InteractionCriteria(json); - FieldManager fieldManager = new FieldManager(targetContext, new VersionHistory(), new EventData(), new Person(), new Device(), appRelease , new DefaultRandomPercentProvider(targetContext, "id")); + FieldManager fieldManager = new FieldManager(targetContext, new VersionHistory(), new EventData(), new Person(), new Device(), appRelease, new DefaultRandomPercentProvider(targetContext, "id")); assertTrue(criteria.isMet(fieldManager)); } @@ -171,7 +170,7 @@ public void criteriaApplicationVersionName() throws JSONException { String json = loadTextAssetAsString(TEST_DATA_DIR + "criteria/testCriteriaApplicationVersionName.json"); json = json.replace("APPLICATION_VERSION_NAME", appRelease.getVersionName()); InteractionCriteria criteria = new InteractionCriteria(json); - FieldManager fieldManager = new FieldManager(targetContext, new VersionHistory(), new EventData(), new Person(), new Device(), appRelease , new DefaultRandomPercentProvider(targetContext, "id")); + FieldManager fieldManager = new FieldManager(targetContext, new VersionHistory(), new EventData(), new Person(), new Device(), appRelease, new DefaultRandomPercentProvider(targetContext, "id")); assertTrue(criteria.isMet(fieldManager)); } @@ -181,7 +180,7 @@ public void criteriaApplicationDebug() throws JSONException { String json = loadTextAssetAsString(TEST_DATA_DIR + "criteria/testCriteriaApplicationDebug.json"); json = json.replace("APPLICATION_DEBUG", Boolean.toString(appRelease.isDebug())); InteractionCriteria criteria = new InteractionCriteria(json); - FieldManager fieldManager = new FieldManager(targetContext, new VersionHistory(), new EventData(), new Person(), new Device(), appRelease , new DefaultRandomPercentProvider(targetContext, "id")); + FieldManager fieldManager = new FieldManager(targetContext, new VersionHistory(), new EventData(), new Person(), new Device(), appRelease, new DefaultRandomPercentProvider(targetContext, "id")); assertTrue(criteria.isMet(fieldManager)); } @@ -199,7 +198,7 @@ public void criteriaProcessingPerformance() throws JSONException { VersionHistory versionHistory = new VersionHistory(); EventData eventData = new EventData(); - FieldManager fieldManager = new FieldManager(targetContext, versionHistory, eventData, new Person(), new Device(), new AppRelease() , new DefaultRandomPercentProvider(targetContext, "id")); + FieldManager fieldManager = new FieldManager(targetContext, versionHistory, eventData, new Person(), new Device(), new AppRelease(), new DefaultRandomPercentProvider(targetContext, "id")); versionHistory.updateVersionHistory(Util.currentTimeSeconds(), versionCode, versionName); eventData.storeEventForCurrentAppVersion(Util.currentTimeSeconds(), versionCode, versionName, "app.launch"); eventData.storeEventForCurrentAppVersion(Util.currentTimeSeconds(), versionCode, versionName, "app.launch"); @@ -232,7 +231,7 @@ public void savingCodePointAndCheckingForApplicableInteraction() throws JSONExce InteractionCriteria criteria = new InteractionCriteria(json); EventData eventData = new EventData(); - FieldManager fieldManager = new FieldManager(targetContext, new VersionHistory(), eventData, new Person(), new Device(), new AppRelease() , new DefaultRandomPercentProvider(targetContext, "id")); + FieldManager fieldManager = new FieldManager(targetContext, new VersionHistory(), eventData, new Person(), new Device(), new AppRelease(), new DefaultRandomPercentProvider(targetContext, "id")); eventData.storeEventForCurrentAppVersion(Util.currentTimeSeconds(), versionCode, versionName, "app.launch"); eventData.storeEventForCurrentAppVersion(Util.currentTimeSeconds(), versionCode, versionName, "app.launch"); eventData.storeEventForCurrentAppVersion(Util.currentTimeSeconds(), versionCode, versionName, "big.win"); @@ -308,7 +307,7 @@ public void upgradeMessageOnVersionCode() throws JSONException { InteractionCriteria criteria = new InteractionCriteria(json); VersionHistory versionHistory = new VersionHistory(); - FieldManager fieldManager = new FieldManager(targetContext, versionHistory, new EventData(), new Person(), new Device(), appRelease , new DefaultRandomPercentProvider(targetContext, "id")); + FieldManager fieldManager = new FieldManager(targetContext, versionHistory, new EventData(), new Person(), new Device(), appRelease, new DefaultRandomPercentProvider(targetContext, "id")); versionHistory.updateVersionHistory(Util.currentTimeSeconds(), versionCode, versionName); // Test version targeted UpgradeMessage @@ -344,7 +343,7 @@ public void upgradeMessageOnVersionName() throws JSONException { VersionHistory versionHistory = new VersionHistory(); EventData eventData = new EventData(); - FieldManager fieldManager = new FieldManager(targetContext, versionHistory, eventData, new Person(), new Device(), appRelease , new DefaultRandomPercentProvider(targetContext, "id")); + FieldManager fieldManager = new FieldManager(targetContext, versionHistory, eventData, new Person(), new Device(), appRelease, new DefaultRandomPercentProvider(targetContext, "id")); versionHistory.updateVersionHistory(Util.currentTimeSeconds(), versionCode, versionName); eventData.storeEventForCurrentAppVersion(Util.currentTimeSeconds(), versionCode, versionName, "app.launch");