From f1096e3109e34a67ef58185115fa6fe9de86d83d Mon Sep 17 00:00:00 2001 From: ChaseApptentive <85038972+ChaseApptentive@users.noreply.github.com> Date: Thu, 2 Dec 2021 13:16:30 -0600 Subject: [PATCH 001/491] [PBI-2243] Remove Apptentive reset function (#194) * [PBI-2243] Remove Apptentive reset function [PBI-2243] * ktlint * Remove additional internal reset function * Use camel case to match other xml IDs --- .../java/apptentive/com/app/MainActivity.kt | 6 +++-- app/src/main/res/layout/activity_main.xml | 4 ++-- app/src/main/res/values/strings.xml | 4 ++-- .../com/android/feedback/Apptentive.kt | 23 ------------------- .../feedback/ApptentiveDefaultClient.kt | 9 -------- 5 files changed, 8 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/apptentive/com/app/MainActivity.kt b/app/src/main/java/apptentive/com/app/MainActivity.kt index 01d7a3328..d2cb15bfd 100644 --- a/app/src/main/java/apptentive/com/app/MainActivity.kt +++ b/app/src/main/java/apptentive/com/app/MainActivity.kt @@ -1,5 +1,7 @@ package apptentive.com.app +import android.app.ActivityManager +import android.content.Context import android.content.Intent import android.os.Build import android.os.Bundle @@ -75,8 +77,8 @@ class MainActivity : AppCompatActivity() { Apptentive.engage(this, "rating_dialog_event") { handleResult(it) } } - binding.resetSDKStateButton.setOnClickListener { - Apptentive.reset(this) + binding.clearAppDataButton.setOnClickListener { + (getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).clearApplicationUserData() } } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index f07a668ff..71b1d564f 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -166,7 +166,7 @@ app:layout_constraintTop_toBottomOf="@id/rating_dialog_button" /> + app:layout_constraintTop_toBottomOf="@id/clearAppDataButton" /> No event entered Interactions Shows the Apptentive Rating Dialog.\n\'Rate\' sends you to the customAppStoreURL set in ApptentiveConfiguration. - Reset SDK Data - Clears the conversation and calls a \"launch\" event + Clear App Data + Clears all of the app\'s data and closes the app.\nWill require manual opening of the app again. Sanitize Logs Determines if sensitive info should be logged.\nWill require manual opening of the app again. Back button diff --git a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/Apptentive.kt b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/Apptentive.kt index 352381d71..9f3db2785 100644 --- a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/Apptentive.kt +++ b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/Apptentive.kt @@ -2,7 +2,6 @@ package apptentive.com.android.feedback import android.app.Application import android.content.Context -import androidx.annotation.VisibleForTesting import apptentive.com.android.concurrent.Executor import apptentive.com.android.concurrent.ExecutorQueue import apptentive.com.android.concurrent.Executors @@ -16,7 +15,6 @@ import apptentive.com.android.core.TimeInterval import apptentive.com.android.core.format import apptentive.com.android.feedback.engagement.Event import apptentive.com.android.feedback.engagement.interactions.InteractionId -import apptentive.com.android.feedback.utils.RuntimeUtils import apptentive.com.android.feedback.utils.SensitiveDataUtils import apptentive.com.android.feedback.utils.ThrottleUtils import apptentive.com.android.feedback.utils.ThrottleUtils.SHARED_PREF_THROTTLE @@ -29,7 +27,6 @@ import apptentive.com.android.network.HttpNetworkResponse import apptentive.com.android.network.HttpRequest import apptentive.com.android.network.asString import apptentive.com.android.util.Log -import apptentive.com.android.util.LogTags import apptentive.com.android.util.LogTags.NETWORK sealed class EngagementResult { @@ -209,26 +206,6 @@ object Apptentive { } } - //region Debug - - /** - * *** DEBUG ONLY FUNCTION *** - * Clears the Conversation and records an internal "launch" event - * @see apptentive.com.android.feedback.ApptentiveDefaultClient.reset - */ - @VisibleForTesting - fun reset(context: Context) { - if (RuntimeUtils.getApplicationInfo(context).debuggable) { - stateExecutor.execute { - val client = client as? ApptentiveDefaultClient - if (client != null) client.reset() - else Log.e(LogTags.CORE, "Unable to clear event: sdk is not initialized") - } - } else throw IllegalStateException("Can only use reset method in Debug mode") - } - - //endregion - // region Person data updates /** diff --git a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveDefaultClient.kt b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveDefaultClient.kt index 676287258..89aa03bff 100644 --- a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveDefaultClient.kt +++ b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveDefaultClient.kt @@ -338,15 +338,6 @@ internal class ApptentiveDefaultClient( //endregion - //region Debug - - internal fun reset() { - conversationManager.clear() - conversationManager.recordEvent(Event.internal("launch")) // trick sdk to think it was launched - } - - //endregion - companion object { private fun getConversationFile(): File { val conversationsDir = getConversationDir() From 37b6b2d603064b34dff9a06a9416a265b90996b1 Mon Sep 17 00:00:00 2001 From: ChaseApptentive <85038972+ChaseApptentive@users.noreply.github.com> Date: Mon, 6 Dec 2021 13:27:35 -0600 Subject: [PATCH 002/491] [PBI-2368] OS Version parsing (#196) * [PBI-2368] OS Version parsing [PBI-2368] * Fix version tests --- .../feedback/engagement/criteria/DefaultTargetingState.kt | 2 +- .../engagement/criteria/DefaultTargetingStateTest.kt | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/criteria/DefaultTargetingState.kt b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/criteria/DefaultTargetingState.kt index dabc1b7cc..cee14a096 100644 --- a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/criteria/DefaultTargetingState.kt +++ b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/criteria/DefaultTargetingState.kt @@ -68,7 +68,7 @@ internal data class DefaultTargetingState( is person.custom_data -> person.customData[field.key] is device.os_name -> device.osName - is device.os_version -> device.osVersion + is device.os_version -> Version.parse(device.osVersion) is device.os_build -> device.osBuild is device.manufacturer -> device.manufacturer is device.model -> device.model diff --git a/apptentive-feedback/src/test/java/apptentive/com/android/feedback/engagement/criteria/DefaultTargetingStateTest.kt b/apptentive-feedback/src/test/java/apptentive/com/android/feedback/engagement/criteria/DefaultTargetingStateTest.kt index fb131be4e..5aef28945 100644 --- a/apptentive-feedback/src/test/java/apptentive/com/android/feedback/engagement/criteria/DefaultTargetingStateTest.kt +++ b/apptentive-feedback/src/test/java/apptentive/com/android/feedback/engagement/criteria/DefaultTargetingStateTest.kt @@ -297,7 +297,7 @@ class DefaultTargetingStateTest : TestCase() { val state = state.copy( device = mockDevice.copy( osName = "device_os_name", - osVersion = "device_os_version", + osVersion = "12", osBuild = "device_os_build", osApiLevel = 30, manufacturer = "device_manufacturer", @@ -324,7 +324,7 @@ class DefaultTargetingStateTest : TestCase() { ) assertThat(state.getValue(device.os_name)).isEqualTo("device_os_name") - assertThat(state.getValue(device.os_version)).isEqualTo("device_os_version") + assertThat(state.getValue(device.os_version)).isEqualTo(Version(12, 0, 0)) assertThat(state.getValue(device.os_build)).isEqualTo("device_os_build") assertThat(state.getValue(device.manufacturer)).isEqualTo("device_manufacturer") assertThat(state.getValue(device.model)).isEqualTo("device_model") @@ -355,7 +355,7 @@ class DefaultTargetingStateTest : TestCase() { val state = state.copy( device = Device( osName = "device_os_name", - osVersion = "device_os_version", + osVersion = "12", osBuild = "device_os_build", osApiLevel = 30, manufacturer = "device_manufacturer", @@ -376,7 +376,7 @@ class DefaultTargetingStateTest : TestCase() { ) assertThat(state.getValue(device.os_name)).isEqualTo("device_os_name") - assertThat(state.getValue(device.os_version)).isEqualTo("device_os_version") + assertThat(state.getValue(device.os_version)).isEqualTo(Version(12, 0, 0)) assertThat(state.getValue(device.os_build)).isEqualTo("device_os_build") assertThat(state.getValue(device.manufacturer)).isEqualTo("device_manufacturer") assertThat(state.getValue(device.model)).isEqualTo("device_model") From aebd3bc8ff2df1c7d922ef5e68cf8089b355458b Mon Sep 17 00:00:00 2001 From: ChaseApptentive <85038972+ChaseApptentive@users.noreply.github.com> Date: Mon, 6 Dec 2021 13:53:08 -0600 Subject: [PATCH 003/491] Update publish.util and settings gradle (#195) * Update publishutil and settings gradle * I could write to jfrog but sdk-kit couldn't read for some reason. * I changed around the gradle settings a bit and removed the allprojects repos and things are working as expected now. * I can't remember what the projectProperties was for, but after removing it, it still worked. * The only task we'll need to run now to publish everything is publishKit (up for a rename if you want) * Update task names. Add back in parameters for public repo command --- apptentive-sdk-kit/build.gradle | 14 -------------- build.gradle | 6 +----- publishutil.gradle | 25 +++++++++++++++---------- settings.gradle | 19 +++++++++++++++++++ 4 files changed, 35 insertions(+), 29 deletions(-) diff --git a/apptentive-sdk-kit/build.gradle b/apptentive-sdk-kit/build.gradle index a55a1515c..17c9dcc73 100644 --- a/apptentive-sdk-kit/build.gradle +++ b/apptentive-sdk-kit/build.gradle @@ -22,20 +22,6 @@ android { } } -repositories { - maven { - def artifactoryProperties = new Properties() - if (file('./local.properties').exists()) { - artifactoryProperties.load(file('./local.properties').newDataInputStream()) - } - url "https://apptentive.jfrog.io/artifactory/InternalNewAndroidSDK" - credentials { - username = artifactoryProperties['artifactory_username'] ? artifactoryProperties['artifactory_username']:"" - password = artifactoryProperties['artifactory_password'] ? artifactoryProperties['artifactory_password']:"" - } - } -} - dependencies { implementation "com.apptentive:apptentive-core:$versionId" implementation "com.apptentive:apptentive-core-ui:$versionId" diff --git a/build.gradle b/build.gradle index a495dc180..4fef2e78a 100644 --- a/build.gradle +++ b/build.gradle @@ -64,7 +64,7 @@ buildscript { } project.ext { - versionId = '1.0.15' + versionId = '1.0.17' } repositories { @@ -102,10 +102,6 @@ plugins { allprojects { apply plugin: 'com.jfrog.artifactory' apply plugin: 'maven-publish' - repositories { - mavenCentral() - google() - } } task clean(type: Delete) { diff --git a/publishutil.gradle b/publishutil.gradle index e1c47b973..da5d19736 100644 --- a/publishutil.gradle +++ b/publishutil.gradle @@ -1,6 +1,5 @@ -task assembleModules(type: GradleBuild) { +task assembleApptentiveModules(type: GradleBuild) { buildName = "assembleApptentiveModules" - startParameter.projectProperties = project.gradle.startParameter.projectProperties tasks = ['apptentive-core:assembleRelease', 'apptentive-core-ui:assembleRelease', 'apptentive-feedback:assembleRelease', @@ -9,11 +8,10 @@ task assembleModules(type: GradleBuild) { 'apptentive-navigate-to-link:assembleRelease', 'apptentive-notes:assembleRelease', 'apptentive-ratings:assembleRelease', - 'apptentive-survey:assembleRelease', - 'apptentive-sdk-kit:assembleRelease'] + 'apptentive-survey:assembleRelease'] } -task publishModules(type: GradleBuild) { +task publishApptentiveModules(type: GradleBuild) { buildName = "publishApptentiveModules" startParameter.projectProperties = project.gradle.startParameter.projectProperties tasks = ['apptentive-core:artifactoryPublish', @@ -24,12 +22,19 @@ task publishModules(type: GradleBuild) { 'apptentive-navigate-to-link:artifactoryPublish', 'apptentive-notes:artifactoryPublish', 'apptentive-ratings:artifactoryPublish', - 'apptentive-survey:artifactoryPublish', - 'apptentive-sdk-kit:artifactoryPublish'] + 'apptentive-survey:artifactoryPublish'] + dependsOn assembleApptentiveModules +} + +task assembleApptentiveKit(type: GradleBuild) { + buildName = "assembleApptentiveKit" + tasks = ['apptentive-sdk-kit:assembleRelease'] + dependsOn publishApptentiveModules } task publishApptentiveKit(type: GradleBuild) { - //forward projectProperties + buildName = "publishApptentiveKit" startParameter.projectProperties = project.gradle.startParameter.projectProperties - dependsOn assembleModules.finalizedBy(publishModules) -} \ No newline at end of file + tasks = ['apptentive-sdk-kit:artifactoryPublish'] + dependsOn assembleApptentiveKit +} diff --git a/settings.gradle b/settings.gradle index 5efdfa83e..1ae269004 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,4 +15,23 @@ include ':app', ':apptentive-survey', ':apptentive-sdk-kit' +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + maven { + def artifactoryProperties = new Properties() + if (file('./local.properties').exists()) { + artifactoryProperties.load(file('./local.properties').newDataInputStream()) + } + url "https://apptentive.jfrog.io/artifactory/InternalNewAndroidSDK" + credentials { + username = artifactoryProperties['artifactory_username'] ? artifactoryProperties['artifactory_username']:"" + password = artifactoryProperties['artifactory_password'] ? artifactoryProperties['artifactory_password']:"" + } + } + } +} + rootProject.name = 'apptentive-sdk-ktx' From f3b058903f53e592b4959416eed6b06c955a6045 Mon Sep 17 00:00:00 2001 From: Twinkle Sharma Date: Tue, 7 Dec 2021 09:59:33 -0800 Subject: [PATCH 004/491] changed the names of Success and Failure of EngagementResult (#198) --- .../java/apptentive/com/app/test/MainActivity.kt | 4 ++-- .../main/java/apptentive/com/app/MainActivity.kt | 2 +- .../enjoyment/EnjoymentDialogViewModelTest.kt | 2 +- .../feedback/engagement/MockEngagementContext.kt | 4 ++-- .../apptentive/com/android/feedback/Apptentive.kt | 4 ++-- .../feedback/engagement/DefaultEngagement.kt | 6 +++--- .../com/android/feedback/engagement/Engagement.kt | 4 ++-- .../feedback/engagement/InteractionEngagement.kt | 4 ++-- .../feedback/engagement/DefaultEngagementTest.kt | 8 ++++---- .../InAppReviewInteractionLauncherTest.kt | 8 ++++---- .../com/android/feedback/link/LinkNavigatorTest.kt | 2 +- .../feedback/textmodal/TextModalViewModel.kt | 2 +- .../notes/viewmodel/TextModalViewModelTest.kt | 14 +++++++------- .../ratings/appstorerating/StoreNavigatorTest.kt | 2 +- .../ratingdialog/RatingDialogViewModelTest.kt | 6 +++--- .../interaction/SurveyInteractionLauncherTest.kt | 2 +- 16 files changed, 37 insertions(+), 37 deletions(-) diff --git a/app-ui-test/src/main/java/apptentive/com/app/test/MainActivity.kt b/app-ui-test/src/main/java/apptentive/com/app/test/MainActivity.kt index 9b77cc10f..6c840defc 100644 --- a/app-ui-test/src/main/java/apptentive/com/app/test/MainActivity.kt +++ b/app-ui-test/src/main/java/apptentive/com/app/test/MainActivity.kt @@ -78,14 +78,14 @@ class MainActivity : AppCompatActivity() { customData: Map?, extendedData: List? ): EngagementResult { - return EngagementResult.Failure("No runnable interactions") + return EngagementResult.InteractionNotShown("No runnable interactions") } override fun engage( context: EngagementContext, invocations: List ): EngagementResult { - return EngagementResult.Failure("No runnable interactions") + return EngagementResult.InteractionNotShown("No runnable interactions") } }, payloadSender = object : PayloadSender { diff --git a/app/src/main/java/apptentive/com/app/MainActivity.kt b/app/src/main/java/apptentive/com/app/MainActivity.kt index d2cb15bfd..ba28d5362 100644 --- a/app/src/main/java/apptentive/com/app/MainActivity.kt +++ b/app/src/main/java/apptentive/com/app/MainActivity.kt @@ -83,7 +83,7 @@ class MainActivity : AppCompatActivity() { } private fun handleResult(it: EngagementResult) { - if (it !is EngagementResult.Success) { + if (it !is EngagementResult.InteractionShown) { Toast.makeText(this, "Not engaged: $it", Toast.LENGTH_LONG).show() } } diff --git a/apptentive-enjoyment-dialog/src/test/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogViewModelTest.kt b/apptentive-enjoyment-dialog/src/test/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogViewModelTest.kt index 41f0b559d..b47a26528 100644 --- a/apptentive-enjoyment-dialog/src/test/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogViewModelTest.kt +++ b/apptentive-enjoyment-dialog/src/test/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogViewModelTest.kt @@ -17,7 +17,7 @@ class EnjoymentDialogViewModelTest : TestCase() { val context = MockEngagementContext( onEngage = { args -> addResult(args) - EngagementResult.Failure("No runnable interactions") + EngagementResult.InteractionNotShown("No runnable interactions") }, onSendPayload = { payload -> throw AssertionError("We didn't expect any payloads here but this one slipped though: $payload") diff --git a/apptentive-feedback-test/src/main/java/apptentive/com/android/feedback/engagement/MockEngagementContext.kt b/apptentive-feedback-test/src/main/java/apptentive/com/android/feedback/engagement/MockEngagementContext.kt index 96b7231e7..c6452968c 100644 --- a/apptentive-feedback-test/src/main/java/apptentive/com/android/feedback/engagement/MockEngagementContext.kt +++ b/apptentive-feedback-test/src/main/java/apptentive/com/android/feedback/engagement/MockEngagementContext.kt @@ -44,14 +44,14 @@ class MockEngagementContext( customData, extendedData ) - ) ?: EngagementResult.Failure("No runnable interactions") + ) ?: EngagementResult.InteractionNotShown("No runnable interactions") } override fun engage( context: EngagementContext, invocations: List ): EngagementResult { - return onInvoke?.invoke(invocations) ?: EngagementResult.Failure("No runnable interactions") + return onInvoke?.invoke(invocations) ?: EngagementResult.InteractionNotShown("No runnable interactions") } }, payloadSender = MockPayloadSender(onSendPayload), diff --git a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/Apptentive.kt b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/Apptentive.kt index 9f3db2785..f9b9ce8fa 100644 --- a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/Apptentive.kt +++ b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/Apptentive.kt @@ -30,10 +30,10 @@ import apptentive.com.android.util.Log import apptentive.com.android.util.LogTags.NETWORK sealed class EngagementResult { - data class Success(val interactionId: InteractionId) : EngagementResult() { + data class InteractionShown(val interactionId: InteractionId) : EngagementResult() { init { Log.d(INTERACTIONS, "Interaction Engaged => interactionID: $interactionId") } } - data class Failure(val description: String) : EngagementResult() { + data class InteractionNotShown(val description: String) : EngagementResult() { init { Log.d(INTERACTIONS, "Interaction NOT Engaged => $description") } } data class Error(val message: String) : EngagementResult() { diff --git a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/DefaultEngagement.kt b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/DefaultEngagement.kt index 96c11dcb5..dd8a2873d 100644 --- a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/DefaultEngagement.kt +++ b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/DefaultEngagement.kt @@ -42,7 +42,7 @@ internal data class DefaultEngagement( val interactionData = interactionDataProvider.getInteractionData(event) if (interactionData == null) { - return EngagementResult.Failure("No runnable interactions for event '${event.name}'") + return EngagementResult.InteractionNotShown("No runnable interactions for event '${event.name}'") } val interaction = interactionConverter.convert(interactionData) @@ -59,7 +59,7 @@ internal data class DefaultEngagement( ): EngagementResult { val interactionData = interactionDataProvider.getInteractionData(invocations) if (interactionData == null) { - return EngagementResult.Failure("No runnable interactions") + return EngagementResult.InteractionNotShown("No runnable interactions") } val interaction = interactionConverter.convert(interactionData) @@ -75,7 +75,7 @@ internal data class DefaultEngagement( interaction: Interaction ): EngagementResult { val result = interactionEngagement.engage(context, interaction) - if (result is EngagementResult.Success) { + if (result is EngagementResult.InteractionShown) { recordInteraction(interaction) } diff --git a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/Engagement.kt b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/Engagement.kt index 5f0615fb8..8c0375cd0 100644 --- a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/Engagement.kt +++ b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/Engagement.kt @@ -32,13 +32,13 @@ internal class NullEngagement : Engagement { customData: Map?, extendedData: List? ): EngagementResult { - return EngagementResult.Failure("Unable to engage event $event: SDK is not fully initialized") + return EngagementResult.Error("Unable to engage event $event: SDK is not fully initialized") } override fun engage( context: EngagementContext, invocations: List ): EngagementResult { - return EngagementResult.Failure("Unable to engage invocations: SDK is not fully initialized") + return EngagementResult.Error("Unable to engage invocations: SDK is not fully initialized") } } diff --git a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/InteractionEngagement.kt b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/InteractionEngagement.kt index 41feda09c..fe7cbf82a 100644 --- a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/InteractionEngagement.kt +++ b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/InteractionEngagement.kt @@ -24,8 +24,8 @@ internal data class DefaultInteractionEngagement( if (!shouldThrottleInteraction) { launcher.launchInteraction(context, interaction) - EngagementResult.Success(interactionId = interaction.id) - } else EngagementResult.Failure("${interaction.type.name} throttled.") + EngagementResult.InteractionShown(interactionId = interaction.id) + } else EngagementResult.InteractionNotShown("${interaction.type.name} throttled.") } else EngagementResult.Error("Interaction launcher not found: ${interactionClass.name}") } } diff --git a/apptentive-feedback/src/test/java/apptentive/com/android/feedback/engagement/DefaultEngagementTest.kt b/apptentive-feedback/src/test/java/apptentive/com/android/feedback/engagement/DefaultEngagementTest.kt index 76beabcac..6345d5e89 100644 --- a/apptentive-feedback/src/test/java/apptentive/com/android/feedback/engagement/DefaultEngagementTest.kt +++ b/apptentive-feedback/src/test/java/apptentive/com/android/feedback/engagement/DefaultEngagementTest.kt @@ -29,10 +29,10 @@ class DefaultEngagementTest : TestCase() { @Test fun engageSuccessful() { - interactionEngagement.addResult(EngagementResult.Success(mockInteraction.id)) + interactionEngagement.addResult(EngagementResult.InteractionShown(mockInteraction.id)) val result = engagement.engage(MockEngagementContext(), mockEvent) - assertThat(result).isInstanceOf(EngagementResult.Success::class.java) + assertThat(result).isInstanceOf(EngagementResult.InteractionShown::class.java) assertResults( "Event: ${mockEvent.fullName}", @@ -42,10 +42,10 @@ class DefaultEngagementTest : TestCase() { @Test fun engageUnsuccessful() { - interactionEngagement.addResult(EngagementResult.Failure("Something went wrong")) + interactionEngagement.addResult(EngagementResult.InteractionNotShown("Something went wrong")) val result = engagement.engage(MockEngagementContext(), mockEvent) - assertThat(result).isInstanceOf(EngagementResult.Failure::class.java) + assertThat(result).isInstanceOf(EngagementResult.InteractionNotShown::class.java) assertResults("Event: ${mockEvent.fullName}") } diff --git a/apptentive-in-app-review/src/androidTest/java/apptentive/com/android/feedback/rating/interaction/InAppReviewInteractionLauncherTest.kt b/apptentive-in-app-review/src/androidTest/java/apptentive/com/android/feedback/rating/interaction/InAppReviewInteractionLauncherTest.kt index 484db8894..4dca0ebcc 100644 --- a/apptentive-in-app-review/src/androidTest/java/apptentive/com/android/feedback/rating/interaction/InAppReviewInteractionLauncherTest.kt +++ b/apptentive-in-app-review/src/androidTest/java/apptentive/com/android/feedback/rating/interaction/InAppReviewInteractionLauncherTest.kt @@ -33,7 +33,7 @@ class InAppReviewInteractionLauncherTest : TestCase() { launcher.launchInteraction( createEngagementContext(onEngage = { addResult("engage ${it.event}") - EngagementResult.Success(it.interactionId as InteractionId) + EngagementResult.InteractionShown(it.interactionId as InteractionId) }), InAppReviewInteraction("58eebbf2127096704e0000d0") ) @@ -52,7 +52,7 @@ class InAppReviewInteractionLauncherTest : TestCase() { launcher.launchInteraction( createEngagementContext(onEngage = { addResult("engage ${it.event}") - EngagementResult.Failure("Something went wrong") + EngagementResult.InteractionNotShown("Something went wrong") }), InAppReviewInteraction("58eebbf2127096704e0000d0") ) @@ -70,7 +70,7 @@ class InAppReviewInteractionLauncherTest : TestCase() { launcher.launchInteraction( createEngagementContext(context = appContext, onEngage = { addResult("engage ${it.event}") - EngagementResult.Success(it.interactionId as InteractionId) + EngagementResult.InteractionShown(it.interactionId as InteractionId) }), InAppReviewInteraction("58eebbf2127096704e0000d0") ) @@ -99,7 +99,7 @@ class InAppReviewInteractionLauncherTest : TestCase() { ) = MockEngagementContext( onEngage = onEngage ?: { args -> addResult(args) - EngagementResult.Success(args.interactionId as InteractionId) + EngagementResult.InteractionShown(args.interactionId as InteractionId) }, onSendPayload = onSendPayload ?: { payload -> addResult(payload.toJson()) diff --git a/apptentive-navigate-to-link/src/test/java/apptentive/com/android/feedback/link/LinkNavigatorTest.kt b/apptentive-navigate-to-link/src/test/java/apptentive/com/android/feedback/link/LinkNavigatorTest.kt index 1a16a971a..582faddf3 100644 --- a/apptentive-navigate-to-link/src/test/java/apptentive/com/android/feedback/link/LinkNavigatorTest.kt +++ b/apptentive-navigate-to-link/src/test/java/apptentive/com/android/feedback/link/LinkNavigatorTest.kt @@ -54,6 +54,6 @@ class LinkNavigatorTest : TestCase() { private fun createEngagementContext() = MockEngagementContext( onEngage = { addResult(it) - EngagementResult.Failure("No runnable interactions") + EngagementResult.InteractionNotShown("No runnable interactions") }) } \ No newline at end of file diff --git a/apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalViewModel.kt b/apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalViewModel.kt index be0516b1e..224b6f8da 100644 --- a/apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalViewModel.kt +++ b/apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalViewModel.kt @@ -108,7 +108,7 @@ internal class TextModalViewModel( ): Map { // we need to include a target interaction id (if any) if (engagementResult != null) { - val interactionId = (engagementResult as? EngagementResult.Success)?.interactionId + val interactionId = (engagementResult as? EngagementResult.InteractionShown)?.interactionId return mapOf( DATA_ACTION_ID to action.id, DATA_ACTION_LABEL to action.label, diff --git a/apptentive-notes/src/test/java/apptentive/com/android/feedback/notes/viewmodel/TextModalViewModelTest.kt b/apptentive-notes/src/test/java/apptentive/com/android/feedback/notes/viewmodel/TextModalViewModelTest.kt index 3480df6b6..c0ca2ef80 100644 --- a/apptentive-notes/src/test/java/apptentive/com/android/feedback/notes/viewmodel/TextModalViewModelTest.kt +++ b/apptentive-notes/src/test/java/apptentive/com/android/feedback/notes/viewmodel/TextModalViewModelTest.kt @@ -49,7 +49,7 @@ class TextModalViewModelTest : TestCase() { val viewModel = createViewModel( onInvoke = { // trick it to think an interaction has been invoked - EngagementResult.Success(targetInteractionId) + EngagementResult.InteractionShown(targetInteractionId) } ) @@ -84,7 +84,7 @@ class TextModalViewModelTest : TestCase() { val viewModel = createViewModel( onInvoke = { // no interactions to invoke - EngagementResult.Failure("No runnable interactions") + EngagementResult.InteractionNotShown("No runnable interactions") } ) @@ -126,8 +126,8 @@ class TextModalViewModelTest : TestCase() { val viewModel = createViewModel( onEngage = { // trick it to think an interaction has been invoked - if (it.event.name == TARGET_EVENT) EngagementResult.Success(targetInteractionId) - else EngagementResult.Failure("No runnable interactions") + if (it.event.name == TARGET_EVENT) EngagementResult.InteractionShown(targetInteractionId) + else EngagementResult.InteractionNotShown("No runnable interactions") } ) @@ -167,7 +167,7 @@ class TextModalViewModelTest : TestCase() { val viewModel = createViewModel( onEngage = { // no interactions to invoke - EngagementResult.Failure("No runnable interactions") + EngagementResult.InteractionNotShown("No runnable interactions") } ) @@ -279,13 +279,13 @@ class TextModalViewModelTest : TestCase() { onEngage = { args -> addResult(args) onEngage?.invoke(args) - ?: EngagementResult.Failure("No runnable interactions") + ?: EngagementResult.InteractionNotShown("No runnable interactions") }, // record invocations for every engage call onInvoke = { invocations -> addResult(invocations) onInvoke?.invoke(invocations) - ?: EngagementResult.Failure("No runnable interactions") + ?: EngagementResult.InteractionNotShown("No runnable interactions") }, // we don't expect payloads here onSendPayload = { payload -> diff --git a/apptentive-ratings/src/test/java/apptentive/com/android/ratings/appstorerating/StoreNavigatorTest.kt b/apptentive-ratings/src/test/java/apptentive/com/android/ratings/appstorerating/StoreNavigatorTest.kt index 8315a39ee..2cf61f7e7 100644 --- a/apptentive-ratings/src/test/java/apptentive/com/android/ratings/appstorerating/StoreNavigatorTest.kt +++ b/apptentive-ratings/src/test/java/apptentive/com/android/ratings/appstorerating/StoreNavigatorTest.kt @@ -74,6 +74,6 @@ class StoreNavigatorTest : TestCase() { private fun createEngagementContext() = MockEngagementContext( onEngage = { addResult(it) - EngagementResult.Failure("No runnable interactions") + EngagementResult.InteractionNotShown("No runnable interactions") }) } \ No newline at end of file diff --git a/apptentive-ratings/src/test/java/apptentive/com/android/ratings/ratingdialog/RatingDialogViewModelTest.kt b/apptentive-ratings/src/test/java/apptentive/com/android/ratings/ratingdialog/RatingDialogViewModelTest.kt index c7374ba57..36c62083f 100644 --- a/apptentive-ratings/src/test/java/apptentive/com/android/ratings/ratingdialog/RatingDialogViewModelTest.kt +++ b/apptentive-ratings/src/test/java/apptentive/com/android/ratings/ratingdialog/RatingDialogViewModelTest.kt @@ -31,7 +31,7 @@ class RatingDialogViewModelTest : TestCase() { val viewModel = createViewModel( onInvoke = { // trick it to think an interaction has been invoked - EngagementResult.Success(targetInteractionId) + EngagementResult.InteractionShown(targetInteractionId) } ) @@ -88,13 +88,13 @@ class RatingDialogViewModelTest : TestCase() { onEngage = { args -> addResult(args) onEngage?.invoke(args) - ?: EngagementResult.Failure("No runnable interactions") + ?: EngagementResult.InteractionNotShown("No runnable interactions") }, // record invocations for every engage call onInvoke = { invocations -> addResult(invocations) onInvoke?.invoke(invocations) - ?: EngagementResult.Failure("No runnable interactions") + ?: EngagementResult.InteractionNotShown("No runnable interactions") }, // we don't expect payloads here onSendPayload = { payload -> diff --git a/apptentive-survey/src/test/java/apptentive/com/android/feedback/survey/interaction/SurveyInteractionLauncherTest.kt b/apptentive-survey/src/test/java/apptentive/com/android/feedback/survey/interaction/SurveyInteractionLauncherTest.kt index 98ce3f8ec..7c2345e35 100644 --- a/apptentive-survey/src/test/java/apptentive/com/android/feedback/survey/interaction/SurveyInteractionLauncherTest.kt +++ b/apptentive-survey/src/test/java/apptentive/com/android/feedback/survey/interaction/SurveyInteractionLauncherTest.kt @@ -98,7 +98,7 @@ class SurveyInteractionLauncherTest : TestCase() { ) = MockEngagementContext( onEngage = onEngage ?: { args -> addResult(args) - EngagementResult.Failure("No runnable interactions") + EngagementResult.InteractionNotShown("No runnable interactions") }, onSendPayload = onSendPayload ?: { payload -> addResult(payload.toJson()) From 53a6364d1bc2d2189a00a154c8717e867ca7c7ef Mon Sep 17 00:00:00 2001 From: ChaseApptentive <85038972+ChaseApptentive@users.noreply.github.com> Date: Wed, 8 Dec 2021 12:27:48 -0600 Subject: [PATCH 005/491] Update example app with 1 import, customStoreURL, and new Engagement names (#199) * Update example app with 1 import, customStoreURL, and new Engagement names * Update GitHub link --- apptentive-example/build.gradle | 44 ++--------------- .../com/apptentive/example/MainActivity.kt | 49 ++++++++++++------- .../com/apptentive/example/MyApplication.kt | 10 ++-- .../src/main/res/layout/activity_main.xml | 11 +++++ .../src/main/res/values/strings.xml | 1 + 5 files changed, 54 insertions(+), 61 deletions(-) diff --git a/apptentive-example/build.gradle b/apptentive-example/build.gradle index 09e94dd1b..96ef947ad 100644 --- a/apptentive-example/build.gradle +++ b/apptentive-example/build.gradle @@ -22,47 +22,9 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:2.1.1' /** - * TODO Step 1: Uncomment the code below for the interactions you'd like to demo + * TODO Step 1: Uncomment the import line below. * TODO Step 2: Replace APPTENTIVE_VERSION with released version from GitHub - * https://github.com/apptentive/android-sdk - * TODO Dev - (Replace GitHub link with public version) + * https://github.com/apptentive/apptentive-android-sdk */ -// def apptentive_version = APPTENTIVE_VERSION - - // Core Apptentive code for using any Apptentive Interaction -// implementation "com.apptentive:apptentive-core:$apptentive_version" -// implementation "com.apptentive:apptentive-core-ui:$apptentive_version" -// implementation "com.apptentive:apptentive-feedback:$apptentive_version" - - // Love Dialog Interaction - // https://learn.apptentive.com/knowledge-base/getting-started-with-apptentive/#love-dialog -// implementation "com.apptentive:apptentive-enjoyment-dialog:$apptentive_version" - - // Notes Interaction - // https://learn.apptentive.com/knowledge-base/getting-started-with-apptentive/#notes -// implementation "com.apptentive:apptentive-notes:$apptentive_version" - - // Support library for external linking and deep linking -// implementation "com.apptentive:apptentive-navigate-to-link:$apptentive_version" - - // Survey Interaction - // https://learn.apptentive.com/knowledge-base/getting-started-with-apptentive/#surveys -// implementation "com.apptentive:apptentive-survey:$apptentive_version" - - - /** - * The example app is not set up to handle Google In-App Review and Apptentive Rating Dialog. - * Included here (In Gradle) as an implementation example. - * - * Google In-App Review Interaction - Requires the app to be in the Play Store - * Apptentive Rating Dialog Interaction - Requires Android OS < 5 (API level 20 or below) - * or a non-Play Store App (TO BE IMPLEMENTED) - */ - // Google In-App Review - // https://learn.apptentive.com/knowledge-base/requesting-ratings-on-google-play/ -// implementation "com.apptentive:apptentive-in-app-review:$apptentive_version" - - // Apptentive Rating Dialog Interaction - // https://learn.apptentive.com/knowledge-base/getting-started-with-apptentive/#rating-dialog -// implementation "com.apptentive:apptentive-ratings:$apptentive_version" +// implementation "com.apptentive:apptentive-sdk-kit:APPTENTIVE_VERSION" } \ No newline at end of file diff --git a/apptentive-example/src/main/java/com/apptentive/example/MainActivity.kt b/apptentive-example/src/main/java/com/apptentive/example/MainActivity.kt index 43d5977d7..f76c8a2d8 100644 --- a/apptentive-example/src/main/java/com/apptentive/example/MainActivity.kt +++ b/apptentive-example/src/main/java/com/apptentive/example/MainActivity.kt @@ -2,7 +2,6 @@ package com.apptentive.example import android.os.Bundle import android.widget.Button -import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import com.apptentive.apptentive_example.R @@ -14,43 +13,59 @@ class MainActivity : AppCompatActivity() { val loveDialogButton = findViewById(R.id.loveDialogButton) val noteButton = findViewById(R.id.noteButton) val surveyButton = findViewById(R.id.surveyButton) + val ratingButton = findViewById(R.id.ratingButton) /** - * For each interaction you'd like to test out... + * TODO Step 5: Uncomment the code below and import + * There are buttons set up for the Love Dialog, Notes, Survey, and Rating Interactions * - * TODO Step 5: Uncomment the code below for the interactions you'd like to demo and import - * There are buttons set up for Love Dialog, Notes, and Survey + * NOTE ON THE RATING INTERACTION: + * Rating Interaction defaults to Google In-App Review. Google In-App Review is not + * demo-able unless the app is in a Internal Test Track or in Production on the Play Store. + * To demo the Apptentive Rating Dialog, set the customAppStoreURL in your ApptentiveConfiguration * * TODO Step 6: Uncomment the handleResult function and import * * TODO Step 7: Create Interactions on your Apptentive Dashboard. * Use the events below in your WHERE targeting (e.g. "love_dialog") or create - * your own event and change the corresponding included event below. + * your own event and change the corresponding event provided below. * https://learn.apptentive.com/knowledge-base/how-to-use-targeting/#where-targeting */ -// loveDialogButton.setOnClickListener { engage("love_dialog") { handleResult(it } } -// noteButton.setOnClickListener { engage("note") { handleResult(it } } -// surveyButton.setOnClickListener { engage("survey") { handleResult(it } } +// loveDialogButton.setOnClickListener { engage(this, "love_dialog") { handleResult(it) } } +// noteButton.setOnClickListener { engage(this, "note") { handleResult(it) } } +// surveyButton.setOnClickListener { engage(this, "survey") { handleResult(it) } } +// ratingButton.setOnClickListener { engage(this, "rating") { handleResult(it) } } } /** - * Every Apptentive engage call will have a callback which will have some helpful - * info which tells if an interaction succeeded or not and why + * Every Apptentive engage call will have an optional callback which will have some + * helpful info which tells if an interaction succeeded or not and why. * - * Success & Failure are typical results. They should be seen with every engage. - * Success - All criteria was met (set on backend) and the interaction shows - * Failure - Not all criteria was met and the interaction did not show + * InteractionShown & InteractionNotShown are typical results. They should be seen with every engage. + * InteractionShown - All criteria was met (set on the dashboard) and the interaction shows + * InteractionNotShown - Not all criteria was met and the interaction did not show * - * Error & Exception are unusual results. They should be investigated. + * Error & Exception are not typical results. They should be investigated. * Error - There is some discrepancy between what was expected and what was attempted * Exception - There was a breaking error * * @see apptentive.com.android.feedback.EngagementResult */ -// fun handleResult(result: EngagementResult) { -// if (result !is EngagementResult.Success) { -// Toast.makeText(this, "Not engaged: $result", Toast.LENGTH_LONG).show() +// private fun handleResult(result: EngagementResult) { +// when(result) { +// is EngagementResult.InteractionShown -> { +// Toast.makeText(this, "Interaction Shown", Toast.LENGTH_LONG).show() +// } +// is EngagementResult.InteractionNotShown -> { +// Toast.makeText(this, "Interaction Not Shown: ${result.description}", Toast.LENGTH_LONG).show() +// } +// is EngagementResult.Error -> { +// Toast.makeText(this, "Engage Error: ${result.message}", Toast.LENGTH_LONG).show() +// } +// is EngagementResult.Exception -> { +// Toast.makeText(this, "Engage Exception: ${result.error.message}", Toast.LENGTH_LONG).show() +// } // } // } } \ No newline at end of file diff --git a/apptentive-example/src/main/java/com/apptentive/example/MyApplication.kt b/apptentive-example/src/main/java/com/apptentive/example/MyApplication.kt index 07cdeb9bf..d034f8ee2 100644 --- a/apptentive-example/src/main/java/com/apptentive/example/MyApplication.kt +++ b/apptentive-example/src/main/java/com/apptentive/example/MyApplication.kt @@ -9,6 +9,9 @@ class MyApplication : Application() { * TODO Step 3: Uncomment the code below and import. * TODO Step 4: Set your Apptentive Key and Signature * Found on your Apptentive Dashboard at Settings -> API & Development -> SDK Tokens + * + * If you'd like to demo the Apptentive Rating Dialog in this example app, set the + * customAppStoreURL to a URL. Preferably to an app store URL, though any URL will work. */ // val configuration = ApptentiveConfiguration( // apptentiveKey = "YOUR_APPTENTIVE_KEY", @@ -16,9 +19,10 @@ class MyApplication : Application() { // ).apply { // /** // * Optional parameters: -// * logLevel - Default is LogLevel.Info -// * shouldSanitizeLogMessages - Default is true -// * ratingInteractionThrottleLength - Default is TimeUnit.DAYS.toMillis(7) +// * logLevel - Default is LogLevel.Info +// * shouldSanitizeLogMessages - Default is true +// * ratingInteractionThrottleLength - Default is TimeUnit.DAYS.toMillis(7) +// * customAppStoreURL - Default is null (Rating Interaction attempts to show Google In-App Review) // */ // } // diff --git a/apptentive-example/src/main/res/layout/activity_main.xml b/apptentive-example/src/main/res/layout/activity_main.xml index 739708167..f5ac7455c 100644 --- a/apptentive-example/src/main/res/layout/activity_main.xml +++ b/apptentive-example/src/main/res/layout/activity_main.xml @@ -52,4 +52,15 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/noteButton" /> + + \ No newline at end of file diff --git a/apptentive-example/src/main/res/values/strings.xml b/apptentive-example/src/main/res/values/strings.xml index 56470a381..e86bb0b20 100644 --- a/apptentive-example/src/main/res/values/strings.xml +++ b/apptentive-example/src/main/res/values/strings.xml @@ -4,4 +4,5 @@ Love Dialog Note Survey + Rating \ No newline at end of file From bbb2e3bcf6f230211c20e21d122e6ce0b6742955 Mon Sep 17 00:00:00 2001 From: ChaseApptentive <85038972+ChaseApptentive@users.noreply.github.com> Date: Wed, 8 Dec 2021 12:28:41 -0600 Subject: [PATCH 006/491] [PBI-2701] Activity Callback context possible solution (#197) * [PBI-2701] Activity Callback context possible solution [PBI-2701] * Move activity retrieval to a more central location * Make callback retrieval internal * ktlint * Rename to ApptentiveActivityInfo --- .../com/android/feedback/test/MainActivity.kt | 15 +++++++++----- .../apptentive/com/app/test/MainActivity.kt | 10 +++++++++- .../java/apptentive/com/app/MainActivity.kt | 20 +++++++++++++------ .../com/apptentive/example/MainActivity.kt | 1 + .../com/android/feedback/Apptentive.kt | 15 ++++++++++---- .../feedback/ApptentiveActivityInfo.kt | 7 +++++++ .../com/android/feedback/ApptentiveClient.kt | 5 ++--- .../feedback/ApptentiveDefaultClient.kt | 15 ++++++++------ .../lifecycle/ApptentiveLifecycleObserver.kt | 7 +++---- .../platform/AndroidEngagementContext.kt | 4 +++- 10 files changed, 69 insertions(+), 30 deletions(-) create mode 100644 apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveActivityInfo.kt diff --git a/app-legacy-migration-test/src/current/java/apptentive/com/android/feedback/test/MainActivity.kt b/app-legacy-migration-test/src/current/java/apptentive/com/android/feedback/test/MainActivity.kt index e9990717a..237240f92 100644 --- a/app-legacy-migration-test/src/current/java/apptentive/com/android/feedback/test/MainActivity.kt +++ b/app-legacy-migration-test/src/current/java/apptentive/com/android/feedback/test/MainActivity.kt @@ -1,21 +1,22 @@ package apptentive.com.android.feedback.test +import android.app.Activity import android.os.Bundle import android.view.View import android.widget.Toast import androidx.appcompat.app.AppCompatActivity +import apptentive.com.android.feedback.Apptentive import apptentive.com.android.feedback.Apptentive.engage +import apptentive.com.android.feedback.ApptentiveActivityInfo -class MainActivity : AppCompatActivity() { +class MainActivity : AppCompatActivity(), ApptentiveActivityInfo { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + Apptentive.registerApptentiveActivityInfoCallback(this) findViewById(R.id.login).setOnClickListener { notImplemented("Login not supported yet") } findViewById(R.id.engage).setOnClickListener { - engage( - this@MainActivity, - "love_dialog_test" - ) + engage("love_dialog_test") } findViewById(R.id.message_center).setOnClickListener { notImplemented("Message center supported yet") } } @@ -23,4 +24,8 @@ class MainActivity : AppCompatActivity() { private fun notImplemented(message: String) { Toast.makeText(this@MainActivity, message, Toast.LENGTH_LONG).show() } + + override fun getApptentiveActivityInfo(): Activity { + return this + } } \ No newline at end of file diff --git a/app-ui-test/src/main/java/apptentive/com/app/test/MainActivity.kt b/app-ui-test/src/main/java/apptentive/com/app/test/MainActivity.kt index 6c840defc..6d6056fb5 100644 --- a/app-ui-test/src/main/java/apptentive/com/app/test/MainActivity.kt +++ b/app-ui-test/src/main/java/apptentive/com/app/test/MainActivity.kt @@ -1,11 +1,14 @@ package apptentive.com.app.test +import android.app.Activity import android.os.Bundle import android.view.ViewGroup import android.widget.Button import androidx.appcompat.app.AppCompatActivity import apptentive.com.android.concurrent.Executor import apptentive.com.android.concurrent.Executors +import apptentive.com.android.feedback.Apptentive +import apptentive.com.android.feedback.ApptentiveActivityInfo import apptentive.com.android.feedback.EngagementResult import apptentive.com.android.feedback.InteractionModuleComponent import apptentive.com.android.feedback.engagement.Engagement @@ -21,10 +24,11 @@ import apptentive.com.android.feedback.payload.PayloadSender import apptentive.com.android.feedback.platform.AndroidEngagementContext import apptentive.com.android.serialization.json.JsonConverter -class MainActivity : AppCompatActivity() { +class MainActivity : AppCompatActivity(), ApptentiveActivityInfo { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + Apptentive.registerApptentiveActivityInfoCallback(this) val path = intent.getStringExtra(EXTRA_INTERACTIONS_PATH) ?: "interactions/notes" if (path != null) { @@ -102,4 +106,8 @@ class MainActivity : AppCompatActivity() { task() } } + + override fun getApptentiveActivityInfo(): Activity { + return this + } } \ No newline at end of file diff --git a/app/src/main/java/apptentive/com/app/MainActivity.kt b/app/src/main/java/apptentive/com/app/MainActivity.kt index ba28d5362..3ad9521b0 100644 --- a/app/src/main/java/apptentive/com/app/MainActivity.kt +++ b/app/src/main/java/apptentive/com/app/MainActivity.kt @@ -1,5 +1,6 @@ package apptentive.com.app +import android.app.Activity import android.app.ActivityManager import android.content.Context import android.content.Intent @@ -10,13 +11,16 @@ import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES import apptentive.com.android.feedback.Apptentive +import apptentive.com.android.feedback.ApptentiveActivityInfo import apptentive.com.android.feedback.EngagementResult import apptentive.com.app.databinding.ActivityMainBinding -class MainActivity : AppCompatActivity() { +class MainActivity : AppCompatActivity(), ApptentiveActivityInfo { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + Apptentive.registerApptentiveActivityInfoCallback(this) + val prefs = getSharedPreferences(SHARED_PREFS, MODE_PRIVATE) val isNightMode = prefs.getBoolean(EXTRA_NIGHT_MODE, false) @@ -51,7 +55,7 @@ class MainActivity : AppCompatActivity() { binding.engageEventButton.setOnClickListener { val engageEvent = binding.eventTextEditText.text?.toString()?.trim() if (!engageEvent.isNullOrEmpty()) { - Apptentive.engage(this, engageEvent) { handleResult(it) } + Apptentive.engage(engageEvent) { handleResult(it) } binding.eventTextLayout.isErrorEnabled = false binding.eventTextLayout.error = "" binding.eventTextEditText.setText("") @@ -62,19 +66,19 @@ class MainActivity : AppCompatActivity() { } binding.loveDialogButton.setOnClickListener { - Apptentive.engage(this, "love_dialog_event") { handleResult(it) } + Apptentive.engage("love_dialog_event") { handleResult(it) } } binding.surveyButton.setOnClickListener { - Apptentive.engage(this, "survey_event") { handleResult(it) } + Apptentive.engage("survey_event") { handleResult(it) } } binding.noteButton.setOnClickListener { - Apptentive.engage(this, "note_event") { handleResult(it) } + Apptentive.engage("note_event") { handleResult(it) } } binding.ratingDialogButton.setOnClickListener { - Apptentive.engage(this, "rating_dialog_event") { handleResult(it) } + Apptentive.engage("rating_dialog_event") { handleResult(it) } } binding.clearAppDataButton.setOnClickListener { @@ -87,4 +91,8 @@ class MainActivity : AppCompatActivity() { Toast.makeText(this, "Not engaged: $it", Toast.LENGTH_LONG).show() } } + + override fun getApptentiveActivityInfo(): Activity { + return this + } } diff --git a/apptentive-example/src/main/java/com/apptentive/example/MainActivity.kt b/apptentive-example/src/main/java/com/apptentive/example/MainActivity.kt index f76c8a2d8..99a386db9 100644 --- a/apptentive-example/src/main/java/com/apptentive/example/MainActivity.kt +++ b/apptentive-example/src/main/java/com/apptentive/example/MainActivity.kt @@ -5,6 +5,7 @@ import android.widget.Button import androidx.appcompat.app.AppCompatActivity import com.apptentive.apptentive_example.R +// TODO Update Example app to use ApptentiveActivity interface class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/Apptentive.kt b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/Apptentive.kt index f9b9ce8fa..c6b754a20 100644 --- a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/Apptentive.kt +++ b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/Apptentive.kt @@ -46,11 +46,18 @@ sealed class EngagementResult { object Apptentive { private var client: ApptentiveClient = ApptentiveClient.NULL + private var activityInfoCallback: ApptentiveActivityInfo? = null private lateinit var stateExecutor: Executor private lateinit var mainExecutor: Executor //region Initialization + fun registerApptentiveActivityInfoCallback(apptentiveActivityInfo: ApptentiveActivityInfo) { + this.activityInfoCallback = apptentiveActivityInfo + } + + internal fun getApptentiveActivityCallback() = activityInfoCallback + @Suppress("MemberVisibilityCanBePrivate") val registered @Synchronized get() = client != ApptentiveClient.NULL @@ -176,15 +183,15 @@ object Apptentive { //endregion @JvmStatic @JvmOverloads - fun engage(context: Context, eventName: String, callback: EngagementCallback? = null) { + fun engage(eventName: String, callback: EngagementCallback? = null) { // the above statement would not compile without force unwrapping on Kotlin 1.4.x @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") val callbackFunc: ((EngagementResult) -> Unit)? = if (callback != null) callback!!::onComplete else null - engage(context, eventName, callbackFunc) + engage(eventName, callbackFunc) } - fun engage(context: Context, eventName: String, callback: ((EngagementResult) -> Unit)?) { + private fun engage(eventName: String, callback: ((EngagementResult) -> Unit)?) { // user callback should be executed on the main thread val callbackWrapper: ((EngagementResult) -> Unit)? = if (callback != null) { { @@ -198,7 +205,7 @@ object Apptentive { stateExecutor.execute { try { val event = Event.local(eventName) - val result = client.engage(context, event) + val result = client.engage(event) callbackWrapper?.invoke(result) } catch (e: Exception) { callbackWrapper?.invoke(EngagementResult.Exception(error = e)) diff --git a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveActivityInfo.kt b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveActivityInfo.kt new file mode 100644 index 000000000..8d6d23c85 --- /dev/null +++ b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveActivityInfo.kt @@ -0,0 +1,7 @@ +package apptentive.com.android.feedback + +import android.app.Activity + +interface ApptentiveActivityInfo { + fun getApptentiveActivityInfo(): Activity +} diff --git a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveClient.kt b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveClient.kt index 3d8edafd2..6e42975f7 100644 --- a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveClient.kt +++ b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveClient.kt @@ -1,11 +1,10 @@ package apptentive.com.android.feedback -import android.content.Context import apptentive.com.android.feedback.engagement.Event import apptentive.com.android.util.Log internal interface ApptentiveClient { - fun engage(context: Context, event: Event): EngagementResult + fun engage(event: Event): EngagementResult fun updateDevice(customData: Pair? = null, deleteKey: String? = null) fun updatePerson(name: String? = null, email: String? = null, customData: Pair? = null, deleteKey: String? = null) @@ -15,7 +14,7 @@ internal interface ApptentiveClient { } private class ApptentiveNullClient : ApptentiveClient { - override fun engage(context: Context, event: Event): EngagementResult { + override fun engage(event: Event): EngagementResult { return EngagementResult.Error("Apptentive SDK is not initialized") } diff --git a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveDefaultClient.kt b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveDefaultClient.kt index 89aa03bff..605e73059 100644 --- a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveDefaultClient.kt +++ b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveDefaultClient.kt @@ -164,10 +164,7 @@ internal class ApptentiveDefaultClient( executors.main.execute { Log.i(LIFE_CYCLE_OBSERVER, "Observing App lifecycle") ProcessLifecycleOwner.get().lifecycle.addObserver( - ApptentiveLifecycleObserver( - this, - context - ) { conversationManager.tryFetchEngagementManifest() } + ApptentiveLifecycleObserver(this) { conversationManager.tryFetchEngagementManifest() } ) } } @@ -233,8 +230,14 @@ internal class ApptentiveDefaultClient( //region Engagement - override fun engage(context: Context, event: Event): EngagementResult { - return AndroidEngagementContext(context, engagement, payloadSender, executors).engage(event) + override fun engage(event: Event): EngagementResult { + val activity = requireNotNull(Apptentive.getApptentiveActivityCallback()) { + "Apptentive Activity Callback not registered. " + + "Extend ApptentiveActivity.kt, implement the getActivity() function, " + + "and call registerApptentiveActivityCallback(this) " + + "in your Activity's onCreate function." + }.getApptentiveActivityInfo() + return AndroidEngagementContext(activity, engagement, payloadSender, executors).engage(event) } override fun updatePerson( diff --git a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/lifecycle/ApptentiveLifecycleObserver.kt b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/lifecycle/ApptentiveLifecycleObserver.kt index 731169389..ec1384346 100644 --- a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/lifecycle/ApptentiveLifecycleObserver.kt +++ b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/lifecycle/ApptentiveLifecycleObserver.kt @@ -1,6 +1,5 @@ package apptentive.com.android.feedback.lifecycle -import android.content.Context import androidx.annotation.WorkerThread import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver @@ -11,19 +10,19 @@ import apptentive.com.android.feedback.engagement.Event import apptentive.com.android.feedback.engagement.InternalEvent import apptentive.com.android.util.Log -internal class ApptentiveLifecycleObserver(val client: ApptentiveClient, val context: Context, val refreshManifest: () -> Unit) : LifecycleObserver { +internal class ApptentiveLifecycleObserver(val client: ApptentiveClient, val refreshManifest: () -> Unit) : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_STOP) @WorkerThread fun onBackground() { Log.d(LIFE_CYCLE_OBSERVER, "App is in background") - client.engage(context, Event.internal(InternalEvent.APP_EXIT.labelName)) + client.engage(Event.internal(InternalEvent.APP_EXIT.labelName)) } @OnLifecycleEvent(Lifecycle.Event.ON_START) @WorkerThread fun onForeground() { Log.d(LIFE_CYCLE_OBSERVER, "App is in foreground") - client.engage(context, Event.internal(InternalEvent.APP_LAUNCH.labelName)) + client.engage(Event.internal(InternalEvent.APP_LAUNCH.labelName)) refreshManifest() } } diff --git a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/platform/AndroidEngagementContext.kt b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/platform/AndroidEngagementContext.kt index bc0b438f6..c2d4b44cd 100644 --- a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/platform/AndroidEngagementContext.kt +++ b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/platform/AndroidEngagementContext.kt @@ -8,6 +8,7 @@ import androidx.appcompat.view.ContextThemeWrapper import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentManager import apptentive.com.android.concurrent.Executors +import apptentive.com.android.feedback.Apptentive import apptentive.com.android.feedback.engagement.Engagement import apptentive.com.android.feedback.engagement.EngagementContext import apptentive.com.android.feedback.payload.PayloadSender @@ -29,7 +30,7 @@ class AndroidEngagementContext( fun tryStartActivity(intent: Intent) = androidContext.tryStartActivity(intent) - fun getFragmentManager(context: Context = androidContext): FragmentManager { + fun getFragmentManager(context: Context? = Apptentive.getApptentiveActivityCallback()?.getApptentiveActivityInfo()): FragmentManager { return when (context) { is AppCompatActivity, is FragmentActivity -> (context as FragmentActivity).supportFragmentManager is ContextThemeWrapper -> getFragmentManager(context.baseContext) @@ -38,6 +39,7 @@ class AndroidEngagementContext( "Must use an Activity context. " + "Application context does not have a fragment manager." ) + null -> throw Exception("Context is null") else -> throw Exception("Can't retrieve fragment manager. Unknown context type ${context.packageName}") } } From 1eff552550e993789d31a93d85c04a87d7ec5ced Mon Sep 17 00:00:00 2001 From: ChaseApptentive <85038972+ChaseApptentive@users.noreply.github.com> Date: Fri, 10 Dec 2021 11:14:13 -0600 Subject: [PATCH 007/491] PBI-2708 Update example app with ApptentiveActivityInfo (#200) * PBI-2708 Update example app with ApptentiveActivityInfo [PBI-2708] Reworded some parts. Also added a missing step in the AndroidManifest. * Replace key & sig --- apptentive-example/build.gradle | 6 +- .../src/main/AndroidManifest.xml | 3 + .../com/apptentive/example/MainActivity.kt | 71 ++++++++++++------- .../com/apptentive/example/MyApplication.kt | 34 ++++----- build.gradle | 2 +- 5 files changed, 71 insertions(+), 45 deletions(-) diff --git a/apptentive-example/build.gradle b/apptentive-example/build.gradle index 96ef947ad..29427a5b1 100644 --- a/apptentive-example/build.gradle +++ b/apptentive-example/build.gradle @@ -17,13 +17,13 @@ android { dependencies { implementation 'androidx.core:core-ktx:1.7.0' - implementation 'androidx.appcompat:appcompat:1.3.1' + implementation 'androidx.appcompat:appcompat:1.4.0' implementation 'com.google.android.material:material:1.4.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.1' + implementation 'androidx.constraintlayout:constraintlayout:2.1.2' /** * TODO Step 1: Uncomment the import line below. - * TODO Step 2: Replace APPTENTIVE_VERSION with released version from GitHub + * TODO Step 2: Replace APPTENTIVE_VERSION with latest released version on GitHub * https://github.com/apptentive/apptentive-android-sdk */ // implementation "com.apptentive:apptentive-sdk-kit:APPTENTIVE_VERSION" diff --git a/apptentive-example/src/main/AndroidManifest.xml b/apptentive-example/src/main/AndroidManifest.xml index 8ef670c73..30b7b839c 100644 --- a/apptentive-example/src/main/AndroidManifest.xml +++ b/apptentive-example/src/main/AndroidManifest.xml @@ -2,6 +2,9 @@ + (R.id.loveDialogButton) val noteButton = findViewById(R.id.noteButton) val surveyButton = findViewById(R.id.surveyButton) val ratingButton = findViewById(R.id.ratingButton) /** - * TODO Step 5: Uncomment the code below and import + * TODO Step 9: Uncomment the on click listeners below and import * There are buttons set up for the Love Dialog, Notes, Survey, and Rating Interactions * * NOTE ON THE RATING INTERACTION: @@ -25,21 +38,27 @@ class MainActivity : AppCompatActivity() { * demo-able unless the app is in a Internal Test Track or in Production on the Play Store. * To demo the Apptentive Rating Dialog, set the customAppStoreURL in your ApptentiveConfiguration * - * TODO Step 6: Uncomment the handleResult function and import - * - * TODO Step 7: Create Interactions on your Apptentive Dashboard. + * TODO Step 11: Create Interactions on your Apptentive Dashboard. * Use the events below in your WHERE targeting (e.g. "love_dialog") or create * your own event and change the corresponding event provided below. * https://learn.apptentive.com/knowledge-base/how-to-use-targeting/#where-targeting */ +// loveDialogButton.setOnClickListener { Apptentive.engage("love_dialog") { handleResult(it) } } +// noteButton.setOnClickListener { Apptentive.engage("note") { handleResult(it) } } +// surveyButton.setOnClickListener { Apptentive.engage("survey") { handleResult(it) } } +// ratingButton.setOnClickListener { Apptentive.engage("rating") { handleResult(it) } } + } -// loveDialogButton.setOnClickListener { engage(this, "love_dialog") { handleResult(it) } } -// noteButton.setOnClickListener { engage(this, "note") { handleResult(it) } } -// surveyButton.setOnClickListener { engage(this, "survey") { handleResult(it) } } -// ratingButton.setOnClickListener { engage(this, "rating") { handleResult(it) } } + // TODO Step 7: Uncomment the overridden function for the ApptentiveActivityInfo interface. +/* + override fun getApptentiveActivityInfo(): Activity { + return this } +*/ /** + * TODO Step 10: Uncomment the helper function below and import + * * Every Apptentive engage call will have an optional callback which will have some * helpful info which tells if an interaction succeeded or not and why. * @@ -53,20 +72,22 @@ class MainActivity : AppCompatActivity() { * * @see apptentive.com.android.feedback.EngagementResult */ -// private fun handleResult(result: EngagementResult) { -// when(result) { -// is EngagementResult.InteractionShown -> { -// Toast.makeText(this, "Interaction Shown", Toast.LENGTH_LONG).show() -// } -// is EngagementResult.InteractionNotShown -> { -// Toast.makeText(this, "Interaction Not Shown: ${result.description}", Toast.LENGTH_LONG).show() -// } -// is EngagementResult.Error -> { -// Toast.makeText(this, "Engage Error: ${result.message}", Toast.LENGTH_LONG).show() -// } -// is EngagementResult.Exception -> { -// Toast.makeText(this, "Engage Exception: ${result.error.message}", Toast.LENGTH_LONG).show() -// } -// } -// } +/* + private fun handleResult(result: EngagementResult) { + when(result) { + is EngagementResult.InteractionShown -> { + Toast.makeText(this, "Interaction Shown", Toast.LENGTH_LONG).show() + } + is EngagementResult.InteractionNotShown -> { + Toast.makeText(this, "Interaction Not Shown: ${result.description}", Toast.LENGTH_LONG).show() + } + is EngagementResult.Error -> { + Toast.makeText(this, "Engage Error: ${result.message}", Toast.LENGTH_LONG).show() + } + is EngagementResult.Exception -> { + Toast.makeText(this, "Engage Exception: ${result.error.message}", Toast.LENGTH_LONG).show() + } + } + } +*/ } \ No newline at end of file diff --git a/apptentive-example/src/main/java/com/apptentive/example/MyApplication.kt b/apptentive-example/src/main/java/com/apptentive/example/MyApplication.kt index d034f8ee2..1908b9908 100644 --- a/apptentive-example/src/main/java/com/apptentive/example/MyApplication.kt +++ b/apptentive-example/src/main/java/com/apptentive/example/MyApplication.kt @@ -6,26 +6,28 @@ class MyApplication : Application() { override fun onCreate() { super.onCreate() /** - * TODO Step 3: Uncomment the code below and import. - * TODO Step 4: Set your Apptentive Key and Signature + * TODO Step 4: Uncomment the ApptentiveConfiguration code below and import. + * TODO Step 5: Set your Apptentive Key and Signature * Found on your Apptentive Dashboard at Settings -> API & Development -> SDK Tokens * * If you'd like to demo the Apptentive Rating Dialog in this example app, set the * customAppStoreURL to a URL. Preferably to an app store URL, though any URL will work. */ -// val configuration = ApptentiveConfiguration( -// apptentiveKey = "YOUR_APPTENTIVE_KEY", -// apptentiveSignature = "YOUR_APPTENTIVE_SIGNATURE" -// ).apply { -// /** -// * Optional parameters: -// * logLevel - Default is LogLevel.Info -// * shouldSanitizeLogMessages - Default is true -// * ratingInteractionThrottleLength - Default is TimeUnit.DAYS.toMillis(7) -// * customAppStoreURL - Default is null (Rating Interaction attempts to show Google In-App Review) -// */ -// } -// -// Apptentive.register(this, configuration) +/* + val configuration = ApptentiveConfiguration( + apptentiveKey = "YOUR_APPTENTIVE_KEY", + apptentiveSignature = "YOUR_APPTENTIVE_SIGNATURE" + ).apply { + /** + * Optional parameters: + * logLevel - Default is LogLevel.Info + * shouldSanitizeLogMessages - Default is true + * ratingInteractionThrottleLength - Default is TimeUnit.DAYS.toMillis(7) + * customAppStoreURL - Default is null (Rating Interaction attempts to show Google In-App Review) + */ + } + + Apptentive.register(this, configuration) +*/ } } \ No newline at end of file diff --git a/build.gradle b/build.gradle index 4fef2e78a..d4c75568f 100644 --- a/build.gradle +++ b/build.gradle @@ -64,7 +64,7 @@ buildscript { } project.ext { - versionId = '1.0.17' + versionId = '1.0.18' } repositories { From 21f1565fddc4e43578356d7c810c39e1cf81b7b6 Mon Sep 17 00:00:00 2001 From: ChaseApptentive <85038972+ChaseApptentive@users.noreply.github.com> Date: Wed, 5 Jan 2022 09:31:23 -0600 Subject: [PATCH 008/491] PBI-2733 Numbers parsed correctly (#202) * PBI-2733 Numbers parsed correctly [PBI-2733] * I'm unsure if this is the best route to go... but considering you can target with any type of number, we should support evaluating both whole numbers and decimal numbers. * Make custom number send up correct type. * Add better solution. * Convert number value types before they're saved. * Use a better whole number comparison * Remove unnecessary type * Change to use double or Long only for custom data Number --- app/src/main/java/apptentive/com/app/DataActivity.kt | 2 +- .../main/java/apptentive/com/android/feedback/Apptentive.kt | 6 ++++-- .../com/android/feedback/engagement/criteria/Field.kt | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/apptentive/com/app/DataActivity.kt b/app/src/main/java/apptentive/com/app/DataActivity.kt index 256207108..4f1b7aed4 100644 --- a/app/src/main/java/apptentive/com/app/DataActivity.kt +++ b/app/src/main/java/apptentive/com/app/DataActivity.kt @@ -107,7 +107,7 @@ class DataActivity : AppCompatActivity() { addCustomNumberButton.setOnClickListener { val dataType = dataTypeDropdown.text?.toString() val customNumberKey = addCustomNumberKeyEditText.text?.toString()?.trim() - val customNumberValue = addCustomNumberValueEditText.text?.toString()?.trim()?.toFloatOrNull() + val customNumberValue = addCustomNumberValueEditText.text?.toString()?.trim()?.toDoubleOrNull() val hasError = checkHasError( dataTypeLayout, diff --git a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/Apptentive.kt b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/Apptentive.kt index c6b754a20..10350e4e0 100644 --- a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/Apptentive.kt +++ b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/Apptentive.kt @@ -288,7 +288,8 @@ object Apptentive { fun addCustomPersonData(key: String?, value: Number?) { stateExecutor.execute { if (key != null && value != null) { - client.updatePerson(customData = Pair(key, value)) + val supportedTypeValue = if (value as Double % 1.0 == 0.0) value.toLong() else value + client.updatePerson(customData = Pair(key, supportedTypeValue)) } } } @@ -353,7 +354,8 @@ object Apptentive { fun addCustomDeviceData(key: String?, value: Number?) { stateExecutor.execute { if (key != null && value != null) { - client.updateDevice(customData = Pair(key, value)) + val supportedTypeValue = if (value as Double % 1.0 == 0.0) value.toLong() else value + client.updateDevice(customData = Pair(key, supportedTypeValue)) } } } diff --git a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/criteria/Field.kt b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/criteria/Field.kt index 128f0e8a3..4b2a227a4 100644 --- a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/criteria/Field.kt +++ b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/criteria/Field.kt @@ -351,7 +351,7 @@ internal fun Field.convertValue(value: Any?): Any? { return when (type) { Field.Type.String -> converted as String - Field.Type.Number -> converted as Long + Field.Type.Number -> converted as Number Field.Type.Boolean -> converted as Boolean Field.Type.DateTime -> { return when (converted) { @@ -390,7 +390,7 @@ private fun convertComplexValue(value: Any?): Any? { } throw IllegalArgumentException("Unexpected value: $value") } - is Double -> value.toLong() // every number in json parsed as double + is Double -> if (value % 1.0 == 0.0) value.toLong() else value // every number in json parsed as double else -> value // return as-is } } From 2cb9cea3224a2c8c9788e68b3a3c58cc9a07bca0 Mon Sep 17 00:00:00 2001 From: ChaseApptentive <85038972+ChaseApptentive@users.noreply.github.com> Date: Wed, 19 Jan 2022 10:20:44 -0600 Subject: [PATCH 009/491] PBI-2524 Update dependencies and gradle (#204) [PBI-2524] * I added .bundle to the gitignore because of some gem file location was added to it which I think is part of the sdk builder processes. * I'll wait for approval before I publish to jfrog --- .gitignore | 1 + build.gradle | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 09b993d06..0e93a1d7d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ /build /captures .externalNativeBuild +.bundle diff --git a/build.gradle b/build.gradle index d4c75568f..df6eb039b 100644 --- a/build.gradle +++ b/build.gradle @@ -16,11 +16,11 @@ buildscript { // https://developer.android.com/jetpack/androidx/explorer core_ktx_version = '1.7.0' activity_ktx_version = '1.4.0' - livedata_ktx_version = '2.3.1' - appcompat_library_version = '1.3.1' - annotations_version = '1.2.0' - lifecycle_version = '2.3.1' - constraint_layout_version = '2.1.1' + livedata_ktx_version = '2.4.0' + appcompat_library_version = '1.4.0' + annotations_version = '1.3.0' + lifecycle_version = '2.4.0' + constraint_layout_version = '2.1.2' // Material Design // https://github.com/material-components/material-components-android/releases @@ -29,8 +29,8 @@ buildscript { // 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' + play_core_version = '1.10.3' + play_services_base_version = '18.0.1' // Gson // https://github.com/google/gson @@ -64,7 +64,7 @@ buildscript { } project.ext { - versionId = '1.0.18' + versionId = '1.0.19' } repositories { @@ -75,12 +75,12 @@ buildscript { dependencies { // Android Gradle // https://developer.android.com/studio/releases/gradle-plugin - classpath "com.android.tools.build:gradle:7.0.3" + classpath "com.android.tools.build:gradle:7.0.4" // Kotlin Gradle // https://kotlinlang.org/docs/gradle.html // https://kotlinlang.org/docs/releases.html#release-details - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10" // JFrog Gradle // https://plugins.gradle.org/plugin/com.jfrog.artifactory @@ -96,7 +96,7 @@ plugins { // ktlint Gradle // https://github.com/JLLeitschuh/ktlint-gradle - id "org.jlleitschuh.gradle.ktlint" version "10.2.0" + id "org.jlleitschuh.gradle.ktlint" version "10.2.1" } allprojects { From e4e348dc887a43be19fdc9d4ee54ac2ade972718 Mon Sep 17 00:00:00 2001 From: ChaseApptentive <85038972+ChaseApptentive@users.noreply.github.com> Date: Tue, 25 Jan 2022 09:53:29 -0600 Subject: [PATCH 010/491] update codeowners (#206) --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 08a355420..f3310d46d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,3 @@ # https://help.github.com/en/articles/about-code-owners -* @twinklesharma1311 @PoornimaApptentive @ChaseApptentive +* @PoornimaApptentive @ChaseApptentive From fb272da8579cb5298066347d537d390d8d3edd34 Mon Sep 17 00:00:00 2001 From: PoornimaApptentive <85186738+PoornimaApptentive@users.noreply.github.com> Date: Tue, 25 Jan 2022 12:42:36 -0800 Subject: [PATCH 011/491] PBI-2013 Survey OOM issue (#205) [PBI-2013] - Survey OOM issue --- .../com/android/ui/ApptentiveViewModel.kt | 15 --- .../android/ui/ApptentiveViewModelActivity.kt | 23 +--- .../ui/InteractionViewModelFactoryProvider.kt | 36 +------ .../feedback/ApptentiveDefaultClient.kt | 4 + .../engagement/EngagementContextFactory.kt | 28 +++++ apptentive-survey/build.gradle | 1 + .../feedback/survey/BaseSurveyActivity.kt | 25 +++++ .../android/feedback/survey/SurveyActivity.kt | 4 +- .../feedback/survey/SurveyModelFactory.kt | 45 ++++++++ .../interaction/SurveyInteractionLauncher.kt | 102 +----------------- .../feedback/survey/model/SurveyModel.kt | 2 +- .../feedback/survey/utils/ViewModelUtils.kt | 61 +++++++++++ .../survey/viewmodel/SurveyViewModel.kt | 48 ++++++--- .../SurveyInteractionLauncherTest.kt | 7 +- 14 files changed, 211 insertions(+), 190 deletions(-) delete mode 100644 apptentive-core-ui/src/main/java/apptentive/com/android/ui/ApptentiveViewModel.kt create mode 100644 apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/EngagementContextFactory.kt create mode 100644 apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/BaseSurveyActivity.kt create mode 100644 apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/SurveyModelFactory.kt create mode 100644 apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/utils/ViewModelUtils.kt diff --git a/apptentive-core-ui/src/main/java/apptentive/com/android/ui/ApptentiveViewModel.kt b/apptentive-core-ui/src/main/java/apptentive/com/android/ui/ApptentiveViewModel.kt deleted file mode 100644 index 1dad88186..000000000 --- a/apptentive-core-ui/src/main/java/apptentive/com/android/ui/ApptentiveViewModel.kt +++ /dev/null @@ -1,15 +0,0 @@ -package apptentive.com.android.ui - -import androidx.annotation.CallSuper -import androidx.lifecycle.ViewModel -import apptentive.com.android.core.Callback - -open class ApptentiveViewModel : ViewModel() { - internal var clearCallback: Callback? = null - - @CallSuper - override fun onCleared() { - clearCallback?.invoke() - clearCallback = null - } -} \ No newline at end of file diff --git a/apptentive-core-ui/src/main/java/apptentive/com/android/ui/ApptentiveViewModelActivity.kt b/apptentive-core-ui/src/main/java/apptentive/com/android/ui/ApptentiveViewModelActivity.kt index b2bdaa35e..e7a7bf4d0 100644 --- a/apptentive-core-ui/src/main/java/apptentive/com/android/ui/ApptentiveViewModelActivity.kt +++ b/apptentive-core-ui/src/main/java/apptentive/com/android/ui/ApptentiveViewModelActivity.kt @@ -4,26 +4,11 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.os.Bundle -import androidx.activity.viewModels import androidx.annotation.CallSuper import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_UNSPECIFIED -open class ApptentiveViewModelActivity : ApptentiveActivity() { - // instance id must be passed with the intent when starting the activity and will be used to - // resolve an appropriate ViewModel factory - private val _viewModel: ApptentiveViewModel by viewModels { - val instanceId = - intent.getStringExtra(EXTRA_VIEW_MODEL_INSTANCE_ID) ?: throw IllegalArgumentException( - "Missing '$EXTRA_VIEW_MODEL_INSTANCE_ID' extra" - ) - InteractionViewModelFactoryProvider.getViewModelFactory(instanceId) - } - - @Suppress("UNCHECKED_CAST") - protected val viewModel: VM - get() = _viewModel as VM - +open class ApptentiveViewModelActivity : ApptentiveActivity() { @CallSuper override fun onCreate(savedInstanceState: Bundle?) { // set the local night mode to the same value as the parent context has @@ -33,19 +18,15 @@ open class ApptentiveViewModelActivity : ApptentiveAct } companion object { - const val EXTRA_VIEW_MODEL_INSTANCE_ID = - "apptentive.intent.extra.EXTRA_VIEW_MODEL_INSTANCE_ID" const val EXTRA_LOCAL_DARK_MODE = "apptentive.intent.extra.EXTRA_LOCAL_DARK_MODE" } } -inline fun > Context.startViewModelActivity( - instanceId: String, +inline fun Context.startViewModelActivity( extras: Bundle? = null ) { val intent = Intent(this, T::class.java) - intent.putExtra(ApptentiveViewModelActivity.EXTRA_VIEW_MODEL_INSTANCE_ID, instanceId) if (extras != null) { intent.putExtras(extras) } diff --git a/apptentive-core-ui/src/main/java/apptentive/com/android/ui/InteractionViewModelFactoryProvider.kt b/apptentive-core-ui/src/main/java/apptentive/com/android/ui/InteractionViewModelFactoryProvider.kt index 1c2a88183..94833c0f3 100644 --- a/apptentive-core-ui/src/main/java/apptentive/com/android/ui/InteractionViewModelFactoryProvider.kt +++ b/apptentive-core-ui/src/main/java/apptentive/com/android/ui/InteractionViewModelFactoryProvider.kt @@ -4,43 +4,11 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import apptentive.com.android.core.Callback -object InteractionViewModelFactoryProvider { - private val lookup = mutableMapOf() - - @Synchronized - fun registerViewModelFactory(instanceId: String, factory: () -> ApptentiveViewModel) { - val existingFactory = lookup[instanceId] - if (existingFactory != null) { - - } - lookup[instanceId] = ViewModelFactory(factory) { - unregisterViewModelFactory(instanceId) - } - } - - @Synchronized - private fun unregisterViewModelFactory(instanceId: String) { - val factory = lookup.remove(instanceId) - if (factory == null) { - - } - } - - @Synchronized - fun getViewModelFactory(instanceId: String): ViewModelProvider.Factory { - return lookup[instanceId] - ?: throw IllegalArgumentException("View model factory not found: $instanceId") - } -} - -private class ViewModelFactory( - private val factory: () -> ApptentiveViewModel, - private val clearCallback: Callback +class ViewModelFactory( + private val factory: () -> ViewModel, ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { val viewModel = factory() - viewModel.clearCallback = clearCallback - @Suppress("UNCHECKED_CAST") return viewModel as T } diff --git a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveDefaultClient.kt b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveDefaultClient.kt index 605e73059..53b695f8e 100644 --- a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveDefaultClient.kt +++ b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveDefaultClient.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.annotation.WorkerThread import androidx.lifecycle.ProcessLifecycleOwner import apptentive.com.android.concurrent.Executors +import apptentive.com.android.core.DependencyProvider import apptentive.com.android.core.Provider import apptentive.com.android.feedback.backend.ConversationService import apptentive.com.android.feedback.backend.DefaultConversationService @@ -15,6 +16,7 @@ import apptentive.com.android.feedback.conversation.DefaultConversationSerialize import apptentive.com.android.feedback.engagement.DefaultEngagement import apptentive.com.android.feedback.engagement.DefaultInteractionEngagement import apptentive.com.android.feedback.engagement.Engagement +import apptentive.com.android.feedback.engagement.EngagementContextFactoryProvider import apptentive.com.android.feedback.engagement.Event import apptentive.com.android.feedback.engagement.InteractionDataProvider import apptentive.com.android.feedback.engagement.InteractionEngagement @@ -122,6 +124,8 @@ internal class ApptentiveDefaultClient( recordInteraction = ::recordInteraction ) + DependencyProvider.register(EngagementContextFactoryProvider(engagement, payloadSender, executors)) + // once we have received conversationId and conversationToken we can setup payload sender service val conversationId = conversation.conversationId val conversationToken = conversation.conversationToken diff --git a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/EngagementContextFactory.kt b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/EngagementContextFactory.kt new file mode 100644 index 000000000..b85e86d3b --- /dev/null +++ b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/EngagementContextFactory.kt @@ -0,0 +1,28 @@ +package apptentive.com.android.feedback.engagement + +import apptentive.com.android.concurrent.Executors +import apptentive.com.android.core.Provider +import apptentive.com.android.feedback.payload.PayloadSender + +interface EngagementContextFactory { + fun engagementContext(): EngagementContext +} + +class EngagementContextFactoryProvider( + private val engagement: Engagement, + private val payloadSender: PayloadSender, + private val executor: Executors +) : Provider { + + override fun get(): EngagementContextFactory = DefaultEngagementContextFactory(engagement, payloadSender, executor) +} + +private class DefaultEngagementContextFactory( + private val engagement: Engagement, + private val payloadSender: PayloadSender, + private val executor: Executors +) : EngagementContextFactory { + override fun engagementContext(): EngagementContext { + return EngagementContext(engagement, payloadSender, executor) + } +} diff --git a/apptentive-survey/build.gradle b/apptentive-survey/build.gradle index 5ac26eb6e..943479b47 100644 --- a/apptentive-survey/build.gradle +++ b/apptentive-survey/build.gradle @@ -29,6 +29,7 @@ dependencies { implementation project(':apptentive-feedback') implementation "androidx.appcompat:appcompat:$appcompat_library_version" implementation "androidx.core:core-ktx:$core_ktx_version" + implementation "androidx.activity:activity-ktx:$activity_ktx_version" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$livedata_ktx_version" implementation "com.google.android.material:material:$material_design_version" implementation "androidx.constraintlayout:constraintlayout:$constraint_layout_version" diff --git a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/BaseSurveyActivity.kt b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/BaseSurveyActivity.kt new file mode 100644 index 000000000..902c1b12b --- /dev/null +++ b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/BaseSurveyActivity.kt @@ -0,0 +1,25 @@ +package apptentive.com.android.feedback.survey + +import androidx.activity.viewModels +import apptentive.com.android.feedback.survey.utils.createSurveyViewModel +import apptentive.com.android.feedback.survey.viewmodel.SurveyViewModel +import apptentive.com.android.ui.ApptentiveViewModelActivity +import apptentive.com.android.ui.ViewModelFactory + +/** + * Base Activity for Surveys + * + * This class should be used as base activity for Surveys that wish to use + * full interface customization + */ +open class BaseSurveyActivity : ApptentiveViewModelActivity() { + + /** + * @property viewModel [SurveyViewModel] class that is responsible for preparing + * and managing survey data for BaseSurveyActivity + * + */ + val viewModel: SurveyViewModel by viewModels { + ViewModelFactory { createSurveyViewModel() } + } +} \ No newline at end of file diff --git a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/SurveyActivity.kt b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/SurveyActivity.kt index 0e84babc9..8434ab9e6 100644 --- a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/SurveyActivity.kt +++ b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/SurveyActivity.kt @@ -22,9 +22,7 @@ import apptentive.com.android.feedback.survey.viewmodel.SurveyListItem.Type.Head import apptentive.com.android.feedback.survey.viewmodel.SurveyListItem.Type.MultiChoiceQuestion import apptentive.com.android.feedback.survey.viewmodel.SurveyListItem.Type.RangeQuestion import apptentive.com.android.feedback.survey.viewmodel.SurveyListItem.Type.SingleLineQuestion -import apptentive.com.android.feedback.survey.viewmodel.SurveyViewModel import apptentive.com.android.feedback.survey.viewmodel.register -import apptentive.com.android.ui.ApptentiveViewModelActivity import apptentive.com.android.ui.DialogButton import apptentive.com.android.ui.LayoutViewHolderFactory import apptentive.com.android.ui.ListViewAdapter @@ -32,7 +30,7 @@ import apptentive.com.android.ui.hideSoftKeyboard import apptentive.com.android.ui.showConfirmationDialog import com.google.android.material.appbar.MaterialToolbar -internal class SurveyActivity : ApptentiveViewModelActivity() { +internal class SurveyActivity : BaseSurveyActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.apptentive_activity_survey) diff --git a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/SurveyModelFactory.kt b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/SurveyModelFactory.kt new file mode 100644 index 000000000..202decb2b --- /dev/null +++ b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/SurveyModelFactory.kt @@ -0,0 +1,45 @@ +package apptentive.com.android.feedback.survey + +import apptentive.com.android.core.Provider +import apptentive.com.android.feedback.platform.AndroidEngagementContext +import apptentive.com.android.feedback.survey.interaction.DefaultSurveyQuestionConverter +import apptentive.com.android.feedback.survey.interaction.SurveyInteraction +import apptentive.com.android.feedback.survey.model.SurveyModel + +internal interface SurveyModelFactory { + fun getSurveyModel(): SurveyModel +} + +internal class SurveyModelFactoryProvider(val context: AndroidEngagementContext, + val interaction: SurveyInteraction) : Provider { + override fun get(): SurveyModelFactory { + return DefaultSurveyModelFactory(context, interaction) + } +} + +private class DefaultSurveyModelFactory(val context: AndroidEngagementContext, + val interaction: SurveyInteraction) : SurveyModelFactory { + override fun getSurveyModel(): SurveyModel { + return SurveyModel( + questions = interaction.questions.map { config -> + DefaultSurveyQuestionConverter().convert( + config = config, + requiredTextMessage = interaction.requiredText + ?: context.getString(R.string.apptentive_required) + ) + }, + name = interaction.name, + description = interaction.description, + submitText = interaction.submitText, + requiredText = interaction.requiredText, + validationError = interaction.validationError, + showSuccessMessage = interaction.showSuccessMessage, + successMessage = interaction.successMessage, + closeConfirmTitle = interaction.closeConfirmTitle, + closeConfirmMessage = interaction.closeConfirmMessage, + closeConfirmCloseText = interaction.closeConfirmCloseText, + closeConfirmBackText = interaction.closeConfirmBackText + ) + + } +} \ No newline at end of file diff --git a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/interaction/SurveyInteractionLauncher.kt b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/interaction/SurveyInteractionLauncher.kt index 3ab142c53..3c95cd77d 100644 --- a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/interaction/SurveyInteractionLauncher.kt +++ b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/interaction/SurveyInteractionLauncher.kt @@ -1,26 +1,17 @@ package apptentive.com.android.feedback.survey.interaction import androidx.annotation.VisibleForTesting +import apptentive.com.android.core.DependencyProvider import apptentive.com.android.feedback.INTERACTIONS -import apptentive.com.android.feedback.engagement.EngagementContext -import apptentive.com.android.feedback.engagement.Event -import apptentive.com.android.feedback.engagement.interactions.InteractionType import apptentive.com.android.feedback.platform.AndroidEngagementContext import apptentive.com.android.feedback.platform.AndroidViewInteractionLauncher -import apptentive.com.android.feedback.survey.R import apptentive.com.android.feedback.survey.SurveyActivity -import apptentive.com.android.feedback.survey.model.SurveyModel -import apptentive.com.android.feedback.survey.model.SurveyResponsePayload -import apptentive.com.android.feedback.survey.viewmodel.SurveyViewModel -import apptentive.com.android.ui.InteractionViewModelFactoryProvider +import apptentive.com.android.feedback.survey.SurveyModelFactoryProvider import apptentive.com.android.ui.startViewModelActivity import apptentive.com.android.util.Log -import apptentive.com.android.util.generateUUID @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) -class SurveyInteractionLauncher( - private val questionConverter: SurveyQuestionConverter = DefaultSurveyQuestionConverter() -) : AndroidViewInteractionLauncher() { +class SurveyInteractionLauncher : AndroidViewInteractionLauncher() { override fun launchInteraction( context: AndroidEngagementContext, interaction: SurveyInteraction @@ -28,93 +19,10 @@ class SurveyInteractionLauncher( Log.i(INTERACTIONS, "Survey interaction launched with title: ${interaction.name}") Log.v(INTERACTIONS, "Survey interaction data: $interaction") - val model = createSurveyModel(context, interaction) - - /* - we could probably use the interaction's id here but it's safer to pass a random string in - case if user launches the same interaction twice: for example if a "Take Survey" button is - pressed multiple times really fast - in that case multiple activities would show up but - they all will be independent - */ - val surveyInstanceId = generateUUID() - - // this will inject survey view model into the corresponding activity at runtime - InteractionViewModelFactoryProvider.registerViewModelFactory(surveyInstanceId) { - createSurveyViewModel(context, model, interaction.id) - } + DependencyProvider.register(SurveyModelFactoryProvider(context, interaction)) context.executors.main.execute { - context.androidContext.startViewModelActivity(surveyInstanceId) - } - } - - private fun createSurveyModel( - context: AndroidEngagementContext, - interaction: SurveyInteraction - ) = SurveyModel( - questions = interaction.questions.map { config -> - questionConverter.convert( - config = config, - requiredTextMessage = interaction.requiredText - ?: context.getString(R.string.apptentive_required) - ) - }, - name = interaction.name, - description = interaction.description, - submitText = interaction.submitText, - requiredText = interaction.requiredText, - validationError = interaction.validationError, - showSuccessMessage = interaction.showSuccessMessage, - successMessage = interaction.successMessage, - closeConfirmTitle = interaction.closeConfirmTitle, - closeConfirmMessage = interaction.closeConfirmMessage, - closeConfirmCloseText = interaction.closeConfirmCloseText, - closeConfirmBackText = interaction.closeConfirmBackText - ) - - @VisibleForTesting - internal fun createSurveyViewModel( - context: EngagementContext, - model: SurveyModel, - interactionId: String - ) = SurveyViewModel( - model = model, - executors = context.executors, - onSubmit = { answers -> - // send response - context.sendPayload(SurveyResponsePayload.fromAnswers(interactionId, answers)) - - // engage 'submit' event - context.engage( - event = Event.internal(EVENT_SUBMIT, interaction = InteractionType.Survey), - interactionId = interactionId - ) - }, - onCancel = { - context.engage( - event = Event.internal(EVENT_CANCEL, interaction = InteractionType.Survey), - interactionId = interactionId - ) - }, - onClose = { - context.engage( - event = Event.internal(EVENT_CANCEL_PARTIAL, interaction = InteractionType.Survey), - interactionId = interactionId - ) - }, - onBackToSurvey = { - context.engage( - event = Event.internal(EVENT_CONTINUE_PARTIAL, interaction = InteractionType.Survey), - interactionId = interactionId - ) + context.androidContext.startViewModelActivity() } - - ) - - companion object { - private const val EVENT_SUBMIT = "submit" - private const val EVENT_CANCEL = "cancel" - private const val EVENT_CANCEL_PARTIAL = "cancel_partial" - private const val EVENT_CONTINUE_PARTIAL = "continue_partial" } } \ No newline at end of file diff --git a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/model/SurveyModel.kt b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/model/SurveyModel.kt index d2119dcbd..de69b7af3 100644 --- a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/model/SurveyModel.kt +++ b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/model/SurveyModel.kt @@ -4,7 +4,7 @@ import androidx.annotation.WorkerThread import apptentive.com.android.core.BehaviorSubject import apptentive.com.android.core.Observable -internal class SurveyModel( +class SurveyModel( questions: List>, val name: String?, val description: String?, diff --git a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/utils/ViewModelUtils.kt b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/utils/ViewModelUtils.kt new file mode 100644 index 000000000..f93ac2cf6 --- /dev/null +++ b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/utils/ViewModelUtils.kt @@ -0,0 +1,61 @@ +package apptentive.com.android.feedback.survey.utils + +import apptentive.com.android.core.DependencyProvider +import apptentive.com.android.feedback.engagement.EngagementContext +import apptentive.com.android.feedback.engagement.EngagementContextFactory +import apptentive.com.android.feedback.engagement.Event +import apptentive.com.android.feedback.engagement.interactions.InteractionType +import apptentive.com.android.feedback.survey.SurveyModelFactory +import apptentive.com.android.feedback.survey.model.SurveyModel +import apptentive.com.android.feedback.survey.model.SurveyResponsePayload +import apptentive.com.android.feedback.survey.viewmodel.SurveyViewModel +import apptentive.com.android.util.generateUUID + +private const val EVENT_SUBMIT = "submit" +private const val EVENT_CANCEL = "cancel" +private const val EVENT_CANCEL_PARTIAL = "cancel_partial" +private const val EVENT_CONTINUE_PARTIAL = "continue_partial" + +internal fun createSurveyViewModel( + /* + we could probably use the interaction id here but it's safer to pass a random string in + case if user launches the same interaction twice: for example if a "Take Survey" button is + pressed multiple times really fast - in that case multiple activities would show up but + they all will be independent + */ + interactionId: String = generateUUID(), + context: EngagementContext = DependencyProvider.of().engagementContext(), + surveyModel: SurveyModel = DependencyProvider.of().getSurveyModel(), +) = SurveyViewModel( + model = surveyModel, + executors = context.executors, + onSubmit = { answers -> + + // send response + context.sendPayload(SurveyResponsePayload.fromAnswers(interactionId, answers)) + + // engage 'submit' event + context.engage( + event = Event.internal(EVENT_SUBMIT, interaction = InteractionType.Survey), + interactionId = interactionId + ) + }, + onCancel = { + context.engage( + event = Event.internal(EVENT_CANCEL, interaction = InteractionType.Survey), + interactionId = interactionId + ) + }, + onClose = { + context.engage( + event = Event.internal(EVENT_CANCEL_PARTIAL, interaction = InteractionType.Survey), + interactionId = interactionId + ) + }, + onBackToSurvey = { + context.engage( + event = Event.internal(EVENT_CONTINUE_PARTIAL, interaction = InteractionType.Survey), + interactionId = interactionId + ) + } +) \ No newline at end of file diff --git a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/SurveyViewModel.kt b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/SurveyViewModel.kt index 679443f23..a61cd5a5c 100644 --- a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/SurveyViewModel.kt +++ b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/SurveyViewModel.kt @@ -1,10 +1,10 @@ package apptentive.com.android.feedback.survey.viewmodel import androidx.annotation.MainThread -import androidx.annotation.VisibleForTesting import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel import apptentive.com.android.concurrent.Executors import apptentive.com.android.core.LiveEvent import apptentive.com.android.core.asLiveData @@ -15,25 +15,36 @@ import apptentive.com.android.feedback.survey.model.SurveyModel import apptentive.com.android.feedback.survey.model.SurveyQuestion import apptentive.com.android.feedback.survey.model.SurveyQuestionAnswer import apptentive.com.android.feedback.survey.model.update -import apptentive.com.android.ui.ApptentiveViewModel -import java.lang.Thread.sleep - -typealias SurveySubmitCallback = (Map) -> Unit -internal typealias SurveyCancelCallback = () -> Unit -internal typealias SurveyCancelPartialCallback = () -> Unit -internal typealias SurveyContinuePartialCallback = () -> Unit - - -internal class SurveyViewModel( +/** + * ViewModel for Surveys + * + * SurveyViewModel class that is responsible for preparing and managing survey data + * for BaseSurveyActivity + * + * @property model [SurveyModel] data model that represents the survey + * @property executors [Executors] executes submitted runnable tasks. + * + * Apptentive uses two executors + * + * * state - For long running/ Async operations + * * main - UI related tasks + * + * @property onSubmit [SurveySubmitCallback] callback to be executed when survey is submitted + * @property onCancel [SurveyCancelCallback] callback to be executed when survey is cancelled + * @property onClose [SurveyCancelPartialCallback] callback to be executed when survey is closed + * @property onBackToSurvey [SurveyContinuePartialCallback] callback to be executed + * when survey is resumed through after an attempt to close + */ + +class SurveyViewModel( private val model: SurveyModel, private val executors: Executors, private val onSubmit: SurveySubmitCallback, private val onCancel: SurveyCancelCallback, private val onClose: SurveyCancelPartialCallback, private val onBackToSurvey: SurveyContinuePartialCallback, - questionListItemFactory: SurveyQuestionListItemFactory = DefaultSurveyQuestionListItemFactory() -) : ApptentiveViewModel() { +) : ViewModel() { /** LiveData which transforms a list of {SurveyQuestion} into a list of {SurveyQuestionListItem} */ private val questionsStream: LiveData>> = model.questionsStream.asLiveData() @@ -47,7 +58,7 @@ internal class SurveyViewModel( /** LiveData which holds the current list of SurveyQuestionListItem */ val listItems: LiveData> = createQuestionListLiveData( - questionListItemFactory = questionListItemFactory + questionListItemFactory = DefaultSurveyQuestionListItemFactory() ) private val requiredTextEvent = LiveEvent() @@ -255,9 +266,14 @@ internal data class SurveySubmitMessageState( val isValid: Boolean ) -internal data class SurveyCancelConfirmationDisplay( +data class SurveyCancelConfirmationDisplay( val title: String?, val message: String?, val positiveButtonMessage: String?, val negativeButtonMessage: String? -) \ No newline at end of file +) + +typealias SurveySubmitCallback = (Map) -> Unit +internal typealias SurveyCancelCallback = () -> Unit +internal typealias SurveyCancelPartialCallback = () -> Unit +internal typealias SurveyContinuePartialCallback = () -> Unit \ No newline at end of file diff --git a/apptentive-survey/src/test/java/apptentive/com/android/feedback/survey/interaction/SurveyInteractionLauncherTest.kt b/apptentive-survey/src/test/java/apptentive/com/android/feedback/survey/interaction/SurveyInteractionLauncherTest.kt index 7c2345e35..63536e96d 100644 --- a/apptentive-survey/src/test/java/apptentive/com/android/feedback/survey/interaction/SurveyInteractionLauncherTest.kt +++ b/apptentive-survey/src/test/java/apptentive/com/android/feedback/survey/interaction/SurveyInteractionLauncherTest.kt @@ -16,6 +16,7 @@ import apptentive.com.android.feedback.survey.model.SurveyQuestion import apptentive.com.android.feedback.survey.model.createMultiChoiceQuestion import apptentive.com.android.feedback.survey.model.createRangeQuestion import apptentive.com.android.feedback.survey.model.createSingleLineQuestion +import apptentive.com.android.feedback.survey.utils.createSurveyViewModel import apptentive.com.android.toProperJson import org.junit.Rule import org.junit.Test @@ -64,10 +65,10 @@ class SurveyInteractionLauncherTest : TestCase() { ) ) ) - val viewModel = SurveyInteractionLauncher().createSurveyViewModel( + val viewModel = createSurveyViewModel( + interactionId = "interaction_id", context = context, - model = model, - interactionId = "interaction_id" + surveyModel = model, ) viewModel.submit() From abd3b1cdc5dde05ca5180da3ca59938436176c3a Mon Sep 17 00:00:00 2001 From: ChaseApptentive <85038972+ChaseApptentive@users.noreply.github.com> Date: Thu, 27 Jan 2022 09:53:37 -0600 Subject: [PATCH 012/491] PBI-2860 remove android background and support (#208) * PBI-2860 Remove android background and support [PBI-2860] * Remove unused wrappers * ktlint * Update naming --- app/src/main/res/values/styles.xml | 2 +- .../apptentive/com/android/ui/ThemeHelper.kt | 28 ++++++++++----- .../src/main/res/values/styles.xml | 7 ++-- .../enjoyment/EnjoymentDialogFragment.kt | 11 +----- .../textmodal/TextModalDialogFragment.kt | 9 +---- .../ratingdialog/RatingDialogFragment.kt | 9 +---- .../feedback/survey/utils/ViewUtils.kt | 35 ------------------- .../viewmodel/MultiChoiceQuestionListItem.kt | 3 -- .../viewmodel/SingleLineQuestionListItem.kt | 3 -- 9 files changed, 28 insertions(+), 79 deletions(-) delete mode 100644 apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/utils/ViewUtils.kt diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 9626539dc..2afeaff96 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -10,7 +10,7 @@ @color/color_secondary_variant @color/color_on_secondary @color/color_background - + @color/color_background @color/color_on_background @color/color_text_color_secondary @color/color_error diff --git a/apptentive-core-ui/src/main/java/apptentive/com/android/ui/ThemeHelper.kt b/apptentive-core-ui/src/main/java/apptentive/com/android/ui/ThemeHelper.kt index e71a63a8d..e4e963397 100644 --- a/apptentive-core-ui/src/main/java/apptentive/com/android/ui/ThemeHelper.kt +++ b/apptentive-core-ui/src/main/java/apptentive/com/android/ui/ThemeHelper.kt @@ -8,19 +8,28 @@ import apptentive.com.android.R import apptentive.com.android.util.Log import apptentive.com.android.util.LogTags +/** + * Allows inheritance of app's theme + * + * Layers on themes on top of each other by importance. + * Later layers will override previous layers and take priority. + */ internal fun Context.overrideTheme() { - /* Step 1: Apply Apptentive default theme layer. - * If host activity is an activity, the base theme already has Apptentive defaults applied, so skip Step 1. + /* Layer 1: Apptentive default theme. + * If host activity is an activity, the base theme already has Apptentive defaults applied, so skip layer 1. * If parent activity is NOT an activity, first apply Apptentive defaults. */ if (this !is Activity) { theme.applyStyle(R.style.Theme_Apptentive, true) } - /* Step 2: Inherit app default theme */ + /* Layer 2: App default theme */ applyAppTheme() - /* Step 3: Apply optional theme override specified in host app's style */ + /* Layer 3: Disable the problem style -> android:background */ + theme.applyStyle(R.style.DisableAndroidBackgroundStyle, true) + + /* Layer 4: Optional theme override specified in host app's style */ applyApptentiveThemeOverride() } @@ -30,16 +39,19 @@ internal fun Context.overrideTheme() { fun ContextThemeWrapper.overrideTheme() { val contextTheme = themeResId - /* Step 1: Apply Apptentive default theme layer */ + /* Layer 1: Apptentive default theme */ theme.applyStyle(R.style.Theme_Apptentive, true) - /* Step 2: Layer on ContextThemeWrapper's theme it was created with */ + /* Layer 2: ContextThemeWrapper theme the wrapper was created with */ theme.applyStyle(contextTheme, true) - /* Step 3: Inherit app default theme */ + /* Layer 3: App default theme */ applyAppTheme() - /* Step 4: Apply optional theme override specified in host app's style */ + /* Layer 4: Disable the problem style -> android:background */ + theme.applyStyle(R.style.DisableAndroidBackgroundStyle, true) + + /* Layer 5: Optional theme override specified in host app's style */ applyApptentiveThemeOverride() } diff --git a/apptentive-core-ui/src/main/res/values/styles.xml b/apptentive-core-ui/src/main/res/values/styles.xml index 4b9c7c934..357d11c72 100644 --- a/apptentive-core-ui/src/main/res/values/styles.xml +++ b/apptentive-core-ui/src/main/res/values/styles.xml @@ -14,14 +14,15 @@ @style/Widget.MaterialComponents.Button - diff --git a/apptentive-enjoyment-dialog/src/main/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogFragment.kt b/apptentive-enjoyment-dialog/src/main/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogFragment.kt index 4611da378..ba9d03760 100644 --- a/apptentive-enjoyment-dialog/src/main/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogFragment.kt +++ b/apptentive-enjoyment-dialog/src/main/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogFragment.kt @@ -3,10 +3,8 @@ package apptentive.com.android.feedback.enjoyment import android.app.Dialog import android.content.DialogInterface import android.os.Bundle -import androidx.appcompat.view.ContextThemeWrapper import androidx.fragment.app.DialogFragment import apptentive.com.android.feedback.platform.AndroidEngagementContext -import apptentive.com.android.ui.overrideTheme import com.google.android.material.dialog.MaterialAlertDialogBuilder internal class EnjoymentDialogFragment( @@ -17,14 +15,7 @@ internal class EnjoymentDialogFragment( override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { retainInstance = true - val ctx = ContextThemeWrapper( - context.androidContext, - R.style.Theme_Apptentive_Dialog_Alert - ).apply { - overrideTheme() - } - - return MaterialAlertDialogBuilder(ctx).apply { + return MaterialAlertDialogBuilder(context.androidContext).apply { setMessage(viewModel.title) setPositiveButton(viewModel.yesText) { _, _ -> diff --git a/apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalDialogFragment.kt b/apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalDialogFragment.kt index 777fb1079..61b6678a4 100644 --- a/apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalDialogFragment.kt +++ b/apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalDialogFragment.kt @@ -24,14 +24,7 @@ internal class TextModalDialogFragment( override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { retainInstance = true - val ctx = ContextThemeWrapper( - context.androidContext, - R.style.Theme_Apptentive_Dialog_Alert - ).apply { - overrideTheme() - } - - return MaterialAlertDialogBuilder(ctx).apply { + return MaterialAlertDialogBuilder(context.androidContext).apply { val inflater = LayoutInflater.from(context) val contentView = inflater.inflate(R.layout.apptentive_note, null) setView(contentView) diff --git a/apptentive-ratings/src/main/java/apptentive/com/android/feedback/ratingdialog/RatingDialogFragment.kt b/apptentive-ratings/src/main/java/apptentive/com/android/feedback/ratingdialog/RatingDialogFragment.kt index b44224e5b..973fa0483 100644 --- a/apptentive-ratings/src/main/java/apptentive/com/android/feedback/ratingdialog/RatingDialogFragment.kt +++ b/apptentive-ratings/src/main/java/apptentive/com/android/feedback/ratingdialog/RatingDialogFragment.kt @@ -21,14 +21,7 @@ internal class RatingDialogFragment( override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { retainInstance = true - val ctx = ContextThemeWrapper( - context.androidContext, - R.style.Theme_Apptentive_Dialog_Alert - ).apply { - overrideTheme() - } - - return MaterialAlertDialogBuilder(ctx).apply { + return MaterialAlertDialogBuilder(context.androidContext).apply { val inflater = LayoutInflater.from(context) val contentView = inflater.inflate(R.layout.apptentive_rating_dialog, null) setView(contentView) diff --git a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/utils/ViewUtils.kt b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/utils/ViewUtils.kt deleted file mode 100644 index 3f5907c1b..000000000 --- a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/utils/ViewUtils.kt +++ /dev/null @@ -1,35 +0,0 @@ -package apptentive.com.android.feedback.survey.utils - -import com.google.android.material.textfield.TextInputEditText - -/** - * "Fix" for TextInputLayout focused text-less box stroke color. - * When android:background is set in styles, the state mentioned above will be - * the same color as that background and will appear invisible. - * This "fix" adds a space when the box is empty and focused and then sets the - * cursor to the first position. - * - * Trims any white space when unfocused. - * - * Second part to this fix is to put something like - * textInputEditText.hasFocus() && it.isNullOrEmpty() -> textInputEditText.setText(" ") - * into doAfterTextChanged. This cannot be done here because of varying callbacks. - * This second part will ensure there is always at least a space even if the user deletes it. - * - * Side effect: TalkBack says "space" upon entering the field for the first time. - * - * Uses: - * @see apptentive.com.android.feedback.survey.viewmodel.SingleLineQuestionListItem.ViewHolder.bindView - * @see apptentive.com.android.feedback.survey.viewmodel.MultiChoiceQuestionListItem.ViewHolder.bindView - */ -internal fun TextInputEditText.setTextBoxBackgroundFocusFix() { - setOnFocusChangeListener { _, hasFocus -> - when { - hasFocus && text.isNullOrEmpty() -> { - setText(" ") - post { setSelection(0) } - } - text != null -> setText(text?.trim()) - } - } -} \ No newline at end of file diff --git a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/MultiChoiceQuestionListItem.kt b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/MultiChoiceQuestionListItem.kt index 1f955dd2a..be22958d0 100644 --- a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/MultiChoiceQuestionListItem.kt +++ b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/MultiChoiceQuestionListItem.kt @@ -7,7 +7,6 @@ import androidx.core.view.isVisible import androidx.core.widget.doAfterTextChanged import apptentive.com.android.feedback.survey.R import apptentive.com.android.feedback.survey.model.MultiChoiceQuestion -import apptentive.com.android.feedback.survey.utils.setTextBoxBackgroundFocusFix import apptentive.com.android.feedback.survey.view.SurveyQuestionContainerView import apptentive.com.android.ui.ListViewItem import apptentive.com.android.ui.setInvalid @@ -132,9 +131,7 @@ internal class MultiChoiceQuestionListItem( textInputLayout.hint = choice.hint textInputEditText.setText(choice.text) - textInputEditText.setTextBoxBackgroundFocusFix() textInputEditText.doAfterTextChanged { - if (textInputEditText.hasFocus() && it.isNullOrEmpty()) textInputEditText.setText(" ") // Second part to the fix onSelectionChanged.invoke(questionId, choice.id, true, it?.toString().orEmpty().trim()) } // text fields end diff --git a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/SingleLineQuestionListItem.kt b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/SingleLineQuestionListItem.kt index 2676887cc..8042ecb9e 100644 --- a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/SingleLineQuestionListItem.kt +++ b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/SingleLineQuestionListItem.kt @@ -8,7 +8,6 @@ import android.view.inputmethod.EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES import android.view.inputmethod.EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE import androidx.core.widget.doAfterTextChanged import apptentive.com.android.feedback.survey.R -import apptentive.com.android.feedback.survey.utils.setTextBoxBackgroundFocusFix import apptentive.com.android.feedback.survey.view.SurveyQuestionContainerView import apptentive.com.android.ui.setInvalid import com.google.android.material.textfield.TextInputEditText @@ -105,9 +104,7 @@ internal class SingleLineQuestionListItem( } answerEditText.setText(item.text) - answerEditText.setTextBoxBackgroundFocusFix() answerEditText.doAfterTextChanged { - if (answerEditText.hasFocus() && it.isNullOrEmpty()) answerEditText.setText(" ") // Second part to the fix onTextChanged(questionId, it?.toString().orEmpty().trim()) } } From b58370e61133213c2cd4938b23f9ece643140eca Mon Sep 17 00:00:00 2001 From: ChaseApptentive <85038972+ChaseApptentive@users.noreply.github.com> Date: Tue, 1 Feb 2022 14:26:55 -0600 Subject: [PATCH 013/491] PBI-2596 Resource close bug (#209) [PBI-2596] --- .../src/main/java/apptentive/com/android/network/HttpNetwork.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apptentive-core/src/main/java/apptentive/com/android/network/HttpNetwork.kt b/apptentive-core/src/main/java/apptentive/com/android/network/HttpNetwork.kt index c62f5270c..5dadcf5ca 100644 --- a/apptentive-core/src/main/java/apptentive/com/android/network/HttpNetwork.kt +++ b/apptentive-core/src/main/java/apptentive/com/android/network/HttpNetwork.kt @@ -66,7 +66,7 @@ class DefaultHttpNetwork( data = stream.readBytes(), headers = responseHeaders, duration = duration - ) + ).also { stream.close() } } finally { connection.disconnect() } From 086fc8900321643a2002a2a8dbde83dfd2177f6b Mon Sep 17 00:00:00 2001 From: ChaseApptentive <85038972+ChaseApptentive@users.noreply.github.com> Date: Tue, 1 Feb 2022 16:15:31 -0600 Subject: [PATCH 014/491] PBI-2881 Disk read write off main thread (#211) * PBI-2881 Disk read write off main thread [PBI-2881] * Remove wrong annotation * ktlint * clean up additional call --- .../feedback/ApptentiveDefaultClient.kt | 4 ++- .../lifecycle/ApptentiveLifecycleObserver.kt | 25 ++++++++++++------- .../survey/viewmodel/SurveyViewModel.kt | 6 ++--- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveDefaultClient.kt b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveDefaultClient.kt index 53b695f8e..a9dd9c894 100644 --- a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveDefaultClient.kt +++ b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveDefaultClient.kt @@ -168,7 +168,9 @@ internal class ApptentiveDefaultClient( executors.main.execute { Log.i(LIFE_CYCLE_OBSERVER, "Observing App lifecycle") ProcessLifecycleOwner.get().lifecycle.addObserver( - ApptentiveLifecycleObserver(this) { conversationManager.tryFetchEngagementManifest() } + ApptentiveLifecycleObserver(this, executors.state) { + conversationManager.tryFetchEngagementManifest() + } ) } } diff --git a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/lifecycle/ApptentiveLifecycleObserver.kt b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/lifecycle/ApptentiveLifecycleObserver.kt index ec1384346..5925d31fb 100644 --- a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/lifecycle/ApptentiveLifecycleObserver.kt +++ b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/lifecycle/ApptentiveLifecycleObserver.kt @@ -1,28 +1,35 @@ package apptentive.com.android.feedback.lifecycle -import androidx.annotation.WorkerThread import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent +import apptentive.com.android.concurrent.Executor import apptentive.com.android.feedback.ApptentiveClient import apptentive.com.android.feedback.LIFE_CYCLE_OBSERVER import apptentive.com.android.feedback.engagement.Event import apptentive.com.android.feedback.engagement.InternalEvent import apptentive.com.android.util.Log -internal class ApptentiveLifecycleObserver(val client: ApptentiveClient, val refreshManifest: () -> Unit) : LifecycleObserver { +internal class ApptentiveLifecycleObserver( + val client: ApptentiveClient, + private val stateExecutor: Executor, + val refreshManifest: () -> Unit +) : LifecycleObserver { + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) - @WorkerThread fun onBackground() { - Log.d(LIFE_CYCLE_OBSERVER, "App is in background") - client.engage(Event.internal(InternalEvent.APP_EXIT.labelName)) + stateExecutor.execute { + Log.d(LIFE_CYCLE_OBSERVER, "App is in background") + client.engage(Event.internal(InternalEvent.APP_EXIT.labelName)) + } } @OnLifecycleEvent(Lifecycle.Event.ON_START) - @WorkerThread fun onForeground() { - Log.d(LIFE_CYCLE_OBSERVER, "App is in foreground") - client.engage(Event.internal(InternalEvent.APP_LAUNCH.labelName)) - refreshManifest() + stateExecutor.execute { + Log.d(LIFE_CYCLE_OBSERVER, "App is in foreground") + client.engage(Event.internal(InternalEvent.APP_LAUNCH.labelName)) + refreshManifest() + } } } diff --git a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/SurveyViewModel.kt b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/SurveyViewModel.kt index a61cd5a5c..8f53da5b3 100644 --- a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/SurveyViewModel.kt +++ b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/SurveyViewModel.kt @@ -170,7 +170,7 @@ class SurveyViewModel( } fun onBackToSurveyFromConfirmationDialog() { - onBackToSurvey.invoke() + executors.state.execute { onBackToSurvey.invoke() } } @MainThread @@ -185,12 +185,12 @@ class SurveyViewModel( // we don't need to show any confirmation as the customer // didn't interact exitEvent.postValue(true) - onCancel.invoke() + executors.state.execute { onCancel.invoke() } } } else { // we are already in the confirmation dialog, so no need to show confirmation again exitEvent.postValue(true) - onClose.invoke() + executors.state.execute { onClose.invoke() } } } From 534cf1687b3ac7491b0e6291313dd97914686cf1 Mon Sep 17 00:00:00 2001 From: PoornimaApptentive <85186738+PoornimaApptentive@users.noreply.github.com> Date: Fri, 4 Feb 2022 11:22:25 -0800 Subject: [PATCH 015/491] PBI - 2303 DialogFragment crash (#210) * Make no-arg constructors for all dialog fragments, use androidx viewmodels * Declared factory interfaces in separate file, cleaned up the getFragmentManager method --- apptentive-enjoyment-dialog/build.gradle | 1 + .../enjoyment/EnjoymentDialogFragment.kt | 12 +- .../EnjoymentDialogInteractionFactory.kt | 15 +++ .../EnjoymentDialogInteractionLauncher.kt | 8 +- .../enjoyment/EnjoymentDialogViewModel.kt | 11 +- .../enjoyment/EnjoymentDialogViewModelTest.kt | 43 +++++-- .../feedback/ApptentiveDefaultClient.kt | 11 +- .../AndroidEngagementContextFactory.kt | 24 ++++ .../engagement/EngagementContextFactory.kt | 28 ----- .../platform/AndroidEngagementContext.kt | 6 - apptentive-notes/build.gradle | 1 + .../textmodal/TextModalDialogFragment.kt | 16 +-- .../textmodal/TextModalInteraction.kt | 1 + .../textmodal/TextModalInteractionFactory.kt | 16 +++ .../textmodal/TextModalInteractionLauncher.kt | 9 +- .../feedback/textmodal/TextModalViewModel.kt | 12 +- .../notes/viewmodel/TextModalViewModelTest.kt | 105 ++++++++++++------ apptentive-ratings/build.gradle | 1 + .../ratingdialog/RatingDialogFragment.kt | 16 +-- .../RatingDialogInteractionFactory.kt | 15 +++ .../RatingDialogInteractionLauncher.kt | 7 +- .../ratingdialog/RatingDialogViewModel.kt | 12 +- .../ratingdialog/RatingDialogViewModelTest.kt | 35 +++--- .../feedback/survey/utils/ViewModelUtils.kt | 4 +- build.gradle | 1 + 25 files changed, 252 insertions(+), 158 deletions(-) create mode 100644 apptentive-enjoyment-dialog/src/main/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogInteractionFactory.kt create mode 100644 apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/AndroidEngagementContextFactory.kt delete mode 100644 apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/EngagementContextFactory.kt create mode 100644 apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalInteractionFactory.kt create mode 100644 apptentive-ratings/src/main/java/apptentive/com/android/feedback/ratingdialog/RatingDialogInteractionFactory.kt diff --git a/apptentive-enjoyment-dialog/build.gradle b/apptentive-enjoyment-dialog/build.gradle index 1da7a4e43..885fd046f 100644 --- a/apptentive-enjoyment-dialog/build.gradle +++ b/apptentive-enjoyment-dialog/build.gradle @@ -29,6 +29,7 @@ dependencies { implementation project(':apptentive-feedback') implementation "androidx.appcompat:appcompat:$appcompat_library_version" implementation "androidx.core:core-ktx:$core_ktx_version" + implementation "androidx.fragment:fragment-ktx:$fragment_ktx_version" implementation "com.google.android.material:material:$material_design_version" testImplementation project(':apptentive-core-test') diff --git a/apptentive-enjoyment-dialog/src/main/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogFragment.kt b/apptentive-enjoyment-dialog/src/main/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogFragment.kt index ba9d03760..aade39496 100644 --- a/apptentive-enjoyment-dialog/src/main/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogFragment.kt +++ b/apptentive-enjoyment-dialog/src/main/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogFragment.kt @@ -4,18 +4,14 @@ import android.app.Dialog import android.content.DialogInterface import android.os.Bundle import androidx.fragment.app.DialogFragment -import apptentive.com.android.feedback.platform.AndroidEngagementContext +import androidx.fragment.app.viewModels import com.google.android.material.dialog.MaterialAlertDialogBuilder -internal class EnjoymentDialogFragment( - val context: AndroidEngagementContext, - private val viewModel: EnjoymentDialogViewModel -) : DialogFragment() { +internal class EnjoymentDialogFragment : DialogFragment() { + private val viewModel by viewModels() override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - retainInstance = true - - return MaterialAlertDialogBuilder(context.androidContext).apply { + return MaterialAlertDialogBuilder(requireContext()).apply { setMessage(viewModel.title) setPositiveButton(viewModel.yesText) { _, _ -> diff --git a/apptentive-enjoyment-dialog/src/main/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogInteractionFactory.kt b/apptentive-enjoyment-dialog/src/main/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogInteractionFactory.kt new file mode 100644 index 000000000..65065d764 --- /dev/null +++ b/apptentive-enjoyment-dialog/src/main/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogInteractionFactory.kt @@ -0,0 +1,15 @@ +package apptentive.com.android.feedback.enjoyment + +import apptentive.com.android.core.Provider + +internal interface EnjoymentDialogInteractionFactory { + fun getEnjoymentDialogInteraction(): EnjoymentDialogInteraction +} + +internal class EnjoymentDialogInteractionProvider(val interaction: EnjoymentDialogInteraction) : Provider { + override fun get(): EnjoymentDialogInteractionFactory = object : EnjoymentDialogInteractionFactory { + override fun getEnjoymentDialogInteraction(): EnjoymentDialogInteraction { + return interaction + } + } +} diff --git a/apptentive-enjoyment-dialog/src/main/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogInteractionLauncher.kt b/apptentive-enjoyment-dialog/src/main/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogInteractionLauncher.kt index 6cc7c28bd..5a913d283 100644 --- a/apptentive-enjoyment-dialog/src/main/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogInteractionLauncher.kt +++ b/apptentive-enjoyment-dialog/src/main/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogInteractionLauncher.kt @@ -1,5 +1,6 @@ package apptentive.com.android.feedback.enjoyment +import apptentive.com.android.core.DependencyProvider import apptentive.com.android.feedback.INTERACTIONS import apptentive.com.android.feedback.platform.AndroidEngagementContext import apptentive.com.android.feedback.platform.AndroidViewInteractionLauncher @@ -16,10 +17,11 @@ internal class EnjoymentDialogInteractionLauncher : context.executors.main.execute { try { - val fragmentManager = context.getFragmentManager() - val viewModel = EnjoymentDialogViewModel(context, interaction) - val enjoymentDialog = EnjoymentDialogFragment(context, viewModel) + DependencyProvider.register(EnjoymentDialogInteractionProvider(interaction)) + + val fragmentManager = context.getFragmentManager() + val enjoymentDialog = EnjoymentDialogFragment() enjoymentDialog.show(fragmentManager, EnjoymentDialogInteraction.TAG) } catch (exception: Exception) { Log.e(INTERACTIONS, "Could not start Love Dialog interaction", exception) diff --git a/apptentive-enjoyment-dialog/src/main/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogViewModel.kt b/apptentive-enjoyment-dialog/src/main/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogViewModel.kt index c0fe95561..26eddbab9 100644 --- a/apptentive-enjoyment-dialog/src/main/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogViewModel.kt +++ b/apptentive-enjoyment-dialog/src/main/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogViewModel.kt @@ -1,14 +1,15 @@ package apptentive.com.android.feedback.enjoyment +import androidx.lifecycle.ViewModel +import apptentive.com.android.core.DependencyProvider import apptentive.com.android.feedback.INTERACTIONS -import apptentive.com.android.feedback.engagement.EngagementContext +import apptentive.com.android.feedback.engagement.AndroidEngagementContextFactory import apptentive.com.android.feedback.engagement.Event import apptentive.com.android.util.Log -internal data class EnjoymentDialogViewModel( - private val context: EngagementContext, - val interaction: EnjoymentDialogInteraction -) { +internal class EnjoymentDialogViewModel : ViewModel() { + private val context = DependencyProvider.of().engagementContext() + val interaction = DependencyProvider.of().getEnjoymentDialogInteraction() val title = interaction.title val yesText = interaction.yesText val noText = interaction.noText diff --git a/apptentive-enjoyment-dialog/src/test/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogViewModelTest.kt b/apptentive-enjoyment-dialog/src/test/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogViewModelTest.kt index b47a26528..51258f673 100644 --- a/apptentive-enjoyment-dialog/src/test/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogViewModelTest.kt +++ b/apptentive-enjoyment-dialog/src/test/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogViewModelTest.kt @@ -1,8 +1,12 @@ package apptentive.com.android.feedback.enjoyment import apptentive.com.android.TestCase +import apptentive.com.android.core.DependencyProvider +import apptentive.com.android.core.Provider import apptentive.com.android.feedback.EngagementResult +import apptentive.com.android.feedback.engagement.AndroidEngagementContextFactory import apptentive.com.android.feedback.engagement.EngageArgs +import apptentive.com.android.feedback.engagement.EngagementContext import apptentive.com.android.feedback.engagement.Event import apptentive.com.android.feedback.engagement.MockEngagementContext import apptentive.com.android.feedback.enjoyment.EnjoymentDialogViewModel.Companion.CODE_POINT_CANCEL @@ -14,15 +18,6 @@ import org.junit.Test class EnjoymentDialogViewModelTest : TestCase() { @Test fun testEvents() { - val context = MockEngagementContext( - onEngage = { args -> - addResult(args) - EngagementResult.InteractionNotShown("No runnable interactions") - }, - onSendPayload = { payload -> - throw AssertionError("We didn't expect any payloads here but this one slipped though: $payload") - } - ) val interactionId = "123456789" val interaction = EnjoymentDialogInteraction( id = interactionId, @@ -31,11 +26,25 @@ class EnjoymentDialogViewModelTest : TestCase() { noText = "No", dismissText = "Dismiss" ) - val viewModel = EnjoymentDialogViewModel( - context = context, - interaction = interaction + + DependencyProvider.register(EnjoymentDialogInteractionProvider(interaction)) + DependencyProvider.register( + MockEngagementContextFactory + { + MockEngagementContext( + onEngage = { args -> + addResult(args) + EngagementResult.InteractionNotShown("No runnable interactions") + }, + onSendPayload = { payload -> + throw AssertionError("We didn't expect any payloads here but this one slipped though: $payload") + } + ) + } ) + val viewModel = EnjoymentDialogViewModel() + viewModel.onYesButton() assertResults( createCall(CODE_POINT_YES, interactionId = interactionId) @@ -63,3 +72,13 @@ class EnjoymentDialogViewModelTest : TestCase() { interactionId = interactionId ) } + +class MockEngagementContextFactory(val getEngagementContext: () -> EngagementContext) : Provider { + override fun get(): AndroidEngagementContextFactory { + return object : AndroidEngagementContextFactory { + override fun engagementContext(): EngagementContext { + return getEngagementContext() + } + } + } +} diff --git a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveDefaultClient.kt b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveDefaultClient.kt index a9dd9c894..6444a8b3c 100644 --- a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveDefaultClient.kt +++ b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/ApptentiveDefaultClient.kt @@ -13,10 +13,11 @@ import apptentive.com.android.feedback.conversation.ConversationRepository import apptentive.com.android.feedback.conversation.ConversationSerializer import apptentive.com.android.feedback.conversation.DefaultConversationRepository import apptentive.com.android.feedback.conversation.DefaultConversationSerializer +import apptentive.com.android.feedback.engagement.AndroidEngagementContextFactory +import apptentive.com.android.feedback.engagement.AndroidEngagementContextProvider import apptentive.com.android.feedback.engagement.DefaultEngagement import apptentive.com.android.feedback.engagement.DefaultInteractionEngagement import apptentive.com.android.feedback.engagement.Engagement -import apptentive.com.android.feedback.engagement.EngagementContextFactoryProvider import apptentive.com.android.feedback.engagement.Event import apptentive.com.android.feedback.engagement.InteractionDataProvider import apptentive.com.android.feedback.engagement.InteractionEngagement @@ -42,7 +43,6 @@ import apptentive.com.android.feedback.payload.PayloadData import apptentive.com.android.feedback.payload.PayloadSender import apptentive.com.android.feedback.payload.PersistentPayloadQueue import apptentive.com.android.feedback.payload.SerialPayloadSender -import apptentive.com.android.feedback.platform.AndroidEngagementContext import apptentive.com.android.feedback.platform.DefaultAppReleaseFactory import apptentive.com.android.feedback.platform.DefaultDeviceFactory import apptentive.com.android.feedback.platform.DefaultEngagementDataFactory @@ -124,8 +124,6 @@ internal class ApptentiveDefaultClient( recordInteraction = ::recordInteraction ) - DependencyProvider.register(EngagementContextFactoryProvider(engagement, payloadSender, executors)) - // once we have received conversationId and conversationToken we can setup payload sender service val conversationId = conversation.conversationId val conversationToken = conversation.conversationToken @@ -243,7 +241,10 @@ internal class ApptentiveDefaultClient( "and call registerApptentiveActivityCallback(this) " + "in your Activity's onCreate function." }.getApptentiveActivityInfo() - return AndroidEngagementContext(activity, engagement, payloadSender, executors).engage(event) + + DependencyProvider.register(AndroidEngagementContextProvider(activity, engagement, payloadSender, executors)) + + return DependencyProvider.of().engagementContext().engage(event) } override fun updatePerson( diff --git a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/AndroidEngagementContextFactory.kt b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/AndroidEngagementContextFactory.kt new file mode 100644 index 000000000..5ea058f3a --- /dev/null +++ b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/AndroidEngagementContextFactory.kt @@ -0,0 +1,24 @@ +package apptentive.com.android.feedback.engagement + +import android.content.Context +import apptentive.com.android.concurrent.Executors +import apptentive.com.android.core.Provider +import apptentive.com.android.feedback.payload.PayloadSender +import apptentive.com.android.feedback.platform.AndroidEngagementContext + +interface AndroidEngagementContextFactory { + fun engagementContext(): EngagementContext +} + +class AndroidEngagementContextProvider( + private val activity: Context, + private val engagement: Engagement, + private val payloadSender: PayloadSender, + private val executor: Executors +) : Provider { + override fun get(): AndroidEngagementContextFactory = object : AndroidEngagementContextFactory { + override fun engagementContext(): EngagementContext { + return AndroidEngagementContext(activity, engagement, payloadSender, executor) + } + } +} diff --git a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/EngagementContextFactory.kt b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/EngagementContextFactory.kt deleted file mode 100644 index b85e86d3b..000000000 --- a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/engagement/EngagementContextFactory.kt +++ /dev/null @@ -1,28 +0,0 @@ -package apptentive.com.android.feedback.engagement - -import apptentive.com.android.concurrent.Executors -import apptentive.com.android.core.Provider -import apptentive.com.android.feedback.payload.PayloadSender - -interface EngagementContextFactory { - fun engagementContext(): EngagementContext -} - -class EngagementContextFactoryProvider( - private val engagement: Engagement, - private val payloadSender: PayloadSender, - private val executor: Executors -) : Provider { - - override fun get(): EngagementContextFactory = DefaultEngagementContextFactory(engagement, payloadSender, executor) -} - -private class DefaultEngagementContextFactory( - private val engagement: Engagement, - private val payloadSender: PayloadSender, - private val executor: Executors -) : EngagementContextFactory { - override fun engagementContext(): EngagementContext { - return EngagementContext(engagement, payloadSender, executor) - } -} diff --git a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/platform/AndroidEngagementContext.kt b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/platform/AndroidEngagementContext.kt index c2d4b44cd..a5a12a93f 100644 --- a/apptentive-feedback/src/main/java/apptentive/com/android/feedback/platform/AndroidEngagementContext.kt +++ b/apptentive-feedback/src/main/java/apptentive/com/android/feedback/platform/AndroidEngagementContext.kt @@ -1,6 +1,5 @@ package apptentive.com.android.feedback.platform -import android.app.Application import android.content.Context import android.content.Intent import androidx.appcompat.app.AppCompatActivity @@ -34,11 +33,6 @@ class AndroidEngagementContext( return when (context) { is AppCompatActivity, is FragmentActivity -> (context as FragmentActivity).supportFragmentManager is ContextThemeWrapper -> getFragmentManager(context.baseContext) - is Application -> throw Exception( - "Can't retrieve fragment manager. " + - "Must use an Activity context. " + - "Application context does not have a fragment manager." - ) null -> throw Exception("Context is null") else -> throw Exception("Can't retrieve fragment manager. Unknown context type ${context.packageName}") } diff --git a/apptentive-notes/build.gradle b/apptentive-notes/build.gradle index 033fdbfb1..d791ad984 100644 --- a/apptentive-notes/build.gradle +++ b/apptentive-notes/build.gradle @@ -31,6 +31,7 @@ dependencies { implementation project(':apptentive-feedback') implementation "androidx.appcompat:appcompat:$appcompat_library_version" implementation "androidx.core:core-ktx:$core_ktx_version" + implementation "androidx.fragment:fragment-ktx:$fragment_ktx_version" implementation "com.google.android.material:material:$material_design_version" testImplementation project(':apptentive-core-test') diff --git a/apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalDialogFragment.kt b/apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalDialogFragment.kt index 61b6678a4..03392df5f 100644 --- a/apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalDialogFragment.kt +++ b/apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalDialogFragment.kt @@ -8,23 +8,18 @@ import android.view.View.TEXT_ALIGNMENT_CENTER import android.view.View.TEXT_ALIGNMENT_VIEW_END import android.view.ViewGroup import android.widget.TextView -import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.widget.DialogTitle import androidx.fragment.app.DialogFragment +import androidx.fragment.app.viewModels import apptentive.com.android.feedback.notes.R -import apptentive.com.android.feedback.platform.AndroidEngagementContext -import apptentive.com.android.ui.overrideTheme import com.google.android.material.dialog.MaterialAlertDialogBuilder -internal class TextModalDialogFragment( - val context: AndroidEngagementContext, - private val viewModel: TextModalViewModel -) : DialogFragment() { +internal class TextModalDialogFragment : DialogFragment() { - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - retainInstance = true + private val viewModel by viewModels() - return MaterialAlertDialogBuilder(context.androidContext).apply { + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext()).apply { val inflater = LayoutInflater.from(context) val contentView = inflater.inflate(R.layout.apptentive_note, null) setView(contentView) @@ -41,6 +36,7 @@ internal class TextModalDialogFragment( } val viewGroup = contentView.findViewById(R.id.apptentive_note_button_bar) + viewModel.onDismiss = { this@TextModalDialogFragment.dismiss()} viewModel.actions.forEach { action -> val button = inflater.inflate(R.layout.apptentive_note_action, null) as TextView button.text = action.title diff --git a/apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalInteraction.kt b/apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalInteraction.kt index 43fc99947..f1ad2c58f 100644 --- a/apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalInteraction.kt +++ b/apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalInteraction.kt @@ -11,6 +11,7 @@ internal class TextModalInteraction( val body: String?, val actions: List ) : Interaction(id, InteractionType.TextModal) { + override fun toString(): String { return "${javaClass.simpleName} (id=$id, title=\"$title\", body=\"$body\", actions=$actions)" } diff --git a/apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalInteractionFactory.kt b/apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalInteractionFactory.kt new file mode 100644 index 000000000..d61059d66 --- /dev/null +++ b/apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalInteractionFactory.kt @@ -0,0 +1,16 @@ +package apptentive.com.android.feedback.textmodal + +import apptentive.com.android.core.Provider + + +internal interface TextModalInteractionFactory { + fun getTextModalInteraction(): TextModalInteraction +} + +internal class TextModalInteractionProvider(val interaction: TextModalInteraction) : Provider { + override fun get(): TextModalInteractionFactory { + return object : TextModalInteractionFactory { + override fun getTextModalInteraction(): TextModalInteraction = interaction + } + } +} \ No newline at end of file diff --git a/apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalInteractionLauncher.kt b/apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalInteractionLauncher.kt index 8241d2c1c..a71fb7aa0 100644 --- a/apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalInteractionLauncher.kt +++ b/apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalInteractionLauncher.kt @@ -1,5 +1,6 @@ package apptentive.com.android.feedback.textmodal +import apptentive.com.android.core.DependencyProvider import apptentive.com.android.feedback.INTERACTIONS import apptentive.com.android.feedback.platform.AndroidEngagementContext import apptentive.com.android.feedback.platform.AndroidViewInteractionLauncher @@ -19,12 +20,8 @@ internal class TextModalInteractionLauncher : AndroidViewInteractionLauncher().getTextModalInteraction() + val context = DependencyProvider.of().engagementContext() val title = interaction.title val message = interaction.body val actions = interaction.actions.mapIndexed { index, action -> diff --git a/apptentive-notes/src/test/java/apptentive/com/android/feedback/notes/viewmodel/TextModalViewModelTest.kt b/apptentive-notes/src/test/java/apptentive/com/android/feedback/notes/viewmodel/TextModalViewModelTest.kt index c0ca2ef80..b6226faa7 100644 --- a/apptentive-notes/src/test/java/apptentive/com/android/feedback/notes/viewmodel/TextModalViewModelTest.kt +++ b/apptentive-notes/src/test/java/apptentive/com/android/feedback/notes/viewmodel/TextModalViewModelTest.kt @@ -1,17 +1,23 @@ package apptentive.com.android.feedback.notes.viewmodel import apptentive.com.android.TestCase +import apptentive.com.android.core.DependencyProvider +import apptentive.com.android.core.Provider import apptentive.com.android.feedback.EngagementResult import apptentive.com.android.feedback.engagement.EngageArgs import apptentive.com.android.feedback.engagement.EngagementCallback +import apptentive.com.android.feedback.engagement.EngagementContext +import apptentive.com.android.feedback.engagement.AndroidEngagementContextFactory import apptentive.com.android.feedback.engagement.Event import apptentive.com.android.feedback.engagement.InvocationCallback import apptentive.com.android.feedback.engagement.MockEngagementContext import apptentive.com.android.feedback.engagement.criteria.InvocationConverter import apptentive.com.android.feedback.model.InvocationData import apptentive.com.android.feedback.textmodal.TextModalInteraction +import apptentive.com.android.feedback.textmodal.TextModalInteractionProvider import apptentive.com.android.feedback.textmodal.TextModalViewModel import com.google.common.truth.Truth.assertThat +import org.junit.Before import org.junit.Test class TextModalViewModelTest : TestCase() { @@ -41,17 +47,24 @@ class TextModalViewModelTest : TestCase() { ) ) + @Before + fun start() { + DependencyProvider.register(TextModalInteractionProvider(interaction)) + } + //region Interaction @Test fun testInvokeInteraction() { val targetInteractionId = "target_id" - val viewModel = createViewModel( - onInvoke = { - // trick it to think an interaction has been invoked - EngagementResult.InteractionShown(targetInteractionId) - } - ) + DependencyProvider.register(MockEngagementContextFactory { + createEngagementContext( + null, + { EngagementResult.InteractionShown(targetInteractionId) } + ) + }) + + val viewModel = createViewModel() // check action title val action = viewModel.actions[0] @@ -81,12 +94,15 @@ class TextModalViewModelTest : TestCase() { @Test fun testInvokeMissingInteraction() { - val viewModel = createViewModel( - onInvoke = { - // no interactions to invoke - EngagementResult.InteractionNotShown("No runnable interactions") - } - ) + DependencyProvider.register(MockEngagementContextFactory { + createEngagementContext( + null, + { + EngagementResult.InteractionNotShown("No runnable interactions") + } + ) + }) + val viewModel = createViewModel() // check action title val action = viewModel.actions[0] @@ -123,13 +139,19 @@ class TextModalViewModelTest : TestCase() { fun testEventAction() { // NOTE: this is not supported on the backend yet!!! val targetInteractionId = "target_id" - val viewModel = createViewModel( - onEngage = { - // trick it to think an interaction has been invoked - if (it.event.name == TARGET_EVENT) EngagementResult.InteractionShown(targetInteractionId) - else EngagementResult.InteractionNotShown("No runnable interactions") - } - ) + + DependencyProvider.register(MockEngagementContextFactory { + createEngagementContext( + { + // trick it to think an interaction has been invoked + if (it.event.name == TARGET_EVENT) EngagementResult.InteractionShown(targetInteractionId) + else EngagementResult.InteractionNotShown("No runnable interactions") + }, + null + ) + }) + + val viewModel = createViewModel() // check action title val action = viewModel.actions[1] @@ -164,12 +186,18 @@ class TextModalViewModelTest : TestCase() { @Test fun testMissingEventAction() { // NOTE: this is not supported on the backend yet!!! - val viewModel = createViewModel( - onEngage = { - // no interactions to invoke - EngagementResult.InteractionNotShown("No runnable interactions") - } - ) + + DependencyProvider.register(MockEngagementContextFactory { + createEngagementContext( + { + // no interactions to invoke + EngagementResult.InteractionNotShown("No runnable interactions") + }, + null + ) + }) + + val viewModel = createViewModel() // check action title val action = viewModel.actions[1] @@ -207,6 +235,9 @@ class TextModalViewModelTest : TestCase() { @Test fun testDismissAction() { + DependencyProvider.register(MockEngagementContextFactory { + createEngagementContext(null, null) + }) val viewModel = createViewModel() // check action title @@ -239,6 +270,9 @@ class TextModalViewModelTest : TestCase() { @Test fun testCancel() { + DependencyProvider.register(MockEngagementContextFactory { + createEngagementContext(null, null) + }) val viewModel = createViewModel() // invoke action @@ -258,15 +292,8 @@ class TextModalViewModelTest : TestCase() { //region Helpers - private fun createViewModel( - onEngage: EngagementCallback? = null, - onInvoke: InvocationCallback? = null - ): TextModalViewModel { - val context = createEngagementContext(onEngage, onInvoke) - val viewModel = TextModalViewModel( - context = context, - interaction = interaction - ) + private fun createViewModel(): TextModalViewModel { + val viewModel = TextModalViewModel() viewModel.onDismiss = { addResult(RESULT_DISMISS_UI) } return viewModel } @@ -313,4 +340,14 @@ class TextModalViewModelTest : TestCase() { } //endregion +} + +class MockEngagementContextFactory(val getEngagementContext: () -> EngagementContext) : Provider { + override fun get(): AndroidEngagementContextFactory { + return object : AndroidEngagementContextFactory { + override fun engagementContext(): EngagementContext { + return getEngagementContext() + } + } + } } \ No newline at end of file diff --git a/apptentive-ratings/build.gradle b/apptentive-ratings/build.gradle index a12269a0b..0017fc31c 100644 --- a/apptentive-ratings/build.gradle +++ b/apptentive-ratings/build.gradle @@ -31,6 +31,7 @@ dependencies { implementation project(':apptentive-feedback') implementation "androidx.appcompat:appcompat:$appcompat_library_version" implementation "androidx.core:core-ktx:$core_ktx_version" + implementation "androidx.fragment:fragment-ktx:$fragment_ktx_version" implementation "com.google.android.material:material:$material_design_version" testImplementation project(':apptentive-core-test') diff --git a/apptentive-ratings/src/main/java/apptentive/com/android/feedback/ratingdialog/RatingDialogFragment.kt b/apptentive-ratings/src/main/java/apptentive/com/android/feedback/ratingdialog/RatingDialogFragment.kt index 973fa0483..6a9649392 100644 --- a/apptentive-ratings/src/main/java/apptentive/com/android/feedback/ratingdialog/RatingDialogFragment.kt +++ b/apptentive-ratings/src/main/java/apptentive/com/android/feedback/ratingdialog/RatingDialogFragment.kt @@ -1,27 +1,21 @@ package apptentive.com.android.feedback.ratingdialog import android.app.Dialog -import android.content.DialogInterface import android.os.Bundle import android.view.LayoutInflater import android.widget.TextView -import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.widget.DialogTitle import androidx.fragment.app.DialogFragment -import apptentive.com.android.feedback.platform.AndroidEngagementContext +import androidx.fragment.app.viewModels import apptentive.com.android.feedback.ratings.R -import apptentive.com.android.ui.overrideTheme import com.google.android.material.dialog.MaterialAlertDialogBuilder -internal class RatingDialogFragment( - val context: AndroidEngagementContext, - private val viewModel: RatingDialogViewModel -) : DialogFragment() { +internal class RatingDialogFragment : DialogFragment() { - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - retainInstance = true + private val viewModel by viewModels() - return MaterialAlertDialogBuilder(context.androidContext).apply { + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext()).apply { val inflater = LayoutInflater.from(context) val contentView = inflater.inflate(R.layout.apptentive_rating_dialog, null) setView(contentView) diff --git a/apptentive-ratings/src/main/java/apptentive/com/android/feedback/ratingdialog/RatingDialogInteractionFactory.kt b/apptentive-ratings/src/main/java/apptentive/com/android/feedback/ratingdialog/RatingDialogInteractionFactory.kt new file mode 100644 index 000000000..86674fbc6 --- /dev/null +++ b/apptentive-ratings/src/main/java/apptentive/com/android/feedback/ratingdialog/RatingDialogInteractionFactory.kt @@ -0,0 +1,15 @@ +package apptentive.com.android.feedback.ratingdialog + +import apptentive.com.android.core.Provider + +internal interface RatingDialogInteractionFactory { + fun getRatingDialogInteraction(): RatingDialogInteraction +} + +internal class RatingDialogInteractionProvider(val interaction: RatingDialogInteraction) : Provider { + override fun get(): RatingDialogInteractionFactory { + return object : RatingDialogInteractionFactory { + override fun getRatingDialogInteraction(): RatingDialogInteraction = interaction + } + } +} \ No newline at end of file diff --git a/apptentive-ratings/src/main/java/apptentive/com/android/feedback/ratingdialog/RatingDialogInteractionLauncher.kt b/apptentive-ratings/src/main/java/apptentive/com/android/feedback/ratingdialog/RatingDialogInteractionLauncher.kt index 1fa2de87b..0527c1cce 100644 --- a/apptentive-ratings/src/main/java/apptentive/com/android/feedback/ratingdialog/RatingDialogInteractionLauncher.kt +++ b/apptentive-ratings/src/main/java/apptentive/com/android/feedback/ratingdialog/RatingDialogInteractionLauncher.kt @@ -1,5 +1,6 @@ package apptentive.com.android.feedback.ratingdialog +import apptentive.com.android.core.DependencyProvider import apptentive.com.android.feedback.INTERACTIONS import apptentive.com.android.feedback.platform.AndroidEngagementContext import apptentive.com.android.feedback.platform.AndroidViewInteractionLauncher @@ -13,13 +14,11 @@ internal class RatingDialogInteractionLauncher : AndroidViewInteractionLauncher< Log.i(INTERACTIONS, "Rating Dialog interaction launched with title: ${interaction.title}") Log.v(INTERACTIONS, "Rating Dialog interaction data: $interaction") - val viewModel = RatingDialogViewModel(context, interaction) - context.executors.main.execute { try { val fragmentManager = context.getFragmentManager() - - val ratingDialog = RatingDialogFragment(context, viewModel) + DependencyProvider.register(RatingDialogInteractionProvider(interaction)) + val ratingDialog = RatingDialogFragment() ratingDialog.show(fragmentManager, RatingDialogInteraction.TAG) } catch (exception: Exception) { Log.e(INTERACTIONS, "Could not start Rating Dialog interaction", exception) diff --git a/apptentive-ratings/src/main/java/apptentive/com/android/feedback/ratingdialog/RatingDialogViewModel.kt b/apptentive-ratings/src/main/java/apptentive/com/android/feedback/ratingdialog/RatingDialogViewModel.kt index 0ae586da3..a067c85d1 100644 --- a/apptentive-ratings/src/main/java/apptentive/com/android/feedback/ratingdialog/RatingDialogViewModel.kt +++ b/apptentive-ratings/src/main/java/apptentive/com/android/feedback/ratingdialog/RatingDialogViewModel.kt @@ -1,14 +1,16 @@ package apptentive.com.android.feedback.ratingdialog +import androidx.lifecycle.ViewModel +import apptentive.com.android.core.DependencyProvider import apptentive.com.android.feedback.INTERACTIONS -import apptentive.com.android.feedback.engagement.EngagementContext +import apptentive.com.android.feedback.engagement.AndroidEngagementContextFactory import apptentive.com.android.feedback.engagement.Event import apptentive.com.android.util.Log -internal class RatingDialogViewModel( - private val context: EngagementContext, - private val interaction: RatingDialogInteraction -) { +internal class RatingDialogViewModel : ViewModel() { + private val context = DependencyProvider.of().engagementContext() + private val interaction = DependencyProvider.of().getRatingDialogInteraction() + val title = interaction.title val message = interaction.body val rateText = interaction.rateText diff --git a/apptentive-ratings/src/test/java/apptentive/com/android/ratings/ratingdialog/RatingDialogViewModelTest.kt b/apptentive-ratings/src/test/java/apptentive/com/android/ratings/ratingdialog/RatingDialogViewModelTest.kt index 36c62083f..203b91878 100644 --- a/apptentive-ratings/src/test/java/apptentive/com/android/ratings/ratingdialog/RatingDialogViewModelTest.kt +++ b/apptentive-ratings/src/test/java/apptentive/com/android/ratings/ratingdialog/RatingDialogViewModelTest.kt @@ -1,13 +1,18 @@ package apptentive.com.android.ratings.ratingdialog import apptentive.com.android.TestCase +import apptentive.com.android.core.DependencyProvider +import apptentive.com.android.core.Provider import apptentive.com.android.feedback.EngagementResult import apptentive.com.android.feedback.engagement.EngageArgs import apptentive.com.android.feedback.engagement.EngagementCallback +import apptentive.com.android.feedback.engagement.EngagementContext +import apptentive.com.android.feedback.engagement.AndroidEngagementContextFactory import apptentive.com.android.feedback.engagement.Event import apptentive.com.android.feedback.engagement.InvocationCallback import apptentive.com.android.feedback.engagement.MockEngagementContext import apptentive.com.android.feedback.ratingdialog.RatingDialogInteraction +import apptentive.com.android.feedback.ratingdialog.RatingDialogInteractionProvider import apptentive.com.android.feedback.ratingdialog.RatingDialogViewModel import org.junit.Test @@ -28,12 +33,21 @@ class RatingDialogViewModelTest : TestCase() { @Test fun testInvokeInteractions() { val targetInteractionId = "target_id" - val viewModel = createViewModel( - onInvoke = { - // trick it to think an interaction has been invoked - EngagementResult.InteractionShown(targetInteractionId) + + DependencyProvider.register(object : Provider { + override fun get(): AndroidEngagementContextFactory { + return object : AndroidEngagementContextFactory { + override fun engagementContext(): EngagementContext { + return createEngagementContext( + null, + { EngagementResult.InteractionShown(targetInteractionId) } + ) + } + } } - ) + }) + DependencyProvider.register(RatingDialogInteractionProvider(interaction)) + val viewModel = createViewModel() viewModel.onRateButton() viewModel.onRemindButton() @@ -69,15 +83,8 @@ class RatingDialogViewModelTest : TestCase() { //region Helpers - private fun createViewModel( - onEngage: EngagementCallback? = null, - onInvoke: InvocationCallback? = null - ): RatingDialogViewModel { - val context = createEngagementContext(onEngage, onInvoke) - return RatingDialogViewModel( - context = context, - interaction = interaction - ) + private fun createViewModel(): RatingDialogViewModel { + return RatingDialogViewModel() } private fun createEngagementContext( diff --git a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/utils/ViewModelUtils.kt b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/utils/ViewModelUtils.kt index f93ac2cf6..15174dd8b 100644 --- a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/utils/ViewModelUtils.kt +++ b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/utils/ViewModelUtils.kt @@ -2,7 +2,7 @@ package apptentive.com.android.feedback.survey.utils import apptentive.com.android.core.DependencyProvider import apptentive.com.android.feedback.engagement.EngagementContext -import apptentive.com.android.feedback.engagement.EngagementContextFactory +import apptentive.com.android.feedback.engagement.AndroidEngagementContextFactory import apptentive.com.android.feedback.engagement.Event import apptentive.com.android.feedback.engagement.interactions.InteractionType import apptentive.com.android.feedback.survey.SurveyModelFactory @@ -24,7 +24,7 @@ internal fun createSurveyViewModel( they all will be independent */ interactionId: String = generateUUID(), - context: EngagementContext = DependencyProvider.of().engagementContext(), + context: EngagementContext = DependencyProvider.of().engagementContext(), surveyModel: SurveyModel = DependencyProvider.of().getSurveyModel(), ) = SurveyViewModel( model = surveyModel, diff --git a/build.gradle b/build.gradle index df6eb039b..d06ad4e81 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,7 @@ buildscript { // https://developer.android.com/jetpack/androidx/explorer core_ktx_version = '1.7.0' activity_ktx_version = '1.4.0' + fragment_ktx_version = '1.4.0' livedata_ktx_version = '2.4.0' appcompat_library_version = '1.4.0' annotations_version = '1.3.0' From 333375b2ffcfd907b401a39b81f3b1a0c53c1438 Mon Sep 17 00:00:00 2001 From: ChaseApptentive <85038972+ChaseApptentive@users.noreply.github.com> Date: Fri, 4 Feb 2022 13:58:50 -0600 Subject: [PATCH 016/491] PBI-2673 Nexus publish (#207) [PBI-2673] Nexus sonatype publish script along with Dokka & signature support --- .../src/main/res/values/styles.xml | 2 +- apptentive-core/build.gradle | 4 +- apptentive-sdk-kit/build.gradle | 22 ++++--- build.gradle | 53 +++++++++++++++- gradle/wrapper/gradle-wrapper.properties | 2 +- ...shutil.gradle => jfrog-publish-util.gradle | 22 +++---- publish.gradle | 62 ++++++++++++++++++- settings.gradle | 19 ------ sonatype-publish-util.gradle | 34 ++++++++++ 9 files changed, 170 insertions(+), 50 deletions(-) rename publishutil.gradle => jfrog-publish-util.gradle (70%) create mode 100644 sonatype-publish-util.gradle diff --git a/apptentive-core-ui/src/main/res/values/styles.xml b/apptentive-core-ui/src/main/res/values/styles.xml index 357d11c72..22fa5d6c4 100644 --- a/apptentive-core-ui/src/main/res/values/styles.xml +++ b/apptentive-core-ui/src/main/res/values/styles.xml @@ -1,4 +1,4 @@ - + - - - + + + + + + + + + + + + + + diff --git a/apptentive-core-ui/src/main/java/apptentive/com/android/ui/ButtonBarLayout.java b/apptentive-core-ui/src/main/java/apptentive/com/android/ui/ButtonBarLayout.java deleted file mode 100644 index 403c595ff..000000000 --- a/apptentive-core-ui/src/main/java/apptentive/com/android/ui/ButtonBarLayout.java +++ /dev/null @@ -1,168 +0,0 @@ -package apptentive.com.android.ui; - -import android.content.Context; -import android.content.res.TypedArray; -import android.util.AttributeSet; -import android.view.Gravity; -import android.view.View; -import android.widget.LinearLayout; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.R; -import androidx.core.view.ViewCompat; - -/** - * An extension of LinearLayout that automatically switches to vertical - * orientation when it can't fit its child views horizontally. - * - * Note: this is an exact copy from appcompat library. - * The ONLY difference - this implementation does not reverse view order when stacked. - */ -public class ButtonBarLayout extends LinearLayout { - /** Amount of the second button to "peek" above the fold when stacked. */ - private static final int PEEK_BUTTON_DP = 16; - - /** Amount of buttons to show before stacked + 1 (first child is always a Space view) */ - private static final int STACK_AFTER_COUNT = 2; - - /** Whether the current configuration allows stacking. */ - private boolean mAllowStacking; - - private int mLastWidthSize = -1; - - private int mMinimumHeight = 0; - - public ButtonBarLayout(@NonNull Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ButtonBarLayout); - ViewCompat.saveAttributeDataForStyleable(this, context, R.styleable.ButtonBarLayout, - attrs, ta, 0, 0); - mAllowStacking = ta.getBoolean(R.styleable.ButtonBarLayout_allowStacking, true); - ta.recycle(); - } - - public void setAllowStacking(boolean allowStacking) { - if (mAllowStacking != allowStacking) { - mAllowStacking = allowStacking; - if (!mAllowStacking && getOrientation() == LinearLayout.VERTICAL) { - setStacked(false); - } - requestLayout(); - } - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - final int widthSize = MeasureSpec.getSize(widthMeasureSpec); - - if (mAllowStacking) { - if (getChildCount() > STACK_AFTER_COUNT) { - setStacked(true); - } else if (widthSize > mLastWidthSize && isStacked()) { - // We're being measured wider this time, try un-stacking. - setStacked(false); - } - - mLastWidthSize = widthSize; - } - - boolean needsRemeasure = false; - - // If we're not stacked, make sure the measure spec is AT_MOST rather - // than EXACTLY. This ensures that we'll still get TOO_SMALL so that we - // know to stack the buttons. - final int initialWidthMeasureSpec; - if (!isStacked() && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) { - initialWidthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST); - - // We'll need to remeasure again to fill excess space. - needsRemeasure = true; - } else { - initialWidthMeasureSpec = widthMeasureSpec; - } - - super.onMeasure(initialWidthMeasureSpec, heightMeasureSpec); - - if (mAllowStacking && !isStacked()) { - final boolean stack; - - final int measuredWidth = getMeasuredWidthAndState(); - final int measuredWidthState = measuredWidth & View.MEASURED_STATE_MASK; - stack = measuredWidthState == View.MEASURED_STATE_TOO_SMALL; - - if (stack) { - setStacked(true); - // Measure again in the new orientation. - needsRemeasure = true; - } - } - - if (needsRemeasure) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - } - - // Compute minimum height such that, when stacked, some portion of the - // second button is visible. - int minHeight = 0; - final int firstVisible = getNextVisibleChildIndex(0); - if (firstVisible >= 0) { - final View firstButton = getChildAt(firstVisible); - final LayoutParams firstParams = (LayoutParams) firstButton.getLayoutParams(); - minHeight += getPaddingTop() + firstButton.getMeasuredHeight() - + firstParams.topMargin + firstParams.bottomMargin; - if (isStacked()) { - final int secondVisible = getNextVisibleChildIndex(firstVisible + 1); - if (secondVisible >= 0) { - minHeight += getChildAt(secondVisible).getPaddingTop() - + (int) (PEEK_BUTTON_DP * getResources().getDisplayMetrics().density); - } - } else { - minHeight += getPaddingBottom(); - } - } - - if (ViewCompat.getMinimumHeight(this) != minHeight) { - setMinimumHeight(minHeight); - } - } - - private int getNextVisibleChildIndex(int index) { - for (int i = index, count = getChildCount(); i < count; i++) { - if (getChildAt(i).getVisibility() == View.VISIBLE) { - return i; - } - } - return -1; - } - - @Override - public int getMinimumHeight() { - return Math.max(mMinimumHeight, super.getMinimumHeight()); - } - - private void setStacked(boolean stacked) { - setOrientation(stacked ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL); - setGravity(stacked ? Gravity.RIGHT : Gravity.BOTTOM); - - final View spacer = findViewById(R.id.spacer); - if (spacer != null) { - spacer.setVisibility(stacked ? View.GONE : View.INVISIBLE); - } - - /* - NOTE: We deliberately disallow child view order reverse here - - // Reverse the child order. This is specific to the Material button - // bar's layout XML and will probably not generalize. - final int childCount = getChildCount(); - for (int i = childCount - 2; i >= 0; i--) { - bringChildToFront(getChildAt(i)); - } - */ - } - - private boolean isStacked() { - return getOrientation() == LinearLayout.VERTICAL; - } -} \ No newline at end of file diff --git a/apptentive-core-ui/src/main/res/values-night/colors.xml b/apptentive-core-ui/src/main/res/values-night/colors.xml index a2bd110e5..4addc2127 100644 --- a/apptentive-core-ui/src/main/res/values-night/colors.xml +++ b/apptentive-core-ui/src/main/res/values-night/colors.xml @@ -12,4 +12,8 @@ #CF6679 #4589CD + + #424242 + #FFFFFF + #B3FFFFFF \ No newline at end of file diff --git a/apptentive-core-ui/src/main/res/values/apptentive-attrs.xml b/apptentive-core-ui/src/main/res/values/apptentive-attrs.xml index 942f3aaf0..79166a079 100644 --- a/apptentive-core-ui/src/main/res/values/apptentive-attrs.xml +++ b/apptentive-core-ui/src/main/res/values/apptentive-attrs.xml @@ -1,9 +1,40 @@ - - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apptentive-core-ui/src/main/res/values/colors.xml b/apptentive-core-ui/src/main/res/values/colors.xml index f6eb0ec1f..f129928e0 100644 --- a/apptentive-core-ui/src/main/res/values/colors.xml +++ b/apptentive-core-ui/src/main/res/values/colors.xml @@ -11,4 +11,9 @@ #FFFFFF #FFFFFF #E1E1E1 + + + #FFFFFF + #000000 + #80000000 diff --git a/apptentive-core-ui/src/main/res/values/dimens.xml b/apptentive-core-ui/src/main/res/values/dimens.xml new file mode 100644 index 000000000..a5794fdb8 --- /dev/null +++ b/apptentive-core-ui/src/main/res/values/dimens.xml @@ -0,0 +1,6 @@ + + + + 14sp + 18sp + \ No newline at end of file diff --git a/apptentive-core-ui/src/main/res/values/integers.xml b/apptentive-core-ui/src/main/res/values/integers.xml index 41ba94151..8dd3d9de5 100644 --- a/apptentive-core-ui/src/main/res/values/integers.xml +++ b/apptentive-core-ui/src/main/res/values/integers.xml @@ -2,4 +2,10 @@ -1 -2 + + + 0 + 1 + 2 + 3 \ No newline at end of file diff --git a/apptentive-core-ui/src/main/res/values/styles.xml b/apptentive-core-ui/src/main/res/values/styles.xml index 22fa5d6c4..331ab6825 100644 --- a/apptentive-core-ui/src/main/res/values/styles.xml +++ b/apptentive-core-ui/src/main/res/values/styles.xml @@ -11,7 +11,39 @@ @color/apptentive_color_on_background @color/apptentive_color_text_color_secondary @color/apptentive_color_error - @style/Widget.MaterialComponents.Button + + + @style/Apptentive.Note.Layout + @style/Apptentive.TextAppearance.Title.WithMessage + @style/Apptentive.TextAppearance.Title.NoMessage + @style/Apptentive.TextAppearance.Subhead.Secondary + @style/Apptentive.Widget.Button.Borderless + + + @style/Widget.MaterialComponents.Button + + + @dimen/apptentive_text_size_title + @dimen/apptentive_text_size_default + @dimen/apptentive_text_size_default + @dimen/apptentive_text_size_default + + + @color/apptentive_dialog_background_color + + + @color/apptentive_color_text_title + @color/apptentive_color_text_default + @color/apptentive_color_text_default + ?colorPrimary + + + @integer/apptentive_typeface_normal + + ?apptentiveTypefaceDefault + ?apptentiveTypefaceDefault + ?apptentiveTypefaceDefault + ?apptentiveTypefaceDefault @@ -27,4 +59,34 @@ ?attr/textAppearanceHeadline6 + + + + + + + + + + + + diff --git a/apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalDialogFragment.kt b/apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalDialogFragment.kt index 1b1b911f5..81a250381 100644 --- a/apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalDialogFragment.kt +++ b/apptentive-notes/src/main/java/apptentive/com/android/feedback/textmodal/TextModalDialogFragment.kt @@ -1,56 +1,78 @@ package apptentive.com.android.feedback.textmodal +import android.annotation.SuppressLint import android.app.Dialog import android.content.DialogInterface import android.os.Bundle +import android.view.Gravity import android.view.LayoutInflater -import android.view.View -import android.view.View.TEXT_ALIGNMENT_CENTER -import android.view.View.TEXT_ALIGNMENT_VIEW_END -import android.view.ViewGroup -import android.widget.TextView -import androidx.appcompat.widget.DialogTitle +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.widget.LinearLayout +import androidx.appcompat.view.ContextThemeWrapper import androidx.fragment.app.DialogFragment import androidx.fragment.app.viewModels import apptentive.com.android.feedback.notes.R +import apptentive.com.android.ui.overrideTheme +import com.google.android.material.button.MaterialButton import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.textview.MaterialTextView internal class TextModalDialogFragment : DialogFragment() { private val viewModel by viewModels() + @SuppressLint("UseGetLayoutInflater", "InflateParams") override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val dialog = MaterialAlertDialogBuilder(requireContext()).apply { - val inflater = LayoutInflater.from(context) + + val contextWrapper = ContextThemeWrapper(requireContext(), R.style.Theme_Apptentive).apply { + overrideTheme() + } + val inflater = LayoutInflater.from(contextWrapper) val contentView = inflater.inflate(R.layout.apptentive_note, null) setView(contentView) - val titleView = contentView.findViewById(R.id.apptentive_note_title) - if (viewModel.title != null && viewModel.message != null) titleView.text = viewModel.title - else titleView.visibility = View.GONE + val noteLayout = contentView.findViewById(R.id.apptentive_note) - val messageView = contentView.findViewById(R.id.apptentive_note_message) - when { - viewModel.message != null -> messageView.text = viewModel.message - viewModel.title != null -> messageView.text = viewModel.title - else -> messageView.visibility = View.GONE + //region Title + val titleView = inflater.inflate( + if (viewModel.message != null) R.layout.apptentive_note_title_with_message + else R.layout.apptentive_note_title_no_message, null + ) as MaterialTextView + titleView.text = viewModel.title + noteLayout.addView(titleView) + //endregion + + //region Message + if (viewModel.message != null) { + val messageView = inflater.inflate(R.layout.apptentive_note_message, null) as MaterialTextView + messageView.text = viewModel.message + noteLayout.addView(messageView) } + //endregion + + //region Actions + val buttonLayout = inflater.inflate(R.layout.apptentive_note_actions, null) as LinearLayout + noteLayout.addView(buttonLayout) - val viewGroup = contentView.findViewById(R.id.apptentive_note_button_bar) - viewModel.onDismiss = { this@TextModalDialogFragment.dismiss()} viewModel.actions.forEach { action -> - val button = inflater.inflate(R.layout.apptentive_note_action, null) as TextView + val button = inflater.inflate(R.layout.apptentive_note_action, null) as MaterialButton + + button.layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { + gravity = Gravity.END + } button.text = action.title - button.textAlignment = - if (viewModel.actions.size > 1) TEXT_ALIGNMENT_VIEW_END - else TEXT_ALIGNMENT_CENTER - viewGroup.addView(button) + buttonLayout.addView(button) button.setOnClickListener { action.invoke() } } + //endregion + + viewModel.onDismiss = { this@TextModalDialogFragment.dismiss() } }.create() + return dialog.apply { setCanceledOnTouchOutside(false) } diff --git a/apptentive-notes/src/main/res/layout/apptentive_note.xml b/apptentive-notes/src/main/res/layout/apptentive_note.xml index ba0520f0d..c1951719c 100644 --- a/apptentive-notes/src/main/res/layout/apptentive_note.xml +++ b/apptentive-notes/src/main/res/layout/apptentive_note.xml @@ -1,24 +1,17 @@ + android:layout_height="wrap_content" + android:paddingHorizontal="8dp" + android:paddingTop="18dp" + android:paddingBottom="2dp"> + android:orientation="vertical" /> - - - - - - - diff --git a/apptentive-notes/src/main/res/layout/apptentive_note_action.xml b/apptentive-notes/src/main/res/layout/apptentive_note_action.xml index 67c2fd1f2..3d173363b 100644 --- a/apptentive-notes/src/main/res/layout/apptentive_note_action.xml +++ b/apptentive-notes/src/main/res/layout/apptentive_note_action.xml @@ -1,8 +1,6 @@ \ No newline at end of file + android:layout_height="wrap_content" /> \ No newline at end of file diff --git a/apptentive-notes/src/main/res/layout/apptentive_note_actions.xml b/apptentive-notes/src/main/res/layout/apptentive_note_actions.xml index 46fd1318a..524dda06a 100644 --- a/apptentive-notes/src/main/res/layout/apptentive_note_actions.xml +++ b/apptentive-notes/src/main/res/layout/apptentive_note_actions.xml @@ -1,24 +1,10 @@ - - - - + android:layout_gravity="end" + android:orientation="vertical" + android:textAlignment="viewEnd" + android:paddingTop="2dp" /> \ No newline at end of file diff --git a/apptentive-notes/src/main/res/layout/apptentive_note_message.xml b/apptentive-notes/src/main/res/layout/apptentive_note_message.xml index 06cf39a59..01ec2d621 100644 --- a/apptentive-notes/src/main/res/layout/apptentive_note_message.xml +++ b/apptentive-notes/src/main/res/layout/apptentive_note_message.xml @@ -1,8 +1,9 @@ - + + \ No newline at end of file + android:minHeight="48dp" + android:paddingHorizontal="16dp" /> \ No newline at end of file diff --git a/apptentive-notes/src/main/res/layout/apptentive_note_title.xml b/apptentive-notes/src/main/res/layout/apptentive_note_title.xml deleted file mode 100644 index d7b40604f..000000000 --- a/apptentive-notes/src/main/res/layout/apptentive_note_title.xml +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/apptentive-notes/src/main/res/layout/apptentive_note_title_no_message.xml b/apptentive-notes/src/main/res/layout/apptentive_note_title_no_message.xml new file mode 100644 index 000000000..bf138a29b --- /dev/null +++ b/apptentive-notes/src/main/res/layout/apptentive_note_title_no_message.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/apptentive-notes/src/main/res/layout/apptentive_note_title_with_message.xml b/apptentive-notes/src/main/res/layout/apptentive_note_title_with_message.xml new file mode 100644 index 000000000..1cb073179 --- /dev/null +++ b/apptentive-notes/src/main/res/layout/apptentive_note_title_with_message.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/apptentive-survey/src/main/res/layout/apptentive_survey_footer.xml b/apptentive-survey/src/main/res/layout/apptentive_survey_footer.xml index 88388b280..656025215 100644 --- a/apptentive-survey/src/main/res/layout/apptentive_survey_footer.xml +++ b/apptentive-survey/src/main/res/layout/apptentive_survey_footer.xml @@ -8,7 +8,7 @@ Date: Wed, 9 Mar 2022 09:43:48 -0800 Subject: [PATCH 033/491] [PBI-3008] Love Dialog minor interface customization (#228) [PBI-3008] * LoveDialog minor customization --- app/src/main/res/values/styles.xml | 39 ++++++++++++--- .../src/main/res/values/apptentive-attrs.xml | 14 ++++-- .../src/main/res/values/styles.xml | 27 ++++++++--- .../enjoyment/EnjoymentDialogFragment.kt | 28 +++++++++-- .../layout/apptentive_enjoyment_dialog.xml | 47 +++++++++++++++++++ 5 files changed, 135 insertions(+), 20 deletions(-) create mode 100644 apptentive-enjoyment-dialog/src/main/res/layout/apptentive_enjoyment_dialog.xml diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 5e9c5d2e8..1906a65f1 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -23,7 +23,6 @@ - @@ -40,9 +39,6 @@ - - - @@ -50,6 +46,14 @@ + + + + + + + + @@ -64,17 +68,20 @@ + + + @@ -85,7 +92,9 @@ 8sp - + + + + + + + + diff --git a/apptentive-core-ui/src/main/res/values/apptentive-attrs.xml b/apptentive-core-ui/src/main/res/values/apptentive-attrs.xml index 79166a079..26aa8379d 100644 --- a/apptentive-core-ui/src/main/res/values/apptentive-attrs.xml +++ b/apptentive-core-ui/src/main/res/values/apptentive-attrs.xml @@ -7,7 +7,6 @@ - @@ -24,17 +23,22 @@ - - - - + + + + + + + + + \ No newline at end of file diff --git a/apptentive-core-ui/src/main/res/values/styles.xml b/apptentive-core-ui/src/main/res/values/styles.xml index 331ab6825..893f83019 100644 --- a/apptentive-core-ui/src/main/res/values/styles.xml +++ b/apptentive-core-ui/src/main/res/values/styles.xml @@ -17,7 +17,6 @@ @style/Apptentive.TextAppearance.Title.WithMessage @style/Apptentive.TextAppearance.Title.NoMessage @style/Apptentive.TextAppearance.Subhead.Secondary - @style/Apptentive.Widget.Button.Borderless @style/Widget.MaterialComponents.Button @@ -28,9 +27,6 @@ @dimen/apptentive_text_size_default @dimen/apptentive_text_size_default - - @color/apptentive_dialog_background_color - @color/apptentive_color_text_title @color/apptentive_color_text_default @@ -44,6 +40,14 @@ ?apptentiveTypefaceDefault ?apptentiveTypefaceDefault ?apptentiveTypefaceDefault + + + @style/Apptentive.LoveDialog.Layout + @style/Apptentive.TextAppearance.Title.NoMessage + + + @style/Apptentive.Widget.Button.Borderless + @color/apptentive_dialog_background_color @@ -60,18 +64,28 @@ + + - + + - + @@ -92,6 +97,23 @@ 8sp + + + + + + + + diff --git a/apptentive-core-ui/src/main/res/values/apptentive-attrs.xml b/apptentive-core-ui/src/main/res/values/apptentive-attrs.xml index 26aa8379d..6709fe76b 100644 --- a/apptentive-core-ui/src/main/res/values/apptentive-attrs.xml +++ b/apptentive-core-ui/src/main/res/values/apptentive-attrs.xml @@ -8,6 +8,11 @@ + + + + + diff --git a/apptentive-core-ui/src/main/res/values/dimens.xml b/apptentive-core-ui/src/main/res/values/dimens.xml index a5794fdb8..8f0c5d9ef 100644 --- a/apptentive-core-ui/src/main/res/values/dimens.xml +++ b/apptentive-core-ui/src/main/res/values/dimens.xml @@ -3,4 +3,8 @@ 14sp 18sp + 16dp + 8dp + 16dp + 48dp \ No newline at end of file diff --git a/apptentive-core-ui/src/main/res/values/styles.xml b/apptentive-core-ui/src/main/res/values/styles.xml index 893f83019..00ad8e74c 100644 --- a/apptentive-core-ui/src/main/res/values/styles.xml +++ b/apptentive-core-ui/src/main/res/values/styles.xml @@ -18,6 +18,11 @@ @style/Apptentive.TextAppearance.Title.NoMessage @style/Apptentive.TextAppearance.Subhead.Secondary + + @style/Apptentive.RatingDialog.Layout + @style/Apptentive.TextAppearance.Title + @style/Apptentive.TextAppearance.Subhead.Secondary + @style/Widget.MaterialComponents.Button @@ -71,6 +76,12 @@ ?apptentiveDialogBackground + + + + - - - - + - - - + + - - - + + + + - - + - + - + + + + + @@ -97,12 +156,6 @@ 8sp - - - - + + + + + + + + - + + + + + + + + + + + + + + + - - + - - + + + + + + + + + + + + diff --git a/apptentive-core-ui/src/main/res/color/apptentive_mtrl_outlined_stroke_color.xml b/apptentive-core-ui/src/main/res/color/apptentive_mtrl_outlined_stroke_color.xml new file mode 100644 index 000000000..856e8d20d --- /dev/null +++ b/apptentive-core-ui/src/main/res/color/apptentive_mtrl_outlined_stroke_color.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/apptentive-core-ui/src/main/res/color/apptentive_mtrl_selection_control_button_tint.xml b/apptentive-core-ui/src/main/res/color/apptentive_mtrl_selection_control_button_tint.xml new file mode 100644 index 000000000..f4a4167ce --- /dev/null +++ b/apptentive-core-ui/src/main/res/color/apptentive_mtrl_selection_control_button_tint.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/apptentive-core-ui/src/main/res/color/apptentive_mtrl_text_box_text_color.xml b/apptentive-core-ui/src/main/res/color/apptentive_mtrl_text_box_text_color.xml new file mode 100644 index 000000000..8e5fd670d --- /dev/null +++ b/apptentive-core-ui/src/main/res/color/apptentive_mtrl_text_box_text_color.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/apptentive-survey/src/main/res/drawable/ic_baseline_close_24.xml b/apptentive-core-ui/src/main/res/drawable/ic_baseline_close_24.xml similarity index 82% rename from apptentive-survey/src/main/res/drawable/ic_baseline_close_24.xml rename to apptentive-core-ui/src/main/res/drawable/ic_baseline_close_24.xml index 63f071b3c..ab046c7c0 100644 --- a/apptentive-survey/src/main/res/drawable/ic_baseline_close_24.xml +++ b/apptentive-core-ui/src/main/res/drawable/ic_baseline_close_24.xml @@ -1,5 +1,5 @@ diff --git a/apptentive-core-ui/src/main/res/values-night/colors.xml b/apptentive-core-ui/src/main/res/values-night/colors.xml index 4addc2127..bd685be2d 100644 --- a/apptentive-core-ui/src/main/res/values-night/colors.xml +++ b/apptentive-core-ui/src/main/res/values-night/colors.xml @@ -11,9 +11,4 @@ #C9C9C9 #CF6679 #4589CD - - - #424242 - #FFFFFF - #B3FFFFFF \ No newline at end of file diff --git a/apptentive-core-ui/src/main/res/values/apptentive-attrs.xml b/apptentive-core-ui/src/main/res/values/apptentive-attrs.xml index 6709fe76b..f4885e2d1 100644 --- a/apptentive-core-ui/src/main/res/values/apptentive-attrs.xml +++ b/apptentive-core-ui/src/main/res/values/apptentive-attrs.xml @@ -2,47 +2,89 @@ - + + + + + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + - - - + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + diff --git a/apptentive-core-ui/src/main/res/values/colors.xml b/apptentive-core-ui/src/main/res/values/colors.xml index f129928e0..f6eb0ec1f 100644 --- a/apptentive-core-ui/src/main/res/values/colors.xml +++ b/apptentive-core-ui/src/main/res/values/colors.xml @@ -11,9 +11,4 @@ #FFFFFF #FFFFFF #E1E1E1 - - - #FFFFFF - #000000 - #80000000 diff --git a/apptentive-core-ui/src/main/res/values/dimens.xml b/apptentive-core-ui/src/main/res/values/dimens.xml index 8f0c5d9ef..20b85c52b 100644 --- a/apptentive-core-ui/src/main/res/values/dimens.xml +++ b/apptentive-core-ui/src/main/res/values/dimens.xml @@ -1,8 +1,11 @@ - 14sp - 18sp + 14sp + 18sp + 20sp + 16sp + 12sp 16dp 8dp 16dp diff --git a/apptentive-core-ui/src/main/res/values/strings.xml b/apptentive-core-ui/src/main/res/values/strings.xml new file mode 100644 index 000000000..a5c3218bf --- /dev/null +++ b/apptentive-core-ui/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Close + \ No newline at end of file diff --git a/apptentive-core-ui/src/main/res/values/styles.xml b/apptentive-core-ui/src/main/res/values/styles.xml index 00ad8e74c..32368833b 100644 --- a/apptentive-core-ui/src/main/res/values/styles.xml +++ b/apptentive-core-ui/src/main/res/values/styles.xml @@ -12,47 +12,95 @@ @color/apptentive_color_text_color_secondary @color/apptentive_color_error - + + + + + @style/Apptentive.Widget.Dialog.Button + ?colorSurface + + + @style/Apptentive.LoveDialog.Layout + @style/Apptentive.TextAppearance.Dialog.Title.NoMessage + + @style/Apptentive.Note.Layout - @style/Apptentive.TextAppearance.Title.WithMessage - @style/Apptentive.TextAppearance.Title.NoMessage - @style/Apptentive.TextAppearance.Subhead.Secondary + @style/Apptentive.TextAppearance.Dialog.Title.WithMessage + @style/Apptentive.TextAppearance.Dialog.Title.NoMessage + @style/Apptentive.TextAppearance.Dialog.Message - + @style/Apptentive.RatingDialog.Layout - @style/Apptentive.TextAppearance.Title - @style/Apptentive.TextAppearance.Subhead.Secondary + @style/Apptentive.TextAppearance.Dialog.Title + @style/Apptentive.TextAppearance.Dialog.Message + + + + @style/Apptentive.Survey.Layout + + + @style/Apptentive.Widget.Survey.Toolbar + @style/Apptentive.Widget.Survey.BottomAppBar + + + @style/Apptentive.TextAppearance.Survey.Introduction + @style/Apptentive.TextAppearance.Survey.Title.Question + @style/Apptentive.TextAppearance.Survey.Caption.Instructions + @style/Apptentive.TextAppearance.Survey.Caption.Error + @style/Apptentive.TextAppearance.Survey.Caption.RangeLabel + @style/Apptentive.TextAppearance.Survey.Caption.TermsAndConditions + @style/Apptentive.TextAppearance.Survey.Error - - @style/Widget.MaterialComponents.Button + + @style/Apptentive.Widget.Survey.CheckBox + @style/Apptentive.Widget.Survey.RadioButton + @style/Apptentive.Widget.Survey.Range + @style/Apptentive.Widget.Survey.SingleLine.TextInputLayout + @style/Apptentive.Widget.Survey.SingleLine.EditText + @style/Apptentive.Widget.Survey.OtherTextField.TextInputLayout + @style/Apptentive.Widget.Survey.OtherTextField.EditText + @style/Apptentive.Widget.Survey.Button.Submit + ?colorPrimary + - @dimen/apptentive_text_size_title - @dimen/apptentive_text_size_default - @dimen/apptentive_text_size_default + @dimen/apptentive_text_size_dialog_title + @dimen/apptentive_text_size_default + @dimen/apptentive_text_size_question_title + @dimen/apptentive_text_size_survey_introduction + @dimen/apptentive_text_size_default @dimen/apptentive_text_size_default + @dimen/apptentive_text_size_default + @dimen/apptentive_text_size_caption + @dimen/apptentive_text_size_default - @color/apptentive_color_text_title - @color/apptentive_color_text_default - @color/apptentive_color_text_default - ?colorPrimary + + + + ?colorOnSurface + ?colorOnSurface + ?colorOnBackground + ?colorOnBackground + ?colorError + ?colorOnBackground + ?colorOnPrimary + ?colorOnBackground + ?colorOnBackground + ?colorOnBackground + ?colorOnPrimary @integer/apptentive_typeface_normal - + ?apptentiveTypefaceDefault + ?apptentiveTypefaceDefault ?apptentiveTypefaceDefault - ?apptentiveTypefaceDefault - ?apptentiveTypefaceDefault + ?apptentiveTypefaceDefault + ?apptentiveTypefaceDefault ?apptentiveTypefaceDefault - - - @style/Apptentive.LoveDialog.Layout - @style/Apptentive.TextAppearance.Title.NoMessage - - - @style/Apptentive.Widget.Button.Borderless - @color/apptentive_dialog_background_color + ?apptentiveTypefaceDefault + ?apptentiveTypefaceDefault + ?apptentiveTypefaceDefault @@ -61,58 +109,202 @@ @null - - - + + + + + + - + - + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + diff --git a/apptentive-enjoyment-dialog/src/main/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogFragment.kt b/apptentive-enjoyment-dialog/src/main/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogFragment.kt index 775689be4..da11f4567 100644 --- a/apptentive-enjoyment-dialog/src/main/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogFragment.kt +++ b/apptentive-enjoyment-dialog/src/main/java/apptentive/com/android/feedback/enjoyment/EnjoymentDialogFragment.kt @@ -25,17 +25,17 @@ internal class EnjoymentDialogFragment : DialogFragment() { overrideTheme() } val enjoymentDialogView = LayoutInflater.from(ctx).inflate(R.layout.apptentive_enjoyment_dialog, null) - val messageView = enjoymentDialogView.findViewById(R.id.title) + val messageView = enjoymentDialogView.findViewById(R.id.apptentive_enjoyment_dialog_title) messageView.text = viewModel.title - val positiveButtonView = enjoymentDialogView.findViewById(R.id.yes) + val positiveButtonView = enjoymentDialogView.findViewById(R.id.apptentive_enjoyment_dialog_yes) positiveButtonView.text = viewModel.yesText positiveButtonView.setOnClickListener { viewModel.onYesButton() this@EnjoymentDialogFragment.dismiss() } - val negativeButtonView = enjoymentDialogView.findViewById(R.id.no) + val negativeButtonView = enjoymentDialogView.findViewById(R.id.apptentive_enjoyment_dialog_no) negativeButtonView.text = viewModel.noText negativeButtonView.setOnClickListener { viewModel.onNoButton() diff --git a/apptentive-enjoyment-dialog/src/main/res/layout/apptentive_enjoyment_dialog.xml b/apptentive-enjoyment-dialog/src/main/res/layout/apptentive_enjoyment_dialog.xml index 118398d42..26e479e53 100644 --- a/apptentive-enjoyment-dialog/src/main/res/layout/apptentive_enjoyment_dialog.xml +++ b/apptentive-enjoyment-dialog/src/main/res/layout/apptentive_enjoyment_dialog.xml @@ -9,7 +9,7 @@ android:paddingBottom="2dp"> (R.id.apptentive_note) + val noteLayout = contentView.findViewById(R.id.apptentive_note_layout) //region Title val titleView = inflater.inflate( diff --git a/apptentive-notes/src/main/res/layout/apptentive_note.xml b/apptentive-notes/src/main/res/layout/apptentive_note.xml index c1951719c..1a185f1a9 100644 --- a/apptentive-notes/src/main/res/layout/apptentive_note.xml +++ b/apptentive-notes/src/main/res/layout/apptentive_note.xml @@ -9,7 +9,7 @@ android:paddingBottom="2dp"> diff --git a/apptentive-notes/src/main/res/layout/apptentive_note_action.xml b/apptentive-notes/src/main/res/layout/apptentive_note_action.xml index 3d173363b..47088243b 100644 --- a/apptentive-notes/src/main/res/layout/apptentive_note_action.xml +++ b/apptentive-notes/src/main/res/layout/apptentive_note_action.xml @@ -1,6 +1,6 @@ \ No newline at end of file diff --git a/apptentive-notes/src/main/res/layout/apptentive_note_message.xml b/apptentive-notes/src/main/res/layout/apptentive_note_message.xml index 86a83975e..b9b51c368 100644 --- a/apptentive-notes/src/main/res/layout/apptentive_note_message.xml +++ b/apptentive-notes/src/main/res/layout/apptentive_note_message.xml @@ -2,7 +2,7 @@ (R.id.apptentive_rate_title) + val titleView = contentView.findViewById(R.id.apptentive_rating_dialog_title) titleView.text = viewModel.title.orEmpty() - val messageView = contentView.findViewById(R.id.apptentive_rate_message) + val messageView = contentView.findViewById(R.id.apptentive_rating_dialog_message) messageView.text = viewModel.message.orEmpty() - val rateButton = contentView.findViewById(R.id.apptentive_rate_button) + val rateButton = contentView.findViewById(R.id.apptentive_rating_dialog_button) rateButton.text = viewModel.rateText.orEmpty() rateButton.setOnClickListener { viewModel.onRateButton() dismiss() } - val remindButton = contentView.findViewById(R.id.apptentive_remind_button) + val remindButton = contentView.findViewById(R.id.apptentive_rating_dialog_remind_button) remindButton.text = viewModel.remindText.orEmpty() remindButton.setOnClickListener { viewModel.onRemindButton() dismiss() } - val declineButton = contentView.findViewById(R.id.apptentive_decline_button) + val declineButton = contentView.findViewById(R.id.apptentive_rating_dialog_decline_button) declineButton.text = viewModel.declineText.orEmpty() declineButton.setOnClickListener { viewModel.onDeclineButton() diff --git a/apptentive-ratings/src/main/res/layout/apptentive_rating_dialog.xml b/apptentive-ratings/src/main/res/layout/apptentive_rating_dialog.xml index 33c7ee586..e2355b003 100644 --- a/apptentive-ratings/src/main/res/layout/apptentive_rating_dialog.xml +++ b/apptentive-ratings/src/main/res/layout/apptentive_rating_dialog.xml @@ -15,7 +15,7 @@ android:orientation="vertical"> (R.id.apptentive_survey_recycler_view) recyclerView.adapter = adapter - viewModel.listItems.observe(this, Observer { items -> + viewModel.listItems.observe(this) { items -> adapter.submitList(items) - }) + } - viewModel.firstInvalidQuestionIndex.observe(this, Observer { firstErrorPosition -> + viewModel.firstInvalidQuestionIndex.observe(this) { firstErrorPosition -> if (firstErrorPosition != -1) { // Check if item is fully visible on screen before trying to scroll @@ -72,15 +69,14 @@ internal class SurveyActivity : BaseSurveyActivity() { val errorView = layoutManger.findViewByPosition(firstErrorPosition) errorView?.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) } - } - }) + } - viewModel.exitStream.observe(this, Observer { + viewModel.exitStream.observe(this) { finish() - }) + } - viewModel.showConfirmation.observe(this, Observer { + viewModel.showConfirmation.observe(this) { if (it) { with(viewModel.surveyCancelConfirmationDisplay) { confirmationDialog = showConfirmationDialog( @@ -96,13 +92,12 @@ internal class SurveyActivity : BaseSurveyActivity() { confirmationDialog?.show() } } - }) + } supportActionBar?.hide() - val topAppBar = findViewById(R.id.topAppBar) - val title = findViewById(R.id.apptentive_survey_title) - title.text = viewModel.title + val topAppBar = findViewById(R.id.apptentive_top_app_bar) + topAppBar.title = viewModel.title topAppBar.setNavigationOnClickListener { viewModel.exit(showConfirmation = true) } diff --git a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/view/SurveyQuestionContainerView.kt b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/view/SurveyQuestionContainerView.kt index 9fffec972..9e6619817 100644 --- a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/view/SurveyQuestionContainerView.kt +++ b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/view/SurveyQuestionContainerView.kt @@ -5,13 +5,12 @@ import android.content.res.ColorStateList import android.util.AttributeSet import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.widget.FrameLayout import android.widget.LinearLayout -import android.widget.TextView import androidx.core.view.isVisible import apptentive.com.android.feedback.survey.R import apptentive.com.android.ui.getThemeColor +import com.google.android.material.textview.MaterialTextView internal class SurveyQuestionContainerView( context: Context, @@ -19,10 +18,10 @@ internal class SurveyQuestionContainerView( defStyleAttr: Int ) : FrameLayout(context, attrs, defStyleAttr) { private val titleInstructionsLayout: LinearLayout - private val titleTextView: TextView - private val instructionsTextView: TextView - private val answerContainerView: ViewGroup - private val errorMessageView: TextView + private val titleTextView: MaterialTextView + private val instructionsTextView: MaterialTextView + private val answerContainerView: LinearLayout + private val errorMessageView: MaterialTextView var title: CharSequence? get() = titleTextView.text @@ -54,11 +53,11 @@ internal class SurveyQuestionContainerView( val contentView = LayoutInflater.from(context) .inflate(R.layout.apptentive_survey_question_container, this, true) - titleInstructionsLayout = contentView.findViewById(R.id.question_title_instructions_layout) - titleTextView = contentView.findViewById(R.id.question_title) - instructionsTextView = contentView.findViewById(R.id.question_instructions) - answerContainerView = contentView.findViewById(R.id.answer_container) - errorMessageView = contentView.findViewById(R.id.error_message) + titleInstructionsLayout = contentView.findViewById(R.id.apptentive_question_title_instructions_layout) + titleTextView = contentView.findViewById(R.id.apptentive_question_title) + instructionsTextView = contentView.findViewById(R.id.apptentive_question_instructions) + answerContainerView = contentView.findViewById(R.id.apptentive_answer_container) + errorMessageView = contentView.findViewById(R.id.apptentive_question_error_message) titleTextViewDefaultColor = titleTextView.textColors instructionsTextViewDefaultColor = instructionsTextView.textColors diff --git a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/MultiChoiceQuestionListItem.kt b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/MultiChoiceQuestionListItem.kt index be22958d0..250da1178 100644 --- a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/MultiChoiceQuestionListItem.kt +++ b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/MultiChoiceQuestionListItem.kt @@ -10,6 +10,8 @@ import apptentive.com.android.feedback.survey.model.MultiChoiceQuestion import apptentive.com.android.feedback.survey.view.SurveyQuestionContainerView import apptentive.com.android.ui.ListViewItem import apptentive.com.android.ui.setInvalid +import com.google.android.material.checkbox.MaterialCheckBox +import com.google.android.material.radiobutton.MaterialRadioButton import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout @@ -94,7 +96,7 @@ internal class MultiChoiceQuestionListItem( itemView: SurveyQuestionContainerView, private val onSelectionChanged: (questionId: String, choiceId: String, selected: Boolean, text: String?) -> Unit ) : SurveyQuestionListItem.ViewHolder(itemView) { - private val choiceContainer: ViewGroup = itemView.findViewById(R.id.choice_container) + private val choiceContainer: ViewGroup = itemView.findViewById(R.id.apptentive_choice_container) private lateinit var cachedViews: List override fun bindView( @@ -114,7 +116,10 @@ internal class MultiChoiceQuestionListItem( val choiceView = layoutInflater.inflate(choiceLayoutRes, choiceContainer, false) // button (checkbox or radio) - val compoundButton = choiceView.findViewById(R.id.checkbox) + val compoundButton = + if (item.allowMultipleAnswers) choiceView.findViewById(R.id.apptentive_checkbox) + else choiceView.findViewById(R.id.apptentive_radiobutton) + compoundButton.text = choice.title compoundButton.isChecked = choice.isChecked @@ -124,8 +129,8 @@ internal class MultiChoiceQuestionListItem( // button end // text fields - val textInputLayout: TextInputLayout = choiceView.findViewById(R.id.other_text_input_layout) - val textInputEditText: TextInputEditText = choiceView.findViewById(R.id.other_edit_text) + val textInputLayout = choiceView.findViewById(R.id.apptentive_other_text_input_layout) + val textInputEditText = choiceView.findViewById(R.id.apptentive_other_edit_text) textInputLayout.isVisible = choice.isTextInputVisible textInputLayout.hint = choice.hint diff --git a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/RangeQuestionListItem.kt b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/RangeQuestionListItem.kt index 5675a9989..f9aadcb64 100644 --- a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/RangeQuestionListItem.kt +++ b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/RangeQuestionListItem.kt @@ -1,11 +1,10 @@ package apptentive.com.android.feedback.survey.viewmodel import android.content.res.Resources -import android.view.View -import android.widget.TextView import apptentive.com.android.feedback.survey.R import apptentive.com.android.feedback.survey.view.SurveyQuestionContainerView import com.google.android.material.slider.Slider +import com.google.android.material.textview.MaterialTextView /** * Class which represents range question list item state @@ -68,9 +67,9 @@ internal class RangeQuestionListItem( onSelectionChanged: (id: String, selectedIndex: Int) -> Unit ) : SurveyQuestionListItem.ViewHolder(itemView) { - private val rangeSlider = itemView.findViewById(R.id.range_slider) - private val minLabel = itemView.findViewById(R.id.minLabel) - private val maxLabel = itemView.findViewById(R.id.maxLabel) + private val rangeSlider = itemView.findViewById(R.id.apptentive_range_slider) + private val minLabel = itemView.findViewById(R.id.apptentive_min_label) + private val maxLabel = itemView.findViewById(R.id.apptentive_max_label) init { rangeSlider.addOnChangeListener { slider, value, fromUser -> diff --git a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/SingleLineQuestionListItem.kt b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/SingleLineQuestionListItem.kt index 8042ecb9e..4f8367375 100644 --- a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/SingleLineQuestionListItem.kt +++ b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/SingleLineQuestionListItem.kt @@ -76,8 +76,8 @@ internal class SingleLineQuestionListItem( itemView: SurveyQuestionContainerView, val onTextChanged: (id: String, text: String) -> Unit ) : SurveyQuestionListItem.ViewHolder(itemView) { - private val answerTextInputLayout: TextInputLayout = itemView.findViewById(R.id.answer_text_input_layout) - private val answerEditText: TextInputEditText = itemView.findViewById(R.id.answer_text) + private val answerTextInputLayout: TextInputLayout = itemView.findViewById(R.id.apptentive_answer_text_input_layout) + private val answerEditText: TextInputEditText = itemView.findViewById(R.id.apptentive_answer_text) override fun bindView( item: SingleLineQuestionListItem, diff --git a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/SurveyFooterListItem.kt b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/SurveyFooterListItem.kt index 8f0fca5ae..1d76e137e 100644 --- a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/SurveyFooterListItem.kt +++ b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/SurveyFooterListItem.kt @@ -1,12 +1,13 @@ package apptentive.com.android.feedback.survey.viewmodel import android.view.View -import android.widget.TextView import android.widget.Toast import android.widget.Toast.LENGTH_SHORT import apptentive.com.android.feedback.survey.R import apptentive.com.android.ui.ListViewAdapter import apptentive.com.android.ui.ListViewItem +import com.google.android.material.button.MaterialButton +import com.google.android.material.textview.MaterialTextView internal class SurveyFooterListItem( val buttonTitle: String?, @@ -63,8 +64,8 @@ internal class SurveyFooterListItem( itemView: View, private val submitCallback: () -> Unit ) : ListViewAdapter.ViewHolder(itemView) { - private val submitButton: TextView = itemView.findViewById(R.id.submit_button) - private val errorMessageView: TextView = itemView.findViewById(R.id.submit_error_message) + private val submitButton = itemView.findViewById(R.id.apptentive_submit_button) + private val errorMessageView = itemView.findViewById(R.id.apptentive_submit_error_message) init { submitButton.setOnClickListener { diff --git a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/SurveyHeaderListItem.kt b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/SurveyHeaderListItem.kt index ce9fea7d3..1223468eb 100644 --- a/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/SurveyHeaderListItem.kt +++ b/apptentive-survey/src/main/java/apptentive/com/android/feedback/survey/viewmodel/SurveyHeaderListItem.kt @@ -1,10 +1,10 @@ package apptentive.com.android.feedback.survey.viewmodel import android.view.View -import android.widget.TextView import apptentive.com.android.feedback.survey.R import apptentive.com.android.ui.ListViewAdapter import apptentive.com.android.ui.ListViewItem +import com.google.android.material.textview.MaterialTextView internal class SurveyHeaderListItem(val instructions: String) : SurveyListItem( id = "header", @@ -35,7 +35,7 @@ internal class SurveyHeaderListItem(val instructions: String) : SurveyListItem( } class ViewHolder(itemView: View) : ListViewAdapter.ViewHolder(itemView) { - private val introductionView: TextView = itemView.findViewById(R.id.survey_introduction) + private val introductionView = itemView.findViewById(R.id.apptentive_survey_introduction) override fun bindView(item: SurveyHeaderListItem, position: Int) { introductionView.text = item.instructions diff --git a/apptentive-survey/src/main/res/layout/apptentive_activity_survey.xml b/apptentive-survey/src/main/res/layout/apptentive_activity_survey.xml index 37c26be2c..67d99dd29 100644 --- a/apptentive-survey/src/main/res/layout/apptentive_activity_survey.xml +++ b/apptentive-survey/src/main/res/layout/apptentive_activity_survey.xml @@ -13,39 +13,37 @@ android:layout_height="wrap_content"> - - - - + android:layout_height="?actionBarSize"/> + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + tools:background="@null" /> - + android:layout_height="wrap_content" + android:minHeight="@dimen/apptentive_bottom_app_bar_height"> + + + + \ No newline at end of file diff --git a/apptentive-survey/src/main/res/layout/apptentive_survey_footer.xml b/apptentive-survey/src/main/res/layout/apptentive_survey_footer.xml index 656025215..f11ff2d19 100644 --- a/apptentive-survey/src/main/res/layout/apptentive_survey_footer.xml +++ b/apptentive-survey/src/main/res/layout/apptentive_survey_footer.xml @@ -7,25 +7,23 @@ android:orientation="vertical"> - diff --git a/apptentive-survey/src/main/res/layout/apptentive_survey_header.xml b/apptentive-survey/src/main/res/layout/apptentive_survey_header.xml index 4428a7c85..9f6dc468c 100644 --- a/apptentive-survey/src/main/res/layout/apptentive_survey_header.xml +++ b/apptentive-survey/src/main/res/layout/apptentive_survey_header.xml @@ -1,23 +1,12 @@ - - - - - \ No newline at end of file + android:padding="@dimen/apptentive_survey_introduction_padding" + android:textAlignment="center" + tools:targetApi="p" + tools:text="Tell us about your experience!" /> \ No newline at end of file diff --git a/apptentive-survey/src/main/res/layout/apptentive_survey_question_container.xml b/apptentive-survey/src/main/res/layout/apptentive_survey_question_container.xml index 688e89c18..2f6197d66 100644 --- a/apptentive-survey/src/main/res/layout/apptentive_survey_question_container.xml +++ b/apptentive-survey/src/main/res/layout/apptentive_survey_question_container.xml @@ -1,23 +1,16 @@ - - + android:layout_height="wrap_content"> - + android:layout_marginBottom="@dimen/apptentive_question_title_bottom_margin" + tools:text="Question Title" /> - @@ -58,21 +45,19 @@ android:layout_height="1dp" /> - diff --git a/apptentive-survey/src/main/res/layout/apptentive_survey_question_multichoice.xml b/apptentive-survey/src/main/res/layout/apptentive_survey_question_multichoice.xml index 6e82104eb..e774535e3 100644 --- a/apptentive-survey/src/main/res/layout/apptentive_survey_question_multichoice.xml +++ b/apptentive-survey/src/main/res/layout/apptentive_survey_question_multichoice.xml @@ -1,6 +1,6 @@ diff --git a/apptentive-survey/src/main/res/layout/apptentive_survey_question_multichoice_choice.xml b/apptentive-survey/src/main/res/layout/apptentive_survey_question_multichoice_choice.xml index 5f407dc62..bb21a000d 100644 --- a/apptentive-survey/src/main/res/layout/apptentive_survey_question_multichoice_choice.xml +++ b/apptentive-survey/src/main/res/layout/apptentive_survey_question_multichoice_choice.xml @@ -1,11 +1,5 @@ - - diff --git a/apptentive-survey/src/main/res/layout/apptentive_survey_question_multiselect.xml b/apptentive-survey/src/main/res/layout/apptentive_survey_question_multiselect.xml deleted file mode 100644 index ba28a1a0d..000000000 --- a/apptentive-survey/src/main/res/layout/apptentive_survey_question_multiselect.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/apptentive-survey/src/main/res/layout/apptentive_survey_question_multiselect_choice.xml b/apptentive-survey/src/main/res/layout/apptentive_survey_question_multiselect_choice.xml index 5d99fd997..32fbf9f98 100644 --- a/apptentive-survey/src/main/res/layout/apptentive_survey_question_multiselect_choice.xml +++ b/apptentive-survey/src/main/res/layout/apptentive_survey_question_multiselect_choice.xml @@ -1,11 +1,5 @@ - - diff --git a/apptentive-survey/src/main/res/layout/apptentive_survey_question_range.xml b/apptentive-survey/src/main/res/layout/apptentive_survey_question_range.xml index 3a5c842c6..208056d00 100644 --- a/apptentive-survey/src/main/res/layout/apptentive_survey_question_range.xml +++ b/apptentive-survey/src/main/res/layout/apptentive_survey_question_range.xml @@ -1,49 +1,44 @@ + android:layout_marginHorizontal="@dimen/apptentive_radio_checkbox_slider_margin" /> - - diff --git a/apptentive-survey/src/main/res/layout/apptentive_survey_question_singleline.xml b/apptentive-survey/src/main/res/layout/apptentive_survey_question_singleline.xml index fdf02d574..e8efefd5c 100644 --- a/apptentive-survey/src/main/res/layout/apptentive_survey_question_singleline.xml +++ b/apptentive-survey/src/main/res/layout/apptentive_survey_question_singleline.xml @@ -1,21 +1,17 @@ + android:paddingHorizontal="@dimen/apptentive_singleline_layout_horizontal_padding"> + android:layout_marginBottom="@dimen/apptentive_singleline_edittext_bottom_padding" + android:minHeight="@dimen/apptentive_other_text_field_min_height" /> \ No newline at end of file diff --git a/apptentive-survey/src/main/res/layout/apptentive_survey_question_text_input.xml b/apptentive-survey/src/main/res/layout/apptentive_survey_question_text_input.xml index e9170f9c2..5195fa2ac 100644 --- a/apptentive-survey/src/main/res/layout/apptentive_survey_question_text_input.xml +++ b/apptentive-survey/src/main/res/layout/apptentive_survey_question_text_input.xml @@ -1,25 +1,17 @@ + android:layout_marginStart="@dimen/apptentive_other_text_field_start_padding" + android:layout_marginEnd="@dimen/apptentive_other_text_field_end_padding" + android:visibility="gone"> + android:minHeight="@dimen/apptentive_radio_checkbox_min_height" /> \ No newline at end of file diff --git a/apptentive-survey/src/main/res/values/dimens.xml b/apptentive-survey/src/main/res/values/dimens.xml index 65f0bc6d5..e9f2165e7 100644 --- a/apptentive-survey/src/main/res/values/dimens.xml +++ b/apptentive-survey/src/main/res/values/dimens.xml @@ -18,17 +18,25 @@ 4dp 4dp - 2dp - 4dp - 6dp - 8dp - 12dp - 16dp - 24dp - 40dp - 40dp - 11dp - 48dp - 36dp + + 16dp + 12dp + 8dp + 16dp + 4dp + 16dp + 40dp + 11dp + 8dp + 16dp + 4dp + 16dp + 2dp + 48dp + 40dp + 16dp + 36dp + 16dp + 16dp \ No newline at end of file diff --git a/apptentive-survey/src/main/res/values/strings.xml b/apptentive-survey/src/main/res/values/strings.xml index e3a6c0588..8ffe56878 100644 --- a/apptentive-survey/src/main/res/values/strings.xml +++ b/apptentive-survey/src/main/res/values/strings.xml @@ -10,7 +10,6 @@ %1$s = %2$s Close survey? You will lose your progress if you close this survey. - Close Back to survey Lowest Highest From 9c59857f81f9d72267a8516812cc394d76a7df77 Mon Sep 17 00:00:00 2001 From: PoornimaApptentive <85186738+PoornimaApptentive@users.noreply.github.com> Date: Wed, 16 Mar 2022 15:47:35 -0700 Subject: [PATCH 039/491] PBI-3023 apptentive-kit-android (#236) * renamed kit module to apptentive-kit-android [PBI-3023] --- CONTRIBUTING.md | 4 ++-- Jenkinsfile | 2 +- app/build.gradle | 2 +- apptentive-example/build.gradle | 4 ++-- .../.gitignore | 0 .../build.gradle | 2 +- .../consumer-rules.pro | 0 .../src/main/AndroidManifest.xml | 2 +- jfrog-publish-util.gradle | 4 ++-- publish.gradle | 10 +++++----- settings.gradle | 2 +- 11 files changed, 16 insertions(+), 16 deletions(-) rename {apptentive-sdk-kit => apptentive-kit-android}/.gitignore (100%) rename {apptentive-sdk-kit => apptentive-kit-android}/build.gradle (97%) rename {apptentive-sdk-kit => apptentive-kit-android}/consumer-rules.pro (100%) rename {apptentive-sdk-kit => apptentive-kit-android}/src/main/AndroidManifest.xml (69%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 45a6162d5..f26f058c2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,8 +2,8 @@ We love contributions! -Any contributions to the master apptentive-android-sdk 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-kit-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-sdk/pulls). Before we merge, we'll check to make sure you're on the list of people who've signed our CLA. +When you want to submit a change, send us a [pull request](https://github.com/apptentive/aapptentive-kit-android/pulls). Before we merge, we'll check to make sure you're on the list of people who've signed our CLA. Thanks! \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile index e608697b2..80f65c7bf 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -48,7 +48,7 @@ pipeline { gitCommit = apptentiveGetReleaseCommit() imageName = apptentiveDockerBuild('build', gitCommit) container('docker') { - sh "docker run ${imageName} ./gradlew test --exclude-task :apptentive-sdk-kit:test" + sh "docker run ${imageName} ./gradlew test --exclude-task :apptentive-kit-android:test" } } } diff --git a/app/build.gradle b/app/build.gradle index df61caf1c..33d5f87a2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -120,7 +120,7 @@ tasks.getByName('githubRelease').mustRunAfter(tasks.getByName('assemble')) github { owner = 'apptentive' - repo = 'android-sdk' + repo = 'apptentive-kit-android-private' token = githubToken tagName = gitTag targetCommitish = gitBranch diff --git a/apptentive-example/build.gradle b/apptentive-example/build.gradle index 29427a5b1..34e53f385 100644 --- a/apptentive-example/build.gradle +++ b/apptentive-example/build.gradle @@ -24,7 +24,7 @@ dependencies { /** * TODO Step 1: Uncomment the import line below. * TODO Step 2: Replace APPTENTIVE_VERSION with latest released version on GitHub - * https://github.com/apptentive/apptentive-android-sdk + * https://github.com/apptentive/apptentive-kit-android */ -// implementation "com.apptentive:apptentive-sdk-kit:APPTENTIVE_VERSION" +// implementation "com.apptentive:apptentive-kit-android:APPTENTIVE_VERSION" } \ No newline at end of file diff --git a/apptentive-sdk-kit/.gitignore b/apptentive-kit-android/.gitignore similarity index 100% rename from apptentive-sdk-kit/.gitignore rename to apptentive-kit-android/.gitignore diff --git a/apptentive-sdk-kit/build.gradle b/apptentive-kit-android/build.gradle similarity index 97% rename from apptentive-sdk-kit/build.gradle rename to apptentive-kit-android/build.gradle index ade6f07f7..0a6e24ec3 100644 --- a/apptentive-sdk-kit/build.gradle +++ b/apptentive-kit-android/build.gradle @@ -51,7 +51,7 @@ dependencies { project.ext { groupId = 'com.apptentive' - artifactId = 'apptentive-sdk-kit' + artifactId = 'apptentive-kit-android' } apply from: '../publish.gradle' diff --git a/apptentive-sdk-kit/consumer-rules.pro b/apptentive-kit-android/consumer-rules.pro similarity index 100% rename from apptentive-sdk-kit/consumer-rules.pro rename to apptentive-kit-android/consumer-rules.pro diff --git a/apptentive-sdk-kit/src/main/AndroidManifest.xml b/apptentive-kit-android/src/main/AndroidManifest.xml similarity index 69% rename from apptentive-sdk-kit/src/main/AndroidManifest.xml rename to apptentive-kit-android/src/main/AndroidManifest.xml index 794ce5d62..e2f8cf16c 100644 --- a/apptentive-sdk-kit/src/main/AndroidManifest.xml +++ b/apptentive-kit-android/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="com.apptentive.apptentive_kit_android"> \ No newline at end of file diff --git a/jfrog-publish-util.gradle b/jfrog-publish-util.gradle index 2021af71f..c3522215c 100644 --- a/jfrog-publish-util.gradle +++ b/jfrog-publish-util.gradle @@ -28,13 +28,13 @@ task jfrogPublishApptentiveModules(type: GradleBuild) { task jfrogAssembleApptentiveKit(type: GradleBuild) { buildName = "jfrogAssembleApptentiveKit" - tasks = ['apptentive-sdk-kit:assembleRelease'] + tasks = ['apptentive-kit-android:assembleRelease'] dependsOn jfrogPublishApptentiveModules } task jfrogPublishApptentiveKit(type: GradleBuild) { buildName = "jfrogPublishApptentiveKit" startParameter.projectProperties = project.gradle.startParameter.projectProperties - tasks = ['apptentive-sdk-kit:artifactoryPublish'] + tasks = ['apptentive-kit-android:artifactoryPublish'] dependsOn jfrogAssembleApptentiveKit } diff --git a/publish.gradle b/publish.gradle index 816e439f4..cabda87ef 100644 --- a/publish.gradle +++ b/publish.gradle @@ -59,12 +59,12 @@ publishing { pom { name = project.artifactId description = 'Apptentive Android SDK' - url = 'https://github.com/apptentive/apptentive-android-sdk' + url = 'https://github.com/apptentive/apptentive-kit-android' licenses { license { name = 'BSD 3-Clause License' - url = 'https://raw.githubusercontent.com/apptentive/apptentive-android-sdk/master/License.txt' + url = 'https://raw.githubusercontent.com/apptentive/apptentive-kit-android/master/License.txt' distribution = 'repo' } } @@ -76,9 +76,9 @@ publishing { } } scm { - url = 'https://github.com/apptentive/apptentive-android-sdk' - connection = 'scm:git@github.com:apptentive/apptentive-android-sdk.git' - developerConnection = 'scm:git@github.com:apptentive/apptentive-android-sdk.git' + url = 'https://github.com/apptentive/apptentive-kit-android' + connection = 'scm:git@github.com:apptentive/apptentive-kit-android.git' + developerConnection = 'scm:git@github.com:apptentive/apptentive-kit-android.git' } } } diff --git a/settings.gradle b/settings.gradle index 5efdfa83e..a8a653d9f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,6 +13,6 @@ include ':app', ':apptentive-ratings', ':apptentive-in-app-review', ':apptentive-survey', - ':apptentive-sdk-kit' + ':apptentive-kit-android' rootProject.name = 'apptentive-sdk-ktx' From 2415ed77395649b2f5b05aa05e7ea42b4afda9f7 Mon Sep 17 00:00:00 2001 From: ChaseApptentive <85038972+ChaseApptentive@users.noreply.github.com> Date: Fri, 18 Mar 2022 09:35:06 -0500 Subject: [PATCH 040/491] PBI-3050 Survey Image Header Alternate (#235) * PBI-3009 Notes minor interface customization [PBI-3009] --- app/src/main/res/drawable/example_logo.xml | 13 ++++++++ .../main/res/drawable/example_logo_png.png | Bin 0 -> 63929 bytes app/src/main/res/values/styles.xml | 16 ++++++++-- .../src/main/res/values/apptentive-attrs.xml | 2 ++ .../src/main/res/values/styles.xml | 11 +++++-- .../feedback/ApptentiveConfiguration.kt | 30 ++++++++++-------- .../android/feedback/survey/SurveyActivity.kt | 5 ++- .../res/layout/apptentive_activity_survey.xml | 20 +++++++++++- .../src/main/res/values/strings.xml | 1 + 9 files changed, 79 insertions(+), 19 deletions(-) create mode 100644 app/src/main/res/drawable/example_logo.xml create mode 100644 app/src/main/res/drawable/example_logo_png.png diff --git a/app/src/main/res/drawable/example_logo.xml b/app/src/main/res/drawable/example_logo.xml new file mode 100644 index 000000000..844f4fc2a --- /dev/null +++ b/app/src/main/res/drawable/example_logo.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/example_logo_png.png b/app/src/main/res/drawable/example_logo_png.png new file mode 100644 index 0000000000000000000000000000000000000000..833fd2c0f8e741f5155dab1a453966f8662d8279 GIT binary patch literal 63929 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
mpv3s-par=3%k-Ix*{G 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
-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
N1RU6FTZ~o zti!)4E2y`g_<{y*fimpMOEqvQs|BsEvbL)y0kN